diff --git a/plugins/taskboard/README.md b/plugins/taskboard/README.md index 6948df3..e0b1a05 100644 --- a/plugins/taskboard/README.md +++ b/plugins/taskboard/README.md @@ -95,10 +95,10 @@ token, and JQL. Only HTTPS `*.atlassian.net` origins are accepted. The panel opens on BB's current project. Use **Across projects** only when you want an explicit grouped overview. List and Kanban preserve each provider's actual workflow names. The primary delivery stages appear in the focused order -In Review, In Progress, Blocked, Todo, Backlog, then Done by default; -provider-specific stages remain between the closest matching stages. Change -that ordering per project in **Manage → Board preferences**. Rejected writes -roll back the optimistic move. +Backlog, Todo, In Progress, In Review, QA, then the remaining workflow stages +by default; provider-specific stages remain between the closest matching +stages. Change that ordering per project in **Manage → Board preferences**. +Rejected writes roll back the optimistic move. ![Live task detail](https://raw.githubusercontent.com/MateoCerquetella/bb-plugins/main/docs/media/task-detail.png) diff --git a/plugins/taskboard/app.tsx b/plugins/taskboard/app.tsx index 8001799..fa426c5 100644 --- a/plugins/taskboard/app.tsx +++ b/plugins/taskboard/app.tsx @@ -72,17 +72,26 @@ import { } from './credential-contract.js'; import { assigneeFilterOptions, - compareWorkflowStatuses, filterWorkItemsByAttributes, labelFilterOptions, priorityFilterOptions, projectFilterOptions, sortWorkItemsByWorkflow, statusFilterOptions, + workflowStatusLaneKey, + workflowStatusLanes, workflowStatusTone, workflowStatusGroups, type FilterOption } from './browse.js'; +import { + availableContextProjectId, + contextSelectionToken, + previousProjectRouteContext, + projectRouteContext, + type NavigationEntryLike, + type ProjectRouteContext +} from './project-selection.js'; import './app.css'; const PANEL_PATH = 'tasks'; @@ -191,6 +200,30 @@ function storeLastProjectId(projectId: string): void { } } +function loadSourceProjectContext(): ProjectRouteContext | null { + const browserNavigation = ( + window as Window & { + navigation?: { + currentEntry?: { index: number } | null; + entries(): NavigationEntryLike[]; + }; + } + ).navigation; + const currentIndex = browserNavigation?.currentEntry?.index; + if (browserNavigation !== undefined && currentIndex !== undefined) { + try { + return previousProjectRouteContext( + browserNavigation.entries(), + currentIndex, + window.location.origin + ); + } catch { + // Fall back to the document referrer when navigation history is unavailable. + } + } + return projectRouteContext(document.referrer, window.location.origin); +} + function ManageHeaderAction({ subPath }: PluginNavPanelProps) { const route = parseTrackerRoute(subPath); const { projectId: contextProjectId } = useBbContext(); @@ -1403,46 +1436,28 @@ function ListStateGroups({ )); } -interface KanbanLane { - key: string; - name: string; - category: WorkStateCategory; -} - -function kanbanLaneKey(name: string, category: WorkStateCategory): string { - return `${category}:${name.trim().toLocaleLowerCase()}`; +function kanbanItemId(item: WorkItem): string { + return `${item.bbProjectId}:${item.source}:${item.locator}`; } -function kanbanLanes( - items: readonly WorkItem[], - discovered: readonly WorkStatusOption[], - statusOrder: readonly string[] -): KanbanLane[] { - const lanes = new Map(); - for (const group of workflowStatusGroups(items, statusOrder)) { - const key = kanbanLaneKey(group.name, group.category); - lanes.set(key, { - key, - name: group.name, - category: group.category - }); - } - for (const status of discovered) { - const key = kanbanLaneKey(status.name, status.stateCategory); - if (lanes.has(key)) continue; - lanes.set(key, { - key, - name: status.name, - category: status.stateCategory - }); - } - return [...lanes.values()].sort((left, right) => - compareWorkflowStatuses(left, right, statusOrder) +function mergeDiscoveredStatuses( + current: WorkStatusOption[], + incoming: readonly WorkStatusOption[] +): WorkStatusOption[] { + const merged = new Map( + current.map(status => [ + workflowStatusLaneKey(status.name, status.stateCategory), + status + ]) ); -} - -function kanbanItemId(item: WorkItem): string { - return `${item.bbProjectId}:${item.source}:${item.locator}`; + let changed = false; + for (const status of incoming) { + const key = workflowStatusLaneKey(status.name, status.stateCategory); + if (merged.has(key)) continue; + merged.set(key, status); + changed = true; + } + return changed ? [...merged.values()] : current; } type PriorityTone = 'low' | 'medium' | 'high' | 'urgent' | 'neutral'; @@ -1565,6 +1580,7 @@ function KanbanCard({ item, pickedUp, pending, + moveDisabled, onOpen, onPrepare, onDragStart, @@ -1574,6 +1590,7 @@ function KanbanCard({ item: WorkItem; pickedUp: boolean; pending: boolean; + moveDisabled: boolean; onOpen: () => void; onPrepare: () => void; onDragStart: (event: ReactDragEvent) => void; @@ -1590,14 +1607,15 @@ function KanbanCard({ return (