diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/AppsQuickMenu.tsx b/frontend/apps/artcraft/app/src/components/signaled/TopBar/AppsQuickMenu.tsx index 53500c32b7..98da07bbbd 100644 --- a/frontend/apps/artcraft/app/src/components/signaled/TopBar/AppsQuickMenu.tsx +++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/AppsQuickMenu.tsx @@ -1,19 +1,21 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; import { - GENERATE_APPS, - EDIT_APPS, + useGenerateApps, + useEditApps, getBadgeStyles, goToApp, } from "~/config/appMenu"; export const AppsQuickMenu = () => { + const generateApps = useGenerateApps(); + const editApps = useEditApps(); return (

Generate

- {GENERATE_APPS.map((app) => ( + {generateApps.map((app) => (
)} - {tabStore.activeTabId == "MOODBOARD" && ( + {tabStore.activeTabId == "MOODBOARD" && storyboardPageEnabled && (
diff --git a/frontend/apps/artcraft/app/src/pages/PageMoodboard/MoodboardStore.ts b/frontend/apps/artcraft/app/src/pages/PageMoodboard/MoodboardStore.ts index d0ac12c581..bd40e46fb0 100644 --- a/frontend/apps/artcraft/app/src/pages/PageMoodboard/MoodboardStore.ts +++ b/frontend/apps/artcraft/app/src/pages/PageMoodboard/MoodboardStore.ts @@ -21,6 +21,7 @@ interface MoodboardState { selectBoard: (id: string) => void; updateBoard: (id: string, patch: Partial>) => void; reorderBoards: (fromIndex: number, toIndex: number) => void; + reset: () => void; } const syncShotNumbers = (boards: Board[]): Board[] => @@ -77,4 +78,8 @@ export const useMoodboardStore = create((set, get) => ({ return { boards: syncShotNumbers(arr) }; }); }, + + reset: () => { + set({ boards: [], selectedBoardId: null }); + }, })); diff --git a/frontend/libs/components/settings-modal/src/index.ts b/frontend/libs/components/settings-modal/src/index.ts index a00baad6df..b7ea9ed55c 100644 --- a/frontend/libs/components/settings-modal/src/index.ts +++ b/frontend/libs/components/settings-modal/src/index.ts @@ -1,2 +1,6 @@ export * from './lib/settings-modal'; +export { + useExperimentalStore, + useStoryboardPageEnabled, +} from './lib/experimental-store'; diff --git a/frontend/libs/components/settings-modal/src/lib/experimental-store.ts b/frontend/libs/components/settings-modal/src/lib/experimental-store.ts index e76d96de4e..637e9d8727 100644 --- a/frontend/libs/components/settings-modal/src/lib/experimental-store.ts +++ b/frontend/libs/components/settings-modal/src/lib/experimental-store.ts @@ -1,23 +1,24 @@ import { create } from "zustand"; -const STORAGE_KEY = "artcraft_experimental_enabled"; +const ENABLED_STORAGE_KEY = "artcraft_experimental_enabled"; +const STORYBOARD_STORAGE_KEY = "artcraft_experimental_storyboard_page"; -const readInitial = (): boolean => { +const readBoolFlag = (key: string): boolean => { if (typeof window === "undefined") return false; try { - return window.localStorage.getItem(STORAGE_KEY) === "true"; + return window.localStorage.getItem(key) === "true"; } catch { return false; } }; -const writeStorage = (enabled: boolean) => { +const writeBoolFlag = (key: string, enabled: boolean) => { if (typeof window === "undefined") return; try { if (enabled) { - window.localStorage.setItem(STORAGE_KEY, "true"); + window.localStorage.setItem(key, "true"); } else { - window.localStorage.removeItem(STORAGE_KEY); + window.localStorage.removeItem(key); } } catch { // ignore storage failures @@ -26,18 +27,30 @@ const writeStorage = (enabled: boolean) => { interface ExperimentalState { enabled: boolean; + storyboardPageEnabled: boolean; enable: () => void; disable: () => void; + setStoryboardPageEnabled: (enabled: boolean) => void; } export const useExperimentalStore = create((set) => ({ - enabled: readInitial(), + enabled: readBoolFlag(ENABLED_STORAGE_KEY), + storyboardPageEnabled: readBoolFlag(STORYBOARD_STORAGE_KEY), enable: () => { - writeStorage(true); + writeBoolFlag(ENABLED_STORAGE_KEY, true); set({ enabled: true }); }, disable: () => { - writeStorage(false); - set({ enabled: false }); + // Resetting experimental clears every gated feature flag too. + writeBoolFlag(ENABLED_STORAGE_KEY, false); + writeBoolFlag(STORYBOARD_STORAGE_KEY, false); + set({ enabled: false, storyboardPageEnabled: false }); + }, + setStoryboardPageEnabled: (enabled: boolean) => { + writeBoolFlag(STORYBOARD_STORAGE_KEY, enabled); + set({ storyboardPageEnabled: enabled }); }, })); + +export const useStoryboardPageEnabled = () => + useExperimentalStore((s) => s.enabled && s.storyboardPageEnabled); diff --git a/frontend/libs/components/settings-modal/src/lib/panes/ExperimentalSettingsPane.tsx b/frontend/libs/components/settings-modal/src/lib/panes/ExperimentalSettingsPane.tsx index fe1561a3be..7c2d562b7b 100644 --- a/frontend/libs/components/settings-modal/src/lib/panes/ExperimentalSettingsPane.tsx +++ b/frontend/libs/components/settings-modal/src/lib/panes/ExperimentalSettingsPane.tsx @@ -1,15 +1,73 @@ +import { useState } from "react"; import { Label } from "@storyteller/ui-label"; +import { Switch } from "@storyteller/ui-switch"; +import { gtagEvent } from "@storyteller/google-analytics"; +import { useExperimentalStore } from "../experimental-store"; +import { ExperimentalConfirmModal } from "../ExperimentalConfirmModal"; + +interface Props { + // Called immediately before the storyboard flag flips off, so the host app + // can reset page state and navigate away from the soon-to-be-hidden tab. + onStoryboardPageDisable?: () => void; +} + +export const ExperimentalSettingsPane = ({ onStoryboardPageDisable }: Props) => { + const storyboardPageEnabled = useExperimentalStore( + (s) => s.storyboardPageEnabled, + ); + const setStoryboardPageEnabled = useExperimentalStore( + (s) => s.setStoryboardPageEnabled, + ); + const [isDisableConfirmOpen, setIsDisableConfirmOpen] = useState(false); + + const fireToggleEvent = (enabled: boolean) => { + gtagEvent("toggle_experimental_feature", { + feature: "storyboard_page", + enabled: String(enabled), + }); + }; + + const handleStoryboardToggle = (enabled: boolean) => { + if (enabled) { + setStoryboardPageEnabled(true); + fireToggleEvent(true); + } else { + setIsDisableConfirmOpen(true); + } + }; + + const handleConfirmDisable = () => { + onStoryboardPageDisable?.(); + setStoryboardPageEnabled(false); + fireToggleEvent(false); + setIsDisableConfirmOpen(false); + }; -export const ExperimentalSettingsPane = () => { return ( -
-
- -

- This section is reserved for in-development features. There are no - experimental options available right now. -

+ <> +
+
+
+ +

+ Show the Storyboard page in the apps menu. In-development; expect + rough edges. +

+
+ +
-
+ setIsDisableConfirmOpen(false)} + onConfirm={handleConfirmDisable} + title="Disable Storyboard page?" + text="The Storyboard page will be reset and any unsaved changes will be lost." + confirmText="Disable" + /> + ); }; diff --git a/frontend/libs/components/settings-modal/src/lib/settings-modal.tsx b/frontend/libs/components/settings-modal/src/lib/settings-modal.tsx index 911e33ddc8..5e05b66f4e 100644 --- a/frontend/libs/components/settings-modal/src/lib/settings-modal.tsx +++ b/frontend/libs/components/settings-modal/src/lib/settings-modal.tsx @@ -28,6 +28,7 @@ interface SettingsModalProps { isOpen: boolean; onClose: () => void; globalAccountLogoutCallback: () => void; + onStoryboardPageDisable?: () => void; initialSection?: SettingsSection; } @@ -45,6 +46,7 @@ export const SettingsModal = ({ isOpen, onClose, globalAccountLogoutCallback, + onStoryboardPageDisable, initialSection = "general", }: SettingsModalProps) => { const [selectedSection, setSelectedSection] = @@ -110,7 +112,11 @@ export const SettingsModal = ({ case "billing": return ; case "experimental": - return ; + return ( + + ); } };