mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -6,6 +6,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Generic sheet for selecting and adding users to any entity
|
||||
@@ -25,6 +26,8 @@ import { Spinner } from "@/components/ui/spinner";
|
||||
* @param {string} [props.idKey] Key for the user id. Default: "user_id"
|
||||
* @param {string} [props.labelKey] Key for the display name. Default: "full_name"
|
||||
* @param {string} [props.subLabelKey] Optional secondary line (e.g. "email")
|
||||
* @param {string} [props.warningKey] If set, users with a truthy value at this key show
|
||||
* a "will be moved" warning (e.g. "current_group")
|
||||
*
|
||||
* @param {Function} props.onSubmit Called with selected ids[]
|
||||
*
|
||||
@@ -63,6 +66,7 @@ export function AddSheet({
|
||||
idKey = "user_id",
|
||||
labelKey = "full_name",
|
||||
subLabelKey = null,
|
||||
warningKey = null,
|
||||
|
||||
onSubmit,
|
||||
}) {
|
||||
@@ -102,6 +106,14 @@ export function AddSheet({
|
||||
const allFilteredSelected =
|
||||
filtered.length > 0 && filtered.every((u) => selected.includes(u[idKey]));
|
||||
|
||||
const movingCount = useMemo(() => {
|
||||
if (!warningKey) return 0;
|
||||
return selected.filter((id) => {
|
||||
const user = users.find((u) => u[idKey] === id);
|
||||
return !!user?.[warningKey];
|
||||
}).length;
|
||||
}, [selected, users, warningKey, idKey]);
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!selected.length) return;
|
||||
await onSubmit(selected);
|
||||
@@ -166,9 +178,10 @@ export function AddSheet({
|
||||
</p>
|
||||
) : (
|
||||
filtered.map((user) => {
|
||||
const id = user[idKey];
|
||||
const label = user[labelKey];
|
||||
const sub = subLabelKey ? user[subLabelKey] : null;
|
||||
const id = user[idKey];
|
||||
const label = user[labelKey];
|
||||
const sub = subLabelKey ? user[subLabelKey] : null;
|
||||
const warning = warningKey ? user[warningKey] : null;
|
||||
|
||||
return (
|
||||
<label
|
||||
@@ -180,13 +193,19 @@ export function AddSheet({
|
||||
checked={selected.includes(id)}
|
||||
onChange={() => toggle(id)}
|
||||
/>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="truncate">{label}</span>
|
||||
{sub && (
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{sub}
|
||||
</span>
|
||||
)}
|
||||
{warning && (
|
||||
<span className="text-xs text-amber-600 flex items-center gap-1 mt-0.5">
|
||||
<AlertTriangle className="size-3 shrink-0" />
|
||||
Also in: {warning}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
@@ -197,17 +216,25 @@ export function AddSheet({
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="shrink-0 flex gap-2 border-t px-6 py-4">
|
||||
<Button variant="outline" className="flex-1" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
disabled={!selected.length || loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{selected.length > 0 ? `${submitLabel} (${selected.length})` : submitLabel}
|
||||
</Button>
|
||||
<div className="shrink-0 flex flex-col gap-2 border-t px-6 py-4">
|
||||
{movingCount > 0 && (
|
||||
<p className="text-xs text-amber-600 flex items-center gap-1.5">
|
||||
<AlertTriangle className="size-3 shrink-0" />
|
||||
{movingCount} user{movingCount > 1 ? 's' : ''} already belong{movingCount === 1 ? 's' : ''} to other group{movingCount > 1 ? 's' : ''}.
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" className="flex-1" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
disabled={!selected.length || loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{selected.length > 0 ? `${submitLabel} (${selected.length})` : submitLabel}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</SheetContent>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useDateFormat } from "@/hooks/useDateFormat";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@@ -14,14 +15,12 @@ const FIELD_DISPLAY_MAP = {
|
||||
const BOOLEAN_FIELDS = Object.keys(FIELD_DISPLAY_MAP);
|
||||
|
||||
// ─── Generic formatter ────────────────────────────────────────────────────────
|
||||
const formatFilterItem = (item, field, type) => {
|
||||
const formatFilterItem = (item, field, type, fmtDate) => {
|
||||
if (FIELD_DISPLAY_MAP[field]) {
|
||||
return FIELD_DISPLAY_MAP[field][String(item)] ?? item;
|
||||
}
|
||||
if (type === "date" && item) {
|
||||
return new Date(item).toLocaleDateString("en-US", {
|
||||
year: "numeric", month: "long", day: "numeric",
|
||||
});
|
||||
return fmtDate(item);
|
||||
}
|
||||
return item;
|
||||
};
|
||||
@@ -35,6 +34,7 @@ const EmptyState = ({ search }) => (
|
||||
|
||||
// ─── Reusable item list renderer ──────────────────────────────────────────────
|
||||
const FilterList = ({ items, field, type, selected, onToggle, inputType = "checkbox" }) => {
|
||||
const { fmtDate } = useDateFormat();
|
||||
if (items.length === 0) return <EmptyState />;
|
||||
|
||||
return items.map((item) => (
|
||||
@@ -45,7 +45,7 @@ const FilterList = ({ items, field, type, selected, onToggle, inputType = "check
|
||||
checked={selected.includes(String(item))}
|
||||
onChange={() => onToggle(item)}
|
||||
/>
|
||||
{formatFilterItem(item, field, type)}
|
||||
{formatFilterItem(item, field, type, fmtDate)}
|
||||
</label>
|
||||
));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user