@simplysm/solid 13.0.70 → 13.0.72
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 +1 -1
- package/dist/components/data/sheet/DataSheet.d.ts.map +1 -1
- package/dist/components/data/sheet/DataSheet.js +3 -6
- package/dist/components/data/sheet/DataSheet.js.map +1 -1
- package/dist/components/data/sheet/DataSheet.styles.d.ts.map +1 -1
- package/dist/components/data/sheet/DataSheet.styles.js +1 -1
- package/dist/components/data/sheet/DataSheet.styles.js.map +1 -1
- package/dist/components/disclosure/Dropdown.d.ts +6 -4
- package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
- package/dist/components/disclosure/Dropdown.js +24 -8
- package/dist/components/disclosure/Dropdown.js.map +2 -2
- package/dist/components/disclosure/dialogZIndex.d.ts +2 -0
- package/dist/components/disclosure/dialogZIndex.d.ts.map +1 -1
- package/dist/components/disclosure/dialogZIndex.js +4 -0
- package/dist/components/disclosure/dialogZIndex.js.map +1 -1
- package/dist/components/features/crud-detail/CrudDetail.d.ts.map +1 -1
- package/dist/components/features/crud-detail/CrudDetail.js +16 -7
- package/dist/components/features/crud-detail/CrudDetail.js.map +2 -2
- package/dist/components/features/crud-sheet/CrudSheet.d.ts.map +1 -1
- package/dist/components/features/crud-sheet/CrudSheet.js +14 -5
- package/dist/components/features/crud-sheet/CrudSheet.js.map +2 -2
- package/dist/components/features/crudRegistry.d.ts +16 -0
- package/dist/components/features/crudRegistry.d.ts.map +1 -0
- package/dist/components/features/crudRegistry.js +37 -0
- package/dist/components/features/crudRegistry.js.map +6 -0
- package/dist/components/features/permission-table/PermissionTable.d.ts.map +1 -1
- package/dist/components/features/permission-table/PermissionTable.js +71 -86
- package/dist/components/features/permission-table/PermissionTable.js.map +2 -2
- package/dist/components/features/shared-data/SharedDataSelect.js +2 -4
- package/dist/components/features/shared-data/SharedDataSelect.js.map +2 -2
- package/dist/components/features/shared-data/SharedDataSelectList.d.ts +2 -4
- package/dist/components/features/shared-data/SharedDataSelectList.d.ts.map +1 -1
- package/dist/components/features/shared-data/SharedDataSelectList.js +11 -46
- package/dist/components/features/shared-data/SharedDataSelectList.js.map +2 -2
- package/dist/components/form-control/select/Select.d.ts.map +1 -1
- package/dist/components/form-control/select/Select.js +1 -1
- package/dist/components/form-control/select/Select.js.map +1 -1
- package/dist/helpers/createAppStructure.d.ts.map +1 -1
- package/dist/helpers/createAppStructure.js +3 -2
- package/dist/helpers/createAppStructure.js.map +1 -1
- package/dist/helpers/createHmrSafeContext.d.ts +3 -0
- package/dist/helpers/createHmrSafeContext.d.ts.map +1 -0
- package/dist/helpers/createHmrSafeContext.js +10 -0
- package/dist/helpers/createHmrSafeContext.js.map +6 -0
- package/dist/hooks/createSelectionGroup.d.ts.map +1 -1
- package/dist/hooks/createSelectionGroup.js +3 -2
- package/dist/hooks/createSelectionGroup.js.map +2 -2
- package/package.json +6 -5
- package/src/components/data/sheet/DataSheet.styles.ts +1 -1
- package/src/components/data/sheet/DataSheet.tsx +3 -4
- package/src/components/disclosure/Dropdown.tsx +31 -17
- package/src/components/disclosure/dialogZIndex.ts +5 -0
- package/src/components/features/crud-detail/CrudDetail.tsx +16 -5
- package/src/components/features/crud-sheet/CrudSheet.tsx +13 -3
- package/src/components/features/crudRegistry.ts +60 -0
- package/src/components/features/permission-table/PermissionTable.tsx +49 -46
- package/src/components/features/shared-data/SharedDataSelect.tsx +2 -2
- package/src/components/features/shared-data/SharedDataSelectList.tsx +11 -36
- package/src/components/form-control/select/Select.tsx +1 -5
- package/src/helpers/createAppStructure.ts +3 -2
- package/src/helpers/createHmrSafeContext.ts +8 -0
- package/src/hooks/createSelectionGroup.tsx +4 -2
- package/tests/components/data/List.spec.tsx +52 -52
- package/tests/components/data/Pagination.spec.tsx +43 -43
- package/tests/components/data/Table.spec.tsx +4 -4
- package/tests/components/data/kanban/Kanban.selection.spec.tsx +21 -21
- package/tests/components/data/sheet/DataSheet.spec.tsx +50 -50
- package/tests/components/disclosure/Collapse.spec.tsx +24 -24
- package/tests/components/disclosure/Dialog.spec.tsx +33 -33
- package/tests/components/disclosure/DialogProvider.spec.tsx +9 -9
- package/tests/components/disclosure/Dropdown.spec.tsx +134 -14
- package/tests/components/disclosure/Tabs.spec.tsx +21 -21
- package/tests/components/disclosure/dialogZIndex.spec.ts +45 -0
- package/tests/components/display/Alert.spec.tsx +4 -4
- package/tests/components/display/Barcode.spec.tsx +7 -7
- package/tests/components/display/Card.spec.tsx +3 -3
- package/tests/components/display/Link.spec.tsx +5 -5
- package/tests/components/display/Tag.spec.tsx +4 -4
- package/tests/components/features/address/AddressSearch.spec.tsx +3 -3
- package/tests/components/features/crudRegistry.spec.ts +119 -0
- package/tests/components/features/data-select-button/DataSelectButton.spec.tsx +8 -8
- package/tests/components/features/permission-table/PermissionTable.spec.tsx +43 -43
- package/tests/components/features/shared-data/SharedDataSelectList.spec.tsx +2 -17
- package/tests/components/feedback/busy/BusyContainer.spec.tsx +7 -7
- package/tests/components/feedback/notification/NotificationBell.spec.tsx +9 -9
- package/tests/components/feedback/print/Print.spec.tsx +4 -4
- package/tests/components/form-control/Button.spec.tsx +18 -18
- package/tests/components/form-control/checkbox/Checkbox.spec.tsx +20 -20
- package/tests/components/form-control/checkbox/CheckboxGroup.spec.tsx +12 -12
- package/tests/components/form-control/checkbox/Radio.spec.tsx +21 -21
- package/tests/components/form-control/checkbox/RadioGroup.spec.tsx +12 -12
- package/tests/components/form-control/color-picker/ColorPicker.spec.tsx +10 -10
- package/tests/components/form-control/combobox/Combobox.spec.tsx +16 -16
- package/tests/components/form-control/combobox/ComboboxItem.spec.tsx +7 -7
- package/tests/components/form-control/date-range-picker/DateRangePicker.spec.tsx +24 -24
- package/tests/components/form-control/field/DatePicker.spec.tsx +50 -50
- package/tests/components/form-control/field/DateTimePicker.spec.tsx +47 -47
- package/tests/components/form-control/field/NumberInput.spec.tsx +54 -54
- package/tests/components/form-control/field/TextInput.spec.tsx +49 -49
- package/tests/components/form-control/field/Textarea.spec.tsx +33 -33
- package/tests/components/form-control/field/TimePicker.spec.tsx +42 -42
- package/tests/components/form-control/numpad/Numpad.spec.tsx +40 -40
- package/tests/components/form-control/select/Select.spec.tsx +9 -9
- package/tests/components/form-control/select/SelectItem.spec.tsx +10 -10
- package/tests/helpers/createAppStructure.spec.tsx +57 -57
- package/tests/helpers/mergeStyles.spec.ts +31 -31
|
@@ -18,6 +18,7 @@ import { twMerge } from "tailwind-merge";
|
|
|
18
18
|
import { mergeStyles } from "../../helpers/mergeStyles";
|
|
19
19
|
import { createSlotComponent } from "../../helpers/createSlotComponent";
|
|
20
20
|
import { borderSubtle } from "../../styles/tokens.styles";
|
|
21
|
+
import { tabbable } from "tabbable";
|
|
21
22
|
|
|
22
23
|
// --- DropdownContext (internal) ---
|
|
23
24
|
|
|
@@ -70,13 +71,15 @@ export interface DropdownProps {
|
|
|
70
71
|
* Enable keyboard navigation (used in Select, etc)
|
|
71
72
|
*
|
|
72
73
|
* When direction=down:
|
|
73
|
-
* - ArrowDown from trigger -> focus first
|
|
74
|
-
* - ArrowUp
|
|
74
|
+
* - ArrowDown from trigger -> focus first tabbable item in popup
|
|
75
|
+
* - ArrowUp/ArrowDown within popup -> navigate between tabbable items
|
|
76
|
+
* - ArrowUp from first tabbable -> focus trigger
|
|
75
77
|
* - ArrowUp from trigger -> close
|
|
76
78
|
*
|
|
77
79
|
* When direction=up:
|
|
78
|
-
* - ArrowUp from trigger -> focus last
|
|
79
|
-
* - ArrowDown
|
|
80
|
+
* - ArrowUp from trigger -> focus last tabbable item in popup
|
|
81
|
+
* - ArrowUp/ArrowDown within popup -> navigate between tabbable items
|
|
82
|
+
* - ArrowDown from last tabbable -> focus trigger
|
|
80
83
|
* - ArrowDown from trigger -> close
|
|
81
84
|
*/
|
|
82
85
|
keyboardNav?: boolean;
|
|
@@ -319,11 +322,7 @@ export const Dropdown: DropdownComponent = ((props: DropdownProps) => {
|
|
|
319
322
|
if (!popup) return;
|
|
320
323
|
|
|
321
324
|
const dir = direction();
|
|
322
|
-
const focusables =
|
|
323
|
-
...popup.querySelectorAll<HTMLElement>(
|
|
324
|
-
'[tabindex]:not([tabindex="-1"]), button, [data-list-item]',
|
|
325
|
-
),
|
|
326
|
-
];
|
|
325
|
+
const focusables = tabbable(popup);
|
|
327
326
|
|
|
328
327
|
if (dir === "down") {
|
|
329
328
|
if (e.key === "ArrowDown" && focusables.length > 0) {
|
|
@@ -354,16 +353,30 @@ export const Dropdown: DropdownComponent = ((props: DropdownProps) => {
|
|
|
354
353
|
|
|
355
354
|
if (!triggerRef) return;
|
|
356
355
|
|
|
356
|
+
const popup = popupRef();
|
|
357
|
+
if (!popup) return;
|
|
358
|
+
|
|
357
359
|
const dir = direction();
|
|
360
|
+
const allTabbable = tabbable(popup);
|
|
361
|
+
const current = (document.activeElement ?? e.target) as HTMLElement;
|
|
362
|
+
const currentIdx = allTabbable.indexOf(current);
|
|
358
363
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
364
|
+
if (e.key === "ArrowUp") {
|
|
365
|
+
if (currentIdx > 0) {
|
|
366
|
+
e.preventDefault();
|
|
367
|
+
allTabbable[currentIdx - 1].focus();
|
|
368
|
+
} else if (dir === "down") {
|
|
369
|
+
e.preventDefault();
|
|
370
|
+
triggerRef.focus();
|
|
371
|
+
}
|
|
372
|
+
} else if (e.key === "ArrowDown") {
|
|
373
|
+
if (currentIdx >= 0 && currentIdx < allTabbable.length - 1) {
|
|
374
|
+
e.preventDefault();
|
|
375
|
+
allTabbable[currentIdx + 1].focus();
|
|
376
|
+
} else if (dir === "up") {
|
|
377
|
+
e.preventDefault();
|
|
378
|
+
triggerRef.focus();
|
|
379
|
+
}
|
|
367
380
|
}
|
|
368
381
|
};
|
|
369
382
|
|
|
@@ -432,6 +445,7 @@ export const Dropdown: DropdownComponent = ((props: DropdownProps) => {
|
|
|
432
445
|
ref={(el) => {
|
|
433
446
|
triggerRef = el;
|
|
434
447
|
}}
|
|
448
|
+
tabIndex={-1}
|
|
435
449
|
data-dropdown-trigger
|
|
436
450
|
onClick={toggle}
|
|
437
451
|
onKeyDown={handleTriggerKeyDown}
|
|
@@ -46,3 +46,8 @@ function reindex(): void {
|
|
|
46
46
|
export function isTopmost(el: HTMLElement): boolean {
|
|
47
47
|
return stack.length > 0 && stack[stack.length - 1] === el;
|
|
48
48
|
}
|
|
49
|
+
|
|
50
|
+
/** Get the topmost (front-most) Dialog element, or null if none are open */
|
|
51
|
+
export function getTopmostDialog(): HTMLElement | null {
|
|
52
|
+
return stack.length > 0 ? stack[stack.length - 1] : null;
|
|
53
|
+
}
|
|
@@ -2,12 +2,15 @@ import {
|
|
|
2
2
|
children,
|
|
3
3
|
createMemo,
|
|
4
4
|
createSignal,
|
|
5
|
+
createUniqueId,
|
|
5
6
|
type JSX,
|
|
7
|
+
onCleanup,
|
|
6
8
|
onMount,
|
|
7
9
|
Show,
|
|
8
10
|
splitProps,
|
|
9
11
|
useContext,
|
|
10
12
|
} from "solid-js";
|
|
13
|
+
import { registerCrud, unregisterCrud, activateCrud, isActiveCrud } from "../crudRegistry";
|
|
11
14
|
import { reconcile, unwrap } from "solid-js/store";
|
|
12
15
|
import { createControllableStore } from "../../../hooks/createControllableStore";
|
|
13
16
|
import { objClone, objEqual } from "@simplysm/core-common";
|
|
@@ -82,6 +85,8 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
|
|
|
82
85
|
|
|
83
86
|
let formRef: HTMLFormElement | undefined;
|
|
84
87
|
|
|
88
|
+
const crudId = createUniqueId();
|
|
89
|
+
|
|
85
90
|
// -- Load --
|
|
86
91
|
async function doLoad() {
|
|
87
92
|
setBusyCount((c) => c + 1);
|
|
@@ -100,6 +105,10 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
|
|
|
100
105
|
onMount(() => {
|
|
101
106
|
void doLoad();
|
|
102
107
|
});
|
|
108
|
+
onCleanup(() => unregisterCrud(crudId));
|
|
109
|
+
|
|
110
|
+
createEventListener(() => formRef, "pointerdown", () => activateCrud(crudId));
|
|
111
|
+
createEventListener(() => formRef, "focusin", () => activateCrud(crudId));
|
|
103
112
|
|
|
104
113
|
// -- Change Detection --
|
|
105
114
|
function hasChanges(): boolean {
|
|
@@ -177,13 +186,15 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
|
|
|
177
186
|
|
|
178
187
|
// -- Keyboard Shortcuts --
|
|
179
188
|
createEventListener(document, "keydown", (e: KeyboardEvent) => {
|
|
180
|
-
if (!
|
|
189
|
+
if (!isActiveCrud(crudId)) return;
|
|
181
190
|
if (e.ctrlKey && e.key === "s") {
|
|
182
191
|
e.preventDefault();
|
|
183
|
-
|
|
192
|
+
e.stopImmediatePropagation();
|
|
193
|
+
formRef?.requestSubmit();
|
|
184
194
|
}
|
|
185
195
|
if (e.ctrlKey && e.altKey && e.key === "l") {
|
|
186
196
|
e.preventDefault();
|
|
197
|
+
e.stopImmediatePropagation();
|
|
187
198
|
void handleRefresh();
|
|
188
199
|
}
|
|
189
200
|
});
|
|
@@ -273,11 +284,11 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
|
|
|
273
284
|
<BusyContainer
|
|
274
285
|
ready={ready()}
|
|
275
286
|
busy={busyCount() > 0}
|
|
276
|
-
class={clsx("flex h-full flex-col", local.class)}
|
|
287
|
+
class={clsx("flex h-full flex-col gap-2", local.class)}
|
|
277
288
|
>
|
|
278
289
|
{/* Toolbar */}
|
|
279
290
|
<Show when={(!isModal && !topbarCtx) || defs().tools}>
|
|
280
|
-
<div class="flex gap-2
|
|
291
|
+
<div class="flex gap-2 pb-0">
|
|
281
292
|
<Show when={!topbarCtx && !isModal}>
|
|
282
293
|
<Show when={canEdit() && local.submit}>
|
|
283
294
|
<Button
|
|
@@ -324,7 +335,7 @@ const CrudDetailBase = <TData extends object>(props: CrudDetailProps<TData>) =>
|
|
|
324
335
|
<Show when={defs().before}>{(beforeDef) => beforeDef().children}</Show>
|
|
325
336
|
|
|
326
337
|
{/* Form */}
|
|
327
|
-
<form ref={formRef} class="flex-1 overflow-auto
|
|
338
|
+
<form ref={(el) => { formRef = el; registerCrud(crudId, el); }} class="flex-1 overflow-auto" onSubmit={handleFormSubmit}>
|
|
328
339
|
{formContent()}
|
|
329
340
|
</form>
|
|
330
341
|
|
|
@@ -3,8 +3,10 @@ import {
|
|
|
3
3
|
createEffect,
|
|
4
4
|
createMemo,
|
|
5
5
|
createSignal,
|
|
6
|
+
createUniqueId,
|
|
6
7
|
For,
|
|
7
8
|
type JSX,
|
|
9
|
+
onCleanup,
|
|
8
10
|
Show,
|
|
9
11
|
splitProps,
|
|
10
12
|
useContext,
|
|
@@ -41,6 +43,7 @@ import {
|
|
|
41
43
|
IconTrashOff,
|
|
42
44
|
IconUpload,
|
|
43
45
|
} from "@tabler/icons-solidjs";
|
|
46
|
+
import { registerCrud, unregisterCrud, activateCrud, isActiveCrud } from "../crudRegistry";
|
|
44
47
|
import { CrudSheetColumn, isCrudSheetColumnDef } from "./CrudSheetColumn";
|
|
45
48
|
import { CrudSheetFilter, isCrudSheetFilterDef } from "./CrudSheetFilter";
|
|
46
49
|
import { CrudSheetTools, isCrudSheetToolsDef } from "./CrudSheetTools";
|
|
@@ -134,6 +137,11 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
|
|
|
134
137
|
|
|
135
138
|
let formRef: HTMLFormElement | undefined;
|
|
136
139
|
|
|
140
|
+
const crudId = createUniqueId();
|
|
141
|
+
onCleanup(() => unregisterCrud(crudId));
|
|
142
|
+
createEventListener(() => formRef, "pointerdown", () => activateCrud(crudId));
|
|
143
|
+
createEventListener(() => formRef, "focusin", () => activateCrud(crudId));
|
|
144
|
+
|
|
137
145
|
createEffect(() => {
|
|
138
146
|
void doRefresh();
|
|
139
147
|
});
|
|
@@ -389,13 +397,15 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
|
|
|
389
397
|
|
|
390
398
|
// -- Keyboard Shortcuts --
|
|
391
399
|
createEventListener(document, "keydown", async (e: KeyboardEvent) => {
|
|
392
|
-
if (!
|
|
400
|
+
if (!isActiveCrud(crudId)) return;
|
|
393
401
|
if (e.ctrlKey && e.key === "s" && !isSelectMode()) {
|
|
394
402
|
e.preventDefault();
|
|
395
|
-
|
|
403
|
+
e.stopImmediatePropagation();
|
|
404
|
+
formRef?.requestSubmit();
|
|
396
405
|
}
|
|
397
406
|
if (e.ctrlKey && e.altKey && e.key === "l") {
|
|
398
407
|
e.preventDefault();
|
|
408
|
+
e.stopImmediatePropagation();
|
|
399
409
|
if (!checkIgnoreChanges()) return;
|
|
400
410
|
await doRefresh();
|
|
401
411
|
}
|
|
@@ -602,7 +612,7 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
|
|
|
602
612
|
</Show>
|
|
603
613
|
|
|
604
614
|
{/* DataSheet */}
|
|
605
|
-
<form ref={formRef} class="flex-1 overflow-hidden p-2 pt-1" onSubmit={handleFormSubmit}>
|
|
615
|
+
<form ref={(el) => { formRef = el; registerCrud(crudId, el); }} class="flex-1 overflow-hidden p-2 pt-1" onSubmit={handleFormSubmit}>
|
|
606
616
|
<DataSheet
|
|
607
617
|
class="h-full"
|
|
608
618
|
items={items}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crud activation registry
|
|
3
|
+
*
|
|
4
|
+
* Tracks mounted CrudDetail/CrudSheet instances and determines which one
|
|
5
|
+
* should respond to keyboard shortcuts (Ctrl+S, Ctrl+Alt+L).
|
|
6
|
+
*
|
|
7
|
+
* Priority rules:
|
|
8
|
+
* 1. If a Dialog is open, only cruds inside the topmost Dialog are candidates.
|
|
9
|
+
* 2. Among candidates, the most recently activated (interacted) crud wins.
|
|
10
|
+
* 3. On mount, cruds are auto-activated (last mounted = active).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { getTopmostDialog } from "../disclosure/dialogZIndex";
|
|
14
|
+
|
|
15
|
+
interface CrudEntry {
|
|
16
|
+
id: string;
|
|
17
|
+
formEl: HTMLFormElement;
|
|
18
|
+
lastActivatedAt: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const entries: CrudEntry[] = [];
|
|
22
|
+
let _counter = 0;
|
|
23
|
+
|
|
24
|
+
export function registerCrud(id: string, formEl: HTMLFormElement): void {
|
|
25
|
+
const existing = entries.find((e) => e.id === id);
|
|
26
|
+
if (existing) return;
|
|
27
|
+
entries.push({ id, formEl, lastActivatedAt: ++_counter });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function unregisterCrud(id: string): void {
|
|
31
|
+
const idx = entries.findIndex((e) => e.id === id);
|
|
32
|
+
if (idx >= 0) entries.splice(idx, 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function activateCrud(id: string): void {
|
|
36
|
+
const entry = entries.find((e) => e.id === id);
|
|
37
|
+
if (entry) entry.lastActivatedAt = ++_counter;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isActiveCrud(id: string): boolean {
|
|
41
|
+
const entry = entries.find((e) => e.id === id);
|
|
42
|
+
if (!entry) return false;
|
|
43
|
+
|
|
44
|
+
const topDialog = getTopmostDialog();
|
|
45
|
+
|
|
46
|
+
const candidates = topDialog
|
|
47
|
+
? entries.filter((e) => topDialog.contains(e.formEl))
|
|
48
|
+
: entries;
|
|
49
|
+
|
|
50
|
+
if (candidates.length === 0) return false;
|
|
51
|
+
|
|
52
|
+
let best = candidates[0];
|
|
53
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
54
|
+
if (candidates[i].lastActivatedAt > best.lastActivatedAt) {
|
|
55
|
+
best = candidates[i];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return best.id === id;
|
|
60
|
+
}
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
splitProps,
|
|
11
11
|
} from "solid-js";
|
|
12
12
|
import clsx from "clsx";
|
|
13
|
-
import { twMerge } from "tailwind-merge";
|
|
14
13
|
import { DataSheet } from "../../data/sheet/DataSheet";
|
|
15
14
|
import { Checkbox } from "../../form-control/checkbox/Checkbox";
|
|
16
15
|
import { borderDefault } from "../../../styles/tokens.styles";
|
|
@@ -261,51 +260,55 @@ export const PermissionTable: Component<PermissionTableProps> = (props) => {
|
|
|
261
260
|
};
|
|
262
261
|
|
|
263
262
|
return (
|
|
264
|
-
<
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
263
|
+
<DataSheet
|
|
264
|
+
data-permission-table
|
|
265
|
+
items={visibleItems()}
|
|
266
|
+
getChildren={getChildren}
|
|
267
|
+
expandedItems={expandedItems()}
|
|
268
|
+
onExpandedItemsChange={setExpandedItems}
|
|
269
|
+
hideConfigBar
|
|
270
|
+
>
|
|
271
|
+
<DataSheet.Column
|
|
272
|
+
key="title"
|
|
273
|
+
header={i18n?.t("permissionTable.permissionItem") ?? "Permission Item"}
|
|
274
|
+
sortable={false}
|
|
275
|
+
resizable={false}
|
|
271
276
|
>
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
{
|
|
279
|
-
<div class={
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
{
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
<
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
</DataSheet>
|
|
309
|
-
</div>
|
|
277
|
+
{(ctx) => {
|
|
278
|
+
const item = ctx.item as AppPerm;
|
|
279
|
+
return (
|
|
280
|
+
<div class={titleCellClass}>
|
|
281
|
+
<For each={Array.from({ length: ctx.depth })}>
|
|
282
|
+
{() => (
|
|
283
|
+
<div class={indentGuideWrapperClass}>
|
|
284
|
+
<div class={indentGuideLineClass} />
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</For>
|
|
288
|
+
<span class="py-1">{item.title}</span>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}}
|
|
292
|
+
</DataSheet.Column>
|
|
293
|
+
<For each={allPerms()}>
|
|
294
|
+
{(perm) => (
|
|
295
|
+
<DataSheet.Column key={`perm-${perm}`} header={perm} sortable={false} resizable={false}>
|
|
296
|
+
{(ctx) => {
|
|
297
|
+
const item = ctx.item as AppPerm;
|
|
298
|
+
return (
|
|
299
|
+
<Show when={hasPermInTree(item, perm)}>
|
|
300
|
+
<Checkbox
|
|
301
|
+
value={isGroupPermChecked(item, perm, currentValue())}
|
|
302
|
+
onValueChange={(checked) => handlePermChange(item, perm, checked)}
|
|
303
|
+
disabled={local.disabled || isPermDisabled(item, perm, currentValue())}
|
|
304
|
+
inset
|
|
305
|
+
/>
|
|
306
|
+
</Show>
|
|
307
|
+
);
|
|
308
|
+
}}
|
|
309
|
+
</DataSheet.Column>
|
|
310
|
+
)}
|
|
311
|
+
</For>
|
|
312
|
+
</DataSheet>
|
|
310
313
|
);
|
|
311
314
|
};
|
|
@@ -90,12 +90,12 @@ export function SharedDataSelect<TItem>(props: SharedDataSelectProps<TItem>): JS
|
|
|
90
90
|
<Select.ItemTemplate>{local.children}</Select.ItemTemplate>
|
|
91
91
|
{local.modal && (
|
|
92
92
|
<Select.Action onClick={() => void handleOpenModal()} aria-label={i18n?.t("sharedDataSelect.search") ?? "Search"}>
|
|
93
|
-
<Icon icon={IconSearch}
|
|
93
|
+
<Icon icon={IconSearch} />
|
|
94
94
|
</Select.Action>
|
|
95
95
|
)}
|
|
96
96
|
{local.editModal && (
|
|
97
97
|
<Select.Action onClick={() => void handleOpenEditModal()} aria-label={i18n?.t("sharedDataSelect.edit") ?? "Edit"}>
|
|
98
|
-
<Icon icon={IconEdit}
|
|
98
|
+
<Icon icon={IconEdit} />
|
|
99
99
|
</Select.Action>
|
|
100
100
|
)}
|
|
101
101
|
</Select>
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { createEffect, createMemo, createSignal, For, type JSX, Show, splitProps } from "solid-js";
|
|
2
|
-
import { IconExternalLink } from "@tabler/icons-solidjs";
|
|
3
2
|
import clsx from "clsx";
|
|
4
3
|
import { twMerge } from "tailwind-merge";
|
|
5
4
|
import { type SharedDataAccessor } from "../../../providers/shared-data/SharedDataContext";
|
|
6
5
|
import { List } from "../../data/list/List";
|
|
7
6
|
import { Pagination } from "../../data/Pagination";
|
|
8
|
-
import { Button } from "../../form-control/Button";
|
|
9
|
-
import { Icon } from "../../display/Icon";
|
|
10
7
|
import { TextInput } from "../../form-control/field/TextInput";
|
|
11
|
-
import { useDialog } from "../../disclosure/DialogContext";
|
|
12
8
|
import { useI18nOptional } from "../../../providers/i18n/I18nContext";
|
|
13
9
|
import { textMuted } from "../../../styles/tokens.styles";
|
|
14
10
|
import { createSlotSignal } from "../../../hooks/createSlotSignal";
|
|
@@ -39,10 +35,8 @@ export interface SharedDataSelectListProps<TItem> {
|
|
|
39
35
|
canChange?: (item: TItem | undefined) => boolean | Promise<boolean>;
|
|
40
36
|
/** Page size (shows Pagination if provided) */
|
|
41
37
|
pageSize?: number;
|
|
42
|
-
/** Header
|
|
43
|
-
header?:
|
|
44
|
-
/** Management modal component factory */
|
|
45
|
-
modal?: () => JSX.Element;
|
|
38
|
+
/** Header content */
|
|
39
|
+
header?: JSX.Element;
|
|
46
40
|
|
|
47
41
|
/** Compound sub-components (ItemTemplate, Filter) */
|
|
48
42
|
children?: JSX.Element;
|
|
@@ -57,8 +51,6 @@ export interface SharedDataSelectListProps<TItem> {
|
|
|
57
51
|
|
|
58
52
|
const containerClass = clsx("flex-col gap-1");
|
|
59
53
|
|
|
60
|
-
const headerClass = clsx("flex items-center gap-1 px-2 py-1 text-sm font-semibold");
|
|
61
|
-
|
|
62
54
|
// ─── Component ───────────────────────────────────────────
|
|
63
55
|
|
|
64
56
|
export interface SharedDataSelectListComponent {
|
|
@@ -83,10 +75,8 @@ export const SharedDataSelectList: SharedDataSelectListComponent = (<TItem,>(
|
|
|
83
75
|
"canChange",
|
|
84
76
|
"pageSize",
|
|
85
77
|
"header",
|
|
86
|
-
"modal",
|
|
87
78
|
]);
|
|
88
79
|
|
|
89
|
-
const dialog = useDialog();
|
|
90
80
|
const i18n = useI18nOptional();
|
|
91
81
|
|
|
92
82
|
// ─── Slot signals ──────────────────────────────────────
|
|
@@ -192,13 +182,6 @@ export const SharedDataSelectList: SharedDataSelectListComponent = (<TItem,>(
|
|
|
192
182
|
}
|
|
193
183
|
};
|
|
194
184
|
|
|
195
|
-
// ─── Open modal ────────────────────────────────────────
|
|
196
|
-
|
|
197
|
-
const handleOpenModal = async () => {
|
|
198
|
-
if (!local.modal) return;
|
|
199
|
-
await dialog.show(local.modal, {});
|
|
200
|
-
};
|
|
201
|
-
|
|
202
185
|
// ─── Render ────────────────────────────────────────────
|
|
203
186
|
|
|
204
187
|
return (
|
|
@@ -212,26 +195,18 @@ export const SharedDataSelectList: SharedDataSelectListComponent = (<TItem,>(
|
|
|
212
195
|
style={local.style}
|
|
213
196
|
>
|
|
214
197
|
{/* Header */}
|
|
215
|
-
<Show when={local.header != null
|
|
216
|
-
<div class={headerClass}>
|
|
217
|
-
<Show when={local.header != null}>{local.header}</Show>
|
|
218
|
-
<Show when={local.modal != null}>
|
|
219
|
-
<Button size="sm" onClick={() => void handleOpenModal()}>
|
|
220
|
-
<Icon icon={IconExternalLink} />
|
|
221
|
-
</Button>
|
|
222
|
-
</Show>
|
|
223
|
-
</div>
|
|
224
|
-
</Show>
|
|
198
|
+
<Show when={local.header != null}>{local.header}</Show>
|
|
225
199
|
|
|
226
200
|
{/* Search input: when Filter compound is absent and getSearchText exists */}
|
|
227
201
|
<Show when={!filter() && local.data.getSearchText}>
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
202
|
+
<div class={"p-1"}>
|
|
203
|
+
<TextInput
|
|
204
|
+
value={searchText()}
|
|
205
|
+
onValueChange={setSearchText}
|
|
206
|
+
placeholder={i18n?.t("sharedDataSelectList.searchPlaceholder") ?? "Search..."}
|
|
207
|
+
class={"w-full"}
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
235
210
|
</Show>
|
|
236
211
|
|
|
237
212
|
{/* Custom Filter */}
|
|
@@ -44,11 +44,7 @@ const searchInputClass = clsx(
|
|
|
44
44
|
"w-full",
|
|
45
45
|
"rounded-none",
|
|
46
46
|
"border-0 border-b",
|
|
47
|
-
borderSubtle
|
|
48
|
-
"bg-transparent dark:bg-transparent",
|
|
49
|
-
"h-auto",
|
|
50
|
-
"py-1.5",
|
|
51
|
-
"text-sm",
|
|
47
|
+
borderSubtle
|
|
52
48
|
);
|
|
53
49
|
|
|
54
50
|
// Select all/deselect all button area styles
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Component, ParentComponent } from "solid-js";
|
|
2
|
-
import { type Accessor,
|
|
2
|
+
import { type Accessor, createMemo, createRoot, useContext } from "solid-js";
|
|
3
|
+
import { createHmrSafeContext } from "./createHmrSafeContext";
|
|
3
4
|
import type { IconProps } from "@tabler/icons-solidjs";
|
|
4
5
|
|
|
5
6
|
// ── Input Types ──
|
|
@@ -497,7 +498,7 @@ export function createAppStructure<TModule, const TItems extends AppStructureIte
|
|
|
497
498
|
} {
|
|
498
499
|
type TRet = AppStructure<TModule> & { perms: InferPerms<TItems> };
|
|
499
500
|
|
|
500
|
-
const Ctx =
|
|
501
|
+
const Ctx = createHmrSafeContext<TRet>("AppStructure");
|
|
501
502
|
|
|
502
503
|
const AppStructureProvider: ParentComponent = (props) => {
|
|
503
504
|
const structure = buildAppStructure(getOpts());
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Context, createContext } from "solid-js";
|
|
2
|
+
|
|
3
|
+
const CACHE_KEY = "__simplysm_ctx__";
|
|
4
|
+
const cache = ((globalThis as unknown as Record<string, Record<string, unknown>>)[CACHE_KEY] ??= {});
|
|
5
|
+
|
|
6
|
+
export function createHmrSafeContext<TValue>(key: string): Context<TValue | undefined> {
|
|
7
|
+
return (cache[key] ??= createContext<TValue>()) as Context<TValue | undefined>;
|
|
8
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type JSX,
|
|
3
3
|
type ParentComponent,
|
|
4
|
-
createContext,
|
|
5
4
|
createMemo,
|
|
6
5
|
splitProps,
|
|
7
6
|
useContext,
|
|
8
7
|
} from "solid-js";
|
|
8
|
+
import { createHmrSafeContext } from "../helpers/createHmrSafeContext";
|
|
9
9
|
import { twMerge } from "tailwind-merge";
|
|
10
10
|
import { createControllableSignal } from "./createControllableSignal";
|
|
11
11
|
import { Invalid } from "../components/form-control/Invalid";
|
|
@@ -106,7 +106,9 @@ export function createSelectionGroup(config: MultiGroupConfig | SingleGroupConfi
|
|
|
106
106
|
Item: <TValue = unknown>(props: SelectionGroupItemProps<TValue>) => JSX.Element;
|
|
107
107
|
};
|
|
108
108
|
} {
|
|
109
|
-
const Context =
|
|
109
|
+
const Context = createHmrSafeContext<MultiSelectContext | SingleSelectContext>(
|
|
110
|
+
`SelectionGroup_${config.contextName}`,
|
|
111
|
+
);
|
|
110
112
|
const ItemComponent = config.ItemComponent;
|
|
111
113
|
|
|
112
114
|
function ItemInner<TValue>(props: SelectionGroupItemProps<TValue>) {
|