import { NgTemplateOutlet } from "@angular/common";
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
input,
model,
signal,
ViewEncapsulation,
} from "@angular/core";
import { obj } from "@simplysm/core-common";
import type { SdPermission } from "../../core/app-structure/sd-app-structure.types";
import { SdCheckbox } from "../../controls/checkbox/sd-checkbox";
import { SdCollapseIcon } from "../../controls/collapse/sd-collapse-icon";
import { SdTypedTemplate } from "../../core/template/sd-typed-template";
import { SdAnchor } from "../../controls/button/sd-anchor";
@Component({
selector: "sd-permission-table",
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [
SdTypedTemplate,
NgTemplateOutlet,
SdCollapseIcon,
SdCheckbox,
SdAnchor,
],
styles: [
/* language=SCSS */ `
sd-permission-table {
table {
border-collapse: collapse;
> * > tr {
> * {
padding: var(--gap-sm) var(--gap-lg);
position: sticky;
top: 0;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
color: var(--text-trans-default);
> * {
color: var(--text-trans-default) !important;
}
&._title {
border-top-left-radius: 14px;
border-bottom-left-radius: 14px;
padding-left: var(--gap-lg);
}
}
&[data-sd-collapse="true"] {
display: none;
}
&[data-sd-theme="first"] {
> * {
&._title,
&._after {
background: var(--theme-info-default);
color: var(--text-trans-rev-default);
> * {
color: var(--text-trans-rev-default) !important;
}
}
}
}
&[data-sd-theme="info"] {
> * {
&._title,
&._after {
background: var(--theme-info-lightest);
}
}
}
&[data-sd-theme="warning"] {
> * {
&._title,
&._after {
background: var(--theme-warning-lightest);
}
}
}
&[data-sd-theme="success"] {
> * {
&._title,
&._after {
background: var(--theme-success-lightest);
}
}
}
}
}
}
`,
],
template: `
@for (item of items(); track item.codeChain.join(".")) {
}
@if (
(item.children && item.children.length !== 0) || (item.perms && item.perms.length > 0)
) {
@for (i of arr(depth + 1); track i) {
| |
}
@if (item.children && item.children.length > 0) {
{{ item.title }}
} @else {
{{ item.title }}
}
|
@for (i of arr(depthLength() - (depth + 1)); track i) {
|
}
@if (getIsPermExists(item, "use")) {
사용
}
|
@if (getIsPermExists(item, "edit")) {
편집
}
|
}
@if (item.children && item.children.length > 0) {
@for (child of item.children; track child.codeChain.join(".")) {
}
}
`,
})
export class SdPermissionTable {
value = model>({});
items = input[]>([]);
disabled = input(false, { transform: booleanAttribute });
collapsedItems = signal(new Set());
depthLength = computed(() => {
return this._getDepthLength(this.items(), 0);
});
private readonly _permExistsCache = computed(() => {
const cache = new Map();
const walk = (item: SdPermission, type: "use" | "edit"): boolean => {
const key = item.codeChain.join(".") + "." + type;
const cached = cache.get(key);
if (cached != null) return cached;
if (item.perms) {
if (item.children) {
for (const child of item.children) {
walk(child, type);
}
}
const result = item.perms.includes(type);
cache.set(key, result);
return result;
}
let result = false;
if (item.children) {
for (const child of item.children) {
if (walk(child, type)) {
result = true;
}
}
}
cache.set(key, result);
return result;
};
for (const item of this.items()) {
walk(item, "use");
walk(item, "edit");
}
return cache;
});
private readonly _permCheckedCache = computed(() => {
const cache = new Map();
const value = this.value();
const walk = (item: SdPermission, type: "use" | "edit"): boolean => {
const key = item.codeChain.join(".") + "." + type;
const cached = cache.get(key);
if (cached != null) return cached;
if (item.perms) {
if (item.children) {
for (const child of item.children) {
walk(child, type);
}
}
const permCode = item.codeChain.join(".");
const result = value[permCode + "." + type] ?? false;
cache.set(key, result);
return result;
}
let result = false;
if (item.children) {
for (const child of item.children) {
if (walk(child, type)) {
result = true;
}
}
}
cache.set(key, result);
return result;
};
for (const item of this.items()) {
walk(item, "use");
walk(item, "edit");
}
return cache;
});
private readonly _editDisabledCache = computed(() => {
const cache = new Map();
const isDisabled = this.disabled();
const existsCache = this._permExistsCache();
const checkedCache = this._permCheckedCache();
const walk = (item: SdPermission): boolean => {
const key = item.codeChain.join(".");
const cached = cache.get(key);
if (cached != null) return cached;
if (isDisabled) {
if (item.children) {
for (const child of item.children) {
walk(child);
}
}
cache.set(key, true);
return true;
}
if (item.perms) {
const useExists = existsCache.get(key + ".use") ?? false;
const useChecked = checkedCache.get(key + ".use") ?? false;
const result = useExists && !useChecked;
cache.set(key, result);
return result;
}
if (item.children) {
for (const child of item.children) {
walk(child);
}
const result = item.children.every(
(child) =>
!(existsCache.get(child.codeChain.join(".") + ".edit") ?? false) ||
(cache.get(child.codeChain.join(".")) ?? false),
);
cache.set(key, result);
return result;
}
cache.set(key, false);
return false;
};
for (const item of this.items()) {
walk(item);
}
return cache;
});
private readonly _arrCache = new Map();
arr(len: number): number[] {
let cached = this._arrCache.get(len);
if (cached == null) {
cached = Array(len)
.fill(0)
.map((_, i) => i);
this._arrCache.set(len, cached);
}
return cached;
}
getIsPermCollapsed(item: SdPermission): boolean {
return this.collapsedItems().has(item.codeChain.join("."));
}
getAllChildren(item: SdPermission): SdPermission[] {
return item.children?.mapMany((child) => [child, ...this.getAllChildren(child)]) ?? [];
}
getEditDisabled(item: SdPermission): boolean {
return this._editDisabledCache().get(item.codeChain.join(".")) ?? false;
}
getIsPermExists(item: SdPermission, type: "use" | "edit"): boolean {
return this._permExistsCache().get(item.codeChain.join(".") + "." + type) ?? false;
}
getIsPermChecked(item: SdPermission, type: "use" | "edit"): boolean {
return this._permCheckedCache().get(item.codeChain.join(".") + "." + type) ?? false;
}
onPermCollapseToggle(item: SdPermission) {
const key = item.codeChain.join(".");
this.collapsedItems.update((v) => {
const r = new Set(v);
if (r.has(key)) {
r.delete(key);
} else {
r.add(key);
const allChildren = this.getAllChildren(item);
for (const allChild of allChildren) {
r.add(allChild.codeChain.join("."));
}
}
return r;
});
}
onPermCheckChange(item: SdPermission, type: "use" | "edit", val: boolean) {
this.value.update((v) => {
const r = obj.clone(v);
this._changePermCheck(r, item, type, val);
return r;
});
}
private _changePermCheck(
value: Record,
item: SdPermission,
type: "use" | "edit",
val: boolean,
) {
let changed = false;
if (item.perms) {
const permCode = item.codeChain.join(".");
if (
type === "edit" &&
val &&
this.getIsPermExists(item, "use") &&
!(value[permCode + ".use"] ?? false)
) {
// use가 체크되지 않은 상태에서 edit 체크 시도 → 무시
} else {
if (this.getIsPermExists(item, type) && value[permCode + "." + type] !== val) {
value[permCode + "." + type] = val;
changed = true;
}
}
// USE권한 지우면 EDIT권한도 자동으로 지움
if (
type === "use" &&
!val &&
this.getIsPermExists(item, "edit") &&
value[permCode + ".edit"]
) {
value[permCode + ".edit"] = false;
changed = true;
}
}
// 하위 권한을 함께 변경함
if (item.children) {
for (const child of item.children) {
const childChanged = this._changePermCheck(value, child, type, val);
if (childChanged) {
changed = true;
}
}
}
return changed;
}
private _getDepthLength(items: SdPermission[], depth: number): number {
return (
items.max((item) => {
if (item.children) {
return this._getDepthLength(item.children, depth + 1);
} else {
return depth + 1;
}
}) ?? depth
);
}
protected readonly itemTemplateType!: {
item: SdPermission;
parentKey: string;
depth: number;
parent: SdPermission | undefined;
};
}