From 2e6f027b71707e12a48431eb0903f2a35ebe4713 Mon Sep 17 00:00:00 2001 From: koko210Serve Date: Sun, 4 Oct 2026 18:16:03 +0300 Subject: [PATCH] Add full editing for client proofing links --- app/admin/components/ProofingView.tsx | 220 ++++++++++++++++++++++++++ app/api/admin/proofing/[id]/route.ts | 15 +- lib/proofSessions.ts | 13 +- 3 files changed, 245 insertions(+), 3 deletions(-) diff --git a/app/admin/components/ProofingView.tsx b/app/admin/components/ProofingView.tsx index eac3c04..77707d7 100644 --- a/app/admin/components/ProofingView.tsx +++ b/app/admin/components/ProofingView.tsx @@ -208,6 +208,94 @@ function SessionCard({ const [picks, setPicks] = useState(null); const [copied, setCopied] = useState<'link' | 'lightroom' | null>(null); const [error, setError] = useState(null); + + const [editing, setEditing] = useState(false); + const [albums, setAlbums] = useState(null); + + const [editClientName, setEditClientName] = useState(session.clientName); + const [editAlias, setEditAlias] = useState(session.alias ?? ''); + const [editAlbumId, setEditAlbumId] = useState(session.albumId); + const [editExpiresOn, setEditExpiresOn] = useState(session.expiresOn ?? ''); + const [editDownloadsAllowed, setEditDownloadsAllowed] = useState( + session.download !== 'none', + ); + const [editLimit, setEditLimit] = useState( + session.downloadLimit !== null + ? String(session.downloadLimit) + : '', + ); + const [editBusy, setEditBusy] = useState(false); + + const beginEdit = async () => { + setEditClientName(session.clientName); + setEditAlias(session.alias ?? ''); + setEditAlbumId(session.albumId); + setEditExpiresOn(session.expiresOn ?? ''); + setEditDownloadsAllowed(session.download !== 'none'); + setEditLimit( + session.downloadLimit !== null + ? String(session.downloadLimit) + : '', + ); + + setError(null); + + if (!albums) { + try { + const data = await api<{ albums: AlbumOption[] }>('/api/admin/albums'); + + setAlbums( + [...data.albums].sort((a, b) => + a.albumName.localeCompare(b.albumName), + ), + ); + } catch (err) { + setError((err as Error).message); + return; + } + } + + setEditing(true); + }; + + const saveEdit = async () => { + setEditBusy(true); + setError(null); + + try { + await api(`/api/admin/proofing/${session.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + clientName: editClientName.trim(), + alias: editAlias.trim() || null, + albumId: editAlbumId, + expiresOn: editExpiresOn || null, + + // We keep "selection" internally as our generic + // "downloads allowed" state. + download: editDownloadsAllowed + ? 'selection' + : 'none', + + downloadLimit: + editDownloadsAllowed && editLimit + ? Number(editLimit) + : null, + }), + }); + + setEditing(false); + onChanged(); + } catch (err) { + setError((err as Error).message); + } finally { + setEditBusy(false); + } + }; + const linkUrl = typeof window !== 'undefined' ? `${window.location.origin}${session.link}` : ''; useEffect(() => { @@ -307,6 +395,13 @@ function SessionCard({ {copied === 'link' ? : }{' '} {copied === 'link' ? 'Copied' : 'Copy link'} + + + + + + )} + {expanded && (
diff --git a/app/api/admin/proofing/[id]/route.ts b/app/api/admin/proofing/[id]/route.ts index 423ebc9..d1159f1 100644 --- a/app/api/admin/proofing/[id]/route.ts +++ b/app/api/admin/proofing/[id]/route.ts @@ -39,8 +39,19 @@ export const PATCH = withAdmin(async (request: NextRequest, { params }: Context) const album = await immich.getProofingAlbum(session.albumId).catch(() => null); return NextResponse.json({ session: toAdminSession(session, album) }); } catch (err) { - if (err instanceof ProofError) - return NextResponse.json({ error: err.message }, { status: 404 }); + if (err instanceof ProofError) { + return NextResponse.json( + { error: err.message }, + { + status: + err.code === 'alias-taken' + ? 409 + : err.code === 'not-found' + ? 404 + : 400, + }, + ); + } throw err; } }); diff --git a/lib/proofSessions.ts b/lib/proofSessions.ts index 410db33..dd42f7d 100644 --- a/lib/proofSessions.ts +++ b/lib/proofSessions.ts @@ -134,7 +134,6 @@ export const sessionInputSchema = z.object({ export type SessionInput = z.infer; export const sessionPatchSchema = sessionInputSchema - .omit({ albumId: true }) .partial() .extend({ /** Unlock a submitted selection so the client can change it again. */ @@ -295,6 +294,18 @@ export async function updateSession(id: string, patch: SessionPatch): Promise