refactor(group): remove deprecated user group components
Remove unused user group management components as user groups have been deprecated in favor of node groups only.
This commit is contained in:
parent
ae31019477
commit
4b4edd48e3
@ -1,210 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@workspace/ui/components/dialog";
|
|
||||||
import { Input } from "@workspace/ui/components/input";
|
|
||||||
import { Label } from "@workspace/ui/components/label";
|
|
||||||
import { Textarea } from "@workspace/ui/components/textarea";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@workspace/ui/components/select";
|
|
||||||
import { Switch } from "@workspace/ui/components/switch";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { forwardRef, useEffect, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
interface UserGroupFormProps {
|
|
||||||
initialValues?: Partial<API.UserGroup>;
|
|
||||||
loading?: boolean;
|
|
||||||
nodeGroups?: API.NodeGroup[];
|
|
||||||
onSubmit: (values: Record<string, unknown>) => Promise<boolean>;
|
|
||||||
title: string;
|
|
||||||
trigger: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UserGroupForm = forwardRef<
|
|
||||||
HTMLButtonElement,
|
|
||||||
UserGroupFormProps
|
|
||||||
>(({ initialValues, loading, nodeGroups = [], onSubmit, title, trigger }, ref) => {
|
|
||||||
const { t } = useTranslation("group");
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
|
|
||||||
const [values, setValues] = useState({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
sort: 0,
|
|
||||||
node_group_id: null as number | null,
|
|
||||||
for_calculation: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
if (initialValues) {
|
|
||||||
setValues({
|
|
||||||
name: initialValues.name || "",
|
|
||||||
description: initialValues.description || "",
|
|
||||||
sort: initialValues.sort ?? 0,
|
|
||||||
node_group_id: initialValues.node_group_id || null,
|
|
||||||
for_calculation: initialValues.for_calculation ?? true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setValues({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
sort: 0,
|
|
||||||
node_group_id: null,
|
|
||||||
for_calculation: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [initialValues, open]);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setSubmitting(true);
|
|
||||||
const success = await onSubmit(values);
|
|
||||||
setSubmitting(false);
|
|
||||||
if (success) {
|
|
||||||
setOpen(false);
|
|
||||||
setValues({
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
sort: 0,
|
|
||||||
node_group_id: null,
|
|
||||||
for_calculation: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild ref={ref}>
|
|
||||||
{trigger}
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-[500px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{title}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t("userGroupFormDescription", "Configure user group settings")}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="name">
|
|
||||||
{t("name", "Name")} *
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
value={values.name}
|
|
||||||
onChange={(e) =>
|
|
||||||
setValues({ ...values, name: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder={t("namePlaceholder", "Enter name")}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between space-x-2">
|
|
||||||
<Label htmlFor="for_calculation">{t("forCalculation", "For Calculation")}</Label>
|
|
||||||
<Switch
|
|
||||||
id="for_calculation"
|
|
||||||
checked={values.for_calculation}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setValues({ ...values, for_calculation: checked })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="description">
|
|
||||||
{t("description", "Description")}
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
id="description"
|
|
||||||
value={values.description}
|
|
||||||
onChange={(e) =>
|
|
||||||
setValues({ ...values, description: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder={t("descriptionPlaceholder", "Enter description")}
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="node_group_id">{t("nodeGroup", "Node Group")}</Label>
|
|
||||||
<Select
|
|
||||||
value={values.node_group_id ? String(values.node_group_id) : "0"}
|
|
||||||
onValueChange={(val) =>
|
|
||||||
setValues({
|
|
||||||
...values,
|
|
||||||
node_group_id: val === "0" ? null : parseInt(val),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="node_group_id" className="w-full">
|
|
||||||
<SelectValue placeholder={t("selectNodeGroupPlaceholder", "Select a node group...")} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="0">
|
|
||||||
{t("unbound", "Unbound")}
|
|
||||||
</SelectItem>
|
|
||||||
{nodeGroups.map((nodeGroup) => (
|
|
||||||
<SelectItem key={nodeGroup.id} value={String(nodeGroup.id)}>
|
|
||||||
{nodeGroup.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="sort">{t("sort", "Sort Order")}</Label>
|
|
||||||
<Input
|
|
||||||
id="sort"
|
|
||||||
type="number"
|
|
||||||
value={values.sort}
|
|
||||||
onChange={(e) =>
|
|
||||||
setValues({ ...values, sort: parseInt(e.target.value) || 0 })
|
|
||||||
}
|
|
||||||
min={0}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
className="rounded-md border px-4 py-2 text-sm"
|
|
||||||
disabled={submitting || loading}
|
|
||||||
>
|
|
||||||
{t("cancel", "Cancel")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={submitting || loading}
|
|
||||||
className="flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm text-primary-foreground disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{submitting && <Loader2 className="h-4 w-4 animate-spin" />}
|
|
||||||
{t("save", "Save")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
UserGroupForm.displayName = "UserGroupForm";
|
|
||||||
|
|
||||||
export default UserGroupForm;
|
|
||||||
@ -1,200 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Button } from "@workspace/ui/components/button";
|
|
||||||
import { Badge } from "@workspace/ui/components/badge";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@workspace/ui/components/card";
|
|
||||||
import { ConfirmButton } from "@workspace/ui/composed/confirm-button";
|
|
||||||
import {
|
|
||||||
ProTable,
|
|
||||||
type ProTableActions,
|
|
||||||
} from "@workspace/ui/composed/pro-table/pro-table";
|
|
||||||
import {
|
|
||||||
createUserGroup,
|
|
||||||
deleteUserGroup,
|
|
||||||
getUserGroupList,
|
|
||||||
getNodeGroupList,
|
|
||||||
updateUserGroup,
|
|
||||||
} from "@workspace/ui/services/admin/group";
|
|
||||||
import { useRef, useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import UserGroupForm from "./user-group-form";
|
|
||||||
|
|
||||||
export default function UserGroups() {
|
|
||||||
const { t } = useTranslation("group");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const ref = useRef<ProTableActions>(null);
|
|
||||||
|
|
||||||
const { data: nodeGroupsData } = useQuery({
|
|
||||||
queryKey: ["nodeGroups"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getNodeGroupList({ page: 1, size: 1000 });
|
|
||||||
return data.data?.list || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>{t("userGroups", "User Groups")}</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{t("userGroupsDescription", "Manage user groups for node access control")}
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ProTable<API.UserGroup, API.GetUserGroupListRequest>
|
|
||||||
action={ref}
|
|
||||||
request={async (params) => {
|
|
||||||
const { data } = await getUserGroupList({
|
|
||||||
page: params.page || 1,
|
|
||||||
size: params.size || 10,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
list: data.data?.list || [],
|
|
||||||
total: data.data?.total || 0,
|
|
||||||
};
|
|
||||||
}}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
id: "id",
|
|
||||||
accessorKey: "id",
|
|
||||||
header: t("id", "ID"),
|
|
||||||
cell: ({ row }: { row: any }) => <span className="text-muted-foreground">#{row.getValue("id")}</span>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "name",
|
|
||||||
accessorKey: "name",
|
|
||||||
header: t("name", "Name"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "for_calculation",
|
|
||||||
accessorKey: "for_calculation",
|
|
||||||
header: t("forCalculation", "For Calculation"),
|
|
||||||
cell: ({ row }: { row: any }) => {
|
|
||||||
const forCalculation = row.getValue("for_calculation");
|
|
||||||
return forCalculation ? (
|
|
||||||
<Badge variant="default">{t("yes", "Yes")}</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge variant="secondary">{t("no", "No")}</Badge>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "description",
|
|
||||||
accessorKey: "description",
|
|
||||||
header: t("description", "Description"),
|
|
||||||
cell: ({ row }: { row: any }) => row.getValue("description") || "--",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "node_group_id",
|
|
||||||
accessorKey: "node_group_id",
|
|
||||||
header: t("nodeGroup", "Node Group"),
|
|
||||||
cell: ({ row }: { row: any }) => {
|
|
||||||
const nodeGroupId = row.getValue("node_group_id");
|
|
||||||
const group = nodeGroupsData?.find((g) => g.id === nodeGroupId);
|
|
||||||
return group?.name || "--";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "sort",
|
|
||||||
accessorKey: "sort",
|
|
||||||
header: t("sort", "Sort"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
actions={{
|
|
||||||
render: (row: any) => [
|
|
||||||
<UserGroupForm
|
|
||||||
key={`edit-${row.id}`}
|
|
||||||
initialValues={row}
|
|
||||||
loading={loading}
|
|
||||||
nodeGroups={nodeGroupsData || []}
|
|
||||||
onSubmit={async (values) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
await updateUserGroup({
|
|
||||||
id: row.id,
|
|
||||||
...values,
|
|
||||||
} as unknown as API.UpdateUserGroupRequest);
|
|
||||||
toast.success(t("updated", "Updated successfully"));
|
|
||||||
ref.current?.refresh();
|
|
||||||
setLoading(false);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
setLoading(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={t("editUserGroup", "Edit User Group")}
|
|
||||||
trigger={
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
{t("edit", "Edit")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
<ConfirmButton
|
|
||||||
key="delete"
|
|
||||||
cancelText={t("cancel", "Cancel")}
|
|
||||||
confirmText={t("confirm", "Confirm")}
|
|
||||||
description={t(
|
|
||||||
"deleteUserGroupConfirm",
|
|
||||||
"This will delete the user group. Users in this group will be reassigned to the default group."
|
|
||||||
)}
|
|
||||||
onConfirm={async () => {
|
|
||||||
await deleteUserGroup({ id: row.id });
|
|
||||||
toast.success(t("deleted", "Deleted successfully"));
|
|
||||||
ref.current?.refresh();
|
|
||||||
setLoading(false);
|
|
||||||
}}
|
|
||||||
title={t("confirmDelete", "Confirm Delete")}
|
|
||||||
trigger={
|
|
||||||
<Button variant="destructive" size="sm">
|
|
||||||
{t("delete", "Delete")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
],
|
|
||||||
}}
|
|
||||||
header={{
|
|
||||||
title: t("userGroups", "User Groups"),
|
|
||||||
toolbar: (
|
|
||||||
<UserGroupForm
|
|
||||||
key="create"
|
|
||||||
initialValues={undefined}
|
|
||||||
loading={loading}
|
|
||||||
nodeGroups={nodeGroupsData || []}
|
|
||||||
onSubmit={async (values) => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
await createUserGroup(values as API.CreateUserGroupRequest);
|
|
||||||
toast.success(t("created", "Created successfully"));
|
|
||||||
ref.current?.refresh();
|
|
||||||
setLoading(false);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
setLoading(false);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={t("createUserGroup", "Create User Group")}
|
|
||||||
trigger={
|
|
||||||
<Button>
|
|
||||||
{t("create", "Create")}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,215 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Button } from "@workspace/ui/components/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@workspace/ui/components/dialog";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@workspace/ui/components/select";
|
|
||||||
import { Label } from "@workspace/ui/components/label";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Badge } from "@workspace/ui/components/badge";
|
|
||||||
import { AlertTriangle } from "lucide-react";
|
|
||||||
import {
|
|
||||||
getSubscribeMapping,
|
|
||||||
getUserGroupList,
|
|
||||||
migrateUsersToGroup,
|
|
||||||
} from "@workspace/ui/services/admin/group";
|
|
||||||
|
|
||||||
interface MigrateUsersDialogProps {
|
|
||||||
subscribeId: number;
|
|
||||||
subscribeName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MigrateUsersDialog({
|
|
||||||
subscribeId,
|
|
||||||
subscribeName,
|
|
||||||
}: MigrateUsersDialogProps) {
|
|
||||||
const { t } = useTranslation("product");
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [selectedGroupId, setSelectedGroupId] = useState<number>(0);
|
|
||||||
const [migrating, setMigrating] = useState(false);
|
|
||||||
|
|
||||||
// Fetch subscribe mapping to get current user group
|
|
||||||
const { data: mappingsData, isLoading: mappingsLoading } = useQuery({
|
|
||||||
enabled: open,
|
|
||||||
queryKey: ["subscribeMapping"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getSubscribeMapping({ page: 1, size: 1000 });
|
|
||||||
return data.data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch user groups for the dropdown
|
|
||||||
const { data: userGroupsData } = useQuery({
|
|
||||||
enabled: open,
|
|
||||||
queryKey: ["userGroups"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getUserGroupList({ page: 1, size: 1000 });
|
|
||||||
return data.data?.list || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find the current mapping for this subscribe
|
|
||||||
const currentMapping = mappingsData?.list?.find(
|
|
||||||
(m) => m.subscribe_id === subscribeId
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentGroupId = currentMapping?.user_group_id;
|
|
||||||
const currentUserGroup = currentGroupId
|
|
||||||
? userGroupsData?.find((g) => g.id === currentGroupId)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const handleMigrate = async () => {
|
|
||||||
if (!selectedGroupId) {
|
|
||||||
toast.error(t("selectTargetGroupFirst", "Please select a target group first"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedGroupId === currentGroupId) {
|
|
||||||
toast.error(t("cannotMigrateToSameGroup", "Cannot migrate to the same group"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setMigrating(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await migrateUsersToGroup({
|
|
||||||
from_user_group_id: currentGroupId!,
|
|
||||||
to_user_group_id: selectedGroupId,
|
|
||||||
include_locked: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
toast.success(t("migrateUsersSuccess", "Successfully migrated users to the target group"));
|
|
||||||
|
|
||||||
setOpen(false);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to migrate users:", error);
|
|
||||||
toast.error(t("migrateUsersFailed", "Failed to migrate users"));
|
|
||||||
} finally {
|
|
||||||
setMigrating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const availableGroups = userGroupsData?.filter(
|
|
||||||
(g) => !currentGroupId || g.id !== currentGroupId
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog onOpenChange={setOpen} open={open}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button variant="outline">{t("migrateUsers", "Migrate Users")}</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-w-md">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t("migrateUsersTitle", "Migrate Users")} - {subscribeName}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t(
|
|
||||||
"migrateUsersDescription",
|
|
||||||
"Migrate all users from the current user group to another group"
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
{mappingsLoading ? (
|
|
||||||
<div className="py-8 text-center text-muted-foreground">
|
|
||||||
{t("loading", "Loading...")}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
{/* Current User Group Info */}
|
|
||||||
<div className="rounded-lg border p-4 space-y-2">
|
|
||||||
<Label>{t("currentUserGroup", "Current User Group")}:</Label>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{currentUserGroup ? (
|
|
||||||
<>
|
|
||||||
<Badge variant="outline">{currentUserGroup.name}</Badge>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{t("noMapping", "No mapping set")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Warning Message */}
|
|
||||||
{currentUserGroup && (
|
|
||||||
<div className="flex items-start gap-2 rounded-md bg-yellow-50 p-3 text-sm text-yellow-800 dark:bg-yellow-900/20 dark:text-yellow-400">
|
|
||||||
<AlertTriangle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
|
||||||
<p>
|
|
||||||
{t("migrateUsersWarning", "This will migrate users from \"{group}\" to the target group. This action cannot be undone.")
|
|
||||||
.replace("{group}", currentUserGroup.name || "")
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Target User Group Selection */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="target-group">{t("targetUserGroup", "Target User Group")}:</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedGroupId?.toString() || ""}
|
|
||||||
onValueChange={(val) => setSelectedGroupId(Number(val))}
|
|
||||||
disabled={!currentGroupId}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="target-group">
|
|
||||||
<SelectValue
|
|
||||||
placeholder={
|
|
||||||
currentGroupId
|
|
||||||
? t("selectTargetGroup", "Select a target group...")
|
|
||||||
: t("noSourceGroup", "No source group available")
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{availableGroups?.map((group) => (
|
|
||||||
<SelectItem key={group.id} value={String(group.id)}>
|
|
||||||
{group.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Selected Target Group Info */}
|
|
||||||
{selectedGroupId && (
|
|
||||||
<div className="rounded-md bg-muted p-3">
|
|
||||||
<span className="text-sm font-medium">{t("selectedGroup", "Selected Group")}: </span>
|
|
||||||
<Badge variant="secondary">
|
|
||||||
{availableGroups?.find((g) => g.id === selectedGroupId)?.name}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
||||||
{t("cancel", "Cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleMigrate}
|
|
||||||
disabled={!selectedGroupId || !currentGroupId || migrating}
|
|
||||||
>
|
|
||||||
{migrating ? t("migrating", "Migrating...") : t("confirm", "Confirm")}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,199 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Button } from "@workspace/ui/components/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@workspace/ui/components/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@workspace/ui/components/form";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@workspace/ui/components/select";
|
|
||||||
import {
|
|
||||||
getUserGroupList,
|
|
||||||
} from "@workspace/ui/services/admin/group";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
|
|
||||||
const editUserGroupSchema = z.object({
|
|
||||||
user_group_id: z.number().min(0),
|
|
||||||
group_locked: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type EditUserGroupFormValues = z.infer<typeof editUserGroupSchema>;
|
|
||||||
|
|
||||||
interface EditUserGroupDialogProps {
|
|
||||||
userId: number;
|
|
||||||
userSubscribeId?: number;
|
|
||||||
currentGroupId?: number | undefined;
|
|
||||||
currentLocked?: boolean | undefined;
|
|
||||||
currentGroupIds?: number[] | null | undefined;
|
|
||||||
trigger: React.ReactNode;
|
|
||||||
onSubmit?: (values: EditUserGroupFormValues) => Promise<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EditUserGroupDialog({
|
|
||||||
userId: _userId,
|
|
||||||
userSubscribeId: _userSubscribeId,
|
|
||||||
currentGroupId,
|
|
||||||
currentLocked,
|
|
||||||
currentGroupIds,
|
|
||||||
trigger,
|
|
||||||
onSubmit,
|
|
||||||
}: EditUserGroupDialogProps) {
|
|
||||||
const { t } = useTranslation("user");
|
|
||||||
const [open, setOpen] = React.useState(false);
|
|
||||||
|
|
||||||
// Fetch user groups list
|
|
||||||
const { data: groupsData } = useQuery({
|
|
||||||
enabled: open,
|
|
||||||
queryKey: ["getUserGroupList"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const { data } = await getUserGroupList({
|
|
||||||
page: 1,
|
|
||||||
size: 100,
|
|
||||||
});
|
|
||||||
return data.data?.list || [];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm<EditUserGroupFormValues>({
|
|
||||||
resolver: zodResolver(editUserGroupSchema),
|
|
||||||
defaultValues: {
|
|
||||||
user_group_id: 0,
|
|
||||||
group_locked: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset form when dialog closes
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
form.reset();
|
|
||||||
}
|
|
||||||
}, [open, form]);
|
|
||||||
|
|
||||||
// Set form values when dialog opens
|
|
||||||
useEffect(() => {
|
|
||||||
if (open) {
|
|
||||||
// Support both usage scenarios:
|
|
||||||
// 1. User list page: currentGroupId (single number)
|
|
||||||
// 2. Subscribe detail page: currentGroupIds (array)
|
|
||||||
const groupId = currentGroupId || (currentGroupIds?.[0]) || 0;
|
|
||||||
|
|
||||||
form.reset({
|
|
||||||
user_group_id: groupId,
|
|
||||||
group_locked: currentLocked || false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [open, currentGroupId, currentGroupIds, currentLocked, form]);
|
|
||||||
|
|
||||||
const handleSubmit = async (values: EditUserGroupFormValues) => {
|
|
||||||
if (onSubmit) {
|
|
||||||
const success = await onSubmit(values);
|
|
||||||
if (success) {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t("editUserGroup", "Edit User Group")}</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{t(
|
|
||||||
"editUserGroupDescription",
|
|
||||||
"Edit user group assignment and lock status"
|
|
||||||
)}
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<Form {...form}>
|
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="user_group_id"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>{t("userGroup", "User Group")}</FormLabel>
|
|
||||||
<Select
|
|
||||||
value={field.value > 0 ? String(field.value) : undefined}
|
|
||||||
onValueChange={(value) => field.onChange(parseInt(value))}
|
|
||||||
>
|
|
||||||
<FormControl>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue placeholder={t("selectGroup", "Select a group")} />
|
|
||||||
</SelectTrigger>
|
|
||||||
</FormControl>
|
|
||||||
<SelectContent>
|
|
||||||
{groupsData?.map((group: API.UserGroup) => (
|
|
||||||
<SelectItem key={group.id} value={String(group.id)}>
|
|
||||||
{group.name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="group_locked"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel>{t("lockGroup", "Lock Group")}</FormLabel>
|
|
||||||
<div className="text-[0.8rem] text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"lockGroupDescription",
|
|
||||||
"Prevent automatic grouping from changing this user's group"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={field.value}
|
|
||||||
onChange={(e) => field.onChange(e.target.checked)}
|
|
||||||
className="h-4 w-4"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="submit">
|
|
||||||
{t("save", "Save")}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user