@simplysm/solid 13.0.41 → 13.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/data/permission-table/PermissionTable.d.ts +5 -11
- package/dist/components/data/permission-table/PermissionTable.d.ts.map +1 -1
- package/dist/components/data/permission-table/PermissionTable.js.map +1 -1
- package/dist/components/layout/sidebar/Sidebar.d.ts +1 -1
- package/dist/components/layout/sidebar/Sidebar.d.ts.map +1 -1
- package/dist/components/layout/sidebar/SidebarMenu.d.ts +2 -8
- package/dist/components/layout/sidebar/SidebarMenu.d.ts.map +1 -1
- package/dist/components/layout/sidebar/SidebarMenu.js.map +1 -1
- package/dist/helpers/createAppStructure.d.ts +22 -4
- package/dist/helpers/createAppStructure.d.ts.map +1 -1
- package/dist/helpers/createAppStructure.js +126 -27
- package/dist/helpers/createAppStructure.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/docs/helpers.md +29 -6
- package/docs/hooks.md +62 -7
- package/docs/styling.md +36 -14
- package/package.json +3 -3
- package/src/components/data/permission-table/PermissionTable.tsx +25 -35
- package/src/components/layout/sidebar/Sidebar.tsx +1 -1
- package/src/components/layout/sidebar/SidebarMenu.tsx +8 -15
- package/src/helpers/createAppStructure.ts +196 -36
- package/src/index.ts +3 -0
package/docs/styling.md
CHANGED
|
@@ -110,26 +110,45 @@ import {
|
|
|
110
110
|
|
|
111
111
|
```typescript
|
|
112
112
|
import {
|
|
113
|
-
type FieldSize,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
type FieldSize, // "sm" | "lg" | "xl"
|
|
114
|
+
fieldBaseClass, // Base wrapper classes (inline-flex, field surface, h-field)
|
|
115
|
+
fieldSizeClasses, // Size variant classes per FieldSize
|
|
116
|
+
fieldInsetClass, // Inset mode wrapper classes
|
|
117
|
+
fieldInsetHeightClass, // Inset height class (h-field-inset, excludes border)
|
|
118
|
+
fieldInsetSizeHeightClasses, // Inset height classes per size
|
|
119
|
+
fieldDisabledClass, // Disabled state classes
|
|
120
|
+
textAreaBaseClass, // Base textarea wrapper classes
|
|
121
|
+
textAreaSizeClasses, // Textarea size classes per FieldSize
|
|
122
|
+
fieldInputClass, // Base classes for field <input> element
|
|
123
|
+
fieldGapClasses, // Gap classes per size (with prefix icon)
|
|
124
|
+
getFieldWrapperClass, // Utility to build field wrapper class string
|
|
125
|
+
getTextareaWrapperClass, // Utility to build textarea wrapper class string
|
|
118
126
|
} from "@simplysm/solid";
|
|
119
127
|
```
|
|
120
128
|
|
|
129
|
+
**`getFieldWrapperClass` options:**
|
|
130
|
+
|
|
131
|
+
| Option | Type | Description |
|
|
132
|
+
|--------|------|-------------|
|
|
133
|
+
| `size` | `FieldSize` | Size variant |
|
|
134
|
+
| `disabled` | `boolean` | Apply disabled styles |
|
|
135
|
+
| `inset` | `boolean` | Apply inset styles |
|
|
136
|
+
| `includeCustomClass` | `string \| false` | Additional CSS class (or `false` to skip) |
|
|
137
|
+
| `extra` | `string \| false` | Extra classes inserted before size/disabled/inset |
|
|
138
|
+
|
|
121
139
|
### Checkbox.styles
|
|
122
140
|
|
|
123
141
|
```typescript
|
|
124
142
|
import {
|
|
125
|
-
type CheckboxSize,
|
|
126
|
-
checkboxBaseClass,
|
|
127
|
-
indicatorBaseClass,
|
|
128
|
-
checkedClass,
|
|
129
|
-
checkboxSizeClasses,
|
|
130
|
-
checkboxInsetClass,
|
|
131
|
-
|
|
132
|
-
|
|
143
|
+
type CheckboxSize, // "sm" | "lg" | "xl"
|
|
144
|
+
checkboxBaseClass, // Base wrapper classes
|
|
145
|
+
indicatorBaseClass, // Base indicator (box) classes
|
|
146
|
+
checkedClass, // Checked state classes (primary color)
|
|
147
|
+
checkboxSizeClasses, // Size variant classes
|
|
148
|
+
checkboxInsetClass, // Inset mode classes
|
|
149
|
+
checkboxInsetSizeHeightClasses, // Inset height classes per size (excludes border)
|
|
150
|
+
checkboxInlineClass, // Inline display classes
|
|
151
|
+
checkboxDisabledClass, // Disabled state classes
|
|
133
152
|
} from "@simplysm/solid";
|
|
134
153
|
```
|
|
135
154
|
|
|
@@ -142,8 +161,11 @@ import {
|
|
|
142
161
|
sortableThClass, sortIconClass, toolbarClass, fixedClass, fixedLastClass,
|
|
143
162
|
resizerClass, resizeIndicatorClass, featureThClass, featureTdClass,
|
|
144
163
|
expandIndentGuideClass, expandIndentGuideLineClass, expandToggleClass,
|
|
164
|
+
selectSingleClass, selectSingleSelectedClass, selectSingleUnselectedClass,
|
|
145
165
|
reorderHandleClass, reorderIndicatorClass, configButtonClass,
|
|
146
|
-
|
|
166
|
+
featureCellWrapperClass, featureCellBodyWrapperClass,
|
|
167
|
+
featureCellClickableClass, featureCellBodyClickableClass,
|
|
168
|
+
reorderCellWrapperClass,
|
|
147
169
|
} from "@simplysm/solid";
|
|
148
170
|
```
|
|
149
171
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/solid",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.43",
|
|
4
4
|
"description": "심플리즘 패키지 - SolidJS 라이브러리",
|
|
5
5
|
"author": "김석래",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"solid-tiptap": "^0.8.0",
|
|
50
50
|
"tailwind-merge": "^3.5.0",
|
|
51
51
|
"tailwindcss": "^3.4.19",
|
|
52
|
-
"@simplysm/core-common": "13.0.
|
|
53
|
-
"@simplysm/core-browser": "13.0.
|
|
52
|
+
"@simplysm/core-common": "13.0.43",
|
|
53
|
+
"@simplysm/core-browser": "13.0.43"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@solidjs/testing-library": "^0.8.10"
|
|
@@ -14,6 +14,7 @@ import { twMerge } from "tailwind-merge";
|
|
|
14
14
|
import { DataSheet } from "../sheet/DataSheet";
|
|
15
15
|
import { Checkbox } from "../../form-control/checkbox/Checkbox";
|
|
16
16
|
import { borderDefault } from "../../../styles/tokens.styles";
|
|
17
|
+
import type { AppPerm } from "../../../helpers/createAppStructure";
|
|
17
18
|
|
|
18
19
|
const titleCellClass = clsx("flex items-stretch", "px-2");
|
|
19
20
|
const indentGuideWrapperClass = clsx("mr-1 flex w-3", "justify-center");
|
|
@@ -21,16 +22,8 @@ const indentGuideLineClass = clsx("w-0 self-stretch", "border-r", borderDefault)
|
|
|
21
22
|
|
|
22
23
|
// --- 타입 ---
|
|
23
24
|
|
|
24
|
-
export interface PermissionItem<TModule = string> {
|
|
25
|
-
title: string;
|
|
26
|
-
href?: string;
|
|
27
|
-
modules?: TModule[];
|
|
28
|
-
perms?: string[];
|
|
29
|
-
children?: PermissionItem<TModule>[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
25
|
export interface PermissionTableProps<TModule = string> {
|
|
33
|
-
items?:
|
|
26
|
+
items?: AppPerm<TModule>[];
|
|
34
27
|
value?: Record<string, boolean>;
|
|
35
28
|
onValueChange?: (value: Record<string, boolean>) => void;
|
|
36
29
|
modules?: TModule[];
|
|
@@ -42,9 +35,9 @@ export interface PermissionTableProps<TModule = string> {
|
|
|
42
35
|
// --- 유틸리티 (테스트에서도 사용) ---
|
|
43
36
|
|
|
44
37
|
/** 트리에서 모든 고유 perm 타입을 수집 */
|
|
45
|
-
export function collectAllPerms<TModule>(items:
|
|
38
|
+
export function collectAllPerms<TModule>(items: AppPerm<TModule>[]): string[] {
|
|
46
39
|
const set = new Set<string>();
|
|
47
|
-
const walk = (list:
|
|
40
|
+
const walk = (list: AppPerm<TModule>[]) => {
|
|
48
41
|
for (const item of list) {
|
|
49
42
|
if (item.perms) {
|
|
50
43
|
for (const p of item.perms) set.add(p);
|
|
@@ -58,12 +51,12 @@ export function collectAllPerms<TModule>(items: PermissionItem<TModule>[]): stri
|
|
|
58
51
|
|
|
59
52
|
/** modules 필터: 활성 모듈과 교차가 있는 아이템만 남김 */
|
|
60
53
|
export function filterByModules<TModule>(
|
|
61
|
-
items:
|
|
54
|
+
items: AppPerm<TModule>[],
|
|
62
55
|
modules: TModule[] | undefined,
|
|
63
|
-
):
|
|
56
|
+
): AppPerm<TModule>[] {
|
|
64
57
|
if (!modules || modules.length === 0) return items;
|
|
65
58
|
|
|
66
|
-
const result:
|
|
59
|
+
const result: AppPerm<TModule>[] = [];
|
|
67
60
|
|
|
68
61
|
for (const item of items) {
|
|
69
62
|
if (item.modules && !item.modules.some((m) => modules.includes(m))) {
|
|
@@ -82,13 +75,13 @@ export function filterByModules<TModule>(
|
|
|
82
75
|
/** 체크 변경 시 cascading 처리 */
|
|
83
76
|
export function changePermCheck<TModule>(
|
|
84
77
|
value: Record<string, boolean>,
|
|
85
|
-
item:
|
|
78
|
+
item: AppPerm<TModule>,
|
|
86
79
|
perm: string,
|
|
87
80
|
checked: boolean,
|
|
88
81
|
): Record<string, boolean> {
|
|
89
82
|
const result = { ...value };
|
|
90
83
|
|
|
91
|
-
const apply = (target:
|
|
84
|
+
const apply = (target: AppPerm<TModule>) => {
|
|
92
85
|
if (target.perms && target.href != null && target.href !== "") {
|
|
93
86
|
const permIndex = target.perms.indexOf(perm);
|
|
94
87
|
|
|
@@ -122,10 +115,7 @@ export function changePermCheck<TModule>(
|
|
|
122
115
|
// --- 내부 헬퍼 ---
|
|
123
116
|
|
|
124
117
|
/** 모듈 필터에 의해 보이는지 확인 (객체 참조 유지) */
|
|
125
|
-
function isItemVisible<TModule>(
|
|
126
|
-
item: PermissionItem<TModule>,
|
|
127
|
-
modules: TModule[] | undefined,
|
|
128
|
-
): boolean {
|
|
118
|
+
function isItemVisible<TModule>(item: AppPerm<TModule>, modules: TModule[] | undefined): boolean {
|
|
129
119
|
if (!modules || modules.length === 0) return true;
|
|
130
120
|
if (item.modules && !item.modules.some((m) => modules.includes(m))) return false;
|
|
131
121
|
if (!item.perms && item.children) {
|
|
@@ -136,12 +126,12 @@ function isItemVisible<TModule>(
|
|
|
136
126
|
|
|
137
127
|
/** 보이는 아이템에서만 고유 perm 타입 수집 */
|
|
138
128
|
function collectVisiblePerms<TModule>(
|
|
139
|
-
items:
|
|
129
|
+
items: AppPerm<TModule>[],
|
|
140
130
|
modules: TModule[] | undefined,
|
|
141
131
|
): string[] {
|
|
142
132
|
const set = new Set<string>();
|
|
143
133
|
|
|
144
|
-
function walk(list:
|
|
134
|
+
function walk(list: AppPerm<TModule>[]) {
|
|
145
135
|
for (const item of list) {
|
|
146
136
|
if (!isItemVisible(item, modules)) continue;
|
|
147
137
|
if (item.perms) {
|
|
@@ -157,7 +147,7 @@ function collectVisiblePerms<TModule>(
|
|
|
157
147
|
|
|
158
148
|
/** 그룹 노드의 체크 상태: 하위 중 하나라도 체크면 true */
|
|
159
149
|
function isGroupPermChecked<TModule>(
|
|
160
|
-
item:
|
|
150
|
+
item: AppPerm<TModule>,
|
|
161
151
|
perm: string,
|
|
162
152
|
value: Record<string, boolean>,
|
|
163
153
|
): boolean {
|
|
@@ -171,7 +161,7 @@ function isGroupPermChecked<TModule>(
|
|
|
171
161
|
}
|
|
172
162
|
|
|
173
163
|
/** 하위에 특정 perm이 하나라도 있으면 true */
|
|
174
|
-
function hasPermInTree<TModule>(item:
|
|
164
|
+
function hasPermInTree<TModule>(item: AppPerm<TModule>, perm: string): boolean {
|
|
175
165
|
if (item.perms?.includes(perm)) return true;
|
|
176
166
|
if (item.children) {
|
|
177
167
|
return item.children.some((child) => hasPermInTree(child, perm));
|
|
@@ -181,7 +171,7 @@ function hasPermInTree<TModule>(item: PermissionItem<TModule>, perm: string): bo
|
|
|
181
171
|
|
|
182
172
|
/** 기본 권한(perms[0])이 꺼져 있어서 비활성화해야 하는지 */
|
|
183
173
|
function isPermDisabled<TModule>(
|
|
184
|
-
item:
|
|
174
|
+
item: AppPerm<TModule>,
|
|
185
175
|
perm: string,
|
|
186
176
|
value: Record<string, boolean>,
|
|
187
177
|
): boolean {
|
|
@@ -193,12 +183,12 @@ function isPermDisabled<TModule>(
|
|
|
193
183
|
|
|
194
184
|
/** 확장 가능한 모든 아이템 수집 (객체 참조 유지) */
|
|
195
185
|
function collectExpandable<TModule>(
|
|
196
|
-
items:
|
|
197
|
-
getChildren: (item:
|
|
198
|
-
):
|
|
199
|
-
const result:
|
|
186
|
+
items: AppPerm<TModule>[],
|
|
187
|
+
getChildren: (item: AppPerm<TModule>) => AppPerm<TModule>[] | undefined,
|
|
188
|
+
): AppPerm<TModule>[] {
|
|
189
|
+
const result: AppPerm<TModule>[] = [];
|
|
200
190
|
|
|
201
|
-
function walk(list:
|
|
191
|
+
function walk(list: AppPerm<TModule>[]) {
|
|
202
192
|
for (const item of list) {
|
|
203
193
|
const children = getChildren(item);
|
|
204
194
|
if (children && children.length > 0) {
|
|
@@ -233,7 +223,7 @@ export const PermissionTable: Component<PermissionTableProps> = (props) => {
|
|
|
233
223
|
});
|
|
234
224
|
|
|
235
225
|
// Sheet의 getChildren — 모듈 필터 적용, 객체 참조 유지
|
|
236
|
-
const getChildren = (item:
|
|
226
|
+
const getChildren = (item: AppPerm): AppPerm[] | undefined => {
|
|
237
227
|
if (!item.children || item.children.length === 0) return undefined;
|
|
238
228
|
const modules = local.modules;
|
|
239
229
|
if (!modules || modules.length === 0) return item.children;
|
|
@@ -249,7 +239,7 @@ export const PermissionTable: Component<PermissionTableProps> = (props) => {
|
|
|
249
239
|
// 확장 상태 — 기본적으로 모두 펼침
|
|
250
240
|
const getAllExpandable = () => collectExpandable(visibleItems(), getChildren);
|
|
251
241
|
|
|
252
|
-
const [expandedItems, setExpandedItems] = createSignal<
|
|
242
|
+
const [expandedItems, setExpandedItems] = createSignal<AppPerm[]>(getAllExpandable());
|
|
253
243
|
|
|
254
244
|
// 트리 구조 변경 시 모두 다시 펼침 (모듈 필터 변경 등)
|
|
255
245
|
createEffect(
|
|
@@ -262,7 +252,7 @@ export const PermissionTable: Component<PermissionTableProps> = (props) => {
|
|
|
262
252
|
),
|
|
263
253
|
);
|
|
264
254
|
|
|
265
|
-
const handlePermChange = (item:
|
|
255
|
+
const handlePermChange = (item: AppPerm, perm: string, checked: boolean) => {
|
|
266
256
|
const newValue = changePermCheck(currentValue(), item, perm, checked);
|
|
267
257
|
local.onValueChange?.(newValue);
|
|
268
258
|
};
|
|
@@ -278,7 +268,7 @@ export const PermissionTable: Component<PermissionTableProps> = (props) => {
|
|
|
278
268
|
>
|
|
279
269
|
<DataSheet.Column key="title" header="권한 항목" sortable={false} resizable={false}>
|
|
280
270
|
{(ctx) => {
|
|
281
|
-
const item = ctx.item as
|
|
271
|
+
const item = ctx.item as AppPerm;
|
|
282
272
|
return (
|
|
283
273
|
<div class={titleCellClass}>
|
|
284
274
|
<For each={Array.from({ length: ctx.depth })}>
|
|
@@ -297,7 +287,7 @@ export const PermissionTable: Component<PermissionTableProps> = (props) => {
|
|
|
297
287
|
{(perm) => (
|
|
298
288
|
<DataSheet.Column key={`perm-${perm}`} header={perm} sortable={false} resizable={false}>
|
|
299
289
|
{(ctx) => {
|
|
300
|
-
const item = ctx.item as
|
|
290
|
+
const item = ctx.item as AppPerm;
|
|
301
291
|
return (
|
|
302
292
|
<Show when={hasPermInTree(item, perm)}>
|
|
303
293
|
<Checkbox
|
|
@@ -9,7 +9,7 @@ import { SidebarMenu } from "./SidebarMenu";
|
|
|
9
9
|
import { SidebarUser } from "./SidebarUser";
|
|
10
10
|
|
|
11
11
|
export type { SidebarContainerProps } from "./SidebarContainer";
|
|
12
|
-
export type {
|
|
12
|
+
export type { SidebarMenuProps } from "./SidebarMenu";
|
|
13
13
|
export type { SidebarUserMenu, SidebarUserProps } from "./SidebarUser";
|
|
14
14
|
|
|
15
15
|
const baseClass = clsx(
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
import { useLocation, useNavigate } from "@solidjs/router";
|
|
15
15
|
import clsx from "clsx";
|
|
16
16
|
import { twMerge } from "tailwind-merge";
|
|
17
|
-
import type { IconProps } from "@tabler/icons-solidjs";
|
|
18
17
|
import type { ComponentSize } from "../../../styles/tokens.styles";
|
|
18
|
+
import type { AppMenu } from "../../../helpers/createAppStructure";
|
|
19
19
|
import { Icon } from "../../display/Icon";
|
|
20
20
|
import { List } from "../../data/list/List";
|
|
21
21
|
import { ListItem } from "../../data/list/ListItem";
|
|
@@ -31,23 +31,16 @@ const headerClass = clsx(
|
|
|
31
31
|
"tracking-wider",
|
|
32
32
|
);
|
|
33
33
|
|
|
34
|
-
export interface SidebarMenuItem {
|
|
35
|
-
title: string;
|
|
36
|
-
href?: string;
|
|
37
|
-
icon?: Component<IconProps>;
|
|
38
|
-
children?: SidebarMenuItem[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
34
|
export interface SidebarMenuProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
42
35
|
/**
|
|
43
36
|
* 메뉴 아이템 배열
|
|
44
37
|
*/
|
|
45
|
-
menus:
|
|
38
|
+
menus: AppMenu[];
|
|
46
39
|
}
|
|
47
40
|
|
|
48
41
|
// 내부 Context: 초기 펼침 상태 공유
|
|
49
42
|
interface MenuContextValue {
|
|
50
|
-
initialOpenItems: Accessor<Set<
|
|
43
|
+
initialOpenItems: Accessor<Set<AppMenu>>;
|
|
51
44
|
}
|
|
52
45
|
|
|
53
46
|
const MenuContext = createContext<MenuContextValue>();
|
|
@@ -84,10 +77,10 @@ export const SidebarMenu: Component<SidebarMenuProps> = (props) => {
|
|
|
84
77
|
|
|
85
78
|
// 현재 pathname과 일치하는 메뉴의 부모들을 찾아 펼침 상태 계산
|
|
86
79
|
const findSelectedPath = (
|
|
87
|
-
menus:
|
|
80
|
+
menus: AppMenu[],
|
|
88
81
|
pathname: string,
|
|
89
|
-
path:
|
|
90
|
-
):
|
|
82
|
+
path: AppMenu[] = [],
|
|
83
|
+
): AppMenu[] | null => {
|
|
91
84
|
for (const menu of menus) {
|
|
92
85
|
const currentPath = [...path, menu];
|
|
93
86
|
if (menu.href === pathname) {
|
|
@@ -106,7 +99,7 @@ export const SidebarMenu: Component<SidebarMenuProps> = (props) => {
|
|
|
106
99
|
const selectedPath = findSelectedPath(local.menus, location.pathname);
|
|
107
100
|
return selectedPath
|
|
108
101
|
? new Set(selectedPath.slice(0, -1)) // 마지막 항목(선택된 메뉴)은 제외하고 부모들만 펼침
|
|
109
|
-
: new Set<
|
|
102
|
+
: new Set<AppMenu>();
|
|
110
103
|
});
|
|
111
104
|
|
|
112
105
|
const getClassName = () => twMerge("flex-1 overflow-y-auto", local.class);
|
|
@@ -124,7 +117,7 @@ export const SidebarMenu: Component<SidebarMenuProps> = (props) => {
|
|
|
124
117
|
};
|
|
125
118
|
|
|
126
119
|
interface MenuItemProps {
|
|
127
|
-
menu:
|
|
120
|
+
menu: AppMenu;
|
|
128
121
|
size?: ComponentSize;
|
|
129
122
|
}
|
|
130
123
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type Accessor, createMemo, createRoot } from "solid-js";
|
|
2
2
|
import type { Component } from "solid-js";
|
|
3
3
|
import type { IconProps } from "@tabler/icons-solidjs";
|
|
4
|
-
import type { SidebarMenuItem } from "../components/layout/sidebar/SidebarMenu";
|
|
5
4
|
|
|
6
5
|
// ── 입력 타입 ──
|
|
7
6
|
|
|
@@ -40,6 +39,27 @@ export interface AppStructureSubPerm<TModule> {
|
|
|
40
39
|
|
|
41
40
|
// ── 출력 타입 ──
|
|
42
41
|
|
|
42
|
+
export interface AppMenu {
|
|
43
|
+
title: string;
|
|
44
|
+
href?: string;
|
|
45
|
+
icon?: Component<IconProps>;
|
|
46
|
+
children?: AppMenu[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AppPerm<TModule = string> {
|
|
50
|
+
title: string;
|
|
51
|
+
href?: string;
|
|
52
|
+
modules?: TModule[];
|
|
53
|
+
perms?: string[];
|
|
54
|
+
children?: AppPerm<TModule>[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AppFlatPerm<TModule = string> {
|
|
58
|
+
titleChain: string[];
|
|
59
|
+
code: string;
|
|
60
|
+
modulesChain: TModule[][];
|
|
61
|
+
}
|
|
62
|
+
|
|
43
63
|
export interface AppRoute {
|
|
44
64
|
path: string;
|
|
45
65
|
component: Component;
|
|
@@ -52,10 +72,11 @@ export interface AppFlatMenu {
|
|
|
52
72
|
|
|
53
73
|
export interface AppStructure<TModule> {
|
|
54
74
|
items: AppStructureItem<TModule>[];
|
|
55
|
-
|
|
56
|
-
usableMenus: Accessor<
|
|
75
|
+
usableRoutes: Accessor<AppRoute[]>;
|
|
76
|
+
usableMenus: Accessor<AppMenu[]>;
|
|
57
77
|
usableFlatMenus: Accessor<AppFlatMenu[]>;
|
|
58
|
-
|
|
78
|
+
usablePerms: Accessor<AppPerm<TModule>[]>;
|
|
79
|
+
flatPerms: AppFlatPerm<TModule>[];
|
|
59
80
|
getTitleChainByHref(href: string): string[];
|
|
60
81
|
}
|
|
61
82
|
|
|
@@ -85,42 +106,46 @@ function checkModules<TModule>(
|
|
|
85
106
|
return true;
|
|
86
107
|
}
|
|
87
108
|
|
|
88
|
-
|
|
109
|
+
// ── Routes ──
|
|
110
|
+
|
|
111
|
+
function buildUsableRoutes<TModule>(
|
|
89
112
|
items: AppStructureItem<TModule>[],
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
113
|
+
routeBasePath: string,
|
|
114
|
+
permBasePath: string,
|
|
115
|
+
usableModules: TModule[] | undefined,
|
|
116
|
+
permRecord: Record<string, boolean> | undefined,
|
|
117
|
+
): AppRoute[] {
|
|
118
|
+
const result: AppRoute[] = [];
|
|
119
|
+
|
|
93
120
|
for (const item of items) {
|
|
94
|
-
|
|
121
|
+
if (!checkModules(item.modules, item.requiredModules, usableModules)) continue;
|
|
122
|
+
|
|
123
|
+
const routePath = routeBasePath + "/" + item.code;
|
|
124
|
+
const permPath = permBasePath + "/" + item.code;
|
|
95
125
|
|
|
96
126
|
if (isGroupItem(item)) {
|
|
97
|
-
|
|
127
|
+
result.push(
|
|
128
|
+
...buildUsableRoutes(item.children, routePath, permPath, usableModules, permRecord),
|
|
129
|
+
);
|
|
98
130
|
} else if (item.component !== undefined) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
});
|
|
131
|
+
if (permRecord !== undefined && item.perms?.includes("use") && !permRecord[permPath + "/use"])
|
|
132
|
+
continue;
|
|
133
|
+
result.push({ path: routePath, component: item.component });
|
|
103
134
|
}
|
|
104
135
|
}
|
|
105
|
-
}
|
|
106
136
|
|
|
107
|
-
|
|
108
|
-
const routes: AppRoute[] = [];
|
|
109
|
-
for (const top of items) {
|
|
110
|
-
if (isGroupItem(top)) {
|
|
111
|
-
collectRoutes(top.children, [], routes);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return routes;
|
|
137
|
+
return result;
|
|
115
138
|
}
|
|
116
139
|
|
|
140
|
+
// ── Menus ──
|
|
141
|
+
|
|
117
142
|
function buildMenus<TModule>(
|
|
118
143
|
items: AppStructureItem<TModule>[],
|
|
119
144
|
basePath: string,
|
|
120
145
|
usableModules: TModule[] | undefined,
|
|
121
|
-
permRecord: Record<string, boolean
|
|
122
|
-
):
|
|
123
|
-
const result:
|
|
146
|
+
permRecord: Record<string, boolean> | undefined,
|
|
147
|
+
): AppMenu[] {
|
|
148
|
+
const result: AppMenu[] = [];
|
|
124
149
|
|
|
125
150
|
for (const item of items) {
|
|
126
151
|
if (!checkModules(item.modules, item.requiredModules, usableModules)) continue;
|
|
@@ -134,7 +159,7 @@ function buildMenus<TModule>(
|
|
|
134
159
|
}
|
|
135
160
|
} else {
|
|
136
161
|
if (item.isNotMenu) continue;
|
|
137
|
-
if (item.perms?.includes("use") && !permRecord[href + "/use"]) continue;
|
|
162
|
+
if (item.perms?.includes("use") && !permRecord?.[href + "/use"]) continue;
|
|
138
163
|
|
|
139
164
|
result.push({ title: item.title, href, icon: item.icon });
|
|
140
165
|
}
|
|
@@ -143,7 +168,7 @@ function buildMenus<TModule>(
|
|
|
143
168
|
return result;
|
|
144
169
|
}
|
|
145
170
|
|
|
146
|
-
function flattenMenus(menus:
|
|
171
|
+
function flattenMenus(menus: AppMenu[], titleChain: string[] = []): AppFlatMenu[] {
|
|
147
172
|
const result: AppFlatMenu[] = [];
|
|
148
173
|
|
|
149
174
|
for (const menu of menus) {
|
|
@@ -159,6 +184,117 @@ function flattenMenus(menus: SidebarMenuItem[], titleChain: string[] = []): AppF
|
|
|
159
184
|
return result;
|
|
160
185
|
}
|
|
161
186
|
|
|
187
|
+
// ── Perms ──
|
|
188
|
+
|
|
189
|
+
function buildPerms<TModule>(
|
|
190
|
+
items: AppStructureItem<TModule>[],
|
|
191
|
+
basePath: string,
|
|
192
|
+
usableModules: TModule[] | undefined,
|
|
193
|
+
): AppPerm<TModule>[] {
|
|
194
|
+
const result: AppPerm<TModule>[] = [];
|
|
195
|
+
|
|
196
|
+
for (const item of items) {
|
|
197
|
+
if (!checkModules(item.modules, item.requiredModules, usableModules)) continue;
|
|
198
|
+
|
|
199
|
+
const href = basePath + "/" + item.code;
|
|
200
|
+
|
|
201
|
+
if (isGroupItem(item)) {
|
|
202
|
+
const children = buildPerms(item.children, href, usableModules);
|
|
203
|
+
result.push({
|
|
204
|
+
title: item.title,
|
|
205
|
+
modules: item.modules,
|
|
206
|
+
children,
|
|
207
|
+
});
|
|
208
|
+
} else {
|
|
209
|
+
result.push({
|
|
210
|
+
title: item.title,
|
|
211
|
+
href,
|
|
212
|
+
modules: item.modules,
|
|
213
|
+
perms: item.perms,
|
|
214
|
+
children: item.subPerms
|
|
215
|
+
?.filter((sp) => checkModules(sp.modules, sp.requiredModules, usableModules))
|
|
216
|
+
.map((sp) => ({
|
|
217
|
+
title: sp.title,
|
|
218
|
+
href: href + "/" + sp.code,
|
|
219
|
+
modules: sp.modules,
|
|
220
|
+
perms: sp.perms as string[],
|
|
221
|
+
})),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function collectFlatPerms<TModule>(items: AppStructureItem<TModule>[]): AppFlatPerm<TModule>[] {
|
|
230
|
+
const results: AppFlatPerm<TModule>[] = [];
|
|
231
|
+
|
|
232
|
+
interface QueueItem {
|
|
233
|
+
item: AppStructureItem<TModule>;
|
|
234
|
+
titleChain: string[];
|
|
235
|
+
codePath: string;
|
|
236
|
+
modulesChain: TModule[][];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const queue: QueueItem[] = items.map((item) => ({
|
|
240
|
+
item,
|
|
241
|
+
titleChain: [],
|
|
242
|
+
codePath: "",
|
|
243
|
+
modulesChain: [],
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
while (queue.length > 0) {
|
|
247
|
+
const { item, titleChain, codePath, modulesChain } = queue.shift()!;
|
|
248
|
+
|
|
249
|
+
const currTitleChain = [...titleChain, item.title];
|
|
250
|
+
const currCodePath = codePath + "/" + item.code;
|
|
251
|
+
const currModulesChain: TModule[][] = item.modules
|
|
252
|
+
? [...modulesChain, item.modules]
|
|
253
|
+
: modulesChain;
|
|
254
|
+
|
|
255
|
+
if (isGroupItem(item)) {
|
|
256
|
+
for (const child of item.children) {
|
|
257
|
+
queue.push({
|
|
258
|
+
item: child,
|
|
259
|
+
titleChain: currTitleChain,
|
|
260
|
+
codePath: currCodePath,
|
|
261
|
+
modulesChain: currModulesChain,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
if (item.perms) {
|
|
266
|
+
for (const perm of item.perms) {
|
|
267
|
+
results.push({
|
|
268
|
+
titleChain: currTitleChain,
|
|
269
|
+
code: currCodePath + "/" + perm,
|
|
270
|
+
modulesChain: currModulesChain,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (item.subPerms) {
|
|
276
|
+
for (const subPerm of item.subPerms) {
|
|
277
|
+
const subModulesChain: TModule[][] = subPerm.modules
|
|
278
|
+
? [...currModulesChain, subPerm.modules]
|
|
279
|
+
: currModulesChain;
|
|
280
|
+
|
|
281
|
+
for (const perm of subPerm.perms) {
|
|
282
|
+
results.push({
|
|
283
|
+
titleChain: currTitleChain,
|
|
284
|
+
code: currCodePath + "/" + subPerm.code + "/" + perm,
|
|
285
|
+
modulesChain: subModulesChain,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return results;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ── Info ──
|
|
297
|
+
|
|
162
298
|
function findItemChainByCodes<TModule>(
|
|
163
299
|
items: AppStructureItem<TModule>[],
|
|
164
300
|
codes: string[],
|
|
@@ -183,17 +319,38 @@ export function createAppStructure<TModule>(opts: {
|
|
|
183
319
|
usableModules?: Accessor<TModule[] | undefined>;
|
|
184
320
|
permRecord?: Accessor<Record<string, boolean>>;
|
|
185
321
|
}): AppStructure<TModule> {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const routes = extractRoutes(opts.items);
|
|
322
|
+
const flatPerms = collectFlatPerms(opts.items);
|
|
189
323
|
|
|
190
324
|
const memos = createRoot(() => {
|
|
325
|
+
const usableRoutes = createMemo(() => {
|
|
326
|
+
const routes: AppRoute[] = [];
|
|
327
|
+
for (const top of opts.items) {
|
|
328
|
+
if (isGroupItem(top)) {
|
|
329
|
+
routes.push(
|
|
330
|
+
...buildUsableRoutes(
|
|
331
|
+
top.children,
|
|
332
|
+
"",
|
|
333
|
+
"/" + top.code,
|
|
334
|
+
opts.usableModules?.(),
|
|
335
|
+
opts.permRecord?.(),
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return routes;
|
|
341
|
+
});
|
|
342
|
+
|
|
191
343
|
const usableMenus = createMemo(() => {
|
|
192
|
-
const menus:
|
|
344
|
+
const menus: AppMenu[] = [];
|
|
193
345
|
for (const top of opts.items) {
|
|
194
346
|
if (isGroupItem(top)) {
|
|
195
347
|
menus.push(
|
|
196
|
-
...buildMenus(
|
|
348
|
+
...buildMenus(
|
|
349
|
+
top.children,
|
|
350
|
+
"/" + top.code,
|
|
351
|
+
opts.usableModules?.(),
|
|
352
|
+
opts.permRecord?.(),
|
|
353
|
+
),
|
|
197
354
|
);
|
|
198
355
|
}
|
|
199
356
|
}
|
|
@@ -202,15 +359,18 @@ export function createAppStructure<TModule>(opts: {
|
|
|
202
359
|
|
|
203
360
|
const usableFlatMenus = createMemo(() => flattenMenus(usableMenus()));
|
|
204
361
|
|
|
205
|
-
|
|
362
|
+
const usablePerms = createMemo(() => buildPerms(opts.items, "", opts.usableModules?.()));
|
|
363
|
+
|
|
364
|
+
return { usableRoutes, usableMenus, usableFlatMenus, usablePerms };
|
|
206
365
|
});
|
|
207
366
|
|
|
208
367
|
return {
|
|
209
368
|
items: opts.items,
|
|
210
|
-
|
|
369
|
+
usableRoutes: memos.usableRoutes,
|
|
211
370
|
usableMenus: memos.usableMenus,
|
|
212
371
|
usableFlatMenus: memos.usableFlatMenus,
|
|
213
|
-
|
|
372
|
+
usablePerms: memos.usablePerms,
|
|
373
|
+
flatPerms,
|
|
214
374
|
getTitleChainByHref(href: string): string[] {
|
|
215
375
|
const codes = href.split("/").filter(Boolean);
|
|
216
376
|
return findItemChainByCodes(opts.items, codes).map((item) => item.title);
|