184 lines
4.7 KiB
TypeScript
184 lines
4.7 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;
|
|
}) {
|
|
// Ambil semua child dari node
|
|
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
|
|
|
|
function toggleNode(label: string) {
|
|
setOpenNodes((prev) => ({ ...prev, [label]: !prev[label] }));
|
|
}
|
|
|
|
function getAllChildKeys(node: Node): string[] {
|
|
let result: string[] = [];
|
|
if (node.children) {
|
|
node.children.forEach((c) => {
|
|
result.push(c.key);
|
|
result = [...result, ...getAllChildKeys(c)];
|
|
});
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Dapatkan parentKey, jika ada
|
|
function getParentKey(key: string) {
|
|
const split = key.split(".");
|
|
if (split.length <= 1) return null;
|
|
split.pop();
|
|
return split.join(".");
|
|
}
|
|
|
|
// Update parent ke atas secara rekursif
|
|
function updateParent(next: string[], parentKey: string | null): string[] {
|
|
if (!parentKey) return next;
|
|
|
|
const allChildKeys = findAllChildKeysFromKey(parentKey);
|
|
|
|
const selectedChild = allChildKeys.filter((c) => next.includes(c));
|
|
|
|
if (selectedChild.length === 0) {
|
|
// Semua child uncheck → parent uncheck
|
|
next = next.filter((x) => x !== parentKey);
|
|
} else if (selectedChild.length === allChildKeys.length) {
|
|
// Semua child check → parent check
|
|
if (!next.includes(parentKey)) {
|
|
next.push(parentKey);
|
|
}
|
|
} else {
|
|
// Sebagian child check → parent intermediate (checked = true, rendered sebagai indeterminate)
|
|
if (!next.includes(parentKey)) {
|
|
next.push(parentKey);
|
|
}
|
|
}
|
|
|
|
// Rekursif naik ke atas
|
|
return updateParent(next, getParentKey(parentKey));
|
|
}
|
|
|
|
// dapatkan child dari string key
|
|
function findAllChildKeysFromKey(parentKey: string) {
|
|
const list: string[] = [];
|
|
|
|
function traverse(nodes: Node[]) {
|
|
nodes.forEach((n) => {
|
|
if (n.key.startsWith(parentKey + ".") && n.key !== parentKey) {
|
|
list.push(n.key);
|
|
}
|
|
if (n.children) traverse(n.children);
|
|
});
|
|
}
|
|
|
|
traverse(permissionConfig.menus);
|
|
return list;
|
|
}
|
|
|
|
const RenderMenu = ({ menu }: { menu: Node }) => {
|
|
const hasChild = menu.children && menu.children.length > 0;
|
|
const open = openNodes[menu.label] ?? false;
|
|
const childKeys = getAllChildKeys(menu);
|
|
const isChecked = selected.includes(menu.key);
|
|
const isIndeterminate =
|
|
!isChecked &&
|
|
selected.some(
|
|
(x) => typeof x === "string" && x.startsWith(menu.key + "."),
|
|
);
|
|
|
|
function handleCheck() {
|
|
let next = [...selected];
|
|
|
|
if (childKeys.length > 0) {
|
|
// klik parent
|
|
if (!isChecked) {
|
|
next = [...new Set([...next, menu.key, ...childKeys])];
|
|
} else {
|
|
next = next.filter((x) => x !== menu.key && !childKeys.includes(x));
|
|
}
|
|
|
|
next = updateParent(next, getParentKey(menu.key));
|
|
onChange(next);
|
|
return;
|
|
}
|
|
|
|
// klik child
|
|
if (isChecked) {
|
|
next = next.filter((x) => x !== menu.key);
|
|
} else {
|
|
next.push(menu.key);
|
|
}
|
|
|
|
next = updateParent(next, getParentKey(menu.key));
|
|
onChange(next);
|
|
}
|
|
|
|
return (
|
|
<Stack gap={4}>
|
|
<Group gap="xs">
|
|
{menu.children && menu.children.length > 0 ? (
|
|
<ActionIcon variant="subtle" onClick={() => toggleNode(menu.label)}>
|
|
{openNodes[menu.label] ? (
|
|
<IconChevronDown size={16} />
|
|
) : (
|
|
<IconChevronRight size={16} />
|
|
)}
|
|
</ActionIcon>
|
|
) : (
|
|
<div style={{ width: 28 }} />
|
|
)}
|
|
|
|
<Checkbox
|
|
label={menu.label}
|
|
checked={isChecked}
|
|
indeterminate={isIndeterminate}
|
|
onChange={handleCheck}
|
|
/>
|
|
</Group>
|
|
|
|
{menu.children && (
|
|
<Collapse in={open}>
|
|
<Stack gap={4} pl="md">
|
|
{menu.children.map((child) => (
|
|
<RenderMenu key={child.key} menu={child} />
|
|
))}
|
|
</Stack>
|
|
</Collapse>
|
|
)}
|
|
</Stack>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<Stack>
|
|
<Text size="sm">Hak Akses</Text>
|
|
{permissionConfig.menus
|
|
.filter(
|
|
(menu: Node) =>
|
|
!menu.key.startsWith("api") && !menu.key.startsWith("credential"),
|
|
)
|
|
.map((menu: Node) => (
|
|
<RenderMenu key={menu.key} menu={menu} />
|
|
))}
|
|
</Stack>
|
|
);
|
|
}
|