100 lines
4.4 KiB
TypeScript
100 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import type { RefObject } from "react";
|
|
import { useEffect, useId, useRef, useState } from "react";
|
|
import { useFormStatus } from "react-dom";
|
|
|
|
export function UserGroupSelect({
|
|
action,
|
|
effectiveGroupId,
|
|
groups,
|
|
returnTo,
|
|
userId,
|
|
userLabel,
|
|
}: {
|
|
action: (formData: FormData) => Promise<void>;
|
|
effectiveGroupId: string;
|
|
groups: Array<{ id: string; name: string }>;
|
|
returnTo: string;
|
|
userId: string;
|
|
userLabel: string;
|
|
}) {
|
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
const titleId = useId();
|
|
const descriptionId = useId();
|
|
const helpId = useId();
|
|
const [selectedGroupId, setSelectedGroupId] = useState(effectiveGroupId);
|
|
const [proposedGroupId, setProposedGroupId] = useState<string | null>(null);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const currentGroup = groups.find((group) => group.id === effectiveGroupId);
|
|
const proposedGroup = groups.find((group) => group.id === proposedGroupId);
|
|
|
|
function resetSelection() {
|
|
setSelectedGroupId(effectiveGroupId);
|
|
setProposedGroupId(null);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<span className="sr-only" id={helpId}>Changing this selection opens a confirmation dialog.</span>
|
|
<select
|
|
aria-describedby={helpId}
|
|
aria-label={`Group for ${userLabel}`}
|
|
className="max-w-44 border border-line bg-canvas px-3 py-2 font-mono text-xs outline-none focus:border-accent disabled:cursor-wait disabled:opacity-60"
|
|
disabled={proposedGroupId !== null || submitting}
|
|
onChange={(event) => {
|
|
const nextGroupId = event.currentTarget.value;
|
|
if (nextGroupId === effectiveGroupId) return;
|
|
setSelectedGroupId(nextGroupId);
|
|
setProposedGroupId(nextGroupId);
|
|
dialogRef.current?.showModal();
|
|
}}
|
|
value={selectedGroupId}
|
|
>
|
|
{groups.map((group) => <option key={group.id} value={group.id}>{group.name}</option>)}
|
|
</select>
|
|
<dialog
|
|
aria-describedby={descriptionId}
|
|
aria-labelledby={titleId}
|
|
className="admin-modal m-auto w-[min(92vw,34rem)] border border-ink bg-panel p-0 text-ink shadow-[10px_10px_0_var(--color-shadow)] backdrop:bg-ink/70"
|
|
onCancel={(event) => { if (submitting) event.preventDefault(); }}
|
|
onClose={() => { if (!submitting) resetSelection(); }}
|
|
ref={dialogRef}
|
|
>
|
|
<form action={action} className="p-6 sm:p-8" onSubmit={() => setSubmitting(true)}>
|
|
<input name="userId" type="hidden" value={userId} />
|
|
<input name="groupId" type="hidden" value={proposedGroupId ?? effectiveGroupId} />
|
|
<input name="returnTo" type="hidden" value={returnTo} />
|
|
<p className="font-mono text-[9px] font-bold uppercase tracking-[0.2em] text-accent">Confirm membership</p>
|
|
<h2 className="mt-3 font-display text-3xl font-black uppercase" id={titleId}>Move {userLabel}?</h2>
|
|
<p className="mt-3 text-sm leading-6 text-muted" id={descriptionId}>
|
|
Change the effective group from <strong className="text-ink">{currentGroup?.name ?? "unknown"}</strong> to <strong className="text-ink">{proposedGroup?.name ?? "unknown"}</strong>. Their access policy changes immediately.
|
|
</p>
|
|
<AssignmentActions dialogRef={dialogRef} onPendingChange={setSubmitting} />
|
|
</form>
|
|
</dialog>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function AssignmentActions({ dialogRef, onPendingChange }: { dialogRef: RefObject<HTMLDialogElement | null>; onPendingChange: (pending: boolean) => void }) {
|
|
const { pending } = useFormStatus();
|
|
const observedPending = useRef(false);
|
|
useEffect(() => {
|
|
if (pending) {
|
|
observedPending.current = true;
|
|
onPendingChange(true);
|
|
} else if (observedPending.current) {
|
|
observedPending.current = false;
|
|
onPendingChange(false);
|
|
}
|
|
}, [onPendingChange, pending]);
|
|
return (
|
|
<div className="mt-8 flex justify-end gap-3 border-t border-line pt-5">
|
|
<button className="border border-line px-5 py-3 font-mono text-[10px] font-bold uppercase" disabled={pending} onClick={() => dialogRef.current?.close()} type="button">Cancel</button>
|
|
<button className="bg-ink px-5 py-3 font-mono text-[10px] font-bold uppercase text-canvas disabled:cursor-wait disabled:opacity-60" disabled={pending} type="submit">{pending ? "Moving…" : "Confirm move"}</button>
|
|
<span aria-live="polite" className="sr-only">{pending ? "Group change in progress." : ""}</span>
|
|
</div>
|
|
);
|
|
}
|