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`.
This commit is contained in:
psychedelicious 2024-08-30 18:40:51 +10:00
parent 2e84327ca4
commit ab11d9af8e
10 changed files with 138 additions and 157 deletions

View File

@ -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<boolean>] => {
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<boolean>) => {
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]();

View File

@ -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 = () => {
<LoadWorkflowFromGraphModal />
</>
)}
<WorkflowEditorSettings />
{isLoading && <IAINoContentFallback label={t('nodes.loadingNodes')} icon={MdDeviceHub} />}
</Flex>
);

View File

@ -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<HTMLInputElement>(null);
const [searchTerm, setSearchTerm] = useState('');
const addNode = useAddNode();
@ -192,7 +191,7 @@ export const AddNodeCmdk = memo(() => {
return (
<Modal
isOpen={addNodeCmdkIsOpen}
isOpen={addNodeCmdk.isTrue}
onClose={onClose}
useInert={false}
initialFocusRef={inputRef}

View File

@ -14,9 +14,9 @@ import {
ModalHeader,
ModalOverlay,
Switch,
useDisclosure,
} from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { buildUseBoolean } from 'common/hooks/useBoolean';
import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopRightPanel/ReloadSchemaButton';
import {
selectionModeChanged,
@ -32,20 +32,17 @@ import {
shouldSnapToGridChanged,
shouldValidateGraphChanged,
} from 'features/nodes/store/workflowSettingsSlice';
import type { ChangeEvent, ReactNode } from 'react';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { SelectionMode } from 'reactflow';
const formLabelProps: FormLabelProps = { flexGrow: 1 };
export const [useWorkflowEditorSettingsModal] = buildUseBoolean(false);
type Props = {
children: (props: { onOpen: () => 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 })}
<Modal isOpen={isOpen} onClose={onClose} size="2xl" isCentered useInert={false}>
<ModalOverlay />
<ModalContent>
<ModalHeader>{t('nodes.workflowSettings')}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex flexDirection="column" gap={4} py={4}>
<Heading size="sm">{t('parameters.general')}</Heading>
<FormControlGroup orientation="vertical" formLabelProps={formLabelProps}>
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.animatedEdges')}</FormLabel>
<Switch onChange={handleChangeShouldAnimate} isChecked={shouldAnimateEdges} />
</Flex>
<FormHelperText>{t('nodes.animatedEdgesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.snapToGrid')}</FormLabel>
<Switch isChecked={shouldSnapToGrid} onChange={handleChangeShouldSnap} />
</Flex>
<FormHelperText>{t('nodes.snapToGridHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.colorCodeEdges')}</FormLabel>
<Switch isChecked={shouldColorEdges} onChange={handleChangeShouldColor} />
</Flex>
<FormHelperText>{t('nodes.colorCodeEdgesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.fullyContainNodes')}</FormLabel>
<Switch isChecked={selectionMode === SelectionMode.Full} onChange={handleChangeSelectionMode} />
</Flex>
<FormHelperText>{t('nodes.fullyContainNodesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.showEdgeLabels')}</FormLabel>
<Switch isChecked={shouldShowEdgeLabels} onChange={handleChangeShouldShowEdgeLabels} />
</Flex>
<FormHelperText>{t('nodes.showEdgeLabelsHelp')}</FormHelperText>
</FormControl>
<Divider />
<Heading size="sm" pt={4}>
{t('common.advanced')}
</Heading>
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.validateConnections')}</FormLabel>
<Switch isChecked={shouldValidateGraph} onChange={handleChangeShouldValidate} />
</Flex>
<FormHelperText>{t('nodes.validateConnectionsHelp')}</FormHelperText>
</FormControl>
<Divider />
</FormControlGroup>
<ReloadNodeTemplatesButton />
</Flex>
</ModalBody>
</ModalContent>
</Modal>
</>
<Modal isOpen={modal.isTrue} onClose={modal.setFalse} size="2xl" isCentered useInert={false}>
<ModalOverlay />
<ModalContent>
<ModalHeader>{t('nodes.workflowSettings')}</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex flexDirection="column" gap={4} py={4}>
<Heading size="sm">{t('parameters.general')}</Heading>
<FormControlGroup orientation="vertical" formLabelProps={formLabelProps}>
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.animatedEdges')}</FormLabel>
<Switch onChange={handleChangeShouldAnimate} isChecked={shouldAnimateEdges} />
</Flex>
<FormHelperText>{t('nodes.animatedEdgesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.snapToGrid')}</FormLabel>
<Switch isChecked={shouldSnapToGrid} onChange={handleChangeShouldSnap} />
</Flex>
<FormHelperText>{t('nodes.snapToGridHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.colorCodeEdges')}</FormLabel>
<Switch isChecked={shouldColorEdges} onChange={handleChangeShouldColor} />
</Flex>
<FormHelperText>{t('nodes.colorCodeEdgesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.fullyContainNodes')}</FormLabel>
<Switch isChecked={selectionMode === SelectionMode.Full} onChange={handleChangeSelectionMode} />
</Flex>
<FormHelperText>{t('nodes.fullyContainNodesHelp')}</FormHelperText>
</FormControl>
<Divider />
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.showEdgeLabels')}</FormLabel>
<Switch isChecked={shouldShowEdgeLabels} onChange={handleChangeShouldShowEdgeLabels} />
</Flex>
<FormHelperText>{t('nodes.showEdgeLabelsHelp')}</FormHelperText>
</FormControl>
<Divider />
<Heading size="sm" pt={4}>
{t('common.advanced')}
</Heading>
<FormControl>
<Flex w="full">
<FormLabel>{t('nodes.validateConnections')}</FormLabel>
<Switch isChecked={shouldValidateGraph} onChange={handleChangeShouldValidate} />
</Flex>
<FormHelperText>{t('nodes.validateConnectionsHelp')}</FormHelperText>
</FormControl>
<Divider />
</FormControlGroup>
<ReloadNodeTemplatesButton />
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
};

View File

@ -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<OnConnectStart>(
(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;

View File

@ -444,8 +444,7 @@ export const $didUpdateEdge = atom(false);
export const $lastEdgeUpdateMouseEvent = atom<MouseEvent | null>(null);
export const $viewport = atom<Viewport>({ 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 => {

View File

@ -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,

View File

@ -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 = () => {
<>
<Modal
closeOnOverlayClick={false}
isOpen={isOpen}
isOpen={refreshModal.isTrue}
onClose={refreshModal.setFalse}
isCentered
closeOnEsc={false}

View File

@ -14,7 +14,6 @@ import {
Switch,
Text,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
@ -42,7 +41,6 @@ import {
} from 'features/system/store/systemSlice';
import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
import { atom } from 'nanostores';
import type { ChangeEvent } from 'react';
import { memo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
@ -68,8 +66,7 @@ type SettingsModalProps = {
config?: ConfigOptions;
};
const $settingsModal = atom(false);
export const useSettingsModal = buildUseBoolean($settingsModal);
export const [useSettingsModal] = buildUseBoolean(false);
const SettingsModal = ({ config = defaultConfig }: SettingsModalProps) => {
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 (
<Modal isOpen={settingsModalIsOpen} onClose={settingsModal.setFalse} size="2xl" isCentered useInert={false}>
<Modal isOpen={settingsModal.isTrue} onClose={settingsModal.setFalse} size="2xl" isCentered useInert={false}>
<ModalOverlay />
<ModalContent maxH="80vh" h="68rem">
<ModalHeader bg="none">{t('common.settingsLabel')}</ModalHeader>

View File

@ -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 (
<WorkflowEditorSettings>
{({ onOpen }) => (
<MenuItem as="button" icon={<RiSettings4Line />} onClick={onOpen}>
{t('nodes.workflowSettings')}
</MenuItem>
)}
</WorkflowEditorSettings>
<MenuItem as="button" icon={<RiSettings4Line />} onClick={modal.setTrue}>
{t('nodes.workflowSettings')}
</MenuItem>
);
};