From ab11d9af8eed8e6d49e748e8382017283d159d3b Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:40:51 +1000 Subject: [PATCH] fix(ui): modals not staying open TBH not sure exactly why this broke. Fixed by rollback back the use of a render prop in favor of global state. Also revised the API of `useBoolean` and `buildUseBoolean`. --- .../web/src/common/hooks/useBoolean.ts | 87 +++++----- .../features/nodes/components/NodeEditor.tsx | 2 + .../flow/AddNodeCmdk/AddNodeCmdk.tsx | 3 +- .../TopRightPanel/WorkflowEditorSettings.tsx | 149 +++++++++--------- .../src/features/nodes/hooks/useConnection.ts | 7 +- .../src/features/nodes/store/nodesSlice.ts | 3 +- .../ClearQueueConfirmationAlertDialog.tsx | 7 +- .../SettingsModal/RefreshAfterResetModal.tsx | 12 +- .../SettingsModal/SettingsModal.tsx | 12 +- .../WorkflowLibraryMenu/SettingsMenuItem.tsx | 13 +- 10 files changed, 138 insertions(+), 157 deletions(-) diff --git a/invokeai/frontend/web/src/common/hooks/useBoolean.ts b/invokeai/frontend/web/src/common/hooks/useBoolean.ts index 46e96d424c..1f1621097e 100644 --- a/invokeai/frontend/web/src/common/hooks/useBoolean.ts +++ b/invokeai/frontend/web/src/common/hooks/useBoolean.ts @@ -1,52 +1,53 @@ +import { useStore } from '@nanostores/react'; import type { WritableAtom } from 'nanostores'; -import { useCallback, useMemo, useState } from 'react'; +import { atom } from 'nanostores'; -export const useBoolean = (initialValue: boolean) => { - const [isTrue, set] = useState(initialValue); - const setTrue = useCallback(() => set(true), []); - const setFalse = useCallback(() => set(false), []); - const toggle = useCallback(() => set((v) => !v), []); +type UseBoolean = { + isTrue: boolean; + setTrue: () => void; + setFalse: () => void; + set: (value: boolean) => void; + toggle: () => void; +}; - const api = useMemo( - () => ({ +/** + * Creates a hook to manage a boolean state. The boolean is stored in a nanostores atom. + * Returns a tuple containing the hook and the atom. Use this for global boolean state. + * @param initialValue Initial value of the boolean + */ +export const buildUseBoolean = (initialValue: boolean): [() => UseBoolean, WritableAtom] => { + const $boolean = atom(initialValue); + + const setTrue = () => { + $boolean.set(true); + }; + const setFalse = () => { + $boolean.set(false); + }; + const set = (value: boolean) => { + $boolean.set(value); + }; + const toggle = () => { + $boolean.set(!$boolean.get()); + }; + + const useBoolean = () => { + const isTrue = useStore($boolean); + + return { isTrue, - set, setTrue, setFalse, + set, toggle, - }), - [isTrue, set, setTrue, setFalse, toggle] - ); - - return api; -}; - -export const buildUseBoolean = ($boolean: WritableAtom) => { - return () => { - const setTrue = useCallback(() => { - $boolean.set(true); - }, []); - const setFalse = useCallback(() => { - $boolean.set(false); - }, []); - const set = useCallback((value: boolean) => { - $boolean.set(value); - }, []); - const toggle = useCallback(() => { - $boolean.set(!$boolean.get()); - }, []); - - const api = useMemo( - () => ({ - setTrue, - setFalse, - set, - toggle, - $boolean, - }), - [set, setFalse, setTrue, toggle] - ); - - return api; + }; }; + + return [useBoolean, $boolean] as const; }; + +/** + * Hook to manage a boolean state. Use this for a local boolean state. + * @param initialValue Initial value of the boolean + */ +export const useBoolean = (initialValue: boolean) => buildUseBoolean(initialValue)[0](); diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 18ac2abdc4..0a118b9d44 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -4,6 +4,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { AddNodeCmdk } from 'features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk'; import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel'; +import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings'; import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal'; import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog'; import { memo } from 'react'; @@ -39,6 +40,7 @@ const NodeEditor = () => { )} + {isLoading && } ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx index b34ca3992b..ffab56cf03 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodeCmdk/AddNodeCmdk.tsx @@ -163,7 +163,6 @@ const cmdkRootSx: SystemStyleObject = { export const AddNodeCmdk = memo(() => { const { t } = useTranslation(); const addNodeCmdk = useAddNodeCmdk(); - const addNodeCmdkIsOpen = useStore(addNodeCmdk.$boolean); const inputRef = useRef(null); const [searchTerm, setSearchTerm] = useState(''); const addNode = useAddNode(); @@ -192,7 +191,7 @@ export const AddNodeCmdk = memo(() => { return ( void }) => ReactNode; -}; - -const WorkflowEditorSettings = ({ children }: Props) => { - const { isOpen, onOpen, onClose } = useDisclosure(); +const WorkflowEditorSettings = () => { const dispatch = useAppDispatch(); + const modal = useWorkflowEditorSettingsModal(); const shouldSnapToGrid = useAppSelector(selectShouldSnapToGrid); const selectionMode = useAppSelector(selectSelectionMode); @@ -99,76 +96,72 @@ const WorkflowEditorSettings = ({ children }: Props) => { const { t } = useTranslation(); return ( - <> - {children({ onOpen })} - - - - - {t('nodes.workflowSettings')} - - - - {t('parameters.general')} - - - - {t('nodes.animatedEdges')} - - - {t('nodes.animatedEdgesHelp')} - - - - - {t('nodes.snapToGrid')} - - - {t('nodes.snapToGridHelp')} - - - - - {t('nodes.colorCodeEdges')} - - - {t('nodes.colorCodeEdgesHelp')} - - - - - {t('nodes.fullyContainNodes')} - - - {t('nodes.fullyContainNodesHelp')} - - - - - {t('nodes.showEdgeLabels')} - - - {t('nodes.showEdgeLabelsHelp')} - - - - {t('common.advanced')} - - - - {t('nodes.validateConnections')} - - - {t('nodes.validateConnectionsHelp')} - - - - - - - - - + + + + {t('nodes.workflowSettings')} + + + + {t('parameters.general')} + + + + {t('nodes.animatedEdges')} + + + {t('nodes.animatedEdgesHelp')} + + + + + {t('nodes.snapToGrid')} + + + {t('nodes.snapToGridHelp')} + + + + + {t('nodes.colorCodeEdges')} + + + {t('nodes.colorCodeEdgesHelp')} + + + + + {t('nodes.fullyContainNodes')} + + + {t('nodes.fullyContainNodesHelp')} + + + + + {t('nodes.showEdgeLabels')} + + + {t('nodes.showEdgeLabelsHelp')} + + + + {t('common.advanced')} + + + + {t('nodes.validateConnections')} + + + {t('nodes.validateConnectionsHelp')} + + + + + + + + ); }; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index c41ef9f689..dffd144730 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -2,12 +2,12 @@ import { useStore } from '@nanostores/react'; import { useAppStore } from 'app/store/storeHooks'; import { $mouseOverNode } from 'features/nodes/hooks/useMouseOverNode'; import { - $addNodeCmdk, $didUpdateEdge, $edgePendingUpdate, $pendingConnection, $templates, edgesChanged, + useAddNodeCmdk, } from 'features/nodes/store/nodesSlice'; import { selectNodes, selectNodesSlice } from 'features/nodes/store/selectors'; import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; @@ -21,6 +21,7 @@ export const useConnection = () => { const store = useAppStore(); const templates = useStore($templates); const updateNodeInternals = useUpdateNodeInternals(); + const addNodeCmdk = useAddNodeCmdk(); const onConnectStart = useCallback( (event, { nodeId, handleId, handleType }) => { @@ -107,9 +108,9 @@ export const useConnection = () => { $pendingConnection.set(null); } else { // The mouse is not over a node - we should open the add node popover - $addNodeCmdk.set(true); + addNodeCmdk.setTrue(); } - }, [store, templates, updateNodeInternals]); + }, [addNodeCmdk, store, templates, updateNodeInternals]); const api = useMemo(() => ({ onConnectStart, onConnect, onConnectEnd }), [onConnectStart, onConnect, onConnectEnd]); return api; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index bdcc7ae815..b67ecd0eb7 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -444,8 +444,7 @@ export const $didUpdateEdge = atom(false); export const $lastEdgeUpdateMouseEvent = atom(null); export const $viewport = atom({ x: 0, y: 0, zoom: 1 }); -export const $addNodeCmdk = atom(false); -export const useAddNodeCmdk = buildUseBoolean($addNodeCmdk); +export const [useAddNodeCmdk, $addNodeCmdk] = buildUseBoolean(false); /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateNodesState = (state: any): any => { diff --git a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx index 8af9ba56e5..1fbd805388 100644 --- a/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx +++ b/invokeai/frontend/web/src/features/queue/components/ClearQueueConfirmationAlertDialog.tsx @@ -5,19 +5,16 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { buildUseBoolean } from 'common/hooks/useBoolean'; import { listCursorChanged, listPriorityChanged } from 'features/queue/store/queueSlice'; import { toast } from 'features/toast/toast'; -import { atom } from 'nanostores'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useClearQueueMutation, useGetQueueStatusQuery } from 'services/api/endpoints/queue'; -const $boolean = atom(false); -const useClearQueueConfirmationAlertDialog = buildUseBoolean($boolean); +const [useClearQueueConfirmationAlertDialog] = buildUseBoolean(false); export const useClearQueue = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const dialog = useClearQueueConfirmationAlertDialog(); - const isOpen = useStore(dialog.$boolean); const { data: queueStatus } = useGetQueueStatusQuery(); const isConnected = useStore($isConnected); const [trigger, { isLoading }] = useClearQueueMutation({ @@ -51,7 +48,7 @@ export const useClearQueue = () => { return { clearQueue, - isOpen, + isOpen: dialog.isTrue, openDialog: dialog.setTrue, closeDialog: dialog.setFalse, isLoading, diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/RefreshAfterResetModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/RefreshAfterResetModal.tsx index 2530b705d1..ee49e26f24 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/RefreshAfterResetModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/RefreshAfterResetModal.tsx @@ -8,31 +8,27 @@ import { ModalOverlay, Text, } from '@invoke-ai/ui-library'; -import { useStore } from '@nanostores/react'; import { buildUseBoolean } from 'common/hooks/useBoolean'; -import { atom } from 'nanostores'; import { memo, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -const $refreshAfterResetModalState = atom(false); -export const useRefreshAfterResetModal = buildUseBoolean($refreshAfterResetModalState); +export const [useRefreshAfterResetModal] = buildUseBoolean(false); const RefreshAfterResetModal = () => { const { t } = useTranslation(); const [countdown, setCountdown] = useState(3); const refreshModal = useRefreshAfterResetModal(); - const isOpen = useStore(refreshModal.$boolean); useEffect(() => { - if (!isOpen) { + if (!refreshModal.isTrue) { return; } const i = window.setInterval(() => setCountdown((prev) => prev - 1), 1000); return () => { window.clearInterval(i); }; - }, [isOpen]); + }, [refreshModal.isTrue]); useEffect(() => { if (countdown <= 0) { @@ -44,7 +40,7 @@ const RefreshAfterResetModal = () => { <> { const dispatch = useAppDispatch(); @@ -96,7 +93,6 @@ const SettingsModal = ({ config = defaultConfig }: SettingsModalProps) => { refetchIntermediatesCount, } = useClearIntermediates(Boolean(config?.shouldShowClearIntermediates)); const settingsModal = useSettingsModal(); - const settingsModalIsOpen = useStore(settingsModal.$boolean); const refreshModal = useRefreshAfterResetModal(); const shouldUseCpuNoise = useAppSelector(selectShouldUseCPUNoise); @@ -110,10 +106,10 @@ const SettingsModal = ({ config = defaultConfig }: SettingsModalProps) => { const clearStorage = useClearStorage(); useEffect(() => { - if (settingsModalIsOpen && Boolean(config?.shouldShowClearIntermediates)) { + if (settingsModal.isTrue && Boolean(config?.shouldShowClearIntermediates)) { refetchIntermediatesCount(); } - }, [config?.shouldShowClearIntermediates, refetchIntermediatesCount, settingsModalIsOpen]); + }, [config?.shouldShowClearIntermediates, refetchIntermediatesCount, settingsModal.isTrue]); const handleClickResetWebUI = useCallback(() => { clearStorage(); @@ -165,7 +161,7 @@ const SettingsModal = ({ config = defaultConfig }: SettingsModalProps) => { ); return ( - + {t('common.settingsLabel')} diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem.tsx index 157e6abc9b..c58dcf902d 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SettingsMenuItem.tsx @@ -1,20 +1,17 @@ import { MenuItem } from '@invoke-ai/ui-library'; -import WorkflowEditorSettings from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings'; +import { useWorkflowEditorSettingsModal } from 'features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { RiSettings4Line } from 'react-icons/ri'; const DownloadWorkflowMenuItem = () => { const { t } = useTranslation(); + const modal = useWorkflowEditorSettingsModal(); return ( - - {({ onOpen }) => ( - } onClick={onOpen}> - {t('nodes.workflowSettings')} - - )} - + } onClick={modal.setTrue}> + {t('nodes.workflowSettings')} + ); };