Deskripsi: - tambah role user - api edit tambah dan delete role user NO Issues
145 lines
4.1 KiB
TypeScript
145 lines
4.1 KiB
TypeScript
import permissionConfig from "@/lib/listPermission.json";
|
|
import { ActionIcon, Checkbox, Collapse, Group, Stack, Text } from "@mantine/core";
|
|
import { IconChevronDown, IconChevronRight } from "@tabler/icons-react";
|
|
import { useState } from "react";
|
|
|
|
interface Node {
|
|
label: string;
|
|
key: string;
|
|
children?: Node[];
|
|
}
|
|
|
|
export default function PermissionTree({
|
|
selected,
|
|
onChange,
|
|
}: {
|
|
selected: string[];
|
|
onChange: (val: string[]) => void;
|
|
}) {
|
|
const [open, setOpen] = useState<Record<string, boolean>>({});
|
|
|
|
const toggle = (key: string) => {
|
|
setOpen((prev) => ({ ...prev, [key]: !prev[key] }));
|
|
};
|
|
|
|
|
|
|
|
// Ambil semua key dari node termasuk semua keturunannya
|
|
const collectKeys = (n: Node): string[] => {
|
|
if (!n.children) return [n.key];
|
|
return [n.key, ...n.children.flatMap(collectKeys)];
|
|
};
|
|
|
|
const checkState = (node: Node): { all: boolean; some: boolean } => {
|
|
const children = node.children || [];
|
|
// Jika tidak ada anak → nilai hanya berdasarkan dirinya sendiri
|
|
if (children.length === 0) {
|
|
const checked = selected.includes(node.key);
|
|
return { all: checked, some: checked };
|
|
}
|
|
// Rekursif ke anak
|
|
let all = selected.includes(node.key);
|
|
let some = selected.includes(node.key);
|
|
for (const c of children) {
|
|
const childState = checkState(c);
|
|
if (!childState.all) all = false;
|
|
if (childState.some) some = true;
|
|
}
|
|
return { all, some };
|
|
};
|
|
|
|
// Untuk ordering sesuai urutan JSON
|
|
const getOrderedKeys = (nodes: Node[]): string[] =>
|
|
nodes.flatMap((n) => [n.key, ...getOrderedKeys(n.children || [])]);
|
|
|
|
|
|
const RenderNode = ({ node }: { node: Node }) => {
|
|
const children = node.children || [];
|
|
|
|
const state = checkState(node); // ← gunakan recursive evaluator
|
|
|
|
const isChecked = state.all;
|
|
const isIndeterminate = !state.all && state.some;
|
|
|
|
const showChildren = open[node.key] ?? false;
|
|
|
|
// Ambil semua key anak + parent
|
|
const collectKeys = (n: Node): string[] => {
|
|
if (!n.children) return [n.key];
|
|
return [n.key, ...n.children.flatMap(collectKeys)];
|
|
};
|
|
|
|
const allKeys = collectKeys(node);
|
|
|
|
const toggleCheck = (checked: boolean) => {
|
|
let updated = new Set(selected);
|
|
|
|
if (checked) {
|
|
// parent + semua child
|
|
allKeys.forEach((k) => updated.add(k));
|
|
} else {
|
|
// hilangkan parent + semua child
|
|
allKeys.forEach((k) => updated.delete(k));
|
|
}
|
|
|
|
// ⬇⬇⬇ PERBAIKAN PENTING ⬇⬇⬇
|
|
//
|
|
// Jika node indeterminate → parent harus tetap ada di selected
|
|
//
|
|
if (isIndeterminate) {
|
|
updated.add(node.key);
|
|
}
|
|
|
|
// Jika semua child tercentang → parent harus checked
|
|
if (isChecked) {
|
|
updated.add(node.key);
|
|
}
|
|
|
|
onChange([...updated]);
|
|
};
|
|
|
|
return (
|
|
<Stack gap={4} pl="xs">
|
|
<Group wrap="nowrap">
|
|
{children.length > 0 ? (
|
|
<ActionIcon variant="subtle" onClick={() => toggle(node.key)}>
|
|
{showChildren ? <IconChevronDown size={16} /> : <IconChevronRight size={16} />}
|
|
</ActionIcon>
|
|
) : (
|
|
<div style={{ width: 24 }} />
|
|
)}
|
|
|
|
<Checkbox
|
|
label={node.label}
|
|
checked={isChecked}
|
|
indeterminate={isIndeterminate}
|
|
onChange={(e) => toggleCheck(e.target.checked)}
|
|
/>
|
|
</Group>
|
|
|
|
{children.length > 0 && (
|
|
<Collapse in={showChildren}>
|
|
<Stack gap={4} pl="md">
|
|
{children.map((c) => (
|
|
<RenderNode key={c.key} node={c} />
|
|
))}
|
|
</Stack>
|
|
</Collapse>
|
|
)}
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
<Stack>
|
|
<Text size="sm">Hak Akses</Text>
|
|
|
|
{permissionConfig.menus.map((menu: Node) => (
|
|
<RenderNode key={menu.key} node={menu} />
|
|
))}
|
|
</Stack>
|
|
);
|
|
}
|