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'}
+
}
+ {editing && (
+
+
+
+
+
+ setEditClientName(e.target.value)}
+ maxLength={120}
+ />
+
+
+
+
+
+ setEditAlias(
+ e.target.value
+ .replace(/\s+/g, '-')
+ .replace(/[^A-Za-z0-9-]/g, ''),
+ )
+ }
+ maxLength={64}
+ placeholder="Mita"
+ />
+
+
+ /c/{editAlias || 'Mita'}
+
+
+
+
+
+
+
+
+
+
+ setEditExpiresOn(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+ {editDownloadsAllowed && (
+
+
+ setEditLimit(e.target.value)}
+ placeholder="Unlimited"
+ />
+
+ )}
+
+
+
+ {editAlbumId !== session.albumId && (
+
+ Changing the album will clear this client's current selection,
+ submission state and download counter.
+
+ )}
+
+
+
+
+
+
+
+ )}
+
{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