This commit is contained in:
rgrgogu
2026-05-07 23:35:31 +08:00
parent 42b842c480
commit ca6eae1d56
35 changed files with 1637 additions and 207 deletions
@@ -0,0 +1,83 @@
// ─── components/ArchiveDialog.jsx ────────────────────────────────────────────
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Spinner } from "@/components/ui/spinner";
/**
* Generic archive dialog — works for any entity (users, groups, etc.)
*
* Single: <ArchiveDialog entity={rowObject} getName={(r) => r.name} ... />
* Bulk: <ArchiveDialog ids={[1, 2, 3]} entityLabel="Group" ... />
*
* @param {Function} onArchive (id | { ids }) => Promise — called with single id or { ids }
* @param {Function} getName (entity) => string — how to display the entity name
* @param {string} entityLabel e.g. "User", "Group"
* @param {boolean} loading from whichever context the parent uses
*/
export function ArchiveDialog({
open,
onOpenChange,
entity,
ids,
entityLabel = "Item",
getName,
onArchive,
loading,
onSuccess,
}) {
const isBulk = Array.isArray(ids) && ids.length > 0;
const count = isBulk ? ids.length : 1;
const displayName = isBulk
? `${count} selected ${entityLabel.toLowerCase()}${count !== 1 ? "s" : ""}`
: (getName?.(entity) ?? entity?.name ?? entity?.email ?? "this item");
const handleArchive = async () => {
const res = isBulk
? await onArchive({ ids })
: await onArchive(entity);
if (res) {
onOpenChange(false);
onSuccess?.();
}
};
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Archive {isBulk ? `${count} ${entityLabel}${count !== 1 ? "s" : ""}` : entityLabel}
</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to archive{" "}
<span className="font-medium text-foreground">{displayName}</span>?{" "}
{isBulk
? "They will be deactivated and lose access immediately."
: "This will deactivate the record immediately."}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleArchive}
disabled={loading}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{loading && <Spinner className="size-4 mr-2" />}
Archive{isBulk ? ` ${count} ${entityLabel}${count !== 1 ? "s" : ""}` : ""}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
@@ -0,0 +1,83 @@
// ─── components/RestoreDialog.jsx ────────────────────────────────────────────
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Spinner } from "@/components/ui/spinner";
/**
* Generic restore dialog — works for any entity (users, groups, etc.)
*
* Single: <RestoreDialog entity={rowObject} getName={(r) => r.name} ... />
* Bulk: <RestoreDialog ids={[1, 2, 3]} entityLabel="Group" ... />
*
* @param {Function} onRestore (id | { ids }) => Promise — called with single id or { ids }
* @param {Function} getName (entity) => string — how to display the entity name
* @param {string} entityLabel e.g. "User", "Group"
* @param {boolean} loading from whichever context the parent uses
*/
export function RestoreDialog({
open,
onOpenChange,
entity,
ids,
entityLabel = "Item",
getName,
onRestore,
loading,
onSuccess,
}) {
const isBulk = Array.isArray(ids) && ids.length > 0;
const count = isBulk ? ids.length : 1;
const displayName = isBulk
? `${count} selected ${entityLabel.toLowerCase()}${count !== 1 ? "s" : ""}`
: (getName?.(entity) ?? entity?.name ?? entity?.email ?? "this item");
const handleRestore = async () => {
const res = isBulk
? await onRestore({ ids })
: await onRestore(entity);
if (res) {
onOpenChange(false);
onSuccess?.();
}
};
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Restore {isBulk ? `${count} ${entityLabel}${count !== 1 ? "s" : ""}` : entityLabel}
</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to restore{" "}
<span className="font-medium text-foreground">{displayName}</span>?{" "}
{isBulk
? "They will regain access to their accounts immediately."
: "This will reactivate the record immediately."}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleRestore}
disabled={loading}
className="bg-emerald-600 text-white hover:bg-emerald-700"
>
{loading && <Spinner className="size-4 mr-2" />}
Restore{isBulk ? ` ${count} ${entityLabel}${count !== 1 ? "s" : ""}` : ""}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}