Allow uploads from CMDK menu

This commit is contained in:
Sam Becker 2025-03-19 18:17:30 -05:00
parent 20976aefa7
commit ee98668727
4 changed files with 59 additions and 33 deletions

View File

@ -59,13 +59,7 @@ export default function AdminAppMenu({
size={15} size={15}
className="translate-x-[0.5px] translate-y-[0.5px]" className="translate-x-[0.5px] translate-y-[0.5px]"
/>, />,
action: () => new Promise(resolve => { action: startUpload,
if (startUpload) {
startUpload(() => resolve());
} else {
resolve();
}
}),
}]; }];
if (uploadsCount) { if (uploadsCount) {

View File

@ -86,7 +86,7 @@ type CommandKItem = {
annotation?: ReactNode annotation?: ReactNode
annotationAria?: string annotationAria?: string
path?: string path?: string
action?: () => void | Promise<void> action?: () => void | Promise<void | boolean>
} }
type CommandKSection = { type CommandKSection = {
@ -124,6 +124,7 @@ export default function CommandKClient({
isUserSignedIn, isUserSignedIn,
clearAuthStateAndRedirect, clearAuthStateAndRedirect,
isCommandKOpen: isOpen, isCommandKOpen: isOpen,
startUpload,
photosCountHidden, photosCountHidden,
uploadsCount, uploadsCount,
tagsCount, tagsCount,
@ -151,19 +152,21 @@ export default function CommandKClient({
const isOpenRef = useRef(isOpen); const isOpenRef = useRef(isOpen);
// Manage action/path waiting state
const [keyWaiting, setKeyWaiting] = useState<string>();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [keyPending, setKeyPending] = useState<string>(); const [isWaitingForAction, setIsWaitingForAction] = useState(false);
const shouldCloseAfterPending = useRef(false); const isWaiting = isPending || isWaitingForAction;
const shouldCloseAfterWaiting = useRef(false);
useEffect(() => { useEffect(() => {
if (!isPending) { if (!isWaiting) {
setKeyPending(undefined); setKeyWaiting(undefined);
if (shouldCloseAfterPending.current) { if (shouldCloseAfterWaiting.current) {
setIsOpen?.(false); setIsOpen?.(false);
shouldCloseAfterPending.current = false; shouldCloseAfterWaiting.current = false;
} }
} }
}, [isPending, setIsOpen]); }, [isWaiting, setIsOpen]);
// Raw query values // Raw query values
const [queryLiveRaw, setQueryLive] = useState(''); const [queryLiveRaw, setQueryLive] = useState('');
@ -427,6 +430,11 @@ export default function CommandKClient({
}; };
if (isUserSignedIn) { if (isUserSignedIn) {
adminSection.items.push({
label: 'Upload Photos',
annotation: <IconLock narrow />,
action: startUpload,
});
if (uploadsCount) { if (uploadsCount) {
adminSection.items.push({ adminSection.items.push({
label: `Uploads (${uploadsCount})`, label: `Uploads (${uploadsCount})`,
@ -558,7 +566,6 @@ export default function CommandKClient({
'max-h-48 sm:max-h-72', 'max-h-48 sm:max-h-72',
'mx-3 md:mx-4', 'mx-3 md:mx-4',
'pt-3 md:pt-4', 'pt-3 md:pt-4',
'pb-4 md:pb-5',
)} style={{ )} style={{
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
maskImage: 'linear-gradient(to bottom, transparent, black 20px, black calc(100% - 20px), transparent)', maskImage: 'linear-gradient(to bottom, transparent, black 20px, black calc(100% - 20px), transparent)',
@ -613,14 +620,24 @@ export default function CommandKClient({
keywords={keywords} keywords={keywords}
onSelect={() => { onSelect={() => {
if (action) { if (action) {
action(); const result = action();
if (!path) { setIsOpen?.(false); } 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) {
if (path !== pathname) { if (path !== pathname) {
setKeyPending(key); setKeyWaiting(key);
shouldCloseAfterWaiting.current = true;
startTransition(() => { startTransition(() => {
shouldCloseAfterPending.current = true;
router.push(path, { scroll: true }); router.push(path, { scroll: true });
}); });
} else { } else {
@ -631,14 +648,17 @@ export default function CommandKClient({
accessory={accessory} accessory={accessory}
annotation={annotation} annotation={annotation}
annotationAria={annotationAria} annotationAria={annotationAria}
loading={key === keyPending} loading={key === keyWaiting}
disabled={isPending && key !== keyPending} disabled={isPending && key !== keyWaiting}
/>; />;
})} })}
</Command.Group>)} </Command.Group>)}
</div> </div>
{footer && !queryLive && {footer && !queryLive &&
<div className="text-center text-base text-dim pt-3 sm:pt-4"> <div className={clsx(
'text-center text-base text-dim pt-3 sm:pt-4',
'pb-5 md:pb-6',
)}>
{footer} {footer}
</div>} </div>}
</Command.List> </Command.List>

View File

@ -26,7 +26,7 @@ export default function MoreMenuItem({
href?: string href?: string
hrefDownloadName?: string hrefDownloadName?: string
className?: string className?: string
action?: () => Promise<void> | void action?: () => Promise<void | boolean> | void
dismissMenu?: () => void dismissMenu?: () => void
shouldPreventDefault?: boolean shouldPreventDefault?: boolean
}) { }) {
@ -68,10 +68,18 @@ export default function MoreMenuItem({
const result = action(); const result = action();
if (result instanceof Promise) { if (result instanceof Promise) {
setIsLoading(true); setIsLoading(true);
await result.finally(() => { await result
setIsLoading(false); .then(shouldClose => {
dismissMenu?.(); if (
}); shouldClose === undefined ||
shouldClose === true
) {
dismissMenu?.();
}
})
.finally(() => {
setIsLoading(false);
});
} else { } else {
dismissMenu?.(); dismissMenu?.();
} }

View File

@ -135,14 +135,18 @@ export default function AppStateProvider({
if (isPathAdmin(pathname)) { router.push(PATH_SIGN_IN); } if (isPathAdmin(pathname)) { router.push(PATH_SIGN_IN); }
}, [router, pathname]); }, [router, pathname]);
const startUpload = useCallback((onStart?: () => void) => { // Returns false when an upload is cancelled
const startUpload = useCallback(() => new Promise<boolean>(resolve => {
if (uploadInputRef.current) { if (uploadInputRef.current) {
uploadInputRef.current.value = ''; uploadInputRef.current.value = '';
uploadInputRef.current.click(); uploadInputRef.current.click();
uploadInputRef.current.oninput = onStart ?? null; uploadInputRef.current.oninput = () => resolve(true);
uploadInputRef.current.oncancel = onStart ?? null; uploadInputRef.current.oncancel = () => resolve(false);
} else {
resolve(false);
} }
}, []); })
, []);
const setUploadState = useCallback((uploadState: Partial<UploadState>) => { const setUploadState = useCallback((uploadState: Partial<UploadState>) => {
_setUploadState(prev => ({ ...prev, ...uploadState })); _setUploadState(prev => ({ ...prev, ...uploadState }));
}, []); }, []);