Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ jobs:
run: bash scripts/verify-csp-hash.sh
- run: pnpm run typecheck
- run: pnpm test
- name: Install Playwright browsers
run: npx playwright install chromium --with-deps
# No `playwright install` step: playwright.config.ts uses channel: "chrome",
# which resolves to ubuntu-latest's preinstalled Google Chrome — Playwright's
# own bundled Chromium download is never used and would be wasted work.
- name: Run E2E tests
run: pnpm test:e2e
env:
Expand Down
4 changes: 3 additions & 1 deletion docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,9 @@ When Jira is connected, a **Jira** tab appears in the tab bar. It shows all open

**Filters:** Status category (New, In Progress) and priority (Highest through Lowest) filters are available in the filter popover.

**Grouping:** Issues are grouped by Jira project key, similar to how GitHub items are grouped by repo.
**Grouping:** Issues are grouped by Jira project key, similar to how GitHub items are grouped by repo — except when Custom order is active (see below), which shows one flat list.

**Custom order:** By default, issues show in one flat list across all projects, ranked in whatever order you've arranged them (each row shows its project as a small badge). Use the up/down arrows on each row to move it — moves work across page boundaries and the page view follows the row. Arrows are only enabled while viewing "Assigned to me" with no status/priority filter applied (your arrangement is still visible otherwise, just not editable from that view). Pick any other option from the sort dropdown to switch to grouped/sorted display instead; a small "Custom order" button next to the dropdown switches back. Project group locking (pinning a project to the top) is only available in grouped views.

**Pagination:** Client-side over up to 100 fetched issues.

Expand Down
15 changes: 14 additions & 1 deletion src/app/components/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import PullRequestsTab from "./PullRequestsTab";
import TrackedTab from "./TrackedTab";
import PersonalSummaryStrip from "./PersonalSummaryStrip";
import { config, setConfig, getCustomTab, isBuiltinTab, isActionsBasedTab, isTabUnscoped, updateJiraConfig, type TrackedUser } from "../../stores/config";
import { viewState, updateViewState, setSortPreference, pruneClosedTrackedItems, removeCustomTabState, untrackJiraItem, setTabFilter, IssueFiltersSchema, PullRequestFiltersSchema, ActionsFiltersSchema } from "../../stores/view";
import { viewState, updateViewState, setSortPreference, pruneClosedTrackedItems, removeCustomTabState, untrackJiraItem, setTabFilter, IssueFiltersSchema, PullRequestFiltersSchema, ActionsFiltersSchema, pruneJiraCustomOrder, JIRA_CUSTOM_ORDER_SCOPE } from "../../stores/view";
import DependenciesTab from "./DependenciesTab";
import { isDependencyPr, expandBotLogins, needsBodyFallback, parseRenovateBody, type VersionInfo } from "../../lib/dependency-detection";
import { isRepoExcludedFromDependencies } from "../../lib/dependency-exclusion";
Expand Down Expand Up @@ -479,6 +479,19 @@ export default function DashboardPage() {
if (!isJiraAuthenticated()) return;
setJiraIssues(result.issues);

// Only prune when the result set is complete. The Enhanced JQL Search
// endpoint (search/jql) does not return a `total` match count — it uses
// cursor-based pagination via `nextPageToken` instead. A defined
// nextPageToken means more results exist beyond this page (pagination
// truncation); its absence means this page is the complete result set.
// Treating "not in this page" as "no longer exists" on a truncated page
// would permanently destroy user-curated custom-order positions for
// issues that are simply on a later page, not actually gone (closed,
// reassigned, or resolved).
if (scope === JIRA_CUSTOM_ORDER_SCOPE && result.nextPageToken === undefined) {
pruneJiraCustomOrder(new Set(result.issues.map((i) => i.key)));
}

// Auto-prune tracked Jira items that are done or deleted (scope-independent).
// Resolves status from current search results first, then bulkFetches only
// keys not covered (items from a different scope than the current view).
Expand Down
634 changes: 433 additions & 201 deletions src/app/components/dashboard/JiraAssignedTab.tsx

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/app/components/shared/SortDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface SortDropdownProps {
value: string;
direction: "asc" | "desc";
onChange: (field: string, direction: "asc" | "desc") => void;
placeholder?: string;
}

interface FlatOption {
Expand Down Expand Up @@ -57,6 +58,7 @@ export default function SortDropdown(props: SortDropdownProps) {
optionTextValue="label"
value={flatOptions().find((o) => o.value === selected()) ?? null}
onChange={(opt) => handleChange(opt?.value ?? null)}
placeholder={props.placeholder ?? "Sort by"}
itemComponent={(itemProps) => (
<Select.Item
item={itemProps.item}
Expand All @@ -70,7 +72,7 @@ export default function SortDropdown(props: SortDropdownProps) {
aria-label="Sort by"
class="btn btn-outline btn-sm compact:btn-xs w-auto min-w-[180px] justify-between"
>
<Select.Value<FlatOption>>{(state) => state.selectedOption()?.label ?? "Sort by"}</Select.Value>
<Select.Value<FlatOption>>{(state) => state.selectedOption()?.label ?? (props.placeholder ?? "Sort by")}</Select.Value>
<Select.Icon class="ml-2">▾</Select.Icon>
</Select.Trigger>
<Select.Portal>
Expand Down
35 changes: 35 additions & 0 deletions src/app/lib/grouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,41 @@ export function orderRepoGroups<G extends { repoFullName: string }>(
return [...locked, ...unlocked];
}

/**
* Orders `items` by `order` (a list of keys produced by `keyFn`), appending any
* items not referenced in `order` at the end, in their original relative order.
* If two items share the same key, the later one in `items` wins. `order` is
* walked as-is and is not itself deduplicated — a key appearing twice in `order`
* will pull the matching item twice into the result.
* Returns `items` unchanged (same reference) when `order` is empty.
*/
export function applyCustomOrder<T>(items: T[], order: string[], keyFn: (item: T) => string): T[] {
if (order.length === 0) return items;

const map = new Map<string, T>();
for (const item of items) {
map.set(keyFn(item), item);
}

const referenced = new Set<string>();
const result: T[] = [];
for (const key of order) {
const item = map.get(key);
if (item !== undefined) {
result.push(item);
referenced.add(key);
}
}

for (const [key, item] of map) {
if (!referenced.has(key)) {
result.push(item);
}
}

return result;
}

/**
* Three-tier involvement check for scope filtering.
* Shared by IssuesTab and PullRequestsTab — keep both call sites in sync.
Expand Down
Loading