@simplysm/solid 13.0.96 → 13.0.98
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/README.md +314 -123
- package/docs/data.md +216 -0
- package/docs/disclosure.md +164 -0
- package/docs/display.md +94 -0
- package/docs/features.md +211 -625
- package/docs/feedback.md +210 -0
- package/docs/form-control.md +537 -0
- package/docs/helpers.md +168 -0
- package/docs/hooks.md +143 -0
- package/docs/layout.md +182 -0
- package/docs/providers.md +177 -0
- package/docs/styles.md +127 -0
- package/package.json +19 -26
- package/docs/display-feedback.md +0 -404
- package/docs/form-controls.md +0 -587
- package/docs/layout-data.md +0 -392
- package/docs/providers-hooks.md +0 -516
package/docs/features.md
CHANGED
|
@@ -1,693 +1,279 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Feature Components
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Source: `src/components/features/**/*.tsx`
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## AddressSearchContent
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Korean address search component using Daum Postcode API.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```tsx
|
|
16
|
-
// CRUD 기반 컴포넌트는 close prop을 받아 그대로 전달하는 것이 관례
|
|
17
|
-
function UserList(props: { close?: (result?: boolean) => void }) {
|
|
18
|
-
return (
|
|
19
|
-
<CrudSheet close={props.close} search={...} getItemKey={...} inlineEdit={...}>
|
|
20
|
-
...
|
|
21
|
-
</CrudSheet>
|
|
22
|
-
);
|
|
9
|
+
```ts
|
|
10
|
+
interface AddressSearchResult {
|
|
11
|
+
postNumber: string | undefined;
|
|
12
|
+
address: string | undefined;
|
|
13
|
+
buildingName: string | undefined;
|
|
23
14
|
}
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// Page 모드 -- Topbar 하위 라우트에 배치, close 미전달
|
|
29
|
-
<Route path="/users" component={UserList} />
|
|
30
|
-
|
|
31
|
-
// Control 모드 -- 다른 페이지의 일부로 임베드, close 미전달
|
|
32
|
-
<div class="grid grid-cols-2">
|
|
33
|
-
<UserList />
|
|
34
|
-
<OrderList />
|
|
35
|
-
</div>
|
|
16
|
+
const AddressSearchContent: Component<{
|
|
17
|
+
close?: (result?: AddressSearchResult) => void;
|
|
18
|
+
}>;
|
|
36
19
|
```
|
|
37
20
|
|
|
38
|
-
|
|
21
|
+
Designed for use with `useDialog().show()`. Loads the Daum Postcode script on mount.
|
|
39
22
|
|
|
40
|
-
##
|
|
23
|
+
## SharedDataSelect
|
|
41
24
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
편집 방식은 `inlineEdit`과 `dialogEdit` 중 하나를 선택한다 (동시 사용 불가).
|
|
45
|
-
|
|
46
|
-
- **inlineEdit**: 셀 직접 편집, 행 추가/삭제, diff 기반 일괄 저장
|
|
47
|
-
- **dialogEdit**: 행 클릭 시 다이얼로그로 편집, 선택 항목 일괄 삭제/복원
|
|
48
|
-
|
|
49
|
-
```tsx
|
|
50
|
-
import { CrudSheet } from "@simplysm/solid";
|
|
51
|
-
|
|
52
|
-
// 인라인 편집
|
|
53
|
-
function UserSheet(props: { close?: (result?: boolean) => void }) {
|
|
54
|
-
return (
|
|
55
|
-
<CrudSheet
|
|
56
|
-
close={props.close}
|
|
57
|
-
search={(filter, page, sorts) => fetchUsers(filter, page, sorts)}
|
|
58
|
-
getItemKey={(item) => item.id}
|
|
59
|
-
inlineEdit={{
|
|
60
|
-
submit: (diffs) => saveUsers(diffs),
|
|
61
|
-
newItem: () => ({ id: undefined, name: "", email: "" }),
|
|
62
|
-
deleteProp: "isDeleted",
|
|
63
|
-
}}
|
|
64
|
-
>
|
|
65
|
-
<CrudSheet.Column header="이름" key="name">
|
|
66
|
-
{(ctx) => <TextInput value={ctx.item.name} onValueChange={(v) => ctx.setItem("name", v)} />}
|
|
67
|
-
</CrudSheet.Column>
|
|
68
|
-
</CrudSheet>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
25
|
+
Select component bound to a shared data definition. Supports single and multiple modes with the same prop variants as `Select`.
|
|
71
26
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
dialogEdit={{
|
|
81
|
-
editItem: (item) => dialog.open(ProductDetail, { item }),
|
|
82
|
-
deleteItems: (items) => deleteProducts(items),
|
|
83
|
-
restoreItems: (items) => restoreProducts(items),
|
|
84
|
-
}}
|
|
85
|
-
>
|
|
86
|
-
<CrudSheet.Column header="상품명" key="name" editTrigger>
|
|
87
|
-
{(ctx) => ctx.item.name}
|
|
88
|
-
</CrudSheet.Column>
|
|
89
|
-
</CrudSheet>
|
|
90
|
-
);
|
|
91
|
-
}
|
|
27
|
+
```ts
|
|
28
|
+
type SharedDataSelectProps<TItem, TDataKey extends string> = {
|
|
29
|
+
dataKey: TDataKey;
|
|
30
|
+
// ...inherits Select props (value, onValueChange, multiple, etc.)
|
|
31
|
+
// plus shared data specific:
|
|
32
|
+
getItemSearchText?: (item: TItem) => string;
|
|
33
|
+
isItemHidden?: (item: TItem) => boolean;
|
|
34
|
+
};
|
|
92
35
|
```
|
|
93
36
|
|
|
94
|
-
###
|
|
95
|
-
|
|
96
|
-
| Prop | 타입 | 설명 |
|
|
97
|
-
|------|------|------|
|
|
98
|
-
| `search` | `(filter, page, sorts) => Promise<SearchResult<TItem>>` | 데이터 조회 함수. `page`가 `undefined`면 전체 조회 (Excel 등) |
|
|
99
|
-
| `getItemKey` | `(item: TItem) => string \| number \| undefined` | 아이템 고유 키 반환 |
|
|
100
|
-
| `close` | `() => void` | Dialog 모드 활성화. 전달 시 Dialog 헤더에 새로고침 버튼 표시 |
|
|
101
|
-
| `inlineEdit` | `InlineEditConfig<TItem>` | 인라인 편집 설정 (`dialogEdit`과 동시 사용 불가) |
|
|
102
|
-
| `dialogEdit` | `DialogEditConfig<TItem>` | 다이얼로그 편집 설정 (`inlineEdit`과 동시 사용 불가) |
|
|
103
|
-
| `editable` | `boolean` | 편집 버튼 표시 여부 (기본값: `true`) |
|
|
104
|
-
| `isItemEditable` | `(item: TItem) => boolean` | 아이템별 편집 가능 여부 (dialogEdit의 editTrigger 링크에 적용) |
|
|
105
|
-
| `isItemDeletable` | `(item: TItem) => boolean` | 아이템별 삭제 가능 여부 (삭제 버튼/링크 비활성화) |
|
|
106
|
-
| `isItemDeleted` | `(item: TItem) => boolean` | 아이템 삭제 상태 확인 (취소선 표시) |
|
|
107
|
-
| `isItemSelectable` | `(item: TItem) => boolean \| string` | 아이템별 선택 가능 여부 |
|
|
108
|
-
| `filterInitial` | `TFilter` | 필터 초기값 |
|
|
109
|
-
| `items` | `TItem[]` | 제어 모드: 외부에서 아이템 배열 전달 |
|
|
110
|
-
| `onItemsChange` | `(items: TItem[]) => void` | 제어 모드: 아이템 변경 콜백 |
|
|
111
|
-
| `storageKey` | `string` | DataSheet 컬럼 너비 등 상태 저장 키 |
|
|
112
|
-
| `lastModifiedAtProp` | `string` | 자동 "최종수정일시" 컬럼 추가 (DateTime 타입, hidden) |
|
|
113
|
-
| `lastModifiedByProp` | `string` | 자동 "수정자" 컬럼 추가 (string 타입, hidden) |
|
|
114
|
-
| `excel` | `ExcelConfig<TItem>` | Excel 가져오기/내보내기 설정 |
|
|
115
|
-
| `selectionMode` | `"single" \| "multiple"` | 선택 모드 활성화 |
|
|
116
|
-
| `selectedKeys` | `(string \| number)[]` | 제어 모드: 선택된 키 배열 |
|
|
117
|
-
| `onSelectedKeysChange` | `(keys) => void` | 제어 모드: 선택 변경 콜백 |
|
|
118
|
-
| `onSelect` | `(result: SelectResult<TItem>) => void` | 선택 확인 콜백 (single 모드는 클릭 시 자동 호출) |
|
|
119
|
-
| `onSubmitComplete` | `() => void` | 저장 완료 후 콜백 |
|
|
120
|
-
| `hideAutoTools` | `boolean` | 자동 생성 툴바 버튼 숨기기 |
|
|
121
|
-
| `class` | `string` | CSS 클래스 |
|
|
122
|
-
|
|
123
|
-
### 설정 타입
|
|
124
|
-
|
|
125
|
-
```typescript
|
|
126
|
-
// 인라인 편집 설정
|
|
127
|
-
interface InlineEditConfig<TItem> {
|
|
128
|
-
submit: (diffs: ArrayOneWayDiffResult<TItem>[]) => Promise<void>;
|
|
129
|
-
newItem: () => TItem;
|
|
130
|
-
deleteProp?: keyof TItem & string; // 삭제 플래그 프로퍼티 (예: "isDeleted")
|
|
131
|
-
diffsExcludes?: string[]; // diff 비교 제외 프로퍼티
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 다이얼로그 편집 설정
|
|
135
|
-
interface DialogEditConfig<TItem> {
|
|
136
|
-
editItem: (item?: TItem) => Promise<boolean | undefined>; // 등록(item 없음)/수정 다이얼로그
|
|
137
|
-
deleteItems?: (items: TItem[]) => Promise<boolean>; // 선택 항목 삭제
|
|
138
|
-
restoreItems?: (items: TItem[]) => Promise<boolean>; // 선택 항목 복원
|
|
139
|
-
}
|
|
37
|
+
### Sub-components
|
|
140
38
|
|
|
141
|
-
|
|
142
|
-
interface ExcelConfig<TItem> {
|
|
143
|
-
download: (items: TItem[]) => Promise<void>; // 전체 데이터 다운로드
|
|
144
|
-
upload?: (file: File) => Promise<void>; // .xlsx 파일 업로드
|
|
145
|
-
}
|
|
39
|
+
- **`SharedDataSelect.ItemTemplate`** -- Render template for items.
|
|
146
40
|
|
|
147
|
-
|
|
148
|
-
interface SearchResult<TItem> {
|
|
149
|
-
items: TItem[];
|
|
150
|
-
pageCount?: number; // 페이지 수 (미설정 시 페이징 없음)
|
|
151
|
-
}
|
|
41
|
+
## SharedDataSelectButton
|
|
152
42
|
|
|
153
|
-
|
|
154
|
-
interface SelectResult<TItem> {
|
|
155
|
-
items: TItem[];
|
|
156
|
-
keys: (string | number)[];
|
|
157
|
-
}
|
|
158
|
-
```
|
|
43
|
+
Button that opens a shared data selection dialog.
|
|
159
44
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
<CrudSheet
|
|
169
|
-
search={searchFn}
|
|
170
|
-
getItemKey={(item) => item.id}
|
|
171
|
-
selectionMode="multiple"
|
|
172
|
-
selectedKeys={selectedKeys()}
|
|
173
|
-
onSelectedKeysChange={setSelectedKeys}
|
|
174
|
-
>
|
|
175
|
-
<CrudSheet.Column key="name" header="이름">
|
|
176
|
-
{(ctx) => <div>{ctx.item.name}</div>}
|
|
177
|
-
</CrudSheet.Column>
|
|
178
|
-
</CrudSheet>
|
|
179
|
-
|
|
180
|
-
// Dialog에서 단일 선택 (클릭 시 자동 확정)
|
|
181
|
-
<CrudSheet
|
|
182
|
-
close={props.close}
|
|
183
|
-
search={searchFn}
|
|
184
|
-
getItemKey={(item) => item.id}
|
|
185
|
-
selectionMode="single"
|
|
186
|
-
onSelect={(result) => props.close?.({ selectedKeys: result.keys })}
|
|
187
|
-
>
|
|
188
|
-
...
|
|
189
|
-
</CrudSheet>
|
|
45
|
+
```ts
|
|
46
|
+
type SharedDataSelectButtonProps<TItem, TDataKey extends string> = {
|
|
47
|
+
dataKey: TDataKey;
|
|
48
|
+
value?: TItem | TItem[];
|
|
49
|
+
onValueChange?: (value: TItem | TItem[] | undefined) => void;
|
|
50
|
+
renderValue?: (value: TItem) => JSX.Element;
|
|
51
|
+
// ...plus button/validation props
|
|
52
|
+
};
|
|
190
53
|
```
|
|
191
54
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
```tsx
|
|
195
|
-
<CrudSheet
|
|
196
|
-
search={searchFn}
|
|
197
|
-
getItemKey={(item) => item.id}
|
|
198
|
-
excel={{
|
|
199
|
-
download: async (items) => {
|
|
200
|
-
// items는 전체 데이터 (페이징 무시하고 재조회)
|
|
201
|
-
await exportToExcel(items);
|
|
202
|
-
},
|
|
203
|
-
upload: async (file) => {
|
|
204
|
-
// .xlsx 파일 업로드 후 자동 새로고침
|
|
205
|
-
await importFromExcel(file);
|
|
206
|
-
},
|
|
207
|
-
}}
|
|
208
|
-
>
|
|
209
|
-
...
|
|
210
|
-
</CrudSheet>
|
|
211
|
-
```
|
|
55
|
+
## SharedDataSelectList
|
|
212
56
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
| 단축키 | 동작 |
|
|
216
|
-
|--------|------|
|
|
217
|
-
| `Ctrl+S` | 저장 (인라인 편집 모드) |
|
|
218
|
-
| `Ctrl+Alt+L` | 새로고침 |
|
|
219
|
-
|
|
220
|
-
활성화 조건: 해당 CrudSheet 영역에 포커스/클릭이 있어야 한다 (다중 CrudSheet 환경에서 충돌 방지).
|
|
221
|
-
|
|
222
|
-
### CrudSheet 서브 컴포넌트
|
|
223
|
-
|
|
224
|
-
| 컴포넌트 | 설명 |
|
|
225
|
-
|----------|------|
|
|
226
|
-
| `CrudSheet.Column` | 컬럼 정의. `editTrigger`로 dialogEdit 시 클릭 편집 링크 표시 |
|
|
227
|
-
| `CrudSheet.Filter` | 검색 필터 영역. render prop으로 `(filter, setFilter)` 전달 |
|
|
228
|
-
| `CrudSheet.Tools` | 커스텀 툴바 버튼. render prop으로 `CrudSheetContext` 전달 |
|
|
229
|
-
| `CrudSheet.Header` | 시트 상단 커스텀 헤더 영역 |
|
|
230
|
-
|
|
231
|
-
### CrudSheet.Column
|
|
232
|
-
|
|
233
|
-
`DataSheetColumn`의 모든 props를 상속하며, 추가로 `editTrigger`와 CRUD 전용 셀 컨텍스트를 제공한다.
|
|
234
|
-
|
|
235
|
-
```tsx
|
|
236
|
-
<CrudSheet.Column<TItem>
|
|
237
|
-
key="name" // 컬럼 식별 키 (정렬, 저장에 사용)
|
|
238
|
-
header="이름" // 헤더 텍스트 또는 JSX
|
|
239
|
-
editTrigger // dialogEdit 시 클릭 편집 링크 표시
|
|
240
|
-
fixed // 고정 컬럼
|
|
241
|
-
hidden // 기본 숨김 (사용자가 토글 가능)
|
|
242
|
-
collapse // 접기 가능
|
|
243
|
-
width="150px" // 초기 너비
|
|
244
|
-
sortable={true} // 정렬 가능 (기본: true)
|
|
245
|
-
resizable={true} // 리사이즈 가능 (기본: true)
|
|
246
|
-
summary={(items) => items.reduce((sum, item) => sum + item.amount, 0)}
|
|
247
|
-
>
|
|
248
|
-
{(ctx) => (
|
|
249
|
-
// ctx: CrudSheetCellContext<TItem>
|
|
250
|
-
// ctx.item: 현재 아이템
|
|
251
|
-
// ctx.index: 배열 인덱스
|
|
252
|
-
// ctx.row: 행 번호
|
|
253
|
-
// ctx.depth: 트리 깊이
|
|
254
|
-
// ctx.setItem(key, value): 인라인 편집 시 값 변경
|
|
255
|
-
<TextInput value={ctx.item.name} onValueChange={(v) => ctx.setItem("name", v)} />
|
|
256
|
-
)}
|
|
257
|
-
</CrudSheet.Column>
|
|
258
|
-
```
|
|
57
|
+
List component bound to shared data, rendering items from a shared data definition.
|
|
259
58
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
<>
|
|
268
|
-
<FormGroup.Item label="검색어">
|
|
269
|
-
<TextInput value={filter.searchText ?? ""} onValueChange={(v) => setFilter("searchText", v)} />
|
|
270
|
-
</FormGroup.Item>
|
|
271
|
-
<FormGroup.Item label="상태">
|
|
272
|
-
<Select
|
|
273
|
-
value={filter.status}
|
|
274
|
-
onValueChange={(v) => setFilter("status", v)}
|
|
275
|
-
items={["active", "inactive"]}
|
|
276
|
-
/>
|
|
277
|
-
</FormGroup.Item>
|
|
278
|
-
</>
|
|
279
|
-
)}
|
|
280
|
-
</CrudSheet.Filter>
|
|
59
|
+
```ts
|
|
60
|
+
interface SharedDataSelectListProps<TItem> {
|
|
61
|
+
dataKey: string;
|
|
62
|
+
value?: unknown[];
|
|
63
|
+
onValueChange?: (value: unknown[]) => void;
|
|
64
|
+
// ...list configuration props
|
|
65
|
+
}
|
|
281
66
|
```
|
|
282
67
|
|
|
283
|
-
###
|
|
284
|
-
|
|
285
|
-
커스텀 툴바 버튼. render prop으로 `CrudSheetContext`를 전달하여 내부 상태 접근 및 액션 호출이 가능하다.
|
|
286
|
-
|
|
287
|
-
```tsx
|
|
288
|
-
<CrudSheet.Tools<TItem>>
|
|
289
|
-
{(ctx) => (
|
|
290
|
-
<>
|
|
291
|
-
<Button size="sm" onClick={() => void handleCustomAction(ctx.selection())}>
|
|
292
|
-
커스텀 액션 ({ctx.selection().length}건)
|
|
293
|
-
</Button>
|
|
294
|
-
<Show when={ctx.hasChanges()}>
|
|
295
|
-
<span class="text-warning-500">변경사항 있음</span>
|
|
296
|
-
</Show>
|
|
297
|
-
</>
|
|
298
|
-
)}
|
|
299
|
-
</CrudSheet.Tools>
|
|
300
|
-
```
|
|
68
|
+
### Sub-components
|
|
301
69
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
| 멤버 | 타입 | 설명 |
|
|
305
|
-
|------|------|------|
|
|
306
|
-
| `items()` | `TItem[]` | 현재 페이지 아이템 목록 |
|
|
307
|
-
| `selection()` | `TItem[]` | 현재 선택된 아이템 목록 |
|
|
308
|
-
| `page()` | `number` | 현재 페이지 번호 |
|
|
309
|
-
| `sorts()` | `SortingDef[]` | 현재 정렬 정의 |
|
|
310
|
-
| `busy()` | `boolean` | 로딩 중 여부 |
|
|
311
|
-
| `hasChanges()` | `boolean` | 변경사항 존재 여부 (인라인 편집) |
|
|
312
|
-
| `save()` | `Promise<void>` | 저장 실행 |
|
|
313
|
-
| `refresh()` | `Promise<void>` | 데이터 새로고침 |
|
|
314
|
-
| `addItem()` | `void` | 행 추가 (인라인 편집) |
|
|
315
|
-
| `clearSelection()` | `void` | 선택 초기화 |
|
|
316
|
-
| `setPage(page)` | `void` | 페이지 변경 |
|
|
317
|
-
| `setSorts(sorts)` | `void` | 정렬 변경 |
|
|
318
|
-
|
|
319
|
-
### CrudSheet.Header
|
|
320
|
-
|
|
321
|
-
시트 상단에 커스텀 헤더 영역을 추가한다.
|
|
322
|
-
|
|
323
|
-
```tsx
|
|
324
|
-
<CrudSheet.Header>
|
|
325
|
-
<div class="p-2 text-lg font-bold">사용자 관리</div>
|
|
326
|
-
</CrudSheet.Header>
|
|
327
|
-
```
|
|
70
|
+
- **`SharedDataSelectList.ItemTemplate`** -- Render template for list items.
|
|
328
71
|
|
|
329
|
-
|
|
72
|
+
## DataSelectButton
|
|
330
73
|
|
|
331
|
-
|
|
74
|
+
Button that opens a custom data selection dialog. Generic over key type and dialog props.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
interface DataSelectButtonProps<TKey, TDialogProps> {
|
|
78
|
+
value?: TKey | TKey[];
|
|
79
|
+
onValueChange?: (value: TKey | TKey[] | undefined) => void;
|
|
80
|
+
renderValue?: (value: TKey) => JSX.Element;
|
|
81
|
+
dialog: Component<TDialogProps>;
|
|
82
|
+
dialogProps?: Omit<TDialogProps, "close">;
|
|
83
|
+
multiple?: boolean;
|
|
84
|
+
disabled?: boolean;
|
|
85
|
+
required?: boolean;
|
|
86
|
+
size?: ComponentSize;
|
|
87
|
+
inset?: boolean;
|
|
88
|
+
class?: string;
|
|
89
|
+
style?: JSX.CSSProperties;
|
|
90
|
+
}
|
|
332
91
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
```tsx
|
|
336
|
-
import { CrudDetail } from "@simplysm/solid";
|
|
337
|
-
|
|
338
|
-
function UserDetail(props: { close?: (result?: boolean) => void; userId: number }) {
|
|
339
|
-
return (
|
|
340
|
-
<CrudDetail
|
|
341
|
-
close={props.close}
|
|
342
|
-
load={() => fetchUser(props.userId)}
|
|
343
|
-
submit={(data) => saveUser(data)}
|
|
344
|
-
toggleDelete={(del) => toggleDeleteUser(props.userId, del)}
|
|
345
|
-
>
|
|
346
|
-
{(ctx) => (
|
|
347
|
-
<FormGroup>
|
|
348
|
-
<FormGroup.Item label="이름">
|
|
349
|
-
<TextInput value={ctx.data.name} onValueChange={(v) => ctx.setData("name", v)} />
|
|
350
|
-
</FormGroup.Item>
|
|
351
|
-
</FormGroup>
|
|
352
|
-
)}
|
|
353
|
-
</CrudDetail>
|
|
354
|
-
);
|
|
92
|
+
interface DataSelectDialogResult<TKey> {
|
|
93
|
+
selectedKeys: TKey[];
|
|
355
94
|
}
|
|
356
95
|
|
|
357
|
-
|
|
358
|
-
|
|
96
|
+
interface SelectDialogBaseProps<TKey> {
|
|
97
|
+
close?: (result?: DataSelectDialogResult<TKey>) => void;
|
|
98
|
+
initialSelectedKeys?: TKey[];
|
|
99
|
+
selectionMode?: "single" | "multiple";
|
|
100
|
+
}
|
|
359
101
|
```
|
|
360
102
|
|
|
361
|
-
|
|
103
|
+
## CrudSheet
|
|
362
104
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
105
|
+
Full-featured CRUD data grid combining DataSheet with search, pagination, sorting, inline/dialog editing, selection, and Excel import/export.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
type CrudSheetProps<TItem, TFilter extends Record<string, unknown>> = {
|
|
109
|
+
search: (filter: TFilter, page: number | undefined, sorts: SortingDef[]) => Promise<SearchResult<TItem>>;
|
|
110
|
+
getItemKey: (item: TItem) => string | number | undefined;
|
|
111
|
+
storageKey?: string;
|
|
112
|
+
editable?: boolean;
|
|
113
|
+
isItemEditable?: (item: TItem) => boolean;
|
|
114
|
+
isItemDeletable?: (item: TItem) => boolean;
|
|
115
|
+
isItemDeleted?: (item: TItem) => boolean;
|
|
116
|
+
isItemSelectable?: (item: TItem) => boolean | string;
|
|
117
|
+
lastModifiedAtProp?: string;
|
|
118
|
+
lastModifiedByProp?: string;
|
|
119
|
+
filterInitial?: TFilter;
|
|
120
|
+
items?: TItem[];
|
|
121
|
+
onItemsChange?: (items: TItem[]) => void;
|
|
122
|
+
excel?: ExcelConfig<TItem>;
|
|
123
|
+
selectionMode?: "single" | "multiple";
|
|
124
|
+
selectedKeys?: (string | number)[];
|
|
125
|
+
onSelectedKeysChange?: (keys: (string | number)[]) => void;
|
|
126
|
+
onSelect?: (result: SelectResult<TItem>) => void;
|
|
127
|
+
onSubmitComplete?: () => void;
|
|
128
|
+
hideAutoTools?: boolean;
|
|
129
|
+
close?: () => void;
|
|
130
|
+
class?: string;
|
|
131
|
+
children: JSX.Element;
|
|
132
|
+
} & (
|
|
133
|
+
| { inlineEdit: InlineEditConfig<TItem>; dialogEdit?: never }
|
|
134
|
+
| { dialogEdit: DialogEditConfig<TItem>; inlineEdit?: never }
|
|
135
|
+
| { inlineEdit?: never; dialogEdit?: never }
|
|
136
|
+
);
|
|
375
137
|
|
|
376
|
-
|
|
138
|
+
interface SearchResult<TItem> {
|
|
139
|
+
items: TItem[];
|
|
140
|
+
pageCount?: number;
|
|
141
|
+
}
|
|
377
142
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
lastModifiedBy?: string; // 최종 수정자
|
|
143
|
+
interface InlineEditConfig<TItem> {
|
|
144
|
+
submit: (diffs: ArrayOneWayDiffResult<TItem>[]) => Promise<void>;
|
|
145
|
+
newItem: () => TItem;
|
|
146
|
+
deleteProp?: keyof TItem & string;
|
|
147
|
+
diffsExcludes?: string[];
|
|
384
148
|
}
|
|
385
|
-
```
|
|
386
149
|
|
|
387
|
-
|
|
150
|
+
interface DialogEditConfig<TItem> {
|
|
151
|
+
editItem: (item?: TItem) => Promise<boolean | undefined>;
|
|
152
|
+
deleteItems?: (items: TItem[]) => Promise<boolean>;
|
|
153
|
+
restoreItems?: (items: TItem[]) => Promise<boolean>;
|
|
154
|
+
}
|
|
388
155
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
setData: SetStoreFunction<TData>; // 데이터 수정 (SolidJS store setter)
|
|
393
|
-
info: () => CrudDetailInfo; // 상세 정보
|
|
394
|
-
busy: () => boolean; // 로딩 중 여부
|
|
395
|
-
hasChanges: () => boolean; // 변경사항 존재 여부
|
|
396
|
-
save: () => Promise<void>; // 저장 실행
|
|
397
|
-
refresh: () => Promise<void>; // 데이터 새로고침
|
|
156
|
+
interface ExcelConfig<TItem> {
|
|
157
|
+
download: (items: TItem[]) => Promise<void>;
|
|
158
|
+
upload?: (file: File) => Promise<void>;
|
|
398
159
|
}
|
|
399
160
|
```
|
|
400
161
|
|
|
401
|
-
###
|
|
402
|
-
|
|
403
|
-
| 컴포넌트 | 설명 |
|
|
404
|
-
|----------|------|
|
|
405
|
-
| `CrudDetail.Tools` | 커스텀 툴바 버튼 |
|
|
406
|
-
| `CrudDetail.Before` | 폼 위쪽 영역 (폼 외부) |
|
|
407
|
-
| `CrudDetail.After` | 폼 아래쪽 영역 (폼 외부) |
|
|
408
|
-
|
|
409
|
-
---
|
|
410
|
-
|
|
411
|
-
## DataSelectButton
|
|
412
|
-
|
|
413
|
-
다이얼로그 기반 데이터 선택 버튼. 선택 다이얼로그를 열어 항목을 선택하고, 선택된 항목을 표시한다.
|
|
414
|
-
|
|
415
|
-
```tsx
|
|
416
|
-
import { DataSelectButton, type SelectDialogBaseProps } from "@simplysm/solid";
|
|
417
|
-
|
|
418
|
-
// 선택 다이얼로그 컴포넌트 정의
|
|
419
|
-
function UserSelectDialog(props: SelectDialogBaseProps & { department?: string }) {
|
|
420
|
-
// props.selectionMode: "single" | "multiple"
|
|
421
|
-
// props.selectedKeys: 기존 선택된 키 배열
|
|
422
|
-
// props.close: 결과 반환 함수
|
|
162
|
+
### Sub-components
|
|
423
163
|
|
|
424
|
-
|
|
425
|
-
props.close?.({ selectedKeys: userIds });
|
|
426
|
-
};
|
|
164
|
+
- **`CrudSheet.Column<TItem>`** -- Column definition with cell context.
|
|
427
165
|
|
|
428
|
-
|
|
166
|
+
```ts
|
|
167
|
+
interface CrudSheetColumnProps<TItem> extends Omit<DataSheetColumnProps<TItem>, "children"> {
|
|
168
|
+
editTrigger?: boolean;
|
|
169
|
+
children: (ctx: CrudSheetCellContext<TItem>) => JSX.Element;
|
|
429
170
|
}
|
|
430
171
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
dialogOptions={{ header: "사용자 선택", width: "600px" }}
|
|
439
|
-
/>
|
|
440
|
-
|
|
441
|
-
// 다중 선택
|
|
442
|
-
<DataSelectButton
|
|
443
|
-
multiple
|
|
444
|
-
value={selectedUserIds()}
|
|
445
|
-
onValueChange={setSelectedUserIds}
|
|
446
|
-
load={async (keys) => await loadUsers(keys)}
|
|
447
|
-
renderItem={(user) => <span>{user.name}</span>}
|
|
448
|
-
dialog={UserSelectDialog}
|
|
449
|
-
dialogOptions={{ header: "사용자 선택" }}
|
|
450
|
-
/>
|
|
451
|
-
|
|
452
|
-
// 다이얼로그에 추가 props 전달
|
|
453
|
-
<DataSelectButton
|
|
454
|
-
value={selectedUserId()}
|
|
455
|
-
onValueChange={setSelectedUserId}
|
|
456
|
-
load={loadUsers}
|
|
457
|
-
renderItem={(user) => <span>{user.name}</span>}
|
|
458
|
-
dialog={UserSelectDialog}
|
|
459
|
-
dialogProps={{ department: "engineering" }}
|
|
460
|
-
/>
|
|
172
|
+
interface CrudSheetCellContext<TItem> {
|
|
173
|
+
item: TItem;
|
|
174
|
+
index: number;
|
|
175
|
+
row: number;
|
|
176
|
+
depth: number;
|
|
177
|
+
setItem: <TKey extends keyof TItem>(key: TKey, value: TItem[TKey]) => void;
|
|
178
|
+
}
|
|
461
179
|
```
|
|
462
180
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
```typescript
|
|
466
|
-
interface SelectDialogBaseProps<TKey = string | number> {
|
|
467
|
-
close?: (result?: DataSelectDialogResult<TKey>) => void;
|
|
468
|
-
selectionMode: "single" | "multiple";
|
|
469
|
-
selectedKeys: TKey[];
|
|
470
|
-
}
|
|
181
|
+
- **`CrudSheet.Filter<TFilter>`** -- Filter panel slot.
|
|
471
182
|
|
|
472
|
-
|
|
473
|
-
|
|
183
|
+
```ts
|
|
184
|
+
interface CrudSheetFilterSlotProps<TFilter> {
|
|
185
|
+
filter: TFilter;
|
|
186
|
+
setFilter: SetStoreFunction<TFilter>;
|
|
474
187
|
}
|
|
475
188
|
```
|
|
476
189
|
|
|
477
|
-
|
|
190
|
+
- **`CrudSheet.Tools<TItem>`** -- Toolbar slot with context.
|
|
478
191
|
|
|
479
|
-
|
|
192
|
+
```ts
|
|
193
|
+
interface CrudSheetToolsSlotProps<TItem> {
|
|
194
|
+
ctx: CrudSheetContext<TItem>;
|
|
195
|
+
}
|
|
480
196
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
<
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
{(item: User) => <span>{item.name}</span>}
|
|
496
|
-
</SharedDataSelect.ItemTemplate>
|
|
497
|
-
</SharedDataSelect>
|
|
498
|
-
|
|
499
|
-
// 다중 선택
|
|
500
|
-
<SharedDataSelect
|
|
501
|
-
multiple
|
|
502
|
-
data={sharedData.users}
|
|
503
|
-
value={selectedUserIds()}
|
|
504
|
-
onValueChange={setSelectedUserIds}
|
|
505
|
-
>
|
|
506
|
-
<SharedDataSelect.ItemTemplate>
|
|
507
|
-
{(item: User) => <span>{item.name}</span>}
|
|
508
|
-
</SharedDataSelect.ItemTemplate>
|
|
509
|
-
</SharedDataSelect>
|
|
510
|
-
|
|
511
|
-
// 다이얼로그 검색 연동
|
|
512
|
-
<SharedDataSelect
|
|
513
|
-
data={sharedData.users}
|
|
514
|
-
value={selectedUserId()}
|
|
515
|
-
onValueChange={setSelectedUserId}
|
|
516
|
-
dialog={UserSearchDialog}
|
|
517
|
-
dialogOptions={{ header: "사용자 검색" }}
|
|
518
|
-
>
|
|
519
|
-
<SharedDataSelect.ItemTemplate>
|
|
520
|
-
{(item: User) => <span>{item.name}</span>}
|
|
521
|
-
</SharedDataSelect.ItemTemplate>
|
|
522
|
-
<SharedDataSelect.Action onClick={handleCustomAction}>
|
|
523
|
-
<Icon icon={IconPlus} />
|
|
524
|
-
</SharedDataSelect.Action>
|
|
525
|
-
</SharedDataSelect>
|
|
526
|
-
|
|
527
|
-
// 아이템 필터링
|
|
528
|
-
<SharedDataSelect
|
|
529
|
-
data={sharedData.departments}
|
|
530
|
-
value={deptId()}
|
|
531
|
-
onValueChange={setDeptId}
|
|
532
|
-
filterFn={(item) => item.isActive}
|
|
533
|
-
>
|
|
534
|
-
<SharedDataSelect.ItemTemplate>
|
|
535
|
-
{(item) => <span>{item.name}</span>}
|
|
536
|
-
</SharedDataSelect.ItemTemplate>
|
|
537
|
-
</SharedDataSelect>
|
|
197
|
+
interface CrudSheetContext<TItem> {
|
|
198
|
+
items(): TItem[];
|
|
199
|
+
selection(): TItem[];
|
|
200
|
+
page(): number;
|
|
201
|
+
sorts(): SortingDef[];
|
|
202
|
+
busy(): boolean;
|
|
203
|
+
hasChanges(): boolean;
|
|
204
|
+
save(): Promise<void>;
|
|
205
|
+
refresh(): Promise<void>;
|
|
206
|
+
addItem(): void;
|
|
207
|
+
clearSelection(): void;
|
|
208
|
+
setPage(page: number): void;
|
|
209
|
+
setSorts(sorts: SortingDef[]): void;
|
|
210
|
+
}
|
|
538
211
|
```
|
|
539
212
|
|
|
540
|
-
|
|
541
|
-
|------|------|------|
|
|
542
|
-
| `data` | `SharedDataAccessor<TItem>` | 공유 데이터 접근자 |
|
|
543
|
-
| `value` / `onValueChange` | `TKey \| TKey[]` | 선택된 키 값 |
|
|
544
|
-
| `multiple` | `boolean` | 다중 선택 모드 |
|
|
545
|
-
| `filterFn` | `(item, index) => boolean` | 아이템 필터 |
|
|
546
|
-
| `dialog` | `Component<TDialogProps>` | 검색 다이얼로그 컴포넌트 |
|
|
547
|
-
| `dialogOptions` | `DialogShowOptions` | 다이얼로그 옵션 |
|
|
548
|
-
| `required`, `disabled`, `size`, `inset` | | 공통 폼 컨트롤 props |
|
|
549
|
-
|
|
550
|
-
### 서브 컴포넌트
|
|
551
|
-
|
|
552
|
-
| 컴포넌트 | 설명 |
|
|
553
|
-
|----------|------|
|
|
554
|
-
| `SharedDataSelect.ItemTemplate` | 드롭다운 아이템 렌더링 커스터마이징 |
|
|
555
|
-
| `SharedDataSelect.Action` | 드롭다운 하단 커스텀 액션 버튼 |
|
|
556
|
-
|
|
557
|
-
---
|
|
558
|
-
|
|
559
|
-
## SharedDataSelectButton
|
|
560
|
-
|
|
561
|
-
SharedData 기반 DataSelectButton. SharedDataAccessor와 다이얼로그를 연결한다.
|
|
562
|
-
|
|
563
|
-
```tsx
|
|
564
|
-
import { SharedDataSelectButton } from "@simplysm/solid";
|
|
565
|
-
|
|
566
|
-
<SharedDataSelectButton
|
|
567
|
-
data={sharedData.users}
|
|
568
|
-
value={selectedUserId()}
|
|
569
|
-
onValueChange={setSelectedUserId}
|
|
570
|
-
dialog={UserSearchDialog}
|
|
571
|
-
dialogOptions={{ header: "사용자 선택" }}
|
|
572
|
-
>
|
|
573
|
-
{(user) => <span>{user.name}</span>}
|
|
574
|
-
</SharedDataSelectButton>
|
|
575
|
-
|
|
576
|
-
// 다중 선택
|
|
577
|
-
<SharedDataSelectButton
|
|
578
|
-
multiple
|
|
579
|
-
data={sharedData.users}
|
|
580
|
-
value={selectedUserIds()}
|
|
581
|
-
onValueChange={setSelectedUserIds}
|
|
582
|
-
dialog={UserSearchDialog}
|
|
583
|
-
>
|
|
584
|
-
{(user) => <span>{user.name}</span>}
|
|
585
|
-
</SharedDataSelectButton>
|
|
586
|
-
```
|
|
213
|
+
## CrudDetail
|
|
587
214
|
|
|
588
|
-
|
|
215
|
+
CRUD detail form with load, save, delete, and change tracking.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
interface CrudDetailProps<TData extends object> {
|
|
219
|
+
load: () => Promise<{ data: TData; info: CrudDetailInfo }>;
|
|
220
|
+
children: (ctx: CrudDetailContext<TData>) => JSX.Element;
|
|
221
|
+
submit?: (data: TData) => Promise<boolean | undefined>;
|
|
222
|
+
toggleDelete?: (del: boolean) => Promise<boolean | undefined>;
|
|
223
|
+
editable?: boolean;
|
|
224
|
+
deletable?: boolean;
|
|
225
|
+
data?: TData;
|
|
226
|
+
onDataChange?: (data: TData) => void;
|
|
227
|
+
close?: (result?: boolean) => void;
|
|
228
|
+
class?: string;
|
|
229
|
+
}
|
|
589
230
|
|
|
590
|
-
|
|
231
|
+
interface CrudDetailInfo {
|
|
232
|
+
isNew: boolean;
|
|
233
|
+
isDeleted: boolean;
|
|
234
|
+
lastModifiedAt?: DateTime;
|
|
235
|
+
lastModifiedBy?: string;
|
|
236
|
+
}
|
|
591
237
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
pageSize={20}
|
|
602
|
-
header={<h3>부서 선택</h3>}
|
|
603
|
-
>
|
|
604
|
-
<SharedDataSelectList.ItemTemplate>
|
|
605
|
-
{(dept: Department) => <span>{dept.name}</span>}
|
|
606
|
-
</SharedDataSelectList.ItemTemplate>
|
|
607
|
-
</SharedDataSelectList>
|
|
608
|
-
|
|
609
|
-
// 커스텀 필터 UI
|
|
610
|
-
<SharedDataSelectList data={sharedData.items} value={v()} onValueChange={setV}>
|
|
611
|
-
<SharedDataSelectList.ItemTemplate>
|
|
612
|
-
{(item) => <span>{item.name}</span>}
|
|
613
|
-
</SharedDataSelectList.ItemTemplate>
|
|
614
|
-
<SharedDataSelectList.Filter>
|
|
615
|
-
<MyCustomFilter />
|
|
616
|
-
</SharedDataSelectList.Filter>
|
|
617
|
-
</SharedDataSelectList>
|
|
238
|
+
interface CrudDetailContext<TData> {
|
|
239
|
+
data: TData;
|
|
240
|
+
setData: SetStoreFunction<TData>;
|
|
241
|
+
info: () => CrudDetailInfo;
|
|
242
|
+
busy: () => boolean;
|
|
243
|
+
hasChanges: () => boolean;
|
|
244
|
+
save: () => Promise<void>;
|
|
245
|
+
refresh: () => Promise<void>;
|
|
246
|
+
}
|
|
618
247
|
```
|
|
619
248
|
|
|
620
|
-
|
|
621
|
-
|------|------|------|
|
|
622
|
-
| `data` | `SharedDataAccessor<TItem>` | 공유 데이터 접근자 |
|
|
623
|
-
| `value` / `onValueChange` | `TItem \| undefined` | 선택된 항목 (아이템 참조) |
|
|
624
|
-
| `filterFn` | `(item, index) => boolean` | 아이템 필터 |
|
|
625
|
-
| `canChange` | `(item) => boolean \| Promise<boolean>` | 변경 가드 |
|
|
626
|
-
| `pageSize` | `number` | 페이지 크기 (설정 시 페이지네이션 표시) |
|
|
627
|
-
| `header` | `JSX.Element` | 상단 헤더 |
|
|
249
|
+
### Sub-components
|
|
628
250
|
|
|
629
|
-
|
|
251
|
+
- **`CrudDetail.Header`** -- Header content slot.
|
|
630
252
|
|
|
631
253
|
## PermissionTable
|
|
632
254
|
|
|
633
|
-
|
|
255
|
+
Permission matrix table displaying app permissions with checkboxes.
|
|
634
256
|
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
onValueChange={setPermissions}
|
|
644
|
-
modules={activeModules()}
|
|
645
|
-
/>
|
|
646
|
-
```
|
|
647
|
-
|
|
648
|
-
| Prop | 타입 | 설명 |
|
|
649
|
-
|------|------|------|
|
|
650
|
-
| `items` | `AppPerm[]` | 권한 트리 (`createAppStructure`의 `usablePerms()`) |
|
|
651
|
-
| `value` | `Record<string, boolean>` | 권한 레코드 (`"href/perm": true`) |
|
|
652
|
-
| `onValueChange` | `(v: Record<string, boolean>) => void` | 변경 콜백 |
|
|
653
|
-
| `modules` | `TModule[]` | 활성 모듈 필터 |
|
|
654
|
-
| `disabled` | `boolean` | 비활성화 |
|
|
655
|
-
|
|
656
|
-
동작 규칙:
|
|
657
|
-
- 기본 권한(`perms[0]`, 보통 `"use"`)이 꺼지면 하위 권한도 자동으로 꺼진다
|
|
658
|
-
- 그룹 노드 체크 시 모든 자식에 대해 일괄 적용
|
|
659
|
-
- 모듈 필터링: `modules` prop에 따라 해당 모듈의 항목만 표시
|
|
660
|
-
|
|
661
|
-
---
|
|
662
|
-
|
|
663
|
-
## AddressSearch
|
|
664
|
-
|
|
665
|
-
다음(카카오) 우편번호 API 기반 주소 검색.
|
|
666
|
-
|
|
667
|
-
```tsx
|
|
668
|
-
import { AddressSearchContent, type AddressSearchResult } from "@simplysm/solid";
|
|
669
|
-
|
|
670
|
-
// Dialog로 사용
|
|
671
|
-
const dialog = useDialog();
|
|
672
|
-
const result = await dialog.show<AddressSearchResult>(AddressSearchContent, {}, {
|
|
673
|
-
header: "주소 검색",
|
|
674
|
-
width: "500px",
|
|
675
|
-
height: "500px",
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
if (result) {
|
|
679
|
-
setPostCode(result.postNumber);
|
|
680
|
-
setAddress(result.address);
|
|
681
|
-
setBuildingName(result.buildingName);
|
|
257
|
+
```ts
|
|
258
|
+
interface PermissionTableProps<TModule = string> {
|
|
259
|
+
perms: AppPerm<TModule>[];
|
|
260
|
+
value?: string[];
|
|
261
|
+
onValueChange?: (value: string[]) => void;
|
|
262
|
+
disabled?: boolean;
|
|
263
|
+
class?: string;
|
|
264
|
+
style?: JSX.CSSProperties;
|
|
682
265
|
}
|
|
683
266
|
```
|
|
684
267
|
|
|
685
|
-
###
|
|
686
|
-
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
268
|
+
### Helper Functions
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
function collectAllPerms<TModule>(items: AppPerm<TModule>[]): string[];
|
|
272
|
+
function filterByModules<TModule>(items: AppPerm<TModule>[], enabledModules: TModule[]): AppPerm<TModule>[];
|
|
273
|
+
function changePermCheck<TModule>(
|
|
274
|
+
items: AppPerm<TModule>[],
|
|
275
|
+
currentPerms: string[],
|
|
276
|
+
changedPerm: string,
|
|
277
|
+
checked: boolean,
|
|
278
|
+
): string[];
|
|
693
279
|
```
|