@simplysm/solid 13.0.84 → 13.0.85
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/sheet/DataSheet.d.ts.map +1 -1
- package/dist/components/data/sheet/DataSheet.js +6 -9
- package/dist/components/data/sheet/DataSheet.js.map +2 -2
- package/dist/components/data/sheet/hooks/createDataSheetExpansion.d.ts.map +1 -1
- package/dist/components/data/sheet/hooks/createDataSheetExpansion.js +15 -17
- package/dist/components/data/sheet/hooks/createDataSheetExpansion.js.map +1 -1
- package/dist/components/data/sheet/hooks/createDataSheetReorder.d.ts.map +1 -1
- package/dist/components/data/sheet/hooks/createDataSheetReorder.js +12 -12
- package/dist/components/data/sheet/hooks/createDataSheetReorder.js.map +1 -1
- package/dist/components/data/sheet/hooks/createDataSheetSelection.d.ts.map +1 -1
- package/dist/components/data/sheet/hooks/createDataSheetSelection.js +9 -3
- package/dist/components/data/sheet/hooks/createDataSheetSelection.js.map +1 -1
- package/dist/components/disclosure/Dialog.d.ts.map +1 -1
- package/dist/components/disclosure/Dialog.js +3 -21
- package/dist/components/disclosure/Dialog.js.map +2 -2
- package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
- package/dist/components/disclosure/Dropdown.js +1 -11
- package/dist/components/disclosure/Dropdown.js.map +2 -2
- package/dist/components/disclosure/Tabs.d.ts.map +1 -1
- package/dist/components/disclosure/Tabs.js +1 -3
- package/dist/components/disclosure/Tabs.js.map +2 -2
- package/dist/components/features/crud-detail/CrudDetail.js +103 -102
- 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 +3 -5
- package/dist/components/features/crud-sheet/CrudSheet.js.map +1 -1
- package/dist/components/feedback/busy/BusyContainer.d.ts.map +1 -1
- package/dist/components/feedback/busy/BusyContainer.js +1 -6
- package/dist/components/feedback/busy/BusyContainer.js.map +2 -2
- package/dist/components/form-control/checkbox/SelectableBase.d.ts.map +1 -1
- package/dist/components/form-control/checkbox/SelectableBase.js +2 -4
- package/dist/components/form-control/checkbox/SelectableBase.js.map +2 -2
- package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts.map +1 -1
- package/dist/components/form-control/date-range-picker/DateRangePicker.js +1 -2
- package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
- package/dist/components/form-control/editor/RichTextEditor.d.ts.map +1 -1
- package/dist/components/form-control/editor/RichTextEditor.js +2 -4
- package/dist/components/form-control/editor/RichTextEditor.js.map +2 -2
- package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
- package/dist/components/form-control/field/NumberInput.js +7 -7
- package/dist/components/form-control/field/NumberInput.js.map +2 -2
- package/dist/components/form-control/field/Textarea.d.ts.map +1 -1
- package/dist/components/form-control/field/Textarea.js +1 -3
- package/dist/components/form-control/field/Textarea.js.map +2 -2
- package/dist/components/form-control/select/Select.d.ts +2 -0
- package/dist/components/form-control/select/Select.d.ts.map +1 -1
- package/dist/components/form-control/select/Select.js +11 -10
- package/dist/components/form-control/select/Select.js.map +2 -2
- package/dist/components/form-control/state-preset/StatePreset.d.ts.map +1 -1
- package/dist/components/form-control/state-preset/StatePreset.js +3 -7
- package/dist/components/form-control/state-preset/StatePreset.js.map +2 -2
- package/dist/components/layout/topbar/Topbar.js +1 -3
- package/dist/components/layout/topbar/Topbar.js.map +2 -2
- package/dist/hooks/createControllableStore.d.ts.map +1 -1
- package/dist/hooks/createControllableStore.js +8 -5
- package/dist/hooks/createControllableStore.js.map +1 -1
- package/dist/hooks/useLocalStorage.d.ts.map +1 -1
- package/dist/hooks/useLocalStorage.js +3 -2
- package/dist/hooks/useLocalStorage.js.map +1 -1
- package/dist/hooks/useSyncConfig.d.ts.map +1 -1
- package/dist/hooks/useSyncConfig.js +5 -4
- package/dist/hooks/useSyncConfig.js.map +1 -1
- package/dist/providers/shared-data/SharedDataProvider.d.ts.map +1 -1
- package/dist/providers/shared-data/SharedDataProvider.js +0 -1
- package/dist/providers/shared-data/SharedDataProvider.js.map +1 -1
- package/package.json +5 -5
- package/src/components/data/sheet/DataSheet.tsx +6 -10
- package/src/components/data/sheet/hooks/createDataSheetExpansion.ts +17 -18
- package/src/components/data/sheet/hooks/createDataSheetReorder.ts +12 -13
- package/src/components/data/sheet/hooks/createDataSheetSelection.ts +9 -3
- package/src/components/disclosure/Dialog.tsx +45 -59
- package/src/components/disclosure/Dropdown.tsx +4 -14
- package/src/components/disclosure/Tabs.tsx +12 -17
- package/src/components/features/crud-detail/CrudDetail.tsx +4 -4
- package/src/components/features/crud-sheet/CrudSheet.tsx +4 -5
- package/src/components/feedback/busy/BusyContainer.tsx +12 -18
- package/src/components/form-control/checkbox/SelectableBase.tsx +10 -16
- package/src/components/form-control/date-range-picker/DateRangePicker.tsx +1 -4
- package/src/components/form-control/editor/RichTextEditor.tsx +11 -12
- package/src/components/form-control/field/NumberInput.tsx +6 -8
- package/src/components/form-control/field/Textarea.tsx +11 -10
- package/src/components/form-control/select/Select.tsx +23 -9
- package/src/components/form-control/state-preset/StatePreset.tsx +15 -22
- package/src/components/layout/topbar/Topbar.tsx +2 -2
- package/src/hooks/createControllableStore.ts +8 -4
- package/src/hooks/useLocalStorage.ts +3 -2
- package/src/hooks/useSyncConfig.ts +5 -4
- package/src/providers/shared-data/SharedDataProvider.tsx +0 -1
- package/tests/components/features/crud-detail/CrudDetail.spec.tsx +49 -0
- package/tests/components/form-control/select/SelectItem.spec.tsx +5 -0
- package/tests/providers/shared-data/SharedDataProvider.spec.tsx +0 -104
|
@@ -59,13 +59,6 @@ export interface TextareaProps {
|
|
|
59
59
|
style?: JSX.CSSProperties;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const textareaBaseClass = clsx(
|
|
63
|
-
"absolute left-0 top-0",
|
|
64
|
-
"size-full",
|
|
65
|
-
"resize-none overflow-hidden",
|
|
66
|
-
"bg-transparent",
|
|
67
|
-
text.placeholder,
|
|
68
|
-
);
|
|
69
62
|
|
|
70
63
|
/**
|
|
71
64
|
* Textarea component
|
|
@@ -164,8 +157,6 @@ export const Textarea: Component<TextareaProps> = (props) => {
|
|
|
164
157
|
"whitespace-pre-wrap break-all",
|
|
165
158
|
);
|
|
166
159
|
|
|
167
|
-
const getTextareaClass = () =>
|
|
168
|
-
twMerge(textareaBaseClass, textAreaSizeClasses[local.size ?? "md"], local.inset && "p-0");
|
|
169
160
|
|
|
170
161
|
// Whether editable
|
|
171
162
|
const isEditable = () => !local.disabled && !local.readOnly;
|
|
@@ -228,7 +219,17 @@ export const Textarea: Component<TextareaProps> = (props) => {
|
|
|
228
219
|
{contentForHeight()}
|
|
229
220
|
</div>
|
|
230
221
|
<textarea
|
|
231
|
-
class={
|
|
222
|
+
class={twMerge(
|
|
223
|
+
clsx(
|
|
224
|
+
"absolute left-0 top-0",
|
|
225
|
+
"size-full",
|
|
226
|
+
"resize-none overflow-hidden",
|
|
227
|
+
"bg-transparent",
|
|
228
|
+
text.placeholder,
|
|
229
|
+
),
|
|
230
|
+
textAreaSizeClasses[local.size ?? "md"],
|
|
231
|
+
local.inset && "p-0",
|
|
232
|
+
)}
|
|
232
233
|
value={value()}
|
|
233
234
|
placeholder={local.placeholder}
|
|
234
235
|
title={local.title}
|
|
@@ -32,6 +32,7 @@ import { TextInput } from "../field/TextInput";
|
|
|
32
32
|
import { useI18n } from "../../../providers/i18n/I18nProvider";
|
|
33
33
|
import {
|
|
34
34
|
listItemBaseClass,
|
|
35
|
+
listItemSizeClasses,
|
|
35
36
|
listItemSelectedClass,
|
|
36
37
|
listItemDisabledClass,
|
|
37
38
|
listItemIndentGuideClass,
|
|
@@ -65,13 +66,15 @@ export interface SelectContextValue<TValue = unknown> {
|
|
|
65
66
|
|
|
66
67
|
/** Register item template */
|
|
67
68
|
setItemTemplate: (fn: ((...args: unknown[]) => JSX.Element) | undefined) => void;
|
|
69
|
+
|
|
70
|
+
/** Trigger size */
|
|
71
|
+
size: Accessor<ComponentSize>;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
export const SelectContext = createContext<SelectContextValue>();
|
|
71
|
-
const SelectCtx = SelectContext;
|
|
72
75
|
|
|
73
76
|
function useSelectContext<TValue = unknown>(): SelectContextValue<TValue> {
|
|
74
|
-
const context = useContext(
|
|
77
|
+
const context = useContext(SelectContext);
|
|
75
78
|
if (!context) {
|
|
76
79
|
throw new Error("useSelectContext can only be used inside Select component");
|
|
77
80
|
}
|
|
@@ -90,6 +93,7 @@ const [SelectActionSlot, createActionSlotAccessor] = createSlot<{ children: JSX.
|
|
|
90
93
|
|
|
91
94
|
const SelectAction = (props: SelectActionProps) => {
|
|
92
95
|
const [local, rest] = splitProps(props, ["children", "class"]);
|
|
96
|
+
const ctx = useSelectContext();
|
|
93
97
|
|
|
94
98
|
const handleClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent> = (e) => {
|
|
95
99
|
if (typeof rest.onClick === "function") {
|
|
@@ -105,7 +109,15 @@ const SelectAction = (props: SelectActionProps) => {
|
|
|
105
109
|
{...rest}
|
|
106
110
|
type="button"
|
|
107
111
|
onClick={handleClick}
|
|
108
|
-
class={twMerge(
|
|
112
|
+
class={twMerge(
|
|
113
|
+
"inline-flex items-center",
|
|
114
|
+
pad[ctx.size()],
|
|
115
|
+
"border",
|
|
116
|
+
border.default,
|
|
117
|
+
groupFocusBorderClass,
|
|
118
|
+
themeTokens.base.hoverBg,
|
|
119
|
+
local.class,
|
|
120
|
+
)}
|
|
109
121
|
data-select-action
|
|
110
122
|
>
|
|
111
123
|
{local.children}
|
|
@@ -116,6 +128,8 @@ const SelectAction = (props: SelectActionProps) => {
|
|
|
116
128
|
|
|
117
129
|
//#endregion
|
|
118
130
|
|
|
131
|
+
const groupFocusBorderClass = "group-focus-within:border-primary-400 dark:group-focus-within:border-primary-400";
|
|
132
|
+
|
|
119
133
|
const selectAllBtnClass = clsx(
|
|
120
134
|
"text-primary-500",
|
|
121
135
|
"hover:text-primary-600 dark:hover:text-primary-400",
|
|
@@ -186,13 +200,12 @@ const SelectItemInner = <TValue,>(
|
|
|
186
200
|
const getClassName = () =>
|
|
187
201
|
twMerge(
|
|
188
202
|
listItemBaseClass,
|
|
203
|
+
listItemSizeClasses[context.size()],
|
|
189
204
|
isSelected() && listItemSelectedClass,
|
|
190
205
|
local.disabled && listItemDisabledClass,
|
|
191
206
|
local.class,
|
|
192
207
|
);
|
|
193
208
|
|
|
194
|
-
const getCheckIconClass = () => getListItemSelectedIconClass(isSelected());
|
|
195
|
-
|
|
196
209
|
return (
|
|
197
210
|
<ItemChildrenProvider>
|
|
198
211
|
<button
|
|
@@ -209,7 +222,7 @@ const SelectItemInner = <TValue,>(
|
|
|
209
222
|
onClick={handleClick}
|
|
210
223
|
>
|
|
211
224
|
<Show when={context.multiple() && !hasChildren()}>
|
|
212
|
-
<Icon icon={IconCheck} class={
|
|
225
|
+
<Icon icon={IconCheck} class={getListItemSelectedIconClass(isSelected())} />
|
|
213
226
|
</Show>
|
|
214
227
|
<span class={listItemContentClass}>{local.children}</span>
|
|
215
228
|
</button>
|
|
@@ -440,6 +453,7 @@ const SelectInnerComponent = <TValue,>(props: SelectProps<TValue>) => {
|
|
|
440
453
|
toggleValue,
|
|
441
454
|
closeDropdown,
|
|
442
455
|
setItemTemplate,
|
|
456
|
+
size: () => local.size ?? "md",
|
|
443
457
|
};
|
|
444
458
|
|
|
445
459
|
// Trigger keyboard handling (only Enter/Space, ArrowUp/Down handled by Dropdown)
|
|
@@ -624,7 +638,7 @@ const SelectInnerComponent = <TValue,>(props: SelectProps<TValue>) => {
|
|
|
624
638
|
action() !== undefined &&
|
|
625
639
|
clsx(
|
|
626
640
|
"rounded-r-none border-r-0",
|
|
627
|
-
|
|
641
|
+
groupFocusBorderClass,
|
|
628
642
|
),
|
|
629
643
|
)}
|
|
630
644
|
style={local.style}
|
|
@@ -698,13 +712,13 @@ const SelectInnerComponent = <TValue,>(props: SelectProps<TValue>) => {
|
|
|
698
712
|
|
|
699
713
|
return (
|
|
700
714
|
<Invalid message={errorMsg()} variant="border" lazyValidation={local.lazyValidation}>
|
|
701
|
-
<
|
|
715
|
+
<SelectContext.Provider value={contextValue as SelectContextValue}>
|
|
702
716
|
<HeaderProvider>
|
|
703
717
|
<ActionProvider>
|
|
704
718
|
<SelectInnerRender>{local.children}</SelectInnerRender>
|
|
705
719
|
</ActionProvider>
|
|
706
720
|
</HeaderProvider>
|
|
707
|
-
</
|
|
721
|
+
</SelectContext.Provider>
|
|
708
722
|
</Invalid>
|
|
709
723
|
);
|
|
710
724
|
};
|
|
@@ -191,33 +191,18 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
|
|
|
191
191
|
|
|
192
192
|
// ── Render ──
|
|
193
193
|
|
|
194
|
-
const containerClass = () => twMerge(clsx("inline-flex items-center", gap.lg, "flex-wrap"), local.class);
|
|
195
|
-
|
|
196
|
-
const resolvedChipClass = () => twMerge(
|
|
197
|
-
clsx("inline-flex items-center", gap.md, "rounded-full", bg.subtle, text.default, "cursor-default"),
|
|
198
|
-
chipSizeClasses[local.size ?? "md"],
|
|
199
|
-
);
|
|
200
|
-
|
|
201
194
|
const resolvedIconBtnClass = () =>
|
|
202
195
|
twMerge("rounded-full", iconBtnSizeClasses[local.size ?? "md"]);
|
|
203
196
|
|
|
204
|
-
const resolvedStarBtnClass = () =>
|
|
205
|
-
twMerge(
|
|
206
|
-
clsx("inline-flex cursor-pointer items-center justify-center rounded-full text-warning-500 transition-colors focus:outline-none", themeTokens.warning.hoverBg),
|
|
207
|
-
starBtnSizeClasses[local.size ?? "md"],
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
const resolvedInputClass = () => twMerge(
|
|
211
|
-
clsx("rounded-full", bg.subtle, text.default, "border border-transparent focus:outline-none focus:ring-1 focus:ring-primary-400", text.placeholder),
|
|
212
|
-
inputSizeClasses[local.size ?? "md"],
|
|
213
|
-
);
|
|
214
|
-
|
|
215
197
|
return (
|
|
216
|
-
<div class={
|
|
198
|
+
<div class={twMerge(clsx("inline-flex items-center", gap.lg, "flex-wrap"), local.class)} style={local.style}>
|
|
217
199
|
{/* Star button - add preset */}
|
|
218
200
|
<button
|
|
219
201
|
type="button"
|
|
220
|
-
class={
|
|
202
|
+
class={twMerge(
|
|
203
|
+
clsx("inline-flex cursor-pointer items-center justify-center rounded-full text-warning-500 transition-colors focus:outline-none", themeTokens.warning.hoverBg),
|
|
204
|
+
starBtnSizeClasses[local.size ?? "md"],
|
|
205
|
+
)}
|
|
221
206
|
onClick={handleStartAdd}
|
|
222
207
|
title={i18n.t("statePreset.addPreset")}
|
|
223
208
|
>
|
|
@@ -227,7 +212,12 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
|
|
|
227
212
|
{/* Preset chips */}
|
|
228
213
|
<For each={presets()}>
|
|
229
214
|
{(preset, index) => (
|
|
230
|
-
<span
|
|
215
|
+
<span
|
|
216
|
+
class={twMerge(
|
|
217
|
+
clsx("inline-flex items-center", gap.md, "rounded-full", bg.subtle, text.default, "cursor-default"),
|
|
218
|
+
chipSizeClasses[local.size ?? "md"],
|
|
219
|
+
)}
|
|
220
|
+
>
|
|
231
221
|
<button
|
|
232
222
|
type="button"
|
|
233
223
|
class="cursor-pointer hover:underline focus:outline-none"
|
|
@@ -266,7 +256,10 @@ function StatePresetInner<TValue>(props: StatePresetProps<TValue>): JSX.Element
|
|
|
266
256
|
requestAnimationFrame(() => el.focus());
|
|
267
257
|
}}
|
|
268
258
|
type="text"
|
|
269
|
-
class={
|
|
259
|
+
class={twMerge(
|
|
260
|
+
clsx("rounded-full", bg.subtle, text.default, "border border-transparent focus:outline-none focus:ring-1 focus:ring-primary-400", text.placeholder),
|
|
261
|
+
inputSizeClasses[local.size ?? "md"],
|
|
262
|
+
)}
|
|
270
263
|
placeholder={i18n.t("statePreset.namePlaceholder")}
|
|
271
264
|
autocomplete="one-time-code"
|
|
272
265
|
value={inputValue()}
|
|
@@ -133,8 +133,8 @@ const TopbarInner: ParentComponent<TopbarProps> = (props) => {
|
|
|
133
133
|
return (
|
|
134
134
|
<header {...rest} data-topbar class={getClassName()}>
|
|
135
135
|
<Show when={sidebarContext}>
|
|
136
|
-
<Button variant="ghost" onClick={handleToggle}
|
|
137
|
-
<Icon icon={IconMenu2}
|
|
136
|
+
<Button variant="ghost" onClick={handleToggle} aria-label={i18n.t("topbar.toggleSidebar")}>
|
|
137
|
+
<Icon icon={IconMenu2} />
|
|
138
138
|
</Button>
|
|
139
139
|
</Show>
|
|
140
140
|
{local.children}
|
|
@@ -39,10 +39,14 @@ export function createControllableStore<TValue extends object>(options: {
|
|
|
39
39
|
|
|
40
40
|
// Wrap setter with a function wrapper to add onChange notification
|
|
41
41
|
const wrappedSet = ((...args: any[]) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
if (options.onChange() != null) {
|
|
43
|
+
const before = obj.clone(unwrap(store));
|
|
44
|
+
(rawSet as any)(...args);
|
|
45
|
+
if (!obj.equal(before, unwrap(store))) {
|
|
46
|
+
options.onChange()!(obj.clone(unwrap(store)));
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
(rawSet as any)(...args);
|
|
46
50
|
}
|
|
47
51
|
}) as SetStoreFunction<TValue>;
|
|
48
52
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Accessor, createSignal } from "solid-js";
|
|
2
|
+
import { json } from "@simplysm/core-common";
|
|
2
3
|
import { useConfig } from "../providers/ConfigContext";
|
|
3
4
|
|
|
4
5
|
type StorageSetter<TValue> = (
|
|
@@ -43,7 +44,7 @@ export function useLocalStorage<TValue>(
|
|
|
43
44
|
try {
|
|
44
45
|
const item = localStorage.getItem(prefixedKey);
|
|
45
46
|
if (item !== null) {
|
|
46
|
-
storedValue =
|
|
47
|
+
storedValue = json.parse<TValue>(item);
|
|
47
48
|
}
|
|
48
49
|
} catch {
|
|
49
50
|
// Use initial value on JSON parse failure
|
|
@@ -67,7 +68,7 @@ export function useLocalStorage<TValue>(
|
|
|
67
68
|
if (resolved === undefined) {
|
|
68
69
|
localStorage.removeItem(prefixedKey);
|
|
69
70
|
} else {
|
|
70
|
-
localStorage.setItem(prefixedKey,
|
|
71
|
+
localStorage.setItem(prefixedKey, json.stringify(resolved));
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
return resolved;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Accessor, type Setter, createEffect, createSignal, untrack } from "solid-js";
|
|
2
|
+
import { json } from "@simplysm/core-common";
|
|
2
3
|
import { useConfig } from "../providers/ConfigContext";
|
|
3
4
|
import { useSyncStorage } from "../providers/SyncStorageProvider";
|
|
4
5
|
|
|
@@ -49,7 +50,7 @@ export function useSyncConfig<TValue>(
|
|
|
49
50
|
try {
|
|
50
51
|
const stored = localStorage.getItem(prefixedKey);
|
|
51
52
|
if (stored !== null && writeVersion === versionBefore) {
|
|
52
|
-
setValue(() =>
|
|
53
|
+
setValue(() => json.parse<TValue>(stored));
|
|
53
54
|
}
|
|
54
55
|
} catch {
|
|
55
56
|
// Ignore parse errors, keep default value
|
|
@@ -62,14 +63,14 @@ export function useSyncConfig<TValue>(
|
|
|
62
63
|
try {
|
|
63
64
|
const stored = await currentAdapter.getItem(prefixedKey);
|
|
64
65
|
if (stored !== null && writeVersion === versionBefore) {
|
|
65
|
-
setValue(() =>
|
|
66
|
+
setValue(() => json.parse<TValue>(stored));
|
|
66
67
|
}
|
|
67
68
|
} catch {
|
|
68
69
|
// Fall back to localStorage on error
|
|
69
70
|
try {
|
|
70
71
|
const stored = localStorage.getItem(prefixedKey);
|
|
71
72
|
if (stored !== null && writeVersion === versionBefore) {
|
|
72
|
-
setValue(() =>
|
|
73
|
+
setValue(() => json.parse<TValue>(stored));
|
|
73
74
|
}
|
|
74
75
|
} catch {
|
|
75
76
|
// Ignore parse errors
|
|
@@ -84,7 +85,7 @@ export function useSyncConfig<TValue>(
|
|
|
84
85
|
createEffect(() => {
|
|
85
86
|
if (!ready()) return; // Don't save until storage has been read
|
|
86
87
|
const currentValue = value();
|
|
87
|
-
const serialized =
|
|
88
|
+
const serialized = json.stringify(currentValue);
|
|
88
89
|
|
|
89
90
|
// Read adapter untracked to avoid re-running save effect when adapter changes
|
|
90
91
|
const currentAdapter = untrack(() => syncStorageCtx?.adapter());
|
|
@@ -5,6 +5,7 @@ import { CrudDetailTools } from "../../../../src/components/features/crud-detail
|
|
|
5
5
|
import { CrudDetailBefore } from "../../../../src/components/features/crud-detail/CrudDetailBefore";
|
|
6
6
|
import { CrudDetailAfter } from "../../../../src/components/features/crud-detail/CrudDetailAfter";
|
|
7
7
|
import { CrudDetail } from "../../../../src/components/features/crud-detail/CrudDetail";
|
|
8
|
+
import { Dialog } from "../../../../src/components/disclosure/Dialog";
|
|
8
9
|
import { ConfigContext, ConfigProvider } from "../../../../src/providers/ConfigContext";
|
|
9
10
|
import { NotificationProvider } from "../../../../src/components/feedback/notification/NotificationProvider";
|
|
10
11
|
import { Topbar } from "../../../../src/components/layout/topbar/Topbar";
|
|
@@ -474,3 +475,51 @@ describe("CrudDetail button layout by mode", () => {
|
|
|
474
475
|
expect(container.textContent).toContain("커스텀도구");
|
|
475
476
|
});
|
|
476
477
|
});
|
|
478
|
+
|
|
479
|
+
describe("CrudDetail dialog mode layout", () => {
|
|
480
|
+
beforeEach(() => {
|
|
481
|
+
localStorage.setItem("test.i18n-locale", JSON.stringify("en"));
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
afterEach(() => {
|
|
485
|
+
localStorage.removeItem("test.i18n-locale");
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it("bottom bar's direct parent should not have gap-2 class", async () => {
|
|
489
|
+
render(() => (
|
|
490
|
+
<ConfigProvider clientName="test"><I18nProvider>
|
|
491
|
+
<TestWrapper>
|
|
492
|
+
<Dialog open={true}>
|
|
493
|
+
<Dialog.Header>Test</Dialog.Header>
|
|
494
|
+
<CrudDetail<TestData>
|
|
495
|
+
load={() =>
|
|
496
|
+
Promise.resolve({
|
|
497
|
+
data: { id: 1, name: "홍길동" },
|
|
498
|
+
info: { isNew: false, isDeleted: false },
|
|
499
|
+
})
|
|
500
|
+
}
|
|
501
|
+
submit={() => Promise.resolve(true)}
|
|
502
|
+
close={() => {}}
|
|
503
|
+
>
|
|
504
|
+
{(ctx) => <div>{ctx.data.name}</div>}
|
|
505
|
+
</CrudDetail>
|
|
506
|
+
</Dialog>
|
|
507
|
+
</TestWrapper>
|
|
508
|
+
</I18nProvider></ConfigProvider>
|
|
509
|
+
));
|
|
510
|
+
|
|
511
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
512
|
+
|
|
513
|
+
// Find the bottom bar via Confirm button
|
|
514
|
+
const confirmBtn = Array.from(document.querySelectorAll("button")).find(
|
|
515
|
+
(btn) => btn.textContent.includes("Confirm"),
|
|
516
|
+
);
|
|
517
|
+
expect(confirmBtn).toBeTruthy();
|
|
518
|
+
|
|
519
|
+
const bottomBar = confirmBtn!.closest(".border-t");
|
|
520
|
+
expect(bottomBar).toBeTruthy();
|
|
521
|
+
|
|
522
|
+
// Bottom bar's direct parent should NOT have gap-2 class
|
|
523
|
+
expect(bottomBar!.parentElement!.classList.contains("gap-2")).toBe(false);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
@@ -23,6 +23,7 @@ describe("SelectItem component", () => {
|
|
|
23
23
|
toggleValue,
|
|
24
24
|
closeDropdown: vi.fn(),
|
|
25
25
|
setItemTemplate: vi.fn(),
|
|
26
|
+
size: () => "md" as const,
|
|
26
27
|
};
|
|
27
28
|
|
|
28
29
|
const { getByText } = render(() => (
|
|
@@ -43,6 +44,7 @@ describe("SelectItem component", () => {
|
|
|
43
44
|
toggleValue: vi.fn(),
|
|
44
45
|
closeDropdown,
|
|
45
46
|
setItemTemplate: vi.fn(),
|
|
47
|
+
size: () => "md" as const,
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
const { getByText } = render(() => (
|
|
@@ -63,6 +65,7 @@ describe("SelectItem component", () => {
|
|
|
63
65
|
toggleValue: vi.fn(),
|
|
64
66
|
closeDropdown,
|
|
65
67
|
setItemTemplate: vi.fn(),
|
|
68
|
+
size: () => "md" as const,
|
|
66
69
|
};
|
|
67
70
|
|
|
68
71
|
const { getByText } = render(() => (
|
|
@@ -84,6 +87,7 @@ describe("SelectItem component", () => {
|
|
|
84
87
|
toggleValue: vi.fn(),
|
|
85
88
|
closeDropdown: vi.fn(),
|
|
86
89
|
setItemTemplate: vi.fn(),
|
|
90
|
+
size: () => "md" as const,
|
|
87
91
|
};
|
|
88
92
|
|
|
89
93
|
render(() => (
|
|
@@ -106,6 +110,7 @@ describe("SelectItem component", () => {
|
|
|
106
110
|
toggleValue,
|
|
107
111
|
closeDropdown: vi.fn(),
|
|
108
112
|
setItemTemplate: vi.fn(),
|
|
113
|
+
size: () => "md" as const,
|
|
109
114
|
};
|
|
110
115
|
|
|
111
116
|
const { getByText } = render(() => (
|
|
@@ -418,108 +418,4 @@ describe("SharedDataProvider", () => {
|
|
|
418
418
|
result.unmount();
|
|
419
419
|
});
|
|
420
420
|
|
|
421
|
-
it("fetches eagerly after configure()", async () => {
|
|
422
|
-
const { serviceClientValue, mockClient } = createMockServiceClient();
|
|
423
|
-
const mockUsers: TestUser[] = [{ id: 1, name: "Alice" }];
|
|
424
|
-
|
|
425
|
-
const fetchFn = vi.fn(() => Promise.resolve(mockUsers));
|
|
426
|
-
|
|
427
|
-
const definitions: { user: SharedDataDefinition<TestUser> } = {
|
|
428
|
-
user: {
|
|
429
|
-
fetch: fetchFn,
|
|
430
|
-
getKey: (item) => item.id,
|
|
431
|
-
orderBy: [[(item) => item.name, "asc"]],
|
|
432
|
-
},
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
function ConfigureOnly() {
|
|
436
|
-
const shared = useTestSharedData();
|
|
437
|
-
shared.configure(() => definitions);
|
|
438
|
-
return <div data-testid="configured">configured</div>;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const result = render(() => (
|
|
442
|
-
<NotificationContext.Provider value={createMockNotification()}>
|
|
443
|
-
<ServiceClientContext.Provider value={serviceClientValue}>
|
|
444
|
-
<SharedDataProvider>
|
|
445
|
-
<ConfigureOnly />
|
|
446
|
-
</SharedDataProvider>
|
|
447
|
-
</ServiceClientContext.Provider>
|
|
448
|
-
</NotificationContext.Provider>
|
|
449
|
-
));
|
|
450
|
-
|
|
451
|
-
await vi.waitFor(() => {
|
|
452
|
-
expect(result.getByTestId("configured").textContent).toBe("configured");
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
// Now configure() triggers eager init
|
|
456
|
-
await vi.waitFor(() => {
|
|
457
|
-
expect(fetchFn).toHaveBeenCalledTimes(1);
|
|
458
|
-
expect(mockClient.addListener).toHaveBeenCalledTimes(1);
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
result.unmount();
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
it("wait() resolves after data is loaded even without items() access", async () => {
|
|
465
|
-
const { serviceClientValue } = createMockServiceClient();
|
|
466
|
-
|
|
467
|
-
let resolveUsers!: (value: TestUser[]) => void;
|
|
468
|
-
const fetchPromise = new Promise<TestUser[]>((resolve) => {
|
|
469
|
-
resolveUsers = resolve;
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
const fetchFn = vi.fn(() => fetchPromise);
|
|
473
|
-
|
|
474
|
-
const definitions: { user: SharedDataDefinition<TestUser> } = {
|
|
475
|
-
user: {
|
|
476
|
-
fetch: fetchFn,
|
|
477
|
-
getKey: (item) => item.id,
|
|
478
|
-
orderBy: [[(item) => item.name, "asc"]],
|
|
479
|
-
},
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
let waitResolved = false;
|
|
483
|
-
|
|
484
|
-
// Component that calls configure() + wait() but never accesses items()
|
|
485
|
-
function ConfigureAndWait() {
|
|
486
|
-
const shared = useTestSharedData();
|
|
487
|
-
shared.configure(() => definitions);
|
|
488
|
-
|
|
489
|
-
// Call wait() immediately — should NOT resolve until fetch completes
|
|
490
|
-
void shared.wait().then(() => {
|
|
491
|
-
waitResolved = true;
|
|
492
|
-
});
|
|
493
|
-
|
|
494
|
-
return <div data-testid="ready">{String(waitResolved)}</div>;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
const result = render(() => (
|
|
498
|
-
<NotificationContext.Provider value={createMockNotification()}>
|
|
499
|
-
<ServiceClientContext.Provider value={serviceClientValue}>
|
|
500
|
-
<SharedDataProvider>
|
|
501
|
-
<ConfigureAndWait />
|
|
502
|
-
</SharedDataProvider>
|
|
503
|
-
</ServiceClientContext.Provider>
|
|
504
|
-
</NotificationContext.Provider>
|
|
505
|
-
));
|
|
506
|
-
|
|
507
|
-
// fetch should have been called (eager init)
|
|
508
|
-
await vi.waitFor(() => {
|
|
509
|
-
expect(fetchFn).toHaveBeenCalledTimes(1);
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
// wait() should NOT have resolved yet (fetch still pending)
|
|
513
|
-
expect(waitResolved).toBe(false);
|
|
514
|
-
|
|
515
|
-
// Resolve the fetch
|
|
516
|
-
resolveUsers([{ id: 1, name: "Alice" }]);
|
|
517
|
-
|
|
518
|
-
// wait() should now resolve
|
|
519
|
-
await vi.waitFor(() => {
|
|
520
|
-
expect(waitResolved).toBe(true);
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
result.unmount();
|
|
524
|
-
});
|
|
525
421
|
});
|