feat(FN-770): add quick-add favorite models to task creation surfaces
- Wire favorite model state into QuickEntryBox with star toggle and dropdown integration - Add favorite model selector to Board and Column quick-add components - Pass favorite models through ListView and App context to entry surfaces - Add regression tests for QuickEntryBox, Board, Column, and ListView favorite behavior - Update dashboard README to document quick-add favorite model support - Remove unused recovery-policy module and dead code from executor/triage/scheduler - Clean up unused types from core and simplify store methods
This commit is contained in:
@@ -514,6 +514,10 @@ function AppInner() {
|
||||
searchQuery={searchQuery}
|
||||
availableModels={availableModels}
|
||||
onOpenFilesForTask={handleOpenChangedFiles}
|
||||
favoriteProviders={favoriteProviders}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ interface BoardProps {
|
||||
*/
|
||||
onSubtaskBreakdown?: (description: string) => void;
|
||||
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string) => void;
|
||||
favoriteProviders?: string[];
|
||||
favoriteModels?: string[];
|
||||
onToggleFavorite?: (provider: string) => void;
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
}
|
||||
|
||||
function sortTasksForColumn(tasks: Task[]): Task[] {
|
||||
@@ -54,7 +58,7 @@ function areTaskArraysEqual(previous: Task[], next: Task[]): boolean {
|
||||
return previous.every((task, index) => task === next[index]);
|
||||
}
|
||||
|
||||
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask }: BoardProps) {
|
||||
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite }: BoardProps) {
|
||||
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
|
||||
const { fetchBatch } = useBatchBadgeFetch(projectId);
|
||||
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -164,6 +168,10 @@ export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onOpenDetai
|
||||
allTasks={filteredTasks}
|
||||
availableModels={availableModels}
|
||||
onOpenFilesForTask={onOpenFilesForTask}
|
||||
favoriteProviders={favoriteProviders}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
{...(col === "triage" ? { onQuickCreate, onNewTask, onPlanningMode, onSubtaskBreakdown } : {})}
|
||||
{...(col === "in-review" ? { autoMerge, onToggleAutoMerge } : {})}
|
||||
{...(col === "done" ? { onArchiveAllDone } : {})}
|
||||
|
||||
@@ -47,9 +47,13 @@ interface ColumnProps {
|
||||
*/
|
||||
onSubtaskBreakdown?: (description: string) => void;
|
||||
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string) => void;
|
||||
favoriteProviders?: string[];
|
||||
favoriteModels?: string[];
|
||||
onToggleFavorite?: (provider: string) => void;
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
}
|
||||
|
||||
function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask }: ColumnProps) {
|
||||
function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite }: ColumnProps) {
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [visibleTaskCount, setVisibleTaskCount] = useState(VISIBLE_TASKS_INITIAL);
|
||||
const countFlashing = useFlashOnIncrease(tasks.length);
|
||||
@@ -196,6 +200,10 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask,
|
||||
onPlanningMode={onPlanningMode}
|
||||
onSubtaskBreakdown={onSubtaskBreakdown}
|
||||
autoExpand={true}
|
||||
favoriteProviders={favoriteProviders}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
/>
|
||||
)}
|
||||
{column === "in-progress" ? (
|
||||
|
||||
@@ -748,6 +748,10 @@ export function ListView({
|
||||
onPlanningMode={onPlanningMode}
|
||||
onSubtaskBreakdown={onSubtaskBreakdown}
|
||||
autoExpand={false}
|
||||
favoriteProviders={favoriteProviders}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
/>
|
||||
</div>
|
||||
{filteredCount === 0 ? (
|
||||
|
||||
@@ -29,6 +29,26 @@ interface QuickEntryBoxProps {
|
||||
* Defaults to true for backward compatibility.
|
||||
*/
|
||||
autoExpand?: boolean;
|
||||
/**
|
||||
* Favorited provider IDs from shared app-level state.
|
||||
* When provided (alongside availableModels), the component uses these
|
||||
* instead of its own internal favorite state.
|
||||
*/
|
||||
favoriteProviders?: string[];
|
||||
/**
|
||||
* Favorited model IDs from shared app-level state.
|
||||
* When provided (alongside availableModels), the component uses these
|
||||
* instead of its own internal favorite state.
|
||||
*/
|
||||
favoriteModels?: string[];
|
||||
/**
|
||||
* Toggle favorite provider callback from shared app-level state.
|
||||
*/
|
||||
onToggleFavorite?: (provider: string) => void;
|
||||
/**
|
||||
* Toggle favorite model callback from shared app-level state.
|
||||
*/
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
}
|
||||
|
||||
function getModelSelectionValue(provider?: string, modelId?: string): string {
|
||||
@@ -51,7 +71,7 @@ function parseModelSelection(value: string): { provider?: string; modelId?: stri
|
||||
};
|
||||
}
|
||||
|
||||
export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, onPlanningMode, onSubtaskBreakdown, autoExpand = true }: QuickEntryBoxProps) {
|
||||
export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels, onPlanningMode, onSubtaskBreakdown, autoExpand = true, favoriteProviders: parentFavoriteProviders, favoriteModels: parentFavoriteModels, onToggleFavorite: parentToggleFavorite, onToggleModelFavorite: parentToggleModelFavorite }: QuickEntryBoxProps) {
|
||||
const [description, setDescription] = useState(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
return localStorage.getItem(STORAGE_KEY) || "";
|
||||
@@ -87,6 +107,10 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
const [isRefining, setIsRefining] = useState(false);
|
||||
const refineMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Use parent-provided favorites when available, otherwise internal state
|
||||
const effectiveFavoriteProviders = parentFavoriteProviders ?? favoriteProviders;
|
||||
const effectiveFavoriteModels = parentFavoriteModels ?? favoriteModels;
|
||||
|
||||
// If onCreate is not provided, the component is disabled
|
||||
const isDisabled = !onCreate;
|
||||
|
||||
@@ -106,8 +130,13 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
.then((response) => {
|
||||
if (!cancelled) {
|
||||
setLoadedModels(response.models);
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
// Only set internal favorites when parent doesn't manage them
|
||||
if (!parentFavoriteProviders) {
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
}
|
||||
if (!parentFavoriteModels) {
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
@@ -374,6 +403,12 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
}, []);
|
||||
|
||||
const handleToggleFavorite = useCallback(async (provider: string) => {
|
||||
// Delegate to parent callback when available
|
||||
if (parentToggleFavorite) {
|
||||
parentToggleFavorite(provider);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFavorites = favoriteProviders;
|
||||
const isFavorite = currentFavorites.includes(provider);
|
||||
const newFavorites = isFavorite
|
||||
@@ -388,9 +423,15 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
// Revert on error
|
||||
setFavoriteProviders(currentFavorites);
|
||||
}
|
||||
}, [favoriteProviders, favoriteModels]);
|
||||
}, [favoriteProviders, favoriteModels, parentToggleFavorite]);
|
||||
|
||||
const handleToggleModelFavorite = useCallback(async (modelId: string) => {
|
||||
// Delegate to parent callback when available
|
||||
if (parentToggleModelFavorite) {
|
||||
parentToggleModelFavorite(modelId);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentFavorites = favoriteModels;
|
||||
const isFavorite = currentFavorites.includes(modelId);
|
||||
const newFavorites = isFavorite
|
||||
@@ -405,7 +446,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
// Revert on error
|
||||
setFavoriteModels(currentFavorites);
|
||||
}
|
||||
}, [favoriteModels, favoriteProviders]);
|
||||
}, [favoriteModels, favoriteProviders, parentToggleModelFavorite]);
|
||||
|
||||
const handlePlanClick = useCallback(() => {
|
||||
const trimmed = description.trim();
|
||||
@@ -473,14 +514,19 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
try {
|
||||
const response = await fetchModels();
|
||||
setLoadedModels(response.models);
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
// Only set internal favorites when parent doesn't manage them
|
||||
if (!parentFavoriteProviders) {
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
}
|
||||
if (!parentFavoriteModels) {
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setModelsError(err?.message || "Failed to load models");
|
||||
} finally {
|
||||
setModelsLoading(false);
|
||||
}
|
||||
}, [availableModels]);
|
||||
}, [availableModels, parentFavoriteProviders, parentFavoriteModels]);
|
||||
|
||||
// Show expanded controls based on disclosure state (user preference), not textarea focus
|
||||
const showExpandedControls = isDisclosureExpanded;
|
||||
@@ -713,9 +759,9 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
|
||||
modelsLoading={modelsLoading}
|
||||
modelsError={modelsError}
|
||||
onRetry={loadModels}
|
||||
favoriteProviders={favoriteProviders}
|
||||
favoriteProviders={effectiveFavoriteProviders}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
favoriteModels={favoriteModels}
|
||||
favoriteModels={effectiveFavoriteModels}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
/>,
|
||||
document.body,
|
||||
|
||||
@@ -32,7 +32,7 @@ vi.mock("../../api", async (importOriginal) => {
|
||||
),
|
||||
loginProvider: vi.fn(() => Promise.resolve({ url: "https://auth.example.com/login" })),
|
||||
logoutProvider: vi.fn(() => Promise.resolve({ success: true })),
|
||||
fetchModels: vi.fn(() => Promise.resolve({ models: [], favoriteProviders: [] })),
|
||||
fetchModels: vi.fn(() => Promise.resolve({ models: [], favoriteProviders: [], favoriteModels: [] })),
|
||||
fetchGitRemotes: vi.fn(() => Promise.resolve([])),
|
||||
fetchAgents: vi.fn(() => Promise.resolve([])),
|
||||
fetchTaskDetail: vi.fn((id: string) => Promise.resolve({ id, title: `Task ${id}` })),
|
||||
|
||||
@@ -10,10 +10,10 @@ const columnRenderCounts: Record<string, number> = {};
|
||||
|
||||
// Mock child components so we only test Board's own rendering
|
||||
vi.mock("../Column", () => ({
|
||||
Column: React.memo(({ column, tasks, onToggleCollapse, availableModels }: { column: string; tasks: Task[]; onToggleCollapse?: () => void; availableModels?: unknown }) => {
|
||||
Column: React.memo(({ column, tasks, onToggleCollapse, availableModels, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite }: { column: string; tasks: Task[]; onToggleCollapse?: () => void; availableModels?: unknown; favoriteProviders?: string[]; favoriteModels?: string[]; onToggleFavorite?: (provider: string) => void; onToggleModelFavorite?: (modelId: string) => void }) => {
|
||||
columnRenderCounts[column] = (columnRenderCounts[column] ?? 0) + 1;
|
||||
return (
|
||||
<div data-testid={`column-${column}`} data-tasks={JSON.stringify(tasks)}>
|
||||
<div data-testid={`column-${column}`} data-tasks={JSON.stringify(tasks)} data-favorite-providers={JSON.stringify(favoriteProviders ?? [])} data-favorite-models={JSON.stringify(favoriteModels ?? [])} data-has-toggle-favorite={onToggleFavorite ? "yes" : "no"} data-has-toggle-model-favorite={onToggleModelFavorite ? "yes" : "no"}>
|
||||
{onToggleCollapse && <button onClick={onToggleCollapse}>toggle-{column}</button>}
|
||||
</div>
|
||||
);
|
||||
@@ -336,4 +336,41 @@ describe("Board", () => {
|
||||
const badge = document.querySelector(".board-project-context");
|
||||
expect(badge).toBeNull();
|
||||
});
|
||||
|
||||
describe("favorite model prop forwarding (FN-770)", () => {
|
||||
it("forwards favoriteProviders and favoriteModels to all columns", () => {
|
||||
const favoriteProviders = ["anthropic"];
|
||||
const favoriteModels = ["claude-sonnet-4-5"];
|
||||
const onToggleFavorite = vi.fn();
|
||||
const onToggleModelFavorite = vi.fn();
|
||||
|
||||
renderBoard({
|
||||
favoriteProviders,
|
||||
favoriteModels,
|
||||
onToggleFavorite,
|
||||
onToggleModelFavorite,
|
||||
});
|
||||
|
||||
// Every column should receive the favorite props
|
||||
for (const col of COLUMNS) {
|
||||
const columnEl = screen.getByTestId(`column-${col}`);
|
||||
expect(columnEl.getAttribute("data-favorite-providers")).toBe(JSON.stringify(favoriteProviders));
|
||||
expect(columnEl.getAttribute("data-favorite-models")).toBe(JSON.stringify(favoriteModels));
|
||||
expect(columnEl.getAttribute("data-has-toggle-favorite")).toBe("yes");
|
||||
expect(columnEl.getAttribute("data-has-toggle-model-favorite")).toBe("yes");
|
||||
}
|
||||
});
|
||||
|
||||
it("passes empty arrays for favorites when not provided", () => {
|
||||
renderBoard();
|
||||
|
||||
for (const col of COLUMNS) {
|
||||
const columnEl = screen.getByTestId(`column-${col}`);
|
||||
expect(columnEl.getAttribute("data-favorite-providers")).toBe("[]");
|
||||
expect(columnEl.getAttribute("data-favorite-models")).toBe("[]");
|
||||
expect(columnEl.getAttribute("data-has-toggle-favorite")).toBe("no");
|
||||
expect(columnEl.getAttribute("data-has-toggle-model-favorite")).toBe("no");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,15 @@ vi.mock("../WorktreeGroup", () => ({
|
||||
WorktreeGroup: () => <div />,
|
||||
}));
|
||||
vi.mock("../QuickEntryBox", () => ({
|
||||
QuickEntryBox: () => <div data-testid="quick-entry-box" />,
|
||||
QuickEntryBox: ({ favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite }: { favoriteProviders?: string[]; favoriteModels?: string[]; onToggleFavorite?: (provider: string) => void; onToggleModelFavorite?: (modelId: string) => void }) => (
|
||||
<div
|
||||
data-testid="quick-entry-box"
|
||||
data-favorite-providers={JSON.stringify(favoriteProviders ?? [])}
|
||||
data-favorite-models={JSON.stringify(favoriteModels ?? [])}
|
||||
data-has-toggle-favorite={onToggleFavorite ? "yes" : "no"}
|
||||
data-has-toggle-model-favorite={onToggleModelFavorite ? "yes" : "no"}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
vi.mock("lucide-react", () => ({
|
||||
Link: () => null,
|
||||
@@ -261,4 +269,47 @@ describe("Column same-column drop", () => {
|
||||
|
||||
expect(onMoveTask).toHaveBeenCalledWith("FN-001", "in-review");
|
||||
});
|
||||
|
||||
describe("favorite model prop forwarding (FN-770)", () => {
|
||||
it("forwards favoriteProviders, favoriteModels, and toggle callbacks to QuickEntryBox", () => {
|
||||
const onToggleFavorite = vi.fn();
|
||||
const onToggleModelFavorite = vi.fn();
|
||||
|
||||
render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="triage"
|
||||
tasks={[]}
|
||||
onQuickCreate={vi.fn().mockResolvedValue({})}
|
||||
favoriteProviders={["anthropic"]}
|
||||
favoriteModels={["claude-sonnet-4-5"]}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
/>,
|
||||
);
|
||||
|
||||
const quickEntry = screen.getByTestId("quick-entry-box");
|
||||
expect(quickEntry.getAttribute("data-favorite-providers")).toBe(JSON.stringify(["anthropic"]));
|
||||
expect(quickEntry.getAttribute("data-favorite-models")).toBe(JSON.stringify(["claude-sonnet-4-5"]));
|
||||
expect(quickEntry.getAttribute("data-has-toggle-favorite")).toBe("yes");
|
||||
expect(quickEntry.getAttribute("data-has-toggle-model-favorite")).toBe("yes");
|
||||
});
|
||||
|
||||
it("passes empty favorites when props not provided", () => {
|
||||
render(
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="triage"
|
||||
tasks={[]}
|
||||
onQuickCreate={vi.fn().mockResolvedValue({})}
|
||||
/>,
|
||||
);
|
||||
|
||||
const quickEntry = screen.getByTestId("quick-entry-box");
|
||||
expect(quickEntry.getAttribute("data-favorite-providers")).toBe("[]");
|
||||
expect(quickEntry.getAttribute("data-favorite-models")).toBe("[]");
|
||||
expect(quickEntry.getAttribute("data-has-toggle-favorite")).toBe("no");
|
||||
expect(quickEntry.getAttribute("data-has-toggle-model-favorite")).toBe("no");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1973,4 +1973,42 @@ describe("ListView - Bulk Selection", () => {
|
||||
// The dropdown would need to be interacted with to enable the button
|
||||
// This test verifies the initial disabled state and button presence
|
||||
});
|
||||
|
||||
it("forwards favoriteProviders and favoriteModels to QuickEntryBox model modal (FN-770)", async () => {
|
||||
const availableModels = [
|
||||
{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: true, contextWindow: 200000 },
|
||||
];
|
||||
const tasks = [createMockTask({ id: "FN-001" })];
|
||||
const onToggleFavorite = vi.fn();
|
||||
const onToggleModelFavorite = vi.fn();
|
||||
|
||||
render(
|
||||
<ListView
|
||||
tasks={tasks}
|
||||
onMoveTask={vi.fn()}
|
||||
onOpenDetail={vi.fn()}
|
||||
addToast={mockAddToast}
|
||||
onQuickCreate={vi.fn().mockResolvedValue(undefined)}
|
||||
availableModels={availableModels}
|
||||
favoriteProviders={["anthropic"]}
|
||||
favoriteModels={["claude-sonnet-4-5"]}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
onToggleModelFavorite={onToggleModelFavorite}
|
||||
/>
|
||||
);
|
||||
|
||||
// Expand the QuickEntryBox and open the model modal
|
||||
const toggleButton = screen.getByTestId("quick-entry-toggle");
|
||||
fireEvent.click(toggleButton);
|
||||
|
||||
const modelButton = await screen.findByTestId("quick-entry-models-button");
|
||||
fireEvent.click(modelButton);
|
||||
|
||||
// The real ModelSelectionModal should render via portal with data-testid
|
||||
const modal = await screen.findByTestId("model-selection-modal");
|
||||
expect(modal).toBeDefined();
|
||||
|
||||
// Verify the modal has content (models are loaded)
|
||||
expect(modal.textContent).toContain("Select Models");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +95,8 @@ vi.mock("../ModelSelectionModal", () => ({
|
||||
modelsLoading,
|
||||
modelsError,
|
||||
onRetry,
|
||||
favoriteProviders,
|
||||
onToggleFavorite,
|
||||
favoriteModels,
|
||||
onToggleModelFavorite,
|
||||
}: {
|
||||
@@ -108,6 +110,8 @@ vi.mock("../ModelSelectionModal", () => ({
|
||||
modelsLoading: boolean;
|
||||
modelsError: string | null;
|
||||
onRetry: () => void;
|
||||
favoriteProviders?: string[];
|
||||
onToggleFavorite?: (provider: string) => void;
|
||||
favoriteModels?: string[];
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
}) => {
|
||||
@@ -119,6 +123,8 @@ vi.mock("../ModelSelectionModal", () => ({
|
||||
<div data-testid="modal-props-validator-value">{validatorValue}</div>
|
||||
<div data-testid="modal-props-loading">{modelsLoading ? "loading" : "not-loading"}</div>
|
||||
<div data-testid="modal-props-error">{modelsError || "no-error"}</div>
|
||||
<div data-testid="modal-props-favorite-providers">{JSON.stringify(favoriteProviders ?? [])}</div>
|
||||
<div data-testid="modal-props-has-toggle-favorite">{onToggleFavorite ? "yes" : "no"}</div>
|
||||
<div data-testid="modal-props-favorite-models">{JSON.stringify(favoriteModels ?? [])}</div>
|
||||
<div data-testid="modal-props-has-toggle-model-favorite">{onToggleModelFavorite ? "yes" : "no"}</div>
|
||||
<button data-testid="modal-close" onClick={onClose}>
|
||||
@@ -612,6 +618,63 @@ describe("QuickEntryBox", () => {
|
||||
expect(screen.getByTestId("modal-props-has-toggle-model-favorite").textContent).toBe("yes");
|
||||
});
|
||||
|
||||
it("passes favoriteProviders and favoriteModels from parent props to ModelSelectionModal (regression FN-770)", () => {
|
||||
const parentToggleFavorite = vi.fn();
|
||||
const parentToggleModelFavorite = vi.fn();
|
||||
renderQuickEntryBox({
|
||||
favoriteProviders: ["anthropic"],
|
||||
favoriteModels: ["claude-sonnet-4-5"],
|
||||
onToggleFavorite: parentToggleFavorite,
|
||||
onToggleModelFavorite: parentToggleModelFavorite,
|
||||
});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with parent favorites" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
|
||||
expect(screen.getByTestId("modal-props-favorite-providers").textContent).toBe(JSON.stringify(["anthropic"]));
|
||||
expect(screen.getByTestId("modal-props-favorite-models").textContent).toBe(JSON.stringify(["claude-sonnet-4-5"]));
|
||||
expect(screen.getByTestId("modal-props-has-toggle-favorite").textContent).toBe("yes");
|
||||
expect(screen.getByTestId("modal-props-has-toggle-model-favorite").textContent).toBe("yes");
|
||||
});
|
||||
|
||||
it("delegates toggle favorite to parent callback when provided (regression FN-770)", () => {
|
||||
const parentToggleFavorite = vi.fn();
|
||||
const parentToggleModelFavorite = vi.fn();
|
||||
renderQuickEntryBox({
|
||||
favoriteProviders: ["anthropic"],
|
||||
favoriteModels: ["claude-sonnet-4-5"],
|
||||
onToggleFavorite: parentToggleFavorite,
|
||||
onToggleModelFavorite: parentToggleModelFavorite,
|
||||
});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Task with parent favorites" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
|
||||
// The modal has toggle callbacks; simulate using them
|
||||
expect(screen.getByTestId("modal-props-has-toggle-favorite").textContent).toBe("yes");
|
||||
expect(screen.getByTestId("modal-props-has-toggle-model-favorite").textContent).toBe("yes");
|
||||
});
|
||||
|
||||
it("falls back to internal favorites when parent props not provided (standalone mode)", () => {
|
||||
// availableModels is supplied but no favorite props — uses internal empty state
|
||||
renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
const textarea = screen.getByTestId("quick-entry-input");
|
||||
|
||||
fireEvent.change(textarea, { target: { value: "Standalone task" } });
|
||||
fireEvent.click(screen.getByTestId("quick-entry-models-button"));
|
||||
|
||||
// When parent doesn't provide favorites, internal state is used (empty by default)
|
||||
expect(screen.getByTestId("modal-props-favorite-providers").textContent).toBe("[]");
|
||||
expect(screen.getByTestId("modal-props-favorite-models").textContent).toBe("[]");
|
||||
expect(screen.getByTestId("modal-props-has-toggle-favorite").textContent).toBe("yes");
|
||||
expect(screen.getByTestId("modal-props-has-toggle-model-favorite").textContent).toBe("yes");
|
||||
});
|
||||
|
||||
it("selects dependencies and includes them in submit payload", async () => {
|
||||
const { props } = renderQuickEntryBox({});
|
||||
expandQuickEntry();
|
||||
|
||||
Reference in New Issue
Block a user