FN-049: apply Appearance settings immediately in embedded Settings
Mirror Appearance drafts into mounted dashboard surfaces immediately while keeping SettingsModal as the persistence owner. - Add immediate setters for task routing, popup, cost badge, task-detail ordering, and chat layout settings. - Wire embedded and overlay Settings controls through the App shell with regression coverage. - Add the published-package fix changeset. Files changed: .changeset/fn-049-live-dashboard-settings.md | 7 +++ packages/dashboard/app/App.tsx | 17 +++++- packages/dashboard/app/components/AppModals.tsx | 19 ++++++ packages/dashboard/app/components/SettingsModal.tsx | 31 ++++++++++ packages/dashboard/app/components/__tests__/SettingsModal.general.test.tsx | 29 ++++++++++ packages/dashboard/app/components/dashboard/MainContent.tsx | 22 +++++++ packages/dashboard/app/components/dashboard/__tests__/MainContent.graph-popout.test.tsx | 67 +++++++++++++++++++++ packages/dashboard/app/components/dashboard/types.ts | 10 +++ packages/dashboard/app/components/settings/sections/AppearanceSection.tsx | 52 +++++++++++++---- packages/dashboard/app/components/settings/sections/__tests__/AppearanceSection.test.tsx | 41 ++++++++++++- packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts | 44 ++++++++++++++ packages/dashboard/app/hooks/useAppSettings.ts | 34 ++++++++++- packages/dashboard/app/task-modal-touch-resize-e2e-fixture.tsx | 2 +- 13 files changed, 359 insertions(+), 16 deletions(-) Fusion-Task-Id: FN-049 Fusion-Task-Lineage: 4140b47f-2f70-4c8b-ab47-2fc28672a2bb Co-authored-by: Fusion <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-049-live-dashboard-settings.md
Normal file
7
.changeset/fn-049-live-dashboard-settings.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Apply mounted Appearance settings immediately from either Settings view.
|
||||||
|
category: fix
|
||||||
|
dev: Mirrors chat layout, task routing, popup, cost badge, and task-detail ordering drafts into the App shell while SettingsModal remains the sole persistence writer.
|
||||||
@@ -878,6 +878,11 @@ function AppInner() {
|
|||||||
goalsEnabled,
|
goalsEnabled,
|
||||||
setQuickChatButtonModeImmediate,
|
setQuickChatButtonModeImmediate,
|
||||||
setChatMessageLayoutImmediate,
|
setChatMessageLayoutImmediate,
|
||||||
|
setOpenTasksInRightSidebarImmediate,
|
||||||
|
setOpenMobileTasksInPopupImmediate,
|
||||||
|
setTaskPopupsBoardListOnlyImmediate,
|
||||||
|
setShowCostBadgeOnCardsImmediate,
|
||||||
|
setTaskDetailChatFirstImmediate,
|
||||||
setMobileNavPrimaryItemsImmediate,
|
setMobileNavPrimaryItemsImmediate,
|
||||||
toggleAutoMerge,
|
toggleAutoMerge,
|
||||||
togglePlanAutoApprove,
|
togglePlanAutoApprove,
|
||||||
@@ -1666,6 +1671,12 @@ function AppInner() {
|
|||||||
setShadcnCustomColors,
|
setShadcnCustomColors,
|
||||||
resolvedThemeMode,
|
resolvedThemeMode,
|
||||||
setQuickChatButtonModeImmediate,
|
setQuickChatButtonModeImmediate,
|
||||||
|
setChatMessageLayoutImmediate,
|
||||||
|
setOpenTasksInRightSidebarImmediate,
|
||||||
|
setOpenMobileTasksInPopupImmediate,
|
||||||
|
setTaskPopupsBoardListOnlyImmediate,
|
||||||
|
setShowCostBadgeOnCardsImmediate,
|
||||||
|
setTaskDetailChatFirstImmediate,
|
||||||
setMobileNavPrimaryItemsImmediate,
|
setMobileNavPrimaryItemsImmediate,
|
||||||
reopenOnboardingWithNav,
|
reopenOnboardingWithNav,
|
||||||
viewMode,
|
viewMode,
|
||||||
@@ -1692,8 +1703,12 @@ function AppInner() {
|
|||||||
mergeStrategy,
|
mergeStrategy,
|
||||||
planAutoApproveEnabled,
|
planAutoApproveEnabled,
|
||||||
settingsLoaded,
|
settingsLoaded,
|
||||||
|
openTasksInRightSidebar,
|
||||||
openMobileTasksInPopup,
|
openMobileTasksInPopup,
|
||||||
|
taskPopupsBoardListOnly,
|
||||||
|
showCostBadgeOnCards,
|
||||||
taskDetailChatFirst,
|
taskDetailChatFirst,
|
||||||
|
chatMessageLayout,
|
||||||
skillsEnabled,
|
skillsEnabled,
|
||||||
experimentalFeatures,
|
experimentalFeatures,
|
||||||
setQuickChatOpen,
|
setQuickChatOpen,
|
||||||
@@ -2265,7 +2280,7 @@ function AppInner() {
|
|||||||
onSubtaskBreakdown={subtaskBreakdownEnabled ? openSubtaskBreakdownWithNav : undefined}
|
onSubtaskBreakdown={subtaskBreakdownEnabled ? openSubtaskBreakdownWithNav : undefined}
|
||||||
taskOperations={{ moveTask, deleteTask, mergeTask, archiveTask, revertTask, retryTask, pauseTask, unpauseTask, bypassReview, resetTask, duplicateTask }}
|
taskOperations={{ moveTask, deleteTask, mergeTask, archiveTask, revertTask, retryTask, pauseTask, unpauseTask, bypassReview, resetTask, duplicateTask }}
|
||||||
deepLink={{ handleDetailClose }}
|
deepLink={{ handleDetailClose }}
|
||||||
settings={{ prAuthAvailable, autoMerge, taskDetailChatFirst, chatMessageLayout, themeMode, colorTheme, dashboardFontScalePct, shadcnCustomColors, resolvedThemeMode, setThemeMode, setColorTheme, setDashboardFontScalePct, setShadcnCustomColors, setQuickChatButtonModeImmediate, setChatMessageLayoutImmediate, setMobileNavPrimaryItemsImmediate }}
|
settings={{ prAuthAvailable, autoMerge, openTasksInRightSidebar, openMobileTasksInPopup, taskPopupsBoardListOnly, showCostBadgeOnCards, taskDetailChatFirst, chatMessageLayout, themeMode, colorTheme, dashboardFontScalePct, shadcnCustomColors, resolvedThemeMode, setThemeMode, setColorTheme, setDashboardFontScalePct, setShadcnCustomColors, setQuickChatButtonModeImmediate, setChatMessageLayoutImmediate, setOpenTasksInRightSidebarImmediate, setOpenMobileTasksInPopupImmediate, setTaskPopupsBoardListOnlyImmediate, setShowCostBadgeOnCardsImmediate, setTaskDetailChatFirstImmediate, setMobileNavPrimaryItemsImmediate }}
|
||||||
onSettingsClose={handleSettingsCloseWithNav}
|
onSettingsClose={handleSettingsCloseWithNav}
|
||||||
onReopenOnboarding={reopenOnboardingWithNav}
|
onReopenOnboarding={reopenOnboardingWithNav}
|
||||||
onOpenApprovals={(_approvalId) => handleTaskViewChange("mailbox")}
|
onOpenApprovals={(_approvalId) => handleTaskViewChange("mailbox")}
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ interface AppModalsProps {
|
|||||||
settings: {
|
settings: {
|
||||||
prAuthAvailable: boolean;
|
prAuthAvailable: boolean;
|
||||||
autoMerge: boolean;
|
autoMerge: boolean;
|
||||||
|
openTasksInRightSidebar: boolean;
|
||||||
|
openMobileTasksInPopup: boolean;
|
||||||
|
taskPopupsBoardListOnly: boolean;
|
||||||
|
showCostBadgeOnCards: boolean;
|
||||||
taskDetailChatFirst: boolean;
|
taskDetailChatFirst: boolean;
|
||||||
chatMessageLayout: ChatMessageLayout;
|
chatMessageLayout: ChatMessageLayout;
|
||||||
themeMode: ThemeMode;
|
themeMode: ThemeMode;
|
||||||
@@ -98,6 +102,11 @@ interface AppModalsProps {
|
|||||||
setShadcnCustomColors: (colors: Record<string, string>) => void;
|
setShadcnCustomColors: (colors: Record<string, string>) => void;
|
||||||
setQuickChatButtonModeImmediate: (mode: "floating" | "footer" | "off") => void;
|
setQuickChatButtonModeImmediate: (mode: "floating" | "footer" | "off") => void;
|
||||||
setChatMessageLayoutImmediate: (layout: ChatMessageLayout) => void;
|
setChatMessageLayoutImmediate: (layout: ChatMessageLayout) => void;
|
||||||
|
setOpenTasksInRightSidebarImmediate: (enabled: boolean) => void;
|
||||||
|
setOpenMobileTasksInPopupImmediate: (enabled: boolean) => void;
|
||||||
|
setTaskPopupsBoardListOnlyImmediate: (enabled: boolean) => void;
|
||||||
|
setShowCostBadgeOnCardsImmediate: (enabled: boolean) => void;
|
||||||
|
setTaskDetailChatFirstImmediate: (enabled: boolean) => void;
|
||||||
setMobileNavPrimaryItemsImmediate: (items: string[]) => void;
|
setMobileNavPrimaryItemsImmediate: (items: string[]) => void;
|
||||||
};
|
};
|
||||||
/** Optional override for the settings modal close handler. When provided, this is called instead of modalManager.closeSettings. */
|
/** Optional override for the settings modal close handler. When provided, this is called instead of modalManager.closeSettings. */
|
||||||
@@ -386,6 +395,16 @@ export function AppModals({
|
|||||||
onQuickChatButtonModeChange={settings.setQuickChatButtonModeImmediate}
|
onQuickChatButtonModeChange={settings.setQuickChatButtonModeImmediate}
|
||||||
chatMessageLayout={settings.chatMessageLayout}
|
chatMessageLayout={settings.chatMessageLayout}
|
||||||
onChatMessageLayoutChange={settings.setChatMessageLayoutImmediate}
|
onChatMessageLayoutChange={settings.setChatMessageLayoutImmediate}
|
||||||
|
openTasksInRightSidebar={settings.openTasksInRightSidebar}
|
||||||
|
onOpenTasksInRightSidebarChange={settings.setOpenTasksInRightSidebarImmediate}
|
||||||
|
openMobileTasksInPopup={settings.openMobileTasksInPopup}
|
||||||
|
onOpenMobileTasksInPopupChange={settings.setOpenMobileTasksInPopupImmediate}
|
||||||
|
taskPopupsBoardListOnly={settings.taskPopupsBoardListOnly}
|
||||||
|
onTaskPopupsBoardListOnlyChange={settings.setTaskPopupsBoardListOnlyImmediate}
|
||||||
|
showCostBadgeOnCards={settings.showCostBadgeOnCards}
|
||||||
|
onShowCostBadgeOnCardsChange={settings.setShowCostBadgeOnCardsImmediate}
|
||||||
|
taskDetailChatFirst={settings.taskDetailChatFirst}
|
||||||
|
onTaskDetailChatFirstChange={settings.setTaskDetailChatFirstImmediate}
|
||||||
onMobileNavPrimaryItemsChange={settings.setMobileNavPrimaryItemsImmediate}
|
onMobileNavPrimaryItemsChange={settings.setMobileNavPrimaryItemsImmediate}
|
||||||
onReopenOnboarding={onReopenOnboarding}
|
onReopenOnboarding={onReopenOnboarding}
|
||||||
onOpenApprovals={onOpenApprovals}
|
onOpenApprovals={onOpenApprovals}
|
||||||
|
|||||||
@@ -665,6 +665,17 @@ interface SettingsModalProps {
|
|||||||
/** Mirrors the pending project conversation layout into mounted chat surfaces immediately. */
|
/** Mirrors the pending project conversation layout into mounted chat surfaces immediately. */
|
||||||
chatMessageLayout?: ChatMessageLayout;
|
chatMessageLayout?: ChatMessageLayout;
|
||||||
onChatMessageLayoutChange?: (layout: ChatMessageLayout) => void;
|
onChatMessageLayoutChange?: (layout: ChatMessageLayout) => void;
|
||||||
|
/** Current App-shell values and optimistic callbacks for mounted Appearance consumers. */
|
||||||
|
openTasksInRightSidebar?: boolean;
|
||||||
|
onOpenTasksInRightSidebarChange?: (enabled: boolean) => void;
|
||||||
|
openMobileTasksInPopup?: boolean;
|
||||||
|
onOpenMobileTasksInPopupChange?: (enabled: boolean) => void;
|
||||||
|
taskPopupsBoardListOnly?: boolean;
|
||||||
|
onTaskPopupsBoardListOnlyChange?: (enabled: boolean) => void;
|
||||||
|
showCostBadgeOnCards?: boolean;
|
||||||
|
onShowCostBadgeOnCardsChange?: (enabled: boolean) => void;
|
||||||
|
taskDetailChatFirst?: boolean;
|
||||||
|
onTaskDetailChatFirstChange?: (enabled: boolean) => void;
|
||||||
/** Mirrors pending mobile quick-action changes into the app shell immediately. */
|
/** Mirrors pending mobile quick-action changes into the app shell immediately. */
|
||||||
onMobileNavPrimaryItemsChange?: (items: string[]) => void;
|
onMobileNavPrimaryItemsChange?: (items: string[]) => void;
|
||||||
/** Optional callback when user wants to reopen the onboarding guide */
|
/** Optional callback when user wants to reopen the onboarding guide */
|
||||||
@@ -932,6 +943,16 @@ export function SettingsModal({
|
|||||||
onQuickChatButtonModeChange,
|
onQuickChatButtonModeChange,
|
||||||
chatMessageLayout = "bubbles",
|
chatMessageLayout = "bubbles",
|
||||||
onChatMessageLayoutChange,
|
onChatMessageLayoutChange,
|
||||||
|
openTasksInRightSidebar,
|
||||||
|
onOpenTasksInRightSidebarChange,
|
||||||
|
openMobileTasksInPopup,
|
||||||
|
onOpenMobileTasksInPopupChange,
|
||||||
|
taskPopupsBoardListOnly,
|
||||||
|
onTaskPopupsBoardListOnlyChange,
|
||||||
|
showCostBadgeOnCards,
|
||||||
|
onShowCostBadgeOnCardsChange,
|
||||||
|
taskDetailChatFirst,
|
||||||
|
onTaskDetailChatFirstChange,
|
||||||
onMobileNavPrimaryItemsChange,
|
onMobileNavPrimaryItemsChange,
|
||||||
onReopenOnboarding,
|
onReopenOnboarding,
|
||||||
onOpenApprovals,
|
onOpenApprovals,
|
||||||
@@ -4214,6 +4235,16 @@ export function SettingsModal({
|
|||||||
onShadcnCustomColorsChange={onShadcnCustomColorsChange}
|
onShadcnCustomColorsChange={onShadcnCustomColorsChange}
|
||||||
chatMessageLayout={chatMessageLayout}
|
chatMessageLayout={chatMessageLayout}
|
||||||
onChatMessageLayoutChange={onChatMessageLayoutChange}
|
onChatMessageLayoutChange={onChatMessageLayoutChange}
|
||||||
|
openTasksInRightSidebar={openTasksInRightSidebar}
|
||||||
|
onOpenTasksInRightSidebarChange={onOpenTasksInRightSidebarChange}
|
||||||
|
openMobileTasksInPopup={openMobileTasksInPopup}
|
||||||
|
onOpenMobileTasksInPopupChange={onOpenMobileTasksInPopupChange}
|
||||||
|
taskPopupsBoardListOnly={taskPopupsBoardListOnly}
|
||||||
|
onTaskPopupsBoardListOnlyChange={onTaskPopupsBoardListOnlyChange}
|
||||||
|
showCostBadgeOnCards={showCostBadgeOnCards}
|
||||||
|
onShowCostBadgeOnCardsChange={onShowCostBadgeOnCardsChange}
|
||||||
|
taskDetailChatFirst={taskDetailChatFirst}
|
||||||
|
onTaskDetailChatFirstChange={onTaskDetailChatFirstChange}
|
||||||
sessionBannersHidden={sessionBannersHidden}
|
sessionBannersHidden={sessionBannersHidden}
|
||||||
setSessionBannersHidden={setSessionBannersHidden}
|
setSessionBannersHidden={setSessionBannersHidden}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -263,6 +263,35 @@ describe("SettingsModal", () => {
|
|||||||
expect(mockUpdateGlobalSettings).not.toHaveBeenCalled();
|
expect(mockUpdateGlobalSettings).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("mirrors mounted Appearance controls through the modal while keeping one persistence path", async () => {
|
||||||
|
const callbacks = {
|
||||||
|
onOpenTasksInRightSidebarChange: vi.fn(),
|
||||||
|
onOpenMobileTasksInPopupChange: vi.fn(),
|
||||||
|
onTaskPopupsBoardListOnlyChange: vi.fn(),
|
||||||
|
onShowCostBadgeOnCardsChange: vi.fn(),
|
||||||
|
onTaskDetailChatFirstChange: vi.fn(),
|
||||||
|
};
|
||||||
|
renderModal({ initialSection: "appearance", ...callbacks });
|
||||||
|
await waitForSettingsModalReady();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByLabelText("Open tasks in the right sidebar"));
|
||||||
|
fireEvent.click(screen.getByLabelText("Open tasks as popups"));
|
||||||
|
fireEvent.click(screen.getByLabelText("Keep task popups on the view where they were opened"));
|
||||||
|
fireEvent.click(screen.getByLabelText("Show cost badges on task cards"));
|
||||||
|
fireEvent.click(screen.getByLabelText("Open task details with Chat first"));
|
||||||
|
|
||||||
|
expect(callbacks.onOpenTasksInRightSidebarChange).toHaveBeenCalledWith(true);
|
||||||
|
expect(callbacks.onOpenMobileTasksInPopupChange).toHaveBeenCalledWith(true);
|
||||||
|
expect(callbacks.onTaskPopupsBoardListOnlyChange).toHaveBeenCalledWith(true);
|
||||||
|
expect(callbacks.onShowCostBadgeOnCardsChange).toHaveBeenCalledWith(true);
|
||||||
|
expect(callbacks.onTaskDetailChatFirstChange).toHaveBeenCalledWith(true);
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
await flushSettingsAutoSave();
|
||||||
|
vi.useRealTimers();
|
||||||
|
expect(mockUpdateGlobalSettings).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("renders recommendation mailbox notices enabled by default and persists disabling it", async () => {
|
it("renders recommendation mailbox notices enabled by default and persists disabling it", async () => {
|
||||||
renderModal({ initialSection: "general" });
|
renderModal({ initialSection: "general" });
|
||||||
await waitForSettingsModalReady();
|
await waitForSettingsModalReady();
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ export function MainContent({
|
|||||||
setShadcnCustomColors,
|
setShadcnCustomColors,
|
||||||
resolvedThemeMode,
|
resolvedThemeMode,
|
||||||
setQuickChatButtonModeImmediate,
|
setQuickChatButtonModeImmediate,
|
||||||
|
setChatMessageLayoutImmediate,
|
||||||
|
setOpenTasksInRightSidebarImmediate,
|
||||||
|
setOpenMobileTasksInPopupImmediate,
|
||||||
|
setTaskPopupsBoardListOnlyImmediate,
|
||||||
|
setShowCostBadgeOnCardsImmediate,
|
||||||
|
setTaskDetailChatFirstImmediate,
|
||||||
setMobileNavPrimaryItemsImmediate,
|
setMobileNavPrimaryItemsImmediate,
|
||||||
reopenOnboardingWithNav,
|
reopenOnboardingWithNav,
|
||||||
viewMode,
|
viewMode,
|
||||||
@@ -86,8 +92,12 @@ export function MainContent({
|
|||||||
mergeStrategy,
|
mergeStrategy,
|
||||||
planAutoApproveEnabled,
|
planAutoApproveEnabled,
|
||||||
settingsLoaded,
|
settingsLoaded,
|
||||||
|
openTasksInRightSidebar,
|
||||||
openMobileTasksInPopup,
|
openMobileTasksInPopup,
|
||||||
|
taskPopupsBoardListOnly,
|
||||||
|
showCostBadgeOnCards,
|
||||||
taskDetailChatFirst,
|
taskDetailChatFirst,
|
||||||
|
chatMessageLayout,
|
||||||
skillsEnabled,
|
skillsEnabled,
|
||||||
experimentalFeatures,
|
experimentalFeatures,
|
||||||
setQuickChatOpen,
|
setQuickChatOpen,
|
||||||
@@ -328,6 +338,18 @@ export function MainContent({
|
|||||||
onDashboardFontScaleChange={setDashboardFontScalePct}
|
onDashboardFontScaleChange={setDashboardFontScalePct}
|
||||||
onShadcnCustomColorsChange={setShadcnCustomColors}
|
onShadcnCustomColorsChange={setShadcnCustomColors}
|
||||||
onQuickChatButtonModeChange={setQuickChatButtonModeImmediate}
|
onQuickChatButtonModeChange={setQuickChatButtonModeImmediate}
|
||||||
|
chatMessageLayout={chatMessageLayout}
|
||||||
|
onChatMessageLayoutChange={setChatMessageLayoutImmediate}
|
||||||
|
openTasksInRightSidebar={openTasksInRightSidebar}
|
||||||
|
onOpenTasksInRightSidebarChange={setOpenTasksInRightSidebarImmediate}
|
||||||
|
openMobileTasksInPopup={openMobileTasksInPopup}
|
||||||
|
onOpenMobileTasksInPopupChange={setOpenMobileTasksInPopupImmediate}
|
||||||
|
taskPopupsBoardListOnly={taskPopupsBoardListOnly}
|
||||||
|
onTaskPopupsBoardListOnlyChange={setTaskPopupsBoardListOnlyImmediate}
|
||||||
|
showCostBadgeOnCards={showCostBadgeOnCards}
|
||||||
|
onShowCostBadgeOnCardsChange={setShowCostBadgeOnCardsImmediate}
|
||||||
|
taskDetailChatFirst={taskDetailChatFirst}
|
||||||
|
onTaskDetailChatFirstChange={setTaskDetailChatFirstImmediate}
|
||||||
onMobileNavPrimaryItemsChange={setMobileNavPrimaryItemsImmediate}
|
onMobileNavPrimaryItemsChange={setMobileNavPrimaryItemsImmediate}
|
||||||
onReopenOnboarding={reopenOnboardingWithNav}
|
onReopenOnboarding={reopenOnboardingWithNav}
|
||||||
onOpenApprovals={() => handleChangeTaskView("mailbox")}
|
onOpenApprovals={() => handleChangeTaskView("mailbox")}
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ const LazyStub = lazy(async () => ({ default: () => null }));
|
|||||||
const LazySettingsCloseStub = lazy(async () => ({
|
const LazySettingsCloseStub = lazy(async () => ({
|
||||||
default: ({ onClose }: { onClose: () => void }) => <button type="button" onClick={onClose}>Close settings view</button>,
|
default: ({ onClose }: { onClose: () => void }) => <button type="button" onClick={onClose}>Close settings view</button>,
|
||||||
}));
|
}));
|
||||||
|
let embeddedSettingsProps: Record<string, unknown> | undefined;
|
||||||
|
const LazySettingsBridgeStub = lazy(async () => ({
|
||||||
|
default: (props: Record<string, unknown>) => {
|
||||||
|
embeddedSettingsProps = props;
|
||||||
|
return <div>Embedded settings bridge</div>;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
function mainContentProps(overrides: Partial<MainContentProps> = {}): MainContentProps {
|
function mainContentProps(overrides: Partial<MainContentProps> = {}): MainContentProps {
|
||||||
return {
|
return {
|
||||||
@@ -94,6 +101,12 @@ function mainContentProps(overrides: Partial<MainContentProps> = {}): MainConten
|
|||||||
setShadcnCustomColors: vi.fn(),
|
setShadcnCustomColors: vi.fn(),
|
||||||
resolvedThemeMode: "light",
|
resolvedThemeMode: "light",
|
||||||
setQuickChatButtonModeImmediate: vi.fn(),
|
setQuickChatButtonModeImmediate: vi.fn(),
|
||||||
|
setChatMessageLayoutImmediate: vi.fn(),
|
||||||
|
setOpenTasksInRightSidebarImmediate: vi.fn(),
|
||||||
|
setOpenMobileTasksInPopupImmediate: vi.fn(),
|
||||||
|
setTaskPopupsBoardListOnlyImmediate: vi.fn(),
|
||||||
|
setShowCostBadgeOnCardsImmediate: vi.fn(),
|
||||||
|
setTaskDetailChatFirstImmediate: vi.fn(),
|
||||||
reopenOnboardingWithNav: vi.fn(),
|
reopenOnboardingWithNav: vi.fn(),
|
||||||
viewMode: "project",
|
viewMode: "project",
|
||||||
projects: [],
|
projects: [],
|
||||||
@@ -136,6 +149,12 @@ function mainContentProps(overrides: Partial<MainContentProps> = {}): MainConten
|
|||||||
autoMerge: true,
|
autoMerge: true,
|
||||||
mergeStrategy: "direct",
|
mergeStrategy: "direct",
|
||||||
settingsLoaded: true,
|
settingsLoaded: true,
|
||||||
|
openTasksInRightSidebar: false,
|
||||||
|
openMobileTasksInPopup: false,
|
||||||
|
taskPopupsBoardListOnly: true,
|
||||||
|
showCostBadgeOnCards: false,
|
||||||
|
taskDetailChatFirst: false,
|
||||||
|
chatMessageLayout: "bubbles",
|
||||||
skillsEnabled: true,
|
skillsEnabled: true,
|
||||||
experimentalFeatures: {},
|
experimentalFeatures: {},
|
||||||
setQuickChatOpen: vi.fn(),
|
setQuickChatOpen: vi.fn(),
|
||||||
@@ -260,6 +279,54 @@ describe("MainContent graph task pop-out wiring", () => {
|
|||||||
expect(refreshAppSettings).toHaveBeenCalledTimes(1);
|
expect(refreshAppSettings).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forwards every live Appearance value and callback to embedded Settings", async () => {
|
||||||
|
embeddedSettingsProps = undefined;
|
||||||
|
const setters = {
|
||||||
|
setChatMessageLayoutImmediate: vi.fn(),
|
||||||
|
setOpenTasksInRightSidebarImmediate: vi.fn(),
|
||||||
|
setOpenMobileTasksInPopupImmediate: vi.fn(),
|
||||||
|
setTaskPopupsBoardListOnlyImmediate: vi.fn(),
|
||||||
|
setShowCostBadgeOnCardsImmediate: vi.fn(),
|
||||||
|
setTaskDetailChatFirstImmediate: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<MainContent {...mainContentProps({
|
||||||
|
taskView: "settings",
|
||||||
|
chatMessageLayout: "full-width",
|
||||||
|
openTasksInRightSidebar: true,
|
||||||
|
openMobileTasksInPopup: true,
|
||||||
|
taskPopupsBoardListOnly: false,
|
||||||
|
showCostBadgeOnCards: true,
|
||||||
|
taskDetailChatFirst: true,
|
||||||
|
...setters,
|
||||||
|
_SettingsView: LazySettingsBridgeStub as MainContentProps["_SettingsView"],
|
||||||
|
})} />);
|
||||||
|
|
||||||
|
await screen.findByText("Embedded settings bridge");
|
||||||
|
expect(embeddedSettingsProps).toMatchObject({
|
||||||
|
chatMessageLayout: "full-width",
|
||||||
|
openTasksInRightSidebar: true,
|
||||||
|
openMobileTasksInPopup: true,
|
||||||
|
taskPopupsBoardListOnly: false,
|
||||||
|
showCostBadgeOnCards: true,
|
||||||
|
taskDetailChatFirst: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
(embeddedSettingsProps?.onChatMessageLayoutChange as (value: "bubbles" | "full-width") => void)("bubbles");
|
||||||
|
(embeddedSettingsProps?.onOpenTasksInRightSidebarChange as (value: boolean) => void)(false);
|
||||||
|
(embeddedSettingsProps?.onOpenMobileTasksInPopupChange as (value: boolean) => void)(false);
|
||||||
|
(embeddedSettingsProps?.onTaskPopupsBoardListOnlyChange as (value: boolean) => void)(true);
|
||||||
|
(embeddedSettingsProps?.onShowCostBadgeOnCardsChange as (value: boolean) => void)(false);
|
||||||
|
(embeddedSettingsProps?.onTaskDetailChatFirstChange as (value: boolean) => void)(false);
|
||||||
|
|
||||||
|
expect(setters.setChatMessageLayoutImmediate).toHaveBeenCalledWith("bubbles");
|
||||||
|
expect(setters.setOpenTasksInRightSidebarImmediate).toHaveBeenCalledWith(false);
|
||||||
|
expect(setters.setOpenMobileTasksInPopupImmediate).toHaveBeenCalledWith(false);
|
||||||
|
expect(setters.setTaskPopupsBoardListOnlyImmediate).toHaveBeenCalledWith(true);
|
||||||
|
expect(setters.setShowCostBadgeOnCardsImmediate).toHaveBeenCalledWith(false);
|
||||||
|
expect(setters.setTaskDetailChatFirstImmediate).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("routes dependency-graph bridge and rendered task-card opens to the shared pop-out", () => {
|
it("routes dependency-graph bridge and rendered task-card opens to the shared pop-out", () => {
|
||||||
hostContexts.length = 0;
|
hostContexts.length = 0;
|
||||||
const openDetailTask = vi.fn();
|
const openDetailTask = vi.fn();
|
||||||
|
|||||||
@@ -134,8 +134,18 @@ export interface MainContentProps {
|
|||||||
mergeStrategy: string;
|
mergeStrategy: string;
|
||||||
planAutoApproveEnabled: boolean;
|
planAutoApproveEnabled: boolean;
|
||||||
settingsLoaded: boolean;
|
settingsLoaded: boolean;
|
||||||
|
openTasksInRightSidebar: boolean;
|
||||||
openMobileTasksInPopup: boolean;
|
openMobileTasksInPopup: boolean;
|
||||||
|
taskPopupsBoardListOnly: boolean;
|
||||||
|
showCostBadgeOnCards: boolean;
|
||||||
taskDetailChatFirst: boolean;
|
taskDetailChatFirst: boolean;
|
||||||
|
chatMessageLayout: "bubbles" | "full-width";
|
||||||
|
setOpenTasksInRightSidebarImmediate: (enabled: boolean) => void;
|
||||||
|
setOpenMobileTasksInPopupImmediate: (enabled: boolean) => void;
|
||||||
|
setTaskPopupsBoardListOnlyImmediate: (enabled: boolean) => void;
|
||||||
|
setShowCostBadgeOnCardsImmediate: (enabled: boolean) => void;
|
||||||
|
setTaskDetailChatFirstImmediate: (enabled: boolean) => void;
|
||||||
|
setChatMessageLayoutImmediate: (layout: "bubbles" | "full-width") => void;
|
||||||
skillsEnabled: boolean;
|
skillsEnabled: boolean;
|
||||||
experimentalFeatures: Record<string, boolean>;
|
experimentalFeatures: Record<string, boolean>;
|
||||||
setQuickChatOpen: Dispatch<SetStateAction<boolean>>;
|
setQuickChatOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ export interface AppearanceSectionProps extends SectionBaseProps {
|
|||||||
onShadcnCustomColorsChange?: (colors: Record<string, string>) => void;
|
onShadcnCustomColorsChange?: (colors: Record<string, string>) => void;
|
||||||
chatMessageLayout?: ChatMessageLayout;
|
chatMessageLayout?: ChatMessageLayout;
|
||||||
onChatMessageLayoutChange?: (layout: ChatMessageLayout) => void;
|
onChatMessageLayoutChange?: (layout: ChatMessageLayout) => void;
|
||||||
|
openTasksInRightSidebar?: boolean;
|
||||||
|
onOpenTasksInRightSidebarChange?: (enabled: boolean) => void;
|
||||||
|
openMobileTasksInPopup?: boolean;
|
||||||
|
onOpenMobileTasksInPopupChange?: (enabled: boolean) => void;
|
||||||
|
taskPopupsBoardListOnly?: boolean;
|
||||||
|
onTaskPopupsBoardListOnlyChange?: (enabled: boolean) => void;
|
||||||
|
showCostBadgeOnCards?: boolean;
|
||||||
|
onShowCostBadgeOnCardsChange?: (enabled: boolean) => void;
|
||||||
|
taskDetailChatFirst?: boolean;
|
||||||
|
onTaskDetailChatFirstChange?: (enabled: boolean) => void;
|
||||||
sessionBannersHidden: boolean;
|
sessionBannersHidden: boolean;
|
||||||
setSessionBannersHidden: (hidden: boolean) => void;
|
setSessionBannersHidden: (hidden: boolean) => void;
|
||||||
}
|
}
|
||||||
@@ -29,7 +39,7 @@ Rows render through the shared settings primitives rather than hand-rolled `form
|
|||||||
FNXC:SettingsScope 2026-07-15-17:35:
|
FNXC:SettingsScope 2026-07-15-17:35:
|
||||||
Scope badges are per-row because this section genuinely mixes authority levels: theme, color, and font scale are global (DEFAULT_GLOBAL_SETTINGS), while every task-presentation toggle below is project-scoped (DEFAULT_PROJECT_SETTINGS). The nav labels the whole section "global", which is true only of the theme controls, so the badges are what tell an operator which of these travels between projects.
|
Scope badges are per-row because this section genuinely mixes authority levels: theme, color, and font scale are global (DEFAULT_GLOBAL_SETTINGS), while every task-presentation toggle below is project-scoped (DEFAULT_PROJECT_SETTINGS). The nav labels the whole section "global", which is true only of the theme controls, so the badges are what tell an operator which of these travels between projects.
|
||||||
*/
|
*/
|
||||||
export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashboardFontScalePct, shadcnCustomColors = {}, resolvedThemeMode, onThemeModeChange, onColorThemeChange, onDashboardFontScaleChange, onShadcnCustomColorsChange, chatMessageLayout = "bubbles", onChatMessageLayoutChange, sessionBannersHidden, setSessionBannersHidden, }: AppearanceSectionProps) {
|
export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashboardFontScalePct, shadcnCustomColors = {}, resolvedThemeMode, onThemeModeChange, onColorThemeChange, onDashboardFontScaleChange, onShadcnCustomColorsChange, chatMessageLayout = "bubbles", onChatMessageLayoutChange, openTasksInRightSidebar, onOpenTasksInRightSidebarChange, openMobileTasksInPopup, onOpenMobileTasksInPopupChange, taskPopupsBoardListOnly, onTaskPopupsBoardListOnlyChange, showCostBadgeOnCards, onShowCostBadgeOnCardsChange, taskDetailChatFirst, onTaskDetailChatFirstChange, sessionBannersHidden, setSessionBannersHidden, }: AppearanceSectionProps) {
|
||||||
const { t } = useTranslation("app");
|
const { t } = useTranslation("app");
|
||||||
return (<>
|
return (<>
|
||||||
<h4 className="settings-section-heading">{t("settings.appearance.title", "Appearance")}</h4>
|
<h4 className="settings-section-heading">{t("settings.appearance.title", "Appearance")}</h4>
|
||||||
@@ -72,8 +82,12 @@ export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashbo
|
|||||||
help: t("settings.appearance.openTasksInRightSidebarHelp", "When enabled, board task cards open detail in the right sidebar when it is available; mobile and hidden-sidebar states keep the full task panel. Default: disabled."),
|
help: t("settings.appearance.openTasksInRightSidebarHelp", "When enabled, board task cards open detail in the right sidebar when it is available; mobile and hidden-sidebar states keep the full task panel. Default: disabled."),
|
||||||
scope: "project",
|
scope: "project",
|
||||||
}}
|
}}
|
||||||
value={form.openTasksInRightSidebar === true}
|
value={form.openTasksInRightSidebar ?? openTasksInRightSidebar === true}
|
||||||
onChange={(v) => setForm((f) => ({ ...f, openTasksInRightSidebar: v === true }))}
|
onChange={(v) => {
|
||||||
|
const enabled = v === true;
|
||||||
|
setForm((f) => ({ ...f, openTasksInRightSidebar: enabled }));
|
||||||
|
onOpenTasksInRightSidebarChange?.(enabled);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/* FNXC:MobileTaskPopups 2026-07-21-00:00 (FN-8478): Keep the stored openMobileTasksInPopup key for compatibility and explain that board-card deep-tab chips now use the same popup routing, preserving the board behind Changes, Retries, or Workflow detail. */}
|
{/* FNXC:MobileTaskPopups 2026-07-21-00:00 (FN-8478): Keep the stored openMobileTasksInPopup key for compatibility and explain that board-card deep-tab chips now use the same popup routing, preserving the board behind Changes, Retries, or Workflow detail. */}
|
||||||
<SettingsToggleRow
|
<SettingsToggleRow
|
||||||
@@ -83,8 +97,12 @@ export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashbo
|
|||||||
help: t("settings.appearance.openMobileTasksInPopupHelp", "When enabled, board task-card clicks including Changes, Retries, and Workflow chips, plus ordinary List row/card and right-dock Tasks-list clicks, open the existing movable task popup so the board or list remains visible. Other task opens keep their current behavior. Default: disabled."),
|
help: t("settings.appearance.openMobileTasksInPopupHelp", "When enabled, board task-card clicks including Changes, Retries, and Workflow chips, plus ordinary List row/card and right-dock Tasks-list clicks, open the existing movable task popup so the board or list remains visible. Other task opens keep their current behavior. Default: disabled."),
|
||||||
scope: "project",
|
scope: "project",
|
||||||
}}
|
}}
|
||||||
value={form.openMobileTasksInPopup === true}
|
value={form.openMobileTasksInPopup ?? openMobileTasksInPopup === true}
|
||||||
onChange={(v) => setForm((f) => ({ ...f, openMobileTasksInPopup: v === true }))}
|
onChange={(v) => {
|
||||||
|
const enabled = v === true;
|
||||||
|
setForm((f) => ({ ...f, openMobileTasksInPopup: enabled }));
|
||||||
|
onOpenMobileTasksInPopupChange?.(enabled);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/* FNXC:TaskPopupViewGating 2026-07-15-15:20: FN-8016 scopes task popups to their opening dashboard view by default. Operators may explicitly disable it for legacy globally shared popups; hidden scoped entries retain geometry and reopen on return. */}
|
{/* FNXC:TaskPopupViewGating 2026-07-15-15:20: FN-8016 scopes task popups to their opening dashboard view by default. Operators may explicitly disable it for legacy globally shared popups; hidden scoped entries retain geometry and reopen on return. */}
|
||||||
<SettingsToggleRow
|
<SettingsToggleRow
|
||||||
@@ -94,8 +112,12 @@ export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashbo
|
|||||||
help: t("settings.appearance.taskPopupsBoardListOnlyHelp", "When enabled, each open task-detail popup appears only on the view where it was opened. Switching views hides it without closing; returning restores it in the same position. Default: enabled."),
|
help: t("settings.appearance.taskPopupsBoardListOnlyHelp", "When enabled, each open task-detail popup appears only on the view where it was opened. Switching views hides it without closing; returning restores it in the same position. Default: enabled."),
|
||||||
scope: "project",
|
scope: "project",
|
||||||
}}
|
}}
|
||||||
value={form.taskPopupsBoardListOnly === true}
|
value={form.taskPopupsBoardListOnly ?? taskPopupsBoardListOnly === true}
|
||||||
onChange={(v) => setForm((f) => ({ ...f, taskPopupsBoardListOnly: v === true }))}
|
onChange={(v) => {
|
||||||
|
const enabled = v === true;
|
||||||
|
setForm((f) => ({ ...f, taskPopupsBoardListOnly: enabled }));
|
||||||
|
onTaskPopupsBoardListOnlyChange?.(enabled);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/* FNXC:TaskCardCostBadge 2026-07-11-12:15: This project setting is opt-in because board cards are already dense; when enabled, only tasks with recorded positive token usage render a read-time derived spend badge. */}
|
{/* FNXC:TaskCardCostBadge 2026-07-11-12:15: This project setting is opt-in because board cards are already dense; when enabled, only tasks with recorded positive token usage render a read-time derived spend badge. */}
|
||||||
<SettingsToggleRow
|
<SettingsToggleRow
|
||||||
@@ -105,8 +127,12 @@ export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashbo
|
|||||||
help: t("settings.appearance.showCostBadgeOnCardsHelp", "Default: disabled. When enabled, board cards show derived model cost next to execution time; unavailable pricing displays — and tasks without token usage show no badge."),
|
help: t("settings.appearance.showCostBadgeOnCardsHelp", "Default: disabled. When enabled, board cards show derived model cost next to execution time; unavailable pricing displays — and tasks without token usage show no badge."),
|
||||||
scope: "project",
|
scope: "project",
|
||||||
}}
|
}}
|
||||||
value={form.showCostBadgeOnCards === true}
|
value={form.showCostBadgeOnCards ?? showCostBadgeOnCards === true}
|
||||||
onChange={(v) => setForm((f) => ({ ...f, showCostBadgeOnCards: v === true }))}
|
onChange={(v) => {
|
||||||
|
const enabled = v === true;
|
||||||
|
setForm((f) => ({ ...f, showCostBadgeOnCards: enabled }));
|
||||||
|
onShowCostBadgeOnCardsChange?.(enabled);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/* FNXC:TaskDetailActivityFirst 2026-06-30-23:59: The project setting is opt-in because task details now default to Activity-first; explicit Activity/Chat/Logs links keep their destination regardless of this checkbox. */}
|
{/* FNXC:TaskDetailActivityFirst 2026-06-30-23:59: The project setting is opt-in because task details now default to Activity-first; explicit Activity/Chat/Logs links keep their destination regardless of this checkbox. */}
|
||||||
<SettingsToggleRow
|
<SettingsToggleRow
|
||||||
@@ -116,8 +142,12 @@ export function AppearanceSection({ form, setForm, themeMode, colorTheme, dashbo
|
|||||||
help: t("settings.appearance.taskDetailChatFirstHelp", "Off by default: task details list Activity first and omitted non-done opens land on Activity. Turn on to restore Chat-first order/default; explicit Chat links still work either way."),
|
help: t("settings.appearance.taskDetailChatFirstHelp", "Off by default: task details list Activity first and omitted non-done opens land on Activity. Turn on to restore Chat-first order/default; explicit Chat links still work either way."),
|
||||||
scope: "project",
|
scope: "project",
|
||||||
}}
|
}}
|
||||||
value={form.taskDetailChatFirst === true}
|
value={form.taskDetailChatFirst ?? taskDetailChatFirst === true}
|
||||||
onChange={(v) => setForm((f) => ({ ...f, taskDetailChatFirst: v === true }))}
|
onChange={(v) => {
|
||||||
|
const enabled = v === true;
|
||||||
|
setForm((f) => ({ ...f, taskDetailChatFirst: enabled }));
|
||||||
|
onTaskDetailChatFirstChange?.(enabled);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{/*
|
{/*
|
||||||
FNXC:SettingsScope 2026-07-15-17:35:
|
FNXC:SettingsScope 2026-07-15-17:35:
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ vi.mock("../../LanguageSelector", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
function renderAppearanceSection(formOverrides: Partial<Settings> = {}, onChatMessageLayoutChange = vi.fn()) {
|
function renderAppearanceSection(formOverrides: Partial<Settings> = {}, onChatMessageLayoutChange = vi.fn()) {
|
||||||
|
const onOpenTasksInRightSidebarChange = vi.fn();
|
||||||
|
const onOpenMobileTasksInPopupChange = vi.fn();
|
||||||
|
const onTaskPopupsBoardListOnlyChange = vi.fn();
|
||||||
|
const onShowCostBadgeOnCardsChange = vi.fn();
|
||||||
|
const onTaskDetailChatFirstChange = vi.fn();
|
||||||
let form: SettingsFormState = {
|
let form: SettingsFormState = {
|
||||||
maxConcurrent: 2,
|
maxConcurrent: 2,
|
||||||
maxWorktrees: 4,
|
maxWorktrees: 4,
|
||||||
@@ -40,12 +45,30 @@ function renderAppearanceSection(formOverrides: Partial<Settings> = {}, onChatMe
|
|||||||
dashboardFontScalePct={100}
|
dashboardFontScalePct={100}
|
||||||
chatMessageLayout={form.chatMessageLayout}
|
chatMessageLayout={form.chatMessageLayout}
|
||||||
onChatMessageLayoutChange={onChatMessageLayoutChange}
|
onChatMessageLayoutChange={onChatMessageLayoutChange}
|
||||||
|
openTasksInRightSidebar={form.openTasksInRightSidebar}
|
||||||
|
onOpenTasksInRightSidebarChange={onOpenTasksInRightSidebarChange}
|
||||||
|
openMobileTasksInPopup={form.openMobileTasksInPopup}
|
||||||
|
onOpenMobileTasksInPopupChange={onOpenMobileTasksInPopupChange}
|
||||||
|
taskPopupsBoardListOnly={form.taskPopupsBoardListOnly}
|
||||||
|
onTaskPopupsBoardListOnlyChange={onTaskPopupsBoardListOnlyChange}
|
||||||
|
showCostBadgeOnCards={form.showCostBadgeOnCards}
|
||||||
|
onShowCostBadgeOnCardsChange={onShowCostBadgeOnCardsChange}
|
||||||
|
taskDetailChatFirst={form.taskDetailChatFirst}
|
||||||
|
onTaskDetailChatFirstChange={onTaskDetailChatFirstChange}
|
||||||
sessionBannersHidden={false}
|
sessionBannersHidden={false}
|
||||||
setSessionBannersHidden={vi.fn()}
|
setSessionBannersHidden={vi.fn()}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { setForm, getForm: () => form };
|
return {
|
||||||
|
setForm,
|
||||||
|
getForm: () => form,
|
||||||
|
onOpenTasksInRightSidebarChange,
|
||||||
|
onOpenMobileTasksInPopupChange,
|
||||||
|
onTaskPopupsBoardListOnlyChange,
|
||||||
|
onShowCostBadgeOnCardsChange,
|
||||||
|
onTaskDetailChatFirstChange,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("AppearanceSection", () => {
|
describe("AppearanceSection", () => {
|
||||||
@@ -78,6 +101,22 @@ describe("AppearanceSection", () => {
|
|||||||
expect(getForm().openTasksInRightSidebar).toBe(true);
|
expect(getForm().openTasksInRightSidebar).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("mirrors every mounted Appearance toggle to its matching live callback", () => {
|
||||||
|
const callbacks = renderAppearanceSection();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByLabelText("Open tasks in the right sidebar"));
|
||||||
|
fireEvent.click(screen.getByLabelText("Open tasks as popups"));
|
||||||
|
fireEvent.click(screen.getByLabelText("Keep task popups on the view where they were opened"));
|
||||||
|
fireEvent.click(screen.getByLabelText("Show cost badges on task cards"));
|
||||||
|
fireEvent.click(screen.getByLabelText("Open task details with Chat first"));
|
||||||
|
|
||||||
|
expect(callbacks.onOpenTasksInRightSidebarChange).toHaveBeenCalledWith(true);
|
||||||
|
expect(callbacks.onOpenMobileTasksInPopupChange).toHaveBeenCalledWith(true);
|
||||||
|
expect(callbacks.onTaskPopupsBoardListOnlyChange).toHaveBeenCalledWith(false);
|
||||||
|
expect(callbacks.onShowCostBadgeOnCardsChange).toHaveBeenCalledWith(true);
|
||||||
|
expect(callbacks.onTaskDetailChatFirstChange).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("reflects a persisted enabled value", () => {
|
it("reflects a persisted enabled value", () => {
|
||||||
renderAppearanceSection({ openTasksInRightSidebar: true });
|
renderAppearanceSection({ openTasksInRightSidebar: true });
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,50 @@ describe("useAppSettings", () => {
|
|||||||
expect(result.current.chatMessageLayout).toBe(expectedLayout);
|
expect(result.current.chatMessageLayout).toBe(expectedLayout);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updates every mounted Appearance consumer synchronously without fetching", async () => {
|
||||||
|
const { result } = renderHook(() => useAppSettings("proj_123"));
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.settingsLoaded).toBe(true));
|
||||||
|
const settingsFetchesBefore = mockFetchSettings.mock.calls.length;
|
||||||
|
const configFetchesBefore = mockFetchConfig.mock.calls.length;
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setChatMessageLayoutImmediate("full-width");
|
||||||
|
result.current.setOpenTasksInRightSidebarImmediate(true);
|
||||||
|
result.current.setOpenMobileTasksInPopupImmediate(true);
|
||||||
|
result.current.setTaskPopupsBoardListOnlyImmediate(false);
|
||||||
|
result.current.setShowCostBadgeOnCardsImmediate(true);
|
||||||
|
result.current.setTaskDetailChatFirstImmediate(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.chatMessageLayout).toBe("full-width");
|
||||||
|
expect(result.current.openTasksInRightSidebar).toBe(true);
|
||||||
|
expect(result.current.openMobileTasksInPopup).toBe(true);
|
||||||
|
expect(result.current.taskPopupsBoardListOnly).toBe(false);
|
||||||
|
expect(result.current.showCostBadgeOnCards).toBe(true);
|
||||||
|
expect(result.current.taskDetailChatFirst).toBe(true);
|
||||||
|
expect(mockFetchSettings).toHaveBeenCalledTimes(settingsFetchesBefore);
|
||||||
|
expect(mockFetchConfig).toHaveBeenCalledTimes(configFetchesBefore);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.setChatMessageLayoutImmediate("bubbles");
|
||||||
|
result.current.setOpenTasksInRightSidebarImmediate(false);
|
||||||
|
result.current.setOpenMobileTasksInPopupImmediate(false);
|
||||||
|
result.current.setTaskPopupsBoardListOnlyImmediate(true);
|
||||||
|
result.current.setShowCostBadgeOnCardsImmediate(false);
|
||||||
|
result.current.setTaskDetailChatFirstImmediate(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.chatMessageLayout).toBe("bubbles");
|
||||||
|
expect(result.current.openTasksInRightSidebar).toBe(false);
|
||||||
|
expect(result.current.openMobileTasksInPopup).toBe(false);
|
||||||
|
expect(result.current.taskPopupsBoardListOnly).toBe(true);
|
||||||
|
expect(result.current.showCostBadgeOnCards).toBe(false);
|
||||||
|
expect(result.current.taskDetailChatFirst).toBe(false);
|
||||||
|
expect(mockFetchSettings).toHaveBeenCalledTimes(settingsFetchesBefore);
|
||||||
|
expect(mockFetchConfig).toHaveBeenCalledTimes(configFetchesBefore);
|
||||||
|
});
|
||||||
|
|
||||||
it("resets chat message layout to bubbles while a new project hydrates", async () => {
|
it("resets chat message layout to bubbles while a new project hydrates", async () => {
|
||||||
mockFetchSettings
|
mockFetchSettings
|
||||||
.mockResolvedValueOnce({ chatMessageLayout: "full-width" } as never)
|
.mockResolvedValueOnce({ chatMessageLayout: "full-width" } as never)
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ export interface UseAppSettingsResult {
|
|||||||
toggleShowQuickChatFAB: () => Promise<void>;
|
toggleShowQuickChatFAB: () => Promise<void>;
|
||||||
setQuickChatButtonModeImmediate: (mode: QuickChatButtonMode) => void;
|
setQuickChatButtonModeImmediate: (mode: QuickChatButtonMode) => void;
|
||||||
setChatMessageLayoutImmediate: (layout: ChatMessageLayout) => void;
|
setChatMessageLayoutImmediate: (layout: ChatMessageLayout) => void;
|
||||||
|
setOpenTasksInRightSidebarImmediate: (enabled: boolean) => void;
|
||||||
|
setOpenMobileTasksInPopupImmediate: (enabled: boolean) => void;
|
||||||
|
setTaskPopupsBoardListOnlyImmediate: (enabled: boolean) => void;
|
||||||
|
setShowCostBadgeOnCardsImmediate: (enabled: boolean) => void;
|
||||||
|
setTaskDetailChatFirstImmediate: (enabled: boolean) => void;
|
||||||
setMobileNavPrimaryItemsImmediate: (items: string[]) => void;
|
setMobileNavPrimaryItemsImmediate: (items: string[]) => void;
|
||||||
toggleAutoReloadOnVersionChange: () => Promise<void>;
|
toggleAutoReloadOnVersionChange: () => Promise<void>;
|
||||||
/** Re-fetches settings from the backend to pick up changes made externally (e.g., by SettingsModal). */
|
/** Re-fetches settings from the backend to pick up changes made externally (e.g., by SettingsModal). */
|
||||||
@@ -338,12 +343,32 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
|||||||
|
|
||||||
const setChatMessageLayoutImmediate = useCallback((layout: ChatMessageLayout) => {
|
const setChatMessageLayoutImmediate = useCallback((layout: ChatMessageLayout) => {
|
||||||
/*
|
/*
|
||||||
FNXC:ChatMessageLayout 2026-08-18-20:27:
|
FNXC:LiveAppearanceSettings 2026-08-19-18:07:
|
||||||
The Appearance draft must update already-mounted ChatView and task-chat surfaces immediately; SettingsModal remains the sole persistence owner.
|
Both overlay and embedded Settings must mirror every mounted Appearance control into the App shell during its input event. These setters intentionally avoid persistence: SettingsModal remains the only debounced writer and its reconciliation remains authoritative.
|
||||||
*/
|
*/
|
||||||
setChatMessageLayout(normalizeChatMessageLayout(layout));
|
setChatMessageLayout(normalizeChatMessageLayout(layout));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const setOpenTasksInRightSidebarImmediate = useCallback((enabled: boolean) => {
|
||||||
|
setOpenTasksInRightSidebar(enabled === true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setOpenMobileTasksInPopupImmediate = useCallback((enabled: boolean) => {
|
||||||
|
setOpenMobileTasksInPopup(enabled === true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setTaskPopupsBoardListOnlyImmediate = useCallback((enabled: boolean) => {
|
||||||
|
setTaskPopupsBoardListOnly(enabled === true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setShowCostBadgeOnCardsImmediate = useCallback((enabled: boolean) => {
|
||||||
|
setShowCostBadgeOnCards(enabled === true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setTaskDetailChatFirstImmediate = useCallback((enabled: boolean) => {
|
||||||
|
setTaskDetailChatFirst(enabled === true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
FNXC:Navigation 2026-07-17-00:00:
|
FNXC:Navigation 2026-07-17-00:00:
|
||||||
The settings draft previews mobile quick-action order and membership in the app shell before Save;
|
The settings draft previews mobile quick-action order and membership in the app shell before Save;
|
||||||
@@ -413,6 +438,11 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
|||||||
toggleShowQuickChatFAB,
|
toggleShowQuickChatFAB,
|
||||||
setQuickChatButtonModeImmediate,
|
setQuickChatButtonModeImmediate,
|
||||||
setChatMessageLayoutImmediate,
|
setChatMessageLayoutImmediate,
|
||||||
|
setOpenTasksInRightSidebarImmediate,
|
||||||
|
setOpenMobileTasksInPopupImmediate,
|
||||||
|
setTaskPopupsBoardListOnlyImmediate,
|
||||||
|
setShowCostBadgeOnCardsImmediate,
|
||||||
|
setTaskDetailChatFirstImmediate,
|
||||||
setMobileNavPrimaryItemsImmediate,
|
setMobileNavPrimaryItemsImmediate,
|
||||||
toggleAutoReloadOnVersionChange,
|
toggleAutoReloadOnVersionChange,
|
||||||
refresh,
|
refresh,
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ function TaskDetailTitleModalHarness() {
|
|||||||
openNewTaskWithDescription: noop,
|
openNewTaskWithDescription: noop,
|
||||||
openWorkflowEditor: noop,
|
openWorkflowEditor: noop,
|
||||||
};
|
};
|
||||||
return <div data-testid="title-host-modal"><NavigationHistoryProvider value={{ pushNav: noop, replaceCurrent: noop, removeNav: noop }}><AppModals projectId="fixture" tasks={[fixtureTask]} projects={[]} currentProject={null} addToast={noop} toasts={[]} removeToast={noop} modalManager={modalManager as never} projectActions={{} as never} taskHandlers={{} as never} taskOperations={{ moveTask: asyncTask, deleteTask: asyncTask, mergeTask: asyncMerge, archiveTask: asyncTask, retryTask: asyncTask, pauseTask: asyncTask, unpauseTask: asyncTask, resetTask: asyncTask, duplicateTask: asyncTask }} deepLink={{ handleDetailClose: noop }} settings={{ prAuthAvailable: false, autoMerge: true, taskDetailChatFirst: false, chatMessageLayout: "bubbles", themeMode: "system", colorTheme: "default", dashboardFontScalePct: 100, shadcnCustomColors: {}, resolvedThemeMode: "light", setThemeMode: noop, setColorTheme: noop, setDashboardFontScalePct: noop, setShadcnCustomColors: noop, setQuickChatButtonModeImmediate: noop, setChatMessageLayoutImmediate: noop, setMobileNavPrimaryItemsImmediate: noop }} /></NavigationHistoryProvider></div>;
|
return <div data-testid="title-host-modal"><NavigationHistoryProvider value={{ pushNav: noop, replaceCurrent: noop, removeNav: noop }}><AppModals projectId="fixture" tasks={[fixtureTask]} projects={[]} currentProject={null} addToast={noop} toasts={[]} removeToast={noop} modalManager={modalManager as never} projectActions={{} as never} taskHandlers={{} as never} taskOperations={{ moveTask: asyncTask, deleteTask: asyncTask, mergeTask: asyncMerge, archiveTask: asyncTask, retryTask: asyncTask, pauseTask: asyncTask, unpauseTask: asyncTask, resetTask: asyncTask, duplicateTask: asyncTask }} deepLink={{ handleDetailClose: noop }} settings={{ prAuthAvailable: false, autoMerge: true, openTasksInRightSidebar: false, openMobileTasksInPopup: false, taskPopupsBoardListOnly: true, showCostBadgeOnCards: false, taskDetailChatFirst: false, chatMessageLayout: "bubbles", themeMode: "system", colorTheme: "default", dashboardFontScalePct: 100, shadcnCustomColors: {}, resolvedThemeMode: "light", setThemeMode: noop, setColorTheme: noop, setDashboardFontScalePct: noop, setShadcnCustomColors: noop, setQuickChatButtonModeImmediate: noop, setChatMessageLayoutImmediate: noop, setOpenTasksInRightSidebarImmediate: noop, setOpenMobileTasksInPopupImmediate: noop, setTaskPopupsBoardListOnlyImmediate: noop, setShowCostBadgeOnCardsImmediate: noop, setTaskDetailChatFirstImmediate: noop, setMobileNavPrimaryItemsImmediate: noop }} /></NavigationHistoryProvider></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function TaskDetailTitleMainPanelHarness() {
|
function TaskDetailTitleMainPanelHarness() {
|
||||||
|
|||||||
Reference in New Issue
Block a user