FN-7260: consolidate Done actions into column menu
Move Done column archive and sort controls into the shared column actions menu. - Replace separate Done sort and archive header controls with menu items. - Preserve Done sort mode selection and archive-all confirmation behavior for standard and workflow complete columns. - Update menu styling, dashboard docs, release notes, and Column component coverage. Files changed: .changeset/fn-7260-done-actions-menu.md | 7 ++ docs/dashboard-guide.md | 5 +- packages/dashboard/app/components/Column.tsx | 93 ++++++++++------ .../app/components/__tests__/Column.test.tsx | 118 ++++++++++++--------- packages/dashboard/app/styles.css | 61 +++++------ 5 files changed, 168 insertions(+), 116 deletions(-) Fusion-Task-Id: FN-7260 Fusion-Task-Lineage: e336aa55-9dda-47bf-b8a7-9d9bbf6a33ef Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7260-done-actions-menu.md
Normal file
7
.changeset/fn-7260-done-actions-menu.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Group Done column sort and archive actions in one accessible actions menu.
|
||||
category: fix
|
||||
dev: Updates dashboard Done/complete column headers to use the shared column actions dropdown.
|
||||
@@ -145,8 +145,9 @@ FNXC:BoardCardActions 2026-06-30-13:12: Card context menus must not label an act
|
||||
<!-- FNXC:WorkflowBadges 2026-06-30-09:10: Task cards and task detail need workflow-name badges wherever mixed-workflow board contexts can hide the selected lane, especially the Board-only All workflows aggregate. -->
|
||||
<!-- FNXC:BoardDoneSorting 2026-06-29-00:00: The Done board column exposes a local descending sort selector so operators can review either latest completions or highest task IDs without changing other lifecycle columns. -->
|
||||
<!-- FNXC:BoardDoneSorting 2026-06-29-20:28: Document both Done sort modes as descending-only and Done-column-only so legacy Done and workflow complete-lane operators understand the selector does not change other lifecycle columns. -->
|
||||
- Column ordering semantics: `todo` mirrors scheduler pickup order (priority descending, then oldest `createdAt`, then task ID); `triage`, `in-progress`, `in-review`, and `archived` remain priority-first with task-ID tie-breaks; `done` defaults to most recent completion first (`columnMovedAt`, then `updatedAt`, then `createdAt` fallback) and can be switched from the Done column header to descending task ID. In workflow mode, non-archived columns marked with the `complete` flag use the same Done ordering even when their column ID or label is customized.
|
||||
- Done-column sorting has two descending modes: **Completion date (newest first)** keeps the default completion-time order, while **Task ID (newest first)** places the highest numeric task IDs first. The selector is only shown on Done/complete columns, including custom workflow completion lanes.
|
||||
<!-- FNXC:BoardDoneActions 2026-06-30-00:00: Done/complete column sort choices and Archive All Done now live in the column actions dropdown, so docs must point operators to the menu instead of separate header controls. -->
|
||||
- Column ordering semantics: `todo` mirrors scheduler pickup order (priority descending, then oldest `createdAt`, then task ID); `triage`, `in-progress`, `in-review`, and `archived` remain priority-first with task-ID tie-breaks; `done` defaults to most recent completion first (`columnMovedAt`, then `updatedAt`, then `createdAt` fallback) and can be switched from the Done/complete column actions dropdown to descending task ID. In workflow mode, non-archived columns marked with the `complete` flag use the same Done menu items even when their column ID or label is customized.
|
||||
- Done-column sorting has two descending modes: **Completion date (newest first)** keeps the default completion-time order, while **Task ID (newest first)** places the highest numeric task IDs first. The sort actions are only shown in Done/complete column action menus, including custom workflow completion lanes; Archive All Done lives in the same menu when available.
|
||||
- On mobile, both default and workflow-mode boards fill the project viewport while the column strip remains the internal horizontal scroller with contained edge overscroll.
|
||||
<!-- FNXC:WorkflowSelection 2026-06-29-13:34: Board, List, Header, and Graph workflow selectors now share a durable per-project selection so operators return to the same lane after remounts, task refreshes, or respecification flows; stale saved workflow ids must fall back to a valid default/first workflow instead of hiding all tasks. -->
|
||||
<!-- FNXC:WorkflowSelection 2026-06-29-18:37: The Board-only All workflows option renders an aggregate column set across workflows while keeping workflow-specific creates, edits, and durable selection scoped to real workflow ids. -->
|
||||
|
||||
@@ -12,7 +12,7 @@ import { PluginSlot } from "./PluginSlot";
|
||||
import { groupByWorktree } from "../utils/worktreeGrouping";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import type { TaskContextMenuColumnMetadata } from "./TaskContextMenu";
|
||||
import { ChevronDown, ChevronUp, Archive, MoreVertical } from "lucide-react";
|
||||
import { ChevronDown, ChevronUp, MoreVertical } from "lucide-react";
|
||||
import type { ModelInfo, BoardWorkflowColumnFlags } from "../api";
|
||||
import type { BlockerFanoutEntry } from "../hooks/useBlockerFanout";
|
||||
import type { DoneColumnSortMode } from "./taskSorting";
|
||||
@@ -565,9 +565,22 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree
|
||||
*/
|
||||
const isDoneSortColumn = workflowMode ? columnFlags?.complete === true && columnFlags?.archived !== true : column === "done";
|
||||
const showDoneSortControl = isDoneSortColumn && doneSortMode !== undefined && !!onDoneSortModeChange;
|
||||
const showDoneArchiveAction = isDoneSortColumn && !!onArchiveAllDone;
|
||||
const hasDoneMenuActions = showDoneSortControl || showDoneArchiveAction;
|
||||
const hasColumnMenu = hasColumnBulkActions || hasDoneMenuActions;
|
||||
const doneSortControlLabel = t("column.doneSortControlLabel", "Sort Done tasks");
|
||||
const doneSortOptions: Array<{ mode: DoneColumnSortMode; label: string }> = [
|
||||
{ mode: "completion-date-desc", label: t("column.doneSortCompletionDateDesc", "Completion date (newest first)") },
|
||||
{ mode: "task-id-desc", label: t("column.doneSortTaskIdDesc", "Task ID (newest first)") },
|
||||
];
|
||||
|
||||
const handleDoneSortModeSelect = useCallback((mode: DoneColumnSortMode) => {
|
||||
onDoneSortModeChange?.(mode);
|
||||
setIsMenuOpen(false);
|
||||
}, [onDoneSortModeChange]);
|
||||
|
||||
const handleArchiveAll = useCallback(async () => {
|
||||
setIsMenuOpen(false);
|
||||
if (!onArchiveAllDone) return;
|
||||
if (tasks.length === 0) return;
|
||||
|
||||
@@ -614,35 +627,7 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree
|
||||
+ {t("column.newTask", "New Task")}
|
||||
</button>
|
||||
)}
|
||||
{showDoneSortControl && (
|
||||
<label className="done-sort-control" title={doneSortControlLabel}>
|
||||
{/*
|
||||
FNXC:DoneColumnSorting 2026-06-29-18:09:
|
||||
The Done header needs an accessible, Done-only control that preserves Archive All and other header actions while allowing operators to switch between completion-date-desc and task-id-desc display orders.
|
||||
*/}
|
||||
<span className="done-sort-control__label">{t("column.doneSortLabel", "Sort")}</span>
|
||||
<select
|
||||
className="done-sort-control__select"
|
||||
aria-label={doneSortControlLabel}
|
||||
value={doneSortMode}
|
||||
onChange={(event) => onDoneSortModeChange(event.target.value as DoneColumnSortMode)}
|
||||
>
|
||||
<option value="completion-date-desc">{t("column.doneSortCompletionDateDesc", "Completion date (newest first)")}</option>
|
||||
<option value="task-id-desc">{t("column.doneSortTaskIdDesc", "Task ID (newest first)")}</option>
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
{column === "done" && onArchiveAllDone && (
|
||||
<button
|
||||
className="btn btn-icon btn-sm"
|
||||
onClick={handleArchiveAll}
|
||||
disabled={tasks.length === 0}
|
||||
title={t("column.archiveAllDoneTitle", "Archive all done tasks")}
|
||||
aria-label={t("column.archiveAllDoneAriaLabel", "Archive all done tasks")}
|
||||
>
|
||||
<Archive />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isArchived && onToggleCollapse && (
|
||||
<button
|
||||
className="btn btn-icon btn-sm"
|
||||
@@ -654,15 +639,19 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree
|
||||
{collapsed ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||
</button>
|
||||
)}
|
||||
{hasColumnBulkActions && (
|
||||
{hasColumnMenu && (
|
||||
<div className="column-menu" ref={menuRef}>
|
||||
{/**
|
||||
FNXC:DoneColumnActions 2026-06-30-00:00:
|
||||
Done and workflow complete-column archive/sort affordances must share this column actions dropdown with existing bulk actions, preventing duplicate header controls on desktop and mobile while preserving the original sort modes and archive confirmation path.
|
||||
*/}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-icon btn-sm"
|
||||
onClick={() => setIsMenuOpen((v) => !v)}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isMenuOpen}
|
||||
aria-label={t("column.actionsAriaLabel", "{{columnLabel}} column actions", { columnLabel: workflowMode ? (columnDisplayName ?? column) : COLUMN_LABELS[column] })}
|
||||
aria-label={t("column.actionsAriaLabel", "{{columnLabel}} column actions", { columnLabel: columnLabelText })}
|
||||
title={t("column.actionsTitle", "Column actions")}
|
||||
disabled={isMenuBusy}
|
||||
>
|
||||
@@ -670,6 +659,46 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree
|
||||
</button>
|
||||
{isMenuOpen && (
|
||||
<div className="column-menu-popover" role="menu">
|
||||
{showDoneSortControl && (
|
||||
<div className="column-menu-group" role="group" aria-label={doneSortControlLabel}>
|
||||
{doneSortOptions.map((option) => (
|
||||
<button
|
||||
key={option.mode}
|
||||
type="button"
|
||||
role="menuitemradio"
|
||||
aria-checked={doneSortMode === option.mode}
|
||||
className="column-menu-item column-menu-item-radio"
|
||||
onClick={() => handleDoneSortModeSelect(option.mode)}
|
||||
>
|
||||
<span className="column-menu-item-row">
|
||||
<span className="column-menu-item-check" aria-hidden="true">{doneSortMode === option.mode ? "✓" : ""}</span>
|
||||
<span>{option.label}</span>
|
||||
</span>
|
||||
<span className="column-menu-item-hint">
|
||||
{option.mode === "completion-date-desc"
|
||||
? t("column.doneSortCompletionDateDescHint", "Show recently completed tasks first")
|
||||
: t("column.doneSortTaskIdDescHint", "Show highest task IDs first")}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{showDoneArchiveAction && (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="column-menu-item"
|
||||
onClick={() => void handleArchiveAll()}
|
||||
disabled={tasks.length === 0}
|
||||
>
|
||||
{t("column.archiveAllDoneTitle", "Archive all done tasks")}
|
||||
<span className="column-menu-item-hint">
|
||||
{tasks.length === 0
|
||||
? t("column.noDoneTasksToArchive", "No done tasks to archive")
|
||||
: t("column.archiveAllDoneHint", "Archive {{count}} done task{{plural}}", { count: tasks.length, plural: tasks.length === 1 ? "" : "s" })}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
{isTodoLikeColumn && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -651,28 +651,37 @@ describe("Column in-progress/in-review bulk actions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Column Done sort control", () => {
|
||||
it("renders an accessible Done-only sort selector with clear labels", () => {
|
||||
render(
|
||||
describe("Column Done action menu", () => {
|
||||
it("renders one accessible Done actions dropdown with sort choices and archive", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="done"
|
||||
tasks={[{ ...makeTask("FN-001"), column: "done" }]}
|
||||
onArchiveAllDone={vi.fn().mockResolvedValue([])}
|
||||
doneSortMode="completion-date-desc"
|
||||
onDoneSortModeChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByRole("combobox", { name: "Sort Done tasks" });
|
||||
expect(select.closest(".done-sort-control")).toHaveAttribute("title", "Sort Done tasks");
|
||||
expect(screen.getByText("Sort")).toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: "Completion date (newest first)" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: "Task ID (newest first)" })).toBeInTheDocument();
|
||||
expect(select.closest(".done-sort-control")).not.toBeNull();
|
||||
expect(select.closest(".column-header")).not.toBeNull();
|
||||
const header = screen.getByRole("heading", { name: "Done" }).closest(".column-header") as HTMLElement;
|
||||
const actionsButton = screen.getByRole("button", { name: "Done column actions" });
|
||||
expect(actionsButton.closest(".column-header")).toBe(header);
|
||||
expect(header.querySelectorAll(".column-menu")).toHaveLength(1);
|
||||
expect(screen.queryByRole("combobox", { name: "Sort Done tasks" })).toBeNull();
|
||||
expect(container.querySelector(".done-sort-control")).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Archive all done tasks" })).toBeNull();
|
||||
|
||||
await user.click(actionsButton);
|
||||
|
||||
expect(screen.getByRole("menuitemradio", { name: /Completion date \(newest first\)/ })).toHaveAttribute("aria-checked", "true");
|
||||
expect(screen.getByRole("menuitemradio", { name: /Task ID \(newest first\)/ })).toHaveAttribute("aria-checked", "false");
|
||||
expect(screen.getByRole("menuitem", { name: /Archive all done tasks/i })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("renders the selector for workflow complete columns with custom ids", () => {
|
||||
it("renders the same Done dropdown for workflow complete columns with custom ids", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
@@ -681,16 +690,21 @@ describe("Column Done sort control", () => {
|
||||
columnDisplayName="Shipped"
|
||||
columnFlags={{ complete: true }}
|
||||
tasks={[{ ...makeTask("FN-001"), column: "shipped" as ColumnType }]}
|
||||
onArchiveAllDone={vi.fn().mockResolvedValue([])}
|
||||
doneSortMode="completion-date-desc"
|
||||
onDoneSortModeChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("heading", { name: "Shipped" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("combobox", { name: "Sort Done tasks" })).toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: "Shipped column actions" }));
|
||||
|
||||
expect(screen.getByRole("menuitemradio", { name: /Completion date \(newest first\)/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole("menuitemradio", { name: /Task ID \(newest first\)/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole("menuitem", { name: /Archive all done tasks/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("selects task ID descending from the Done header", async () => {
|
||||
it("selects task ID descending from the Done actions menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onDoneSortModeChange = vi.fn();
|
||||
render(
|
||||
@@ -703,12 +717,14 @@ describe("Column Done sort control", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.selectOptions(screen.getByRole("combobox", { name: "Sort Done tasks" }), "task-id-desc");
|
||||
await user.click(screen.getByRole("button", { name: "Done column actions" }));
|
||||
await user.click(screen.getByRole("menuitemradio", { name: /Task ID \(newest first\)/ }));
|
||||
|
||||
expect(onDoneSortModeChange).toHaveBeenCalledWith("task-id-desc");
|
||||
expect(screen.queryByRole("menu")).toBeNull();
|
||||
});
|
||||
|
||||
it("selects completion-date descending from the Done header", async () => {
|
||||
it("selects completion-date descending from the Done actions menu", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onDoneSortModeChange = vi.fn();
|
||||
render(
|
||||
@@ -721,70 +737,69 @@ describe("Column Done sort control", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.selectOptions(screen.getByRole("combobox", { name: "Sort Done tasks" }), "completion-date-desc");
|
||||
await user.click(screen.getByRole("button", { name: "Done column actions" }));
|
||||
await user.click(screen.getByRole("menuitemradio", { name: /Completion date \(newest first\)/ }));
|
||||
|
||||
expect(onDoneSortModeChange).toHaveBeenCalledWith("completion-date-desc");
|
||||
expect(screen.queryByRole("menu")).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps the Done sort selector available when Done is empty", () => {
|
||||
it("archives Done tasks from the menu only after confirmation", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onArchiveAllDone = vi.fn().mockResolvedValue([{ ...makeTask("FN-001"), column: "archived" }]);
|
||||
render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="done"
|
||||
tasks={[{ ...makeTask("FN-001"), column: "done" }]}
|
||||
onArchiveAllDone={onArchiveAllDone}
|
||||
doneSortMode="completion-date-desc"
|
||||
onDoneSortModeChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Done column actions" }));
|
||||
await user.click(screen.getByRole("menuitem", { name: /Archive all done tasks/i }));
|
||||
|
||||
await waitFor(() => expect(onArchiveAllDone).toHaveBeenCalledTimes(1));
|
||||
expect(mockConfirm).toHaveBeenCalledWith({
|
||||
title: "Archive All Done",
|
||||
message: "Archive all 1 done tasks?",
|
||||
danger: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps sort choices available while blocking archive for an empty Done column", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onArchiveAllDone = vi.fn().mockResolvedValue([]);
|
||||
render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="done"
|
||||
tasks={[]}
|
||||
onArchiveAllDone={onArchiveAllDone}
|
||||
doneSortMode="completion-date-desc"
|
||||
onDoneSortModeChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("combobox", { name: "Sort Done tasks" })).toBeInTheDocument();
|
||||
expect(screen.getByText("0")).toHaveClass("column-count");
|
||||
await user.click(screen.getByRole("button", { name: "Done column actions" }));
|
||||
|
||||
expect(screen.getByRole("menuitemradio", { name: /Completion date \(newest first\)/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole("menuitemradio", { name: /Task ID \(newest first\)/ })).toBeInTheDocument();
|
||||
expect(screen.getByRole("menuitem", { name: /Archive all done tasks/i })).toBeDisabled();
|
||||
expect(onArchiveAllDone).not.toHaveBeenCalled();
|
||||
expect(mockConfirm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("coexists with Archive All Done without disabling sort selection", () => {
|
||||
render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="done"
|
||||
tasks={[{ ...makeTask("FN-001"), column: "done" }]}
|
||||
onArchiveAllDone={vi.fn().mockResolvedValue([])}
|
||||
doneSortMode="completion-date-desc"
|
||||
onDoneSortModeChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("combobox", { name: "Sort Done tasks" })).toBeEnabled();
|
||||
expect(screen.getByRole("button", { name: "Archive all done tasks" })).toBeEnabled();
|
||||
const header = screen.getByRole("heading", { name: "Done" }).closest(".column-header") as HTMLElement;
|
||||
expect(header.querySelector(".done-sort-control")).not.toBeNull();
|
||||
expect(header.querySelector(".btn-icon")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("keeps Done header actions in the wrapping-friendly header structure", () => {
|
||||
render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="done"
|
||||
tasks={[{ ...makeTask("FN-001"), column: "done" }]}
|
||||
onArchiveAllDone={vi.fn().mockResolvedValue([])}
|
||||
doneSortMode="completion-date-desc"
|
||||
onDoneSortModeChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const header = screen.getByRole("heading", { name: "Done" }).closest(".column-header") as HTMLElement;
|
||||
expect(header).toBeInTheDocument();
|
||||
expect(header.querySelector(".column-count")?.textContent).toBe("1");
|
||||
expect(screen.getByRole("combobox", { name: "Sort Done tasks" }).closest(".done-sort-control")?.parentElement).toBe(header);
|
||||
expect(screen.getByRole("button", { name: "Archive all done tasks" }).parentElement).toBe(header);
|
||||
});
|
||||
|
||||
it("hides the sort control and leaves no wrapper on non-Done columns", () => {
|
||||
it("hides Done menu items and leaves no standalone wrappers on non-Done columns", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="todo"
|
||||
tasks={[{ ...makeTask("FN-001"), column: "todo" }]}
|
||||
onArchiveAllDone={vi.fn().mockResolvedValue([])}
|
||||
doneSortMode="completion-date-desc"
|
||||
onDoneSortModeChange={vi.fn()}
|
||||
/>,
|
||||
@@ -793,9 +808,15 @@ describe("Column Done sort control", () => {
|
||||
expect(screen.queryByRole("combobox", { name: "Sort Done tasks" })).toBeNull();
|
||||
expect(container.querySelector(".done-sort-control")).toBeNull();
|
||||
expect(container.querySelector("[aria-label='Sort Done tasks']")).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Todo column actions" }));
|
||||
|
||||
expect(screen.queryByRole("menuitemradio", { name: /Completion date \(newest first\)/ })).toBeNull();
|
||||
expect(screen.queryByRole("menuitemradio", { name: /Task ID \(newest first\)/ })).toBeNull();
|
||||
expect(screen.queryByRole("menuitem", { name: /Archive all done tasks/i })).toBeNull();
|
||||
});
|
||||
|
||||
it("hides the sort control on Done when sort props are absent", () => {
|
||||
it("does not render a Done actions menu when Done sort and archive props are absent", () => {
|
||||
const { container } = render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
@@ -804,6 +825,7 @@ describe("Column Done sort control", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole("button", { name: "Done column actions" })).toBeNull();
|
||||
expect(screen.queryByRole("combobox", { name: "Sort Done tasks" })).toBeNull();
|
||||
expect(container.querySelector(".done-sort-control")).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1174,40 +1174,6 @@ body {
|
||||
animation: count-flash-bg 1400ms ease-out;
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:DoneColumnSorting 2026-06-29-18:11:
|
||||
The Done sort selector lives in the column header beside Archive All; compact inline-flex styling keeps desktop headers tight while flex wrapping lets narrow/mobile headers move the selector without leaving empty action shells on non-Done columns.
|
||||
*/
|
||||
.done-sort-control {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.done-sort-control__label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.done-sort-control__select {
|
||||
max-width: 150px;
|
||||
min-height: 28px;
|
||||
min-width: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
font-size: 0.75rem;
|
||||
padding: calc(var(--space-xs) / 2) var(--space-sm);
|
||||
}
|
||||
|
||||
.done-sort-control__select:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--focus-ring-strong);
|
||||
}
|
||||
|
||||
.column-desc {
|
||||
font-size: 0.6875rem;
|
||||
color: var(--text-dim);
|
||||
@@ -3899,6 +3865,15 @@ Toast text must contrast its status background across every dashboard theme and
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.column-menu-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: calc(var(--space-xs) / 2);
|
||||
padding-bottom: var(--space-xs);
|
||||
margin-bottom: var(--space-xs);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.column-menu-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -3930,6 +3905,24 @@ Toast text must contrast its status background across every dashboard theme and
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.column-menu-item-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.column-menu-item-check {
|
||||
width: 1em;
|
||||
color: var(--color-success);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.column-menu-popover {
|
||||
max-width: calc(100vw - (var(--space-lg) * 2));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:Navigation 2026-06-22-00:00:
|
||||
Board card clicks open task detail as a full main-content view that replaces the board ("Full main panel" design). This layout fills the main content area with a scrollable embedded TaskDetailContent body. Theme tokens only; mobile shell renders it unchanged because the panel just fills its host.
|
||||
|
||||
Reference in New Issue
Block a user