From ee9866872700d8df2e20afde0d690638ed8dc0db Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Wed, 19 Mar 2025 18:17:30 -0500 Subject: [PATCH] Allow uploads from CMDK menu --- src/admin/AdminAppMenu.tsx | 8 +--- src/components/cmdk/CommandKClient.tsx | 54 ++++++++++++++++++-------- src/components/more/MoreMenuItem.tsx | 18 ++++++--- src/state/AppStateProvider.tsx | 12 ++++-- 4 files changed, 59 insertions(+), 33 deletions(-) diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index 39377e56..5bdc8521 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -59,13 +59,7 @@ export default function AdminAppMenu({ size={15} className="translate-x-[0.5px] translate-y-[0.5px]" />, - action: () => new Promise(resolve => { - if (startUpload) { - startUpload(() => resolve()); - } else { - resolve(); - } - }), + action: startUpload, }]; if (uploadsCount) { diff --git a/src/components/cmdk/CommandKClient.tsx b/src/components/cmdk/CommandKClient.tsx index eeda69e6..75b5b8e8 100644 --- a/src/components/cmdk/CommandKClient.tsx +++ b/src/components/cmdk/CommandKClient.tsx @@ -86,7 +86,7 @@ type CommandKItem = { annotation?: ReactNode annotationAria?: string path?: string - action?: () => void | Promise + action?: () => void | Promise } type CommandKSection = { @@ -124,6 +124,7 @@ export default function CommandKClient({ isUserSignedIn, clearAuthStateAndRedirect, isCommandKOpen: isOpen, + startUpload, photosCountHidden, uploadsCount, tagsCount, @@ -151,19 +152,21 @@ export default function CommandKClient({ const isOpenRef = useRef(isOpen); + // Manage action/path waiting state + const [keyWaiting, setKeyWaiting] = useState(); const [isPending, startTransition] = useTransition(); - const [keyPending, setKeyPending] = useState(); - const shouldCloseAfterPending = useRef(false); - + const [isWaitingForAction, setIsWaitingForAction] = useState(false); + const isWaiting = isPending || isWaitingForAction; + const shouldCloseAfterWaiting = useRef(false); useEffect(() => { - if (!isPending) { - setKeyPending(undefined); - if (shouldCloseAfterPending.current) { + if (!isWaiting) { + setKeyWaiting(undefined); + if (shouldCloseAfterWaiting.current) { setIsOpen?.(false); - shouldCloseAfterPending.current = false; + shouldCloseAfterWaiting.current = false; } } - }, [isPending, setIsOpen]); + }, [isWaiting, setIsOpen]); // Raw query values const [queryLiveRaw, setQueryLive] = useState(''); @@ -427,6 +430,11 @@ export default function CommandKClient({ }; if (isUserSignedIn) { + adminSection.items.push({ + label: 'Upload Photos', + annotation: , + action: startUpload, + }); if (uploadsCount) { adminSection.items.push({ label: `Uploads (${uploadsCount})`, @@ -558,7 +566,6 @@ export default function CommandKClient({ 'max-h-48 sm:max-h-72', 'mx-3 md:mx-4', 'pt-3 md:pt-4', - 'pb-4 md:pb-5', )} style={{ // eslint-disable-next-line max-len maskImage: 'linear-gradient(to bottom, transparent, black 20px, black calc(100% - 20px), transparent)', @@ -613,14 +620,24 @@ export default function CommandKClient({ keywords={keywords} onSelect={() => { if (action) { - action(); - if (!path) { setIsOpen?.(false); } + const result = action(); + if (result instanceof Promise) { + setKeyWaiting(key); + setIsWaitingForAction(true); + result.then(shouldClose => { + shouldCloseAfterWaiting.current = + shouldClose === true; + setIsWaitingForAction(false); + }); + } else { + if (!path) { setIsOpen?.(false); } + } } if (path) { if (path !== pathname) { - setKeyPending(key); + setKeyWaiting(key); + shouldCloseAfterWaiting.current = true; startTransition(() => { - shouldCloseAfterPending.current = true; router.push(path, { scroll: true }); }); } else { @@ -631,14 +648,17 @@ export default function CommandKClient({ accessory={accessory} annotation={annotation} annotationAria={annotationAria} - loading={key === keyPending} - disabled={isPending && key !== keyPending} + loading={key === keyWaiting} + disabled={isPending && key !== keyWaiting} />; })} )} {footer && !queryLive && -
+
{footer}
} diff --git a/src/components/more/MoreMenuItem.tsx b/src/components/more/MoreMenuItem.tsx index a37ddf61..f021b70b 100644 --- a/src/components/more/MoreMenuItem.tsx +++ b/src/components/more/MoreMenuItem.tsx @@ -26,7 +26,7 @@ export default function MoreMenuItem({ href?: string hrefDownloadName?: string className?: string - action?: () => Promise | void + action?: () => Promise | void dismissMenu?: () => void shouldPreventDefault?: boolean }) { @@ -68,10 +68,18 @@ export default function MoreMenuItem({ const result = action(); if (result instanceof Promise) { setIsLoading(true); - await result.finally(() => { - setIsLoading(false); - dismissMenu?.(); - }); + await result + .then(shouldClose => { + if ( + shouldClose === undefined || + shouldClose === true + ) { + dismissMenu?.(); + } + }) + .finally(() => { + setIsLoading(false); + }); } else { dismissMenu?.(); } diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index 95a66d2a..92d7c1fb 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -135,14 +135,18 @@ export default function AppStateProvider({ if (isPathAdmin(pathname)) { router.push(PATH_SIGN_IN); } }, [router, pathname]); - const startUpload = useCallback((onStart?: () => void) => { + // Returns false when an upload is cancelled + const startUpload = useCallback(() => new Promise(resolve => { if (uploadInputRef.current) { uploadInputRef.current.value = ''; uploadInputRef.current.click(); - uploadInputRef.current.oninput = onStart ?? null; - uploadInputRef.current.oncancel = onStart ?? null; + uploadInputRef.current.oninput = () => resolve(true); + uploadInputRef.current.oncancel = () => resolve(false); + } else { + resolve(false); } - }, []); + }) + , []); const setUploadState = useCallback((uploadState: Partial) => { _setUploadState(prev => ({ ...prev, ...uploadState })); }, []);