upd: dashboard admin
Deskripsi: - tambah role user - edit role user No Issues
This commit is contained in:
@@ -16,112 +16,148 @@ export default function PermissionTree({
|
||||
selected: string[];
|
||||
onChange: (val: string[]) => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState<Record<string, boolean>>({});
|
||||
// Ambil semua child dari node
|
||||
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({});
|
||||
|
||||
const toggle = (key: string) => {
|
||||
setOpen((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||
};
|
||||
function toggleNode(label: string) {
|
||||
setOpenNodes(prev => ({ ...prev, [label]: !prev[label] }));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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 };
|
||||
function getAllChildKeys(node: Node): string[] {
|
||||
let result: string[] = [];
|
||||
if (node.children) {
|
||||
node.children.forEach((c) => {
|
||||
result.push(c.key);
|
||||
result = [...result, ...getAllChildKeys(c)];
|
||||
});
|
||||
}
|
||||
// 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 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);
|
||||
}
|
||||
}
|
||||
return { all, some };
|
||||
};
|
||||
|
||||
// Untuk ordering sesuai urutan JSON
|
||||
const getOrderedKeys = (nodes: Node[]): string[] =>
|
||||
nodes.flatMap((n) => [n.key, ...getOrderedKeys(n.children || [])]);
|
||||
// Rekursif naik ke atas
|
||||
return updateParent(next, getParentKey(parentKey));
|
||||
}
|
||||
|
||||
// dapatkan child dari string key
|
||||
function findAllChildKeysFromKey(parentKey: string) {
|
||||
const list: string[] = [];
|
||||
|
||||
const RenderNode = ({ node }: { node: Node }) => {
|
||||
const children = node.children || [];
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
const state = checkState(node); // ← gunakan recursive evaluator
|
||||
traverse(permissionConfig.menus);
|
||||
return list;
|
||||
}
|
||||
|
||||
const isChecked = state.all;
|
||||
const isIndeterminate = !state.all && state.some;
|
||||
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 + ".")
|
||||
);
|
||||
|
||||
const showChildren = open[node.key] ?? false;
|
||||
function handleCheck() {
|
||||
let next = [...selected];
|
||||
|
||||
// Ambil semua key anak + parent
|
||||
const collectKeys = (n: Node): string[] => {
|
||||
if (!n.children) return [n.key];
|
||||
return [n.key, ...n.children.flatMap(collectKeys)];
|
||||
};
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
next = updateParent(next, getParentKey(menu.key));
|
||||
onChange(next);
|
||||
return;
|
||||
}
|
||||
|
||||
// ⬇⬇⬇ PERBAIKAN PENTING ⬇⬇⬇
|
||||
//
|
||||
// Jika node indeterminate → parent harus tetap ada di selected
|
||||
//
|
||||
if (isIndeterminate) {
|
||||
updated.add(node.key);
|
||||
}
|
||||
|
||||
// Jika semua child tercentang → parent harus checked
|
||||
// klik child
|
||||
if (isChecked) {
|
||||
updated.add(node.key);
|
||||
next = next.filter((x) => x !== menu.key);
|
||||
} else {
|
||||
next.push(menu.key);
|
||||
}
|
||||
|
||||
onChange([...updated]);
|
||||
};
|
||||
next = updateParent(next, getParentKey(menu.key));
|
||||
onChange(next);
|
||||
}
|
||||
|
||||
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} />}
|
||||
<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: 24 }} />
|
||||
<div style={{ width: 28 }} />
|
||||
)}
|
||||
|
||||
<Checkbox
|
||||
label={node.label}
|
||||
label={menu.label}
|
||||
checked={isChecked}
|
||||
indeterminate={isIndeterminate}
|
||||
onChange={(e) => toggleCheck(e.target.checked)}
|
||||
onChange={handleCheck}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
{children.length > 0 && (
|
||||
<Collapse in={showChildren}>
|
||||
{menu.children && (
|
||||
<Collapse in={open}>
|
||||
<Stack gap={4} pl="md">
|
||||
{children.map((c) => (
|
||||
<RenderNode key={c.key} node={c} />
|
||||
{menu.children.map((child) => (
|
||||
<RenderMenu key={child.key} menu={child} />
|
||||
))}
|
||||
</Stack>
|
||||
</Collapse>
|
||||
@@ -130,14 +166,11 @@ export default function PermissionTree({
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Text size="sm">Hak Akses</Text>
|
||||
|
||||
{permissionConfig.menus.map((menu: Node) => (
|
||||
<RenderNode key={menu.key} node={menu} />
|
||||
<RenderMenu key={menu.key} menu={menu} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user