feat(FN-3241): restore list view detail callback type compatibility
Fixes a type compatibility issue with the list view detail callback in the dashboard App component, restoring proper typing for the callback. Fusion-Task-Id: FN-3241
This commit is contained in:
@@ -884,7 +884,7 @@ function AppInner() {
|
||||
onMergeTask={mergeTask}
|
||||
onResetTask={resetTask}
|
||||
onDuplicateTask={duplicateTask}
|
||||
onOpenDetail={modalManager.openDetailTask}
|
||||
onOpenDetail={(task, options) => modalManager.openDetailTask(task, undefined, options)}
|
||||
addToast={addToast}
|
||||
globalPaused={globalPaused}
|
||||
onNewTask={modalManager.openNewTask}
|
||||
|
||||
@@ -164,6 +164,7 @@ export function AppModals({
|
||||
tasks={tasks}
|
||||
onClose={deepLink.handleDetailClose}
|
||||
onOpenDetail={modalManager.openDetailTask}
|
||||
mobileHeaderMode={modalManager.detailTaskOrigin === "list-mobile" ? "back" : "close"}
|
||||
onMoveTask={taskOperations.moveTask}
|
||||
onDeleteTask={taskOperations.deleteTask}
|
||||
onMergeTask={taskOperations.mergeTask}
|
||||
|
||||
@@ -164,7 +164,7 @@ interface ListViewProps {
|
||||
onMergeTask: (id: string) => Promise<MergeResult>;
|
||||
onResetTask?: (id: string) => Promise<Task>;
|
||||
onDuplicateTask?: (id: string) => Promise<Task>;
|
||||
onOpenDetail: (task: Task | TaskDetail) => void;
|
||||
onOpenDetail: (task: Task | TaskDetail, options?: { origin?: "list-mobile" }) => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
globalPaused?: boolean;
|
||||
onNewTask?: () => void;
|
||||
@@ -755,7 +755,7 @@ export function ListView({
|
||||
const handleRowClick = useCallback(
|
||||
(task: Task) => {
|
||||
if (isMobile) {
|
||||
onOpenDetail(task);
|
||||
onOpenDetail(task, { origin: "list-mobile" });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1208,6 +1208,21 @@
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.task-detail-mobile-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
min-height: calc(var(--space-2xl) + var(--space-xs));
|
||||
padding: 0 var(--space-sm);
|
||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.625);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.task-detail-mobile-back svg {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.modal-edit-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1517,4 +1532,9 @@
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.task-detail-mobile-back {
|
||||
min-height: calc(var(--space-2xl) + var(--space-xs));
|
||||
min-width: calc(var(--space-2xl) + var(--space-xs));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "./TaskDetailModal.css";
|
||||
import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch } from "lucide-react";
|
||||
import { Pencil, Bot, X, ChevronDown, ChevronRight, GitBranch, ArrowLeft } from "lucide-react";
|
||||
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
@@ -167,6 +167,8 @@ export interface TaskDetailModalProps {
|
||||
prAuthAvailable?: boolean;
|
||||
/** Open the modal with this tab active instead of "definition" */
|
||||
initialTab?: TabId;
|
||||
/** Mobile-only header affordance mode. */
|
||||
mobileHeaderMode?: "close" | "back";
|
||||
}
|
||||
|
||||
export type TaskDetailContentProps = Omit<TaskDetailModalProps, "onClose"> & {
|
||||
@@ -332,6 +334,7 @@ export function TaskDetailContent({
|
||||
addToast,
|
||||
prAuthAvailable,
|
||||
initialTab = "definition",
|
||||
mobileHeaderMode = "close",
|
||||
embedded = false,
|
||||
onRequestClose,
|
||||
}: TaskDetailContentProps) {
|
||||
@@ -1578,8 +1581,19 @@ export function TaskDetailContent({
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
)}
|
||||
{!embedded && (
|
||||
<button className="modal-close" onClick={requestClose} aria-label="Close">
|
||||
{!embedded && mobileHeaderMode === "back" && (
|
||||
<button
|
||||
className="modal-close task-detail-mobile-back"
|
||||
onClick={requestClose}
|
||||
aria-label="Back to task list"
|
||||
type="button"
|
||||
>
|
||||
<ArrowLeft aria-hidden="true" />
|
||||
<span>Back</span>
|
||||
</button>
|
||||
)}
|
||||
{!embedded && mobileHeaderMode !== "back" && (
|
||||
<button className="modal-close" onClick={requestClose} aria-label="Close" type="button">
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -279,7 +279,7 @@ describe("ListView", () => {
|
||||
const card = document.querySelector('.list-card[data-id="FN-001"]');
|
||||
fireEvent.click(card!);
|
||||
|
||||
expect(mockOnOpenDetail).toHaveBeenCalledWith(tasks[0]);
|
||||
expect(mockOnOpenDetail).toHaveBeenCalledWith(tasks[0], { origin: "list-mobile" });
|
||||
expect(mockOnOpenDetail).toHaveBeenCalledTimes(1);
|
||||
expect(fetchTaskDetail).not.toHaveBeenCalled();
|
||||
viewportSpy.mockRestore();
|
||||
@@ -2748,7 +2748,7 @@ describe("ListView - Bulk Selection", () => {
|
||||
fireEvent.click(container.querySelector('.list-card[data-id="FN-001"]') as HTMLElement);
|
||||
|
||||
// Should call onOpenDetail synchronously with the Task object (no fetch)
|
||||
expect(mockOnOpenDetail).toHaveBeenCalledWith(task);
|
||||
expect(mockOnOpenDetail).toHaveBeenCalledWith(task, { origin: "list-mobile" });
|
||||
expect(mockOnOpenDetail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ vi.mock("lucide-react", () => ({
|
||||
ChevronUp: () => null,
|
||||
ChevronDown: () => null,
|
||||
ChevronRight: (props: any) => <svg data-testid="chevron-right-icon" {...props} />,
|
||||
ArrowLeft: () => null,
|
||||
X: () => null,
|
||||
Maximize2: () => null,
|
||||
Minimize2: () => null,
|
||||
@@ -261,7 +262,7 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("renders modal wrapper structure and close control", () => {
|
||||
it("renders modal wrapper structure and default close control", () => {
|
||||
const { container } = render(
|
||||
<TaskDetailModal
|
||||
task={makeTask()}
|
||||
@@ -277,6 +278,25 @@ describe("TaskDetailModal", () => {
|
||||
expect(container.querySelector(".modal-overlay.open")).toBeTruthy();
|
||||
expect(container.querySelector(".modal.modal-lg.task-detail-modal")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Back to task list" })).toBeNull();
|
||||
});
|
||||
|
||||
it("renders mobile back control variant when requested", () => {
|
||||
render(
|
||||
<TaskDetailModal
|
||||
task={makeTask()}
|
||||
onClose={noop}
|
||||
onMoveTask={noopMove}
|
||||
onDeleteTask={noopDelete}
|
||||
onMergeTask={noopMerge}
|
||||
onOpenDetail={noopOpenDetail}
|
||||
addToast={noop}
|
||||
mobileHeaderMode="back"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Back to task list" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Close" })).toBeNull();
|
||||
});
|
||||
|
||||
it("omits close control in embedded mode while rendering shared content", () => {
|
||||
|
||||
@@ -198,6 +198,18 @@ describe("core modals mobile css coverage", () => {
|
||||
expect(menuBlock).toContain("max-width: calc(100vw - 28px)");
|
||||
});
|
||||
|
||||
it("TaskDetailModal: mobile back control keeps token-based touch-target sizing", () => {
|
||||
const css = loadAllAppCss();
|
||||
const mobileBlock = getMainMobileBlock(css);
|
||||
|
||||
const backControlMatch = mobileBlock.match(
|
||||
/\.task-detail-mobile-back\s*\{[^}]+\}/,
|
||||
);
|
||||
expect(backControlMatch).not.toBeNull();
|
||||
expect(backControlMatch![0]).toContain("min-height: calc(var(--space-2xl) + var(--space-xs))");
|
||||
expect(backControlMatch![0]).toContain("min-width: calc(var(--space-2xl) + var(--space-xs))");
|
||||
});
|
||||
|
||||
it("TaskDetailModal: footer dropdown menus anchor toward available horizontal space", () => {
|
||||
const css = loadAllAppCss();
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ export type DetailTaskTab =
|
||||
| "model"
|
||||
| "workflow";
|
||||
|
||||
export type DetailTaskOrigin = "list-mobile";
|
||||
|
||||
interface UseModalManagerOptions {
|
||||
projectId?: string;
|
||||
planningSessions: Array<{ id: string }>;
|
||||
@@ -31,6 +33,7 @@ export interface ModalManager {
|
||||
// Can be Task (optimistic open) or TaskDetail (full data with prompt)
|
||||
detailTask: (Task | TaskDetail) | null;
|
||||
detailTaskInitialTab: DetailTaskTab;
|
||||
detailTaskOrigin: DetailTaskOrigin | null;
|
||||
settingsOpen: boolean;
|
||||
settingsInitialSection: SectionId | undefined;
|
||||
schedulesOpen: boolean;
|
||||
@@ -66,7 +69,11 @@ export interface ModalManager {
|
||||
openSubtaskWithSession: (sessionId: string) => void;
|
||||
closeSubtask: () => void;
|
||||
|
||||
openDetailTask: (task: Task | TaskDetail, initialTab?: DetailTaskTab) => void;
|
||||
openDetailTask: (
|
||||
task: Task | TaskDetail,
|
||||
initialTab?: DetailTaskTab,
|
||||
options?: { origin?: DetailTaskOrigin },
|
||||
) => void;
|
||||
openDetailWithChangesTab: (task: Task | TaskDetail) => void;
|
||||
updateDetailTask: (updated: Partial<TaskDetail>) => void;
|
||||
closeDetailTask: () => void;
|
||||
@@ -141,6 +148,7 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
|
||||
// Can be Task (optimistic open) or TaskDetail (full data with prompt)
|
||||
const [detailTask, setDetailTask] = useState<(Task | TaskDetail) | null>(null);
|
||||
const [detailTaskInitialTab, setDetailTaskInitialTab] = useState<DetailTaskTab>("definition");
|
||||
const [detailTaskOrigin, setDetailTaskOrigin] = useState<DetailTaskOrigin | null>(null);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [settingsInitialSection, setSettingsInitialSection] = useState<SectionId | undefined>(undefined);
|
||||
const [schedulesOpen, setSchedulesOpen] = useState(false);
|
||||
@@ -221,19 +229,26 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
|
||||
setSubtaskResumeSessionId(undefined);
|
||||
}, []);
|
||||
|
||||
const openDetailTask = useCallback((task: Task | TaskDetail, initialTab: DetailTaskTab = "definition") => {
|
||||
const openDetailTask = useCallback((
|
||||
task: Task | TaskDetail,
|
||||
initialTab: DetailTaskTab = "definition",
|
||||
options?: { origin?: DetailTaskOrigin },
|
||||
) => {
|
||||
setDetailTask(task);
|
||||
setDetailTaskInitialTab(initialTab);
|
||||
setDetailTaskOrigin(options?.origin ?? null);
|
||||
}, []);
|
||||
const openDetailWithChangesTab = useCallback((task: Task | TaskDetail) => {
|
||||
setDetailTask(task);
|
||||
setDetailTaskInitialTab("changes");
|
||||
setDetailTaskOrigin(null);
|
||||
}, []);
|
||||
const updateDetailTask = useCallback((updated: Partial<TaskDetail>) => {
|
||||
setDetailTask((prev) => (prev ? { ...prev, ...updated } : prev));
|
||||
}, []);
|
||||
const closeDetailTask = useCallback(() => {
|
||||
setDetailTask(null);
|
||||
setDetailTaskOrigin(null);
|
||||
}, []);
|
||||
|
||||
const openSettings = useCallback((section?: SectionId) => {
|
||||
@@ -335,6 +350,7 @@ export function useModalManager(options: UseModalManagerOptions): ModalManager {
|
||||
subtaskResumeSessionId,
|
||||
detailTask,
|
||||
detailTaskInitialTab,
|
||||
detailTaskOrigin,
|
||||
settingsOpen,
|
||||
settingsInitialSection,
|
||||
schedulesOpen,
|
||||
|
||||
Reference in New Issue
Block a user