@simplysm/solid 13.0.55 → 13.0.57
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 +3 -1
- package/dist/components/data/crud-detail/CrudDetail.d.ts +14 -0
- package/dist/components/data/crud-detail/CrudDetail.d.ts.map +1 -0
- package/dist/components/data/crud-detail/CrudDetail.js +348 -0
- package/dist/components/data/crud-detail/CrudDetail.js.map +6 -0
- package/dist/components/data/crud-detail/CrudDetailAfter.d.ts +7 -0
- package/dist/components/data/crud-detail/CrudDetailAfter.d.ts.map +1 -0
- package/dist/components/data/crud-detail/CrudDetailAfter.js +14 -0
- package/dist/components/data/crud-detail/CrudDetailAfter.js.map +6 -0
- package/dist/components/data/crud-detail/CrudDetailBefore.d.ts +7 -0
- package/dist/components/data/crud-detail/CrudDetailBefore.d.ts.map +1 -0
- package/dist/components/data/crud-detail/CrudDetailBefore.js +14 -0
- package/dist/components/data/crud-detail/CrudDetailBefore.js.map +6 -0
- package/dist/components/data/crud-detail/CrudDetailTools.d.ts +7 -0
- package/dist/components/data/crud-detail/CrudDetailTools.d.ts.map +1 -0
- package/dist/components/data/crud-detail/CrudDetailTools.js +14 -0
- package/dist/components/data/crud-detail/CrudDetailTools.js.map +6 -0
- package/dist/components/data/crud-detail/types.d.ts +45 -0
- package/dist/components/data/crud-detail/types.d.ts.map +1 -0
- package/dist/components/data/crud-detail/types.js +1 -0
- package/dist/components/data/crud-detail/types.js.map +6 -0
- package/dist/components/data/crud-sheet/CrudSheet.d.ts +17 -0
- package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/CrudSheet.js +679 -0
- package/dist/components/data/crud-sheet/CrudSheet.js.map +6 -0
- package/dist/components/data/crud-sheet/CrudSheetColumn.d.ts +5 -0
- package/dist/components/data/crud-sheet/CrudSheetColumn.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/CrudSheetColumn.js +29 -0
- package/dist/components/data/crud-sheet/CrudSheetColumn.js.map +6 -0
- package/dist/components/data/crud-sheet/CrudSheetFilter.d.ts +7 -0
- package/dist/components/data/crud-sheet/CrudSheetFilter.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/CrudSheetFilter.js +14 -0
- package/dist/components/data/crud-sheet/CrudSheetFilter.js.map +6 -0
- package/dist/components/data/crud-sheet/CrudSheetHeader.d.ts +7 -0
- package/dist/components/data/crud-sheet/CrudSheetHeader.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/CrudSheetHeader.js +14 -0
- package/dist/components/data/crud-sheet/CrudSheetHeader.js.map +6 -0
- package/dist/components/data/crud-sheet/CrudSheetTools.d.ts +7 -0
- package/dist/components/data/crud-sheet/CrudSheetTools.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/CrudSheetTools.js +14 -0
- package/dist/components/data/crud-sheet/CrudSheetTools.js.map +6 -0
- package/dist/components/data/crud-sheet/types.d.ts +109 -0
- package/dist/components/data/crud-sheet/types.d.ts.map +1 -0
- package/dist/components/data/crud-sheet/types.js +1 -0
- package/dist/components/data/crud-sheet/types.js.map +6 -0
- package/dist/components/data/kanban/Kanban.d.ts.map +1 -1
- package/dist/components/data/kanban/Kanban.js +137 -138
- package/dist/components/data/kanban/Kanban.js.map +2 -2
- package/dist/components/data/kanban/KanbanContext.d.ts +5 -1
- package/dist/components/data/kanban/KanbanContext.d.ts.map +1 -1
- package/dist/components/data/kanban/KanbanContext.js.map +1 -1
- package/dist/components/data/list/ListItem.d.ts.map +1 -1
- package/dist/components/data/list/ListItem.js +109 -99
- package/dist/components/data/list/ListItem.js.map +2 -2
- package/dist/components/data/sheet/DataSheet.js +1 -1
- package/dist/components/data/sheet/DataSheet.js.map +2 -2
- 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/Dialog.d.ts +16 -10
- package/dist/components/disclosure/Dialog.d.ts.map +1 -1
- package/dist/components/disclosure/Dialog.js +126 -91
- package/dist/components/disclosure/Dialog.js.map +2 -2
- package/dist/components/disclosure/DialogContext.d.ts +2 -4
- package/dist/components/disclosure/DialogContext.d.ts.map +1 -1
- package/dist/components/disclosure/DialogContext.js.map +1 -1
- package/dist/components/disclosure/DialogProvider.d.ts.map +1 -1
- package/dist/components/disclosure/DialogProvider.js +14 -9
- package/dist/components/disclosure/DialogProvider.js.map +2 -2
- package/dist/components/disclosure/Dropdown.d.ts +46 -22
- package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
- package/dist/components/disclosure/Dropdown.js +100 -65
- package/dist/components/disclosure/Dropdown.js.map +2 -2
- package/dist/components/feedback/notification/NotificationBanner.d.ts.map +1 -1
- package/dist/components/feedback/notification/NotificationBanner.js +3 -3
- package/dist/components/feedback/notification/NotificationBanner.js.map +1 -1
- package/dist/components/feedback/notification/NotificationBell.d.ts.map +1 -1
- package/dist/components/feedback/notification/NotificationBell.js +84 -84
- package/dist/components/feedback/notification/NotificationBell.js.map +2 -2
- package/dist/components/form-control/combobox/Combobox.d.ts +6 -3
- package/dist/components/form-control/combobox/Combobox.d.ts.map +1 -1
- package/dist/components/form-control/combobox/Combobox.js +150 -168
- package/dist/components/form-control/combobox/Combobox.js.map +2 -2
- package/dist/components/form-control/combobox/ComboboxContext.d.ts +3 -0
- package/dist/components/form-control/combobox/ComboboxContext.d.ts.map +1 -1
- package/dist/components/form-control/combobox/ComboboxContext.js.map +1 -1
- package/dist/components/form-control/date-range-picker/DateRangePicker.d.ts +0 -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 +9 -17
- package/dist/components/form-control/date-range-picker/DateRangePicker.js.map +2 -2
- package/dist/components/form-control/field/Field.styles.d.ts.map +1 -1
- package/dist/components/form-control/field/Field.styles.js +2 -1
- package/dist/components/form-control/field/Field.styles.js.map +1 -1
- package/dist/components/form-control/field/NumberInput.d.ts +15 -5
- package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
- package/dist/components/form-control/field/NumberInput.js +181 -141
- package/dist/components/form-control/field/NumberInput.js.map +2 -2
- package/dist/components/form-control/field/TextInput.d.ts +9 -5
- package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
- package/dist/components/form-control/field/TextInput.js +199 -154
- package/dist/components/form-control/field/TextInput.js.map +2 -2
- package/dist/components/form-control/select/Select.d.ts +3 -3
- package/dist/components/form-control/select/Select.d.ts.map +1 -1
- package/dist/components/form-control/select/Select.js +116 -100
- package/dist/components/form-control/select/Select.js.map +2 -2
- package/dist/components/form-control/select/SelectContext.d.ts +9 -1
- package/dist/components/form-control/select/SelectContext.d.ts.map +1 -1
- package/dist/components/form-control/select/SelectContext.js.map +1 -1
- package/dist/components/form-control/select/SelectItem.d.ts.map +1 -1
- package/dist/components/form-control/select/SelectItem.js +77 -67
- package/dist/components/form-control/select/SelectItem.js.map +2 -2
- package/dist/components/layout/topbar/TopbarMenu.d.ts.map +1 -1
- package/dist/components/layout/topbar/TopbarMenu.js +63 -57
- package/dist/components/layout/topbar/TopbarMenu.js.map +2 -2
- package/dist/components/layout/topbar/TopbarUser.d.ts.map +1 -1
- package/dist/components/layout/topbar/TopbarUser.js +53 -54
- package/dist/components/layout/topbar/TopbarUser.js.map +2 -2
- package/dist/hooks/createControllableStore.d.ts +29 -0
- package/dist/hooks/createControllableStore.d.ts.map +1 -0
- package/dist/hooks/createControllableStore.js +19 -0
- package/dist/hooks/createControllableStore.js.map +6 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/styles/patterns.styles.d.ts.map +1 -1
- package/dist/styles/patterns.styles.js +7 -1
- package/dist/styles/patterns.styles.js.map +1 -1
- package/docs/data-components.md +428 -0
- package/docs/disclosure.md +65 -35
- package/docs/form-controls.md +18 -3
- package/docs/helpers.md +0 -39
- package/docs/hooks.md +39 -0
- package/package.json +4 -3
- package/src/components/data/crud-detail/CrudDetail.tsx +346 -0
- package/src/components/data/crud-detail/CrudDetailAfter.tsx +19 -0
- package/src/components/data/crud-detail/CrudDetailBefore.tsx +19 -0
- package/src/components/data/crud-detail/CrudDetailTools.tsx +19 -0
- package/src/components/data/crud-detail/types.ts +58 -0
- package/src/components/data/crud-sheet/CrudSheet.tsx +628 -0
- package/src/components/data/crud-sheet/CrudSheetColumn.tsx +34 -0
- package/src/components/data/crud-sheet/CrudSheetFilter.tsx +21 -0
- package/src/components/data/crud-sheet/CrudSheetHeader.tsx +19 -0
- package/src/components/data/crud-sheet/CrudSheetTools.tsx +21 -0
- package/src/components/data/crud-sheet/types.ts +133 -0
- package/src/components/data/kanban/Kanban.tsx +72 -65
- package/src/components/data/kanban/KanbanContext.ts +7 -1
- package/src/components/data/list/ListItem.tsx +31 -18
- package/src/components/data/sheet/DataSheet.styles.ts +1 -1
- package/src/components/data/sheet/DataSheet.tsx +1 -1
- package/src/components/disclosure/Dialog.tsx +143 -105
- package/src/components/disclosure/DialogContext.ts +2 -4
- package/src/components/disclosure/DialogProvider.tsx +4 -2
- package/src/components/disclosure/Dropdown.tsx +174 -86
- package/src/components/feedback/notification/NotificationBanner.tsx +3 -9
- package/src/components/feedback/notification/NotificationBell.tsx +51 -57
- package/src/components/form-control/combobox/Combobox.tsx +109 -134
- package/src/components/form-control/combobox/ComboboxContext.ts +4 -1
- package/src/components/form-control/date-range-picker/DateRangePicker.tsx +6 -16
- package/src/components/form-control/field/Field.styles.ts +1 -0
- package/src/components/form-control/field/NumberInput.tsx +131 -88
- package/src/components/form-control/field/TextInput.tsx +139 -88
- package/src/components/form-control/select/Select.tsx +85 -67
- package/src/components/form-control/select/SelectContext.ts +12 -1
- package/src/components/form-control/select/SelectItem.tsx +39 -18
- package/src/components/layout/topbar/TopbarMenu.tsx +52 -55
- package/src/components/layout/topbar/TopbarUser.tsx +28 -31
- package/src/hooks/createControllableStore.ts +47 -0
- package/src/index.ts +5 -1
- package/src/styles/patterns.styles.ts +7 -1
- package/tailwind.css +4 -0
- package/dist/helpers/splitSlots.d.ts +0 -25
- package/dist/helpers/splitSlots.d.ts.map +0 -1
- package/dist/helpers/splitSlots.js +0 -25
- package/dist/helpers/splitSlots.js.map +0 -6
- package/dist/hooks/createItemTemplate.d.ts +0 -17
- package/dist/hooks/createItemTemplate.d.ts.map +0 -1
- package/dist/hooks/createItemTemplate.js +0 -40
- package/dist/hooks/createItemTemplate.js.map +0 -6
- package/src/helpers/splitSlots.ts +0 -51
- package/src/hooks/createItemTemplate.tsx +0 -42
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import {
|
|
2
|
+
children,
|
|
3
|
+
createEffect,
|
|
4
|
+
createMemo,
|
|
5
|
+
createSignal,
|
|
6
|
+
For,
|
|
7
|
+
type JSX,
|
|
8
|
+
Show,
|
|
9
|
+
splitProps,
|
|
10
|
+
useContext,
|
|
11
|
+
} from "solid-js";
|
|
12
|
+
import { createStore, produce, reconcile } from "solid-js/store";
|
|
13
|
+
import { createControllableStore } from "../../../hooks/createControllableStore";
|
|
14
|
+
import { objClone } from "@simplysm/core-common";
|
|
15
|
+
import "@simplysm/core-common"; // register extensions
|
|
16
|
+
import type { SortingDef } from "../sheet/types";
|
|
17
|
+
import { DataSheet } from "../sheet/DataSheet";
|
|
18
|
+
import { DataSheetColumn } from "../sheet/DataSheetColumn";
|
|
19
|
+
import { BusyContainer } from "../../feedback/busy/BusyContainer";
|
|
20
|
+
import { useNotification } from "../../feedback/notification/NotificationContext";
|
|
21
|
+
import { Button } from "../../form-control/Button";
|
|
22
|
+
import { Icon } from "../../display/Icon";
|
|
23
|
+
import { FormGroup } from "../../layout/FormGroup";
|
|
24
|
+
import { TopbarContext, createTopbarActions } from "../../layout/topbar/TopbarContext";
|
|
25
|
+
import { useDialogInstance } from "../../disclosure/DialogInstanceContext";
|
|
26
|
+
import { Dialog } from "../../disclosure/Dialog";
|
|
27
|
+
import { Link } from "../../display/Link";
|
|
28
|
+
import { createEventListener } from "@solid-primitives/event-listener";
|
|
29
|
+
import clsx from "clsx";
|
|
30
|
+
import {
|
|
31
|
+
IconDeviceFloppy,
|
|
32
|
+
IconFileExcel,
|
|
33
|
+
IconPlus,
|
|
34
|
+
IconRefresh,
|
|
35
|
+
IconSearch,
|
|
36
|
+
IconTrash,
|
|
37
|
+
IconTrashOff,
|
|
38
|
+
IconUpload,
|
|
39
|
+
} from "@tabler/icons-solidjs";
|
|
40
|
+
import { isCrudSheetColumnDef, CrudSheetColumn } from "./CrudSheetColumn";
|
|
41
|
+
import { isCrudSheetFilterDef, CrudSheetFilter } from "./CrudSheetFilter";
|
|
42
|
+
import { isCrudSheetToolsDef, CrudSheetTools } from "./CrudSheetTools";
|
|
43
|
+
import { isCrudSheetHeaderDef, CrudSheetHeader } from "./CrudSheetHeader";
|
|
44
|
+
import type {
|
|
45
|
+
CrudSheetColumnDef,
|
|
46
|
+
CrudSheetContext,
|
|
47
|
+
CrudSheetFilterDef,
|
|
48
|
+
CrudSheetHeaderDef,
|
|
49
|
+
CrudSheetProps,
|
|
50
|
+
CrudSheetToolsDef,
|
|
51
|
+
SearchResult,
|
|
52
|
+
} from "./types";
|
|
53
|
+
|
|
54
|
+
interface CrudSheetComponent {
|
|
55
|
+
<TItem, TFilter extends Record<string, any>>(props: CrudSheetProps<TItem, TFilter>): JSX.Element;
|
|
56
|
+
Column: typeof CrudSheetColumn;
|
|
57
|
+
Filter: typeof CrudSheetFilter;
|
|
58
|
+
Tools: typeof CrudSheetTools;
|
|
59
|
+
Header: typeof CrudSheetHeader;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
|
|
63
|
+
props: CrudSheetProps<TItem, TFilter>,
|
|
64
|
+
) => {
|
|
65
|
+
const [local, _rest] = splitProps(props, [
|
|
66
|
+
"search",
|
|
67
|
+
"getItemKey",
|
|
68
|
+
"persistKey",
|
|
69
|
+
"itemsPerPage",
|
|
70
|
+
"editable",
|
|
71
|
+
"itemEditable",
|
|
72
|
+
"itemDeletable",
|
|
73
|
+
"filterInitial",
|
|
74
|
+
"items",
|
|
75
|
+
"onItemsChange",
|
|
76
|
+
"inlineEdit",
|
|
77
|
+
"modalEdit",
|
|
78
|
+
"excel",
|
|
79
|
+
"selectMode",
|
|
80
|
+
"onSelect",
|
|
81
|
+
"hideAutoTools",
|
|
82
|
+
"class",
|
|
83
|
+
"children",
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
const noti = useNotification();
|
|
87
|
+
const topbarCtx = useContext(TopbarContext);
|
|
88
|
+
const dialogInstance = useDialogInstance();
|
|
89
|
+
const isModal = dialogInstance !== undefined;
|
|
90
|
+
const isSelectMode = () => local.selectMode != null;
|
|
91
|
+
const canEdit = () => (isSelectMode() ? false : (local.editable ?? true));
|
|
92
|
+
|
|
93
|
+
// -- Children Resolution --
|
|
94
|
+
const resolved = children(() => local.children);
|
|
95
|
+
const defs = createMemo(() => {
|
|
96
|
+
const arr = resolved.toArray();
|
|
97
|
+
return {
|
|
98
|
+
filter: arr.find(isCrudSheetFilterDef) as CrudSheetFilterDef<TFilter> | undefined,
|
|
99
|
+
columns: arr.filter(isCrudSheetColumnDef) as unknown as CrudSheetColumnDef<TItem>[],
|
|
100
|
+
tools: arr.find(isCrudSheetToolsDef) as CrudSheetToolsDef<TItem> | undefined,
|
|
101
|
+
header: arr.find(isCrudSheetHeaderDef) as CrudSheetHeaderDef | undefined,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// -- State --
|
|
106
|
+
const [items, setItems] = createControllableStore<TItem[]>({
|
|
107
|
+
value: () => local.items ?? [],
|
|
108
|
+
onChange: () => local.onItemsChange,
|
|
109
|
+
});
|
|
110
|
+
let originalItems: TItem[] = [];
|
|
111
|
+
|
|
112
|
+
// eslint-disable-next-line solid/reactivity -- filterInitial은 초기값으로만 사용
|
|
113
|
+
const [filter, setFilter] = createStore<TFilter>((local.filterInitial ?? {}) as TFilter);
|
|
114
|
+
const [lastFilter, setLastFilter] = createSignal<TFilter>(objClone(filter));
|
|
115
|
+
|
|
116
|
+
const [page, setPage] = createSignal(1);
|
|
117
|
+
const [totalPageCount, setTotalPageCount] = createSignal(0);
|
|
118
|
+
const [sorts, setSorts] = createSignal<SortingDef[]>([]);
|
|
119
|
+
|
|
120
|
+
const [busyCount, setBusyCount] = createSignal(0);
|
|
121
|
+
const [ready, setReady] = createSignal(false);
|
|
122
|
+
|
|
123
|
+
const [selectedItems, setSelectedItems] = createSignal<TItem[]>([]);
|
|
124
|
+
|
|
125
|
+
let formRef: HTMLFormElement | undefined;
|
|
126
|
+
|
|
127
|
+
// -- Auto Refresh Effect --
|
|
128
|
+
createEffect(() => {
|
|
129
|
+
const currLastFilter = lastFilter();
|
|
130
|
+
const currSorts = sorts();
|
|
131
|
+
const currPage = page();
|
|
132
|
+
|
|
133
|
+
queueMicrotask(async () => {
|
|
134
|
+
setBusyCount((c) => c + 1);
|
|
135
|
+
await noti.try(async () => {
|
|
136
|
+
await refresh(currLastFilter, currSorts, currPage);
|
|
137
|
+
}, "조회 실패");
|
|
138
|
+
setBusyCount((c) => c - 1);
|
|
139
|
+
setReady(true);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
async function refresh(currLastFilter: TFilter, currSorts: SortingDef[], currPage: number) {
|
|
144
|
+
const usePagination = local.itemsPerPage != null;
|
|
145
|
+
const result: SearchResult<TItem> = await local.search(
|
|
146
|
+
currLastFilter,
|
|
147
|
+
usePagination ? currPage : 0,
|
|
148
|
+
currSorts,
|
|
149
|
+
);
|
|
150
|
+
setItems(reconcile(result.items));
|
|
151
|
+
originalItems = objClone(result.items);
|
|
152
|
+
setTotalPageCount(result.pageCount ?? 0);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* eslint-disable solid/reactivity -- 이벤트 핸들러에서만 호출, store 즉시 읽기 */
|
|
156
|
+
function getItemDiffs() {
|
|
157
|
+
return (items as unknown as TItem[]).oneWayDiffs(originalItems, ((item: TItem) => {
|
|
158
|
+
return local.getItemKey(item);
|
|
159
|
+
}) as (item: TItem) => keyof TItem);
|
|
160
|
+
}
|
|
161
|
+
/* eslint-enable solid/reactivity */
|
|
162
|
+
|
|
163
|
+
// -- Filter --
|
|
164
|
+
function handleFilterSubmit(e: Event) {
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
setPage(1);
|
|
167
|
+
setLastFilter(() => objClone(filter));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function handleRefresh() {
|
|
171
|
+
setLastFilter(() => ({ ...lastFilter() }));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// -- Inline Edit --
|
|
175
|
+
function handleAddRow() {
|
|
176
|
+
if (!local.inlineEdit) return;
|
|
177
|
+
setItems(
|
|
178
|
+
produce((draft) => {
|
|
179
|
+
draft.unshift(local.inlineEdit!.newItem());
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function handleToggleDelete(item: TItem, index: number) {
|
|
185
|
+
if (!(local.itemDeletable?.(item) ?? true)) return;
|
|
186
|
+
if (local.inlineEdit?.deleteProp == null) return;
|
|
187
|
+
const dp = local.inlineEdit.deleteProp;
|
|
188
|
+
|
|
189
|
+
if (local.getItemKey(item) == null) {
|
|
190
|
+
setItems(
|
|
191
|
+
produce((draft) => {
|
|
192
|
+
draft.splice(index, 1);
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
setItems(index as any, dp as any, !(item[dp] as boolean) as any);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function handleSave() {
|
|
202
|
+
if (busyCount() > 0) return;
|
|
203
|
+
if (!canEdit()) return;
|
|
204
|
+
if (!local.inlineEdit) return;
|
|
205
|
+
|
|
206
|
+
const diffs = getItemDiffs();
|
|
207
|
+
|
|
208
|
+
if (diffs.length === 0) {
|
|
209
|
+
noti.info("안내", "변경사항이 없습니다.");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const currLastFilter = lastFilter();
|
|
214
|
+
const currSorts = sorts();
|
|
215
|
+
const currPage = page();
|
|
216
|
+
|
|
217
|
+
setBusyCount((c) => c + 1);
|
|
218
|
+
// eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 refresh 호출
|
|
219
|
+
await noti.try(async () => {
|
|
220
|
+
await local.inlineEdit!.submit(diffs);
|
|
221
|
+
noti.success("저장 완료", "저장되었습니다.");
|
|
222
|
+
await refresh(currLastFilter, currSorts, currPage);
|
|
223
|
+
}, "저장 실패");
|
|
224
|
+
setBusyCount((c) => c - 1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function handleFormSubmit(e: Event) {
|
|
228
|
+
e.preventDefault();
|
|
229
|
+
await handleSave();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// -- Modal Edit --
|
|
233
|
+
async function handleEditItem(item?: TItem) {
|
|
234
|
+
if (!local.modalEdit) return;
|
|
235
|
+
const result = await local.modalEdit.editItem(item);
|
|
236
|
+
if (!result) return;
|
|
237
|
+
|
|
238
|
+
setBusyCount((c) => c + 1);
|
|
239
|
+
// eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 refresh 호출
|
|
240
|
+
await noti.try(async () => {
|
|
241
|
+
await refresh(lastFilter(), sorts(), page());
|
|
242
|
+
}, "조회 실패");
|
|
243
|
+
setBusyCount((c) => c - 1);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function handleDeleteItems() {
|
|
247
|
+
if (!local.modalEdit?.deleteItems) return;
|
|
248
|
+
const result = await local.modalEdit.deleteItems(selectedItems());
|
|
249
|
+
if (!result) return;
|
|
250
|
+
|
|
251
|
+
setBusyCount((c) => c + 1);
|
|
252
|
+
// eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 refresh 호출
|
|
253
|
+
await noti.try(async () => {
|
|
254
|
+
await refresh(lastFilter(), sorts(), page());
|
|
255
|
+
noti.success("삭제 완료", "삭제되었습니다.");
|
|
256
|
+
}, "삭제 실패");
|
|
257
|
+
setBusyCount((c) => c - 1);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// -- Excel --
|
|
261
|
+
async function handleExcelDownload() {
|
|
262
|
+
if (!local.excel) return;
|
|
263
|
+
|
|
264
|
+
setBusyCount((c) => c + 1);
|
|
265
|
+
// eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 호출
|
|
266
|
+
await noti.try(async () => {
|
|
267
|
+
const result = await local.search(lastFilter(), 0, sorts());
|
|
268
|
+
await local.excel!.download(result.items);
|
|
269
|
+
}, "엑셀 다운로드 실패");
|
|
270
|
+
setBusyCount((c) => c - 1);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function handleExcelUpload() {
|
|
274
|
+
if (!local.excel?.upload) return;
|
|
275
|
+
|
|
276
|
+
const input = document.createElement("input");
|
|
277
|
+
input.type = "file";
|
|
278
|
+
input.accept = ".xlsx";
|
|
279
|
+
input.onchange = async () => {
|
|
280
|
+
const file = input.files?.[0];
|
|
281
|
+
if (file == null) return;
|
|
282
|
+
|
|
283
|
+
setBusyCount((c) => c + 1);
|
|
284
|
+
// eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 호출
|
|
285
|
+
await noti.try(async () => {
|
|
286
|
+
await local.excel!.upload!(file);
|
|
287
|
+
noti.success("완료", "엑셀 업로드가 완료되었습니다.");
|
|
288
|
+
await refresh(lastFilter(), sorts(), page());
|
|
289
|
+
}, "엑셀 업로드 실패");
|
|
290
|
+
setBusyCount((c) => c - 1);
|
|
291
|
+
};
|
|
292
|
+
input.click();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// -- Select Mode --
|
|
296
|
+
function handleSelectConfirm() {
|
|
297
|
+
local.onSelect?.({
|
|
298
|
+
items: selectedItems(),
|
|
299
|
+
keys: selectedItems()
|
|
300
|
+
.map((item) => local.getItemKey(item))
|
|
301
|
+
.filter((k): k is string | number => k != null),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function handleSelectCancel() {
|
|
306
|
+
local.onSelect?.({ items: [], keys: [] });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// -- Keyboard Shortcuts --
|
|
310
|
+
createEventListener(document, "keydown", (e: KeyboardEvent) => {
|
|
311
|
+
if (e.ctrlKey && e.key === "s" && !isSelectMode()) {
|
|
312
|
+
e.preventDefault();
|
|
313
|
+
formRef?.requestSubmit();
|
|
314
|
+
}
|
|
315
|
+
if (e.ctrlKey && e.altKey && e.key === "l") {
|
|
316
|
+
e.preventDefault();
|
|
317
|
+
handleRefresh();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// -- Topbar Actions --
|
|
322
|
+
if (topbarCtx) {
|
|
323
|
+
createTopbarActions(() => (
|
|
324
|
+
<>
|
|
325
|
+
<Show when={canEdit() && local.inlineEdit}>
|
|
326
|
+
<Button
|
|
327
|
+
size="lg"
|
|
328
|
+
variant="ghost"
|
|
329
|
+
theme="primary"
|
|
330
|
+
onClick={() => formRef?.requestSubmit()}
|
|
331
|
+
>
|
|
332
|
+
<Icon icon={IconDeviceFloppy} class="mr-1" />
|
|
333
|
+
저장
|
|
334
|
+
</Button>
|
|
335
|
+
</Show>
|
|
336
|
+
<Button size="lg" variant="ghost" theme="info" onClick={handleRefresh}>
|
|
337
|
+
<Icon icon={IconRefresh} class="mr-1" />
|
|
338
|
+
새로고침
|
|
339
|
+
</Button>
|
|
340
|
+
</>
|
|
341
|
+
));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// -- Context for Tools --
|
|
345
|
+
const ctx: CrudSheetContext<TItem> = {
|
|
346
|
+
items: () => items as unknown as TItem[],
|
|
347
|
+
selectedItems,
|
|
348
|
+
page,
|
|
349
|
+
sorts,
|
|
350
|
+
busy: () => busyCount() > 0,
|
|
351
|
+
hasChanges: () => {
|
|
352
|
+
if (!local.inlineEdit) return false;
|
|
353
|
+
return getItemDiffs().length > 0;
|
|
354
|
+
},
|
|
355
|
+
save: handleSave,
|
|
356
|
+
refresh: async () => {
|
|
357
|
+
handleRefresh();
|
|
358
|
+
await Promise.resolve();
|
|
359
|
+
},
|
|
360
|
+
addItem: handleAddRow,
|
|
361
|
+
setPage,
|
|
362
|
+
setSorts,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// -- Render --
|
|
366
|
+
const deleteProp = () => local.inlineEdit?.deleteProp;
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<>
|
|
370
|
+
{/* Modal mode: Dialog.Action (refresh button in header) */}
|
|
371
|
+
<Show when={isModal}>
|
|
372
|
+
<Dialog.Action>
|
|
373
|
+
<button
|
|
374
|
+
class="flex items-center px-2 text-base-400 hover:text-base-600"
|
|
375
|
+
onClick={handleRefresh}
|
|
376
|
+
>
|
|
377
|
+
<Icon icon={IconRefresh} />
|
|
378
|
+
</button>
|
|
379
|
+
</Dialog.Action>
|
|
380
|
+
</Show>
|
|
381
|
+
|
|
382
|
+
<BusyContainer
|
|
383
|
+
ready={ready()}
|
|
384
|
+
busy={busyCount() > 0}
|
|
385
|
+
class={clsx("flex h-full flex-col", local.class)}
|
|
386
|
+
>
|
|
387
|
+
{/* Control mode: inline save/refresh bar */}
|
|
388
|
+
<Show when={!isModal && !topbarCtx && canEdit() && local.inlineEdit}>
|
|
389
|
+
<div class="flex gap-2 p-2 pb-0">
|
|
390
|
+
<Button
|
|
391
|
+
size="sm"
|
|
392
|
+
theme="primary"
|
|
393
|
+
variant="ghost"
|
|
394
|
+
onClick={() => formRef?.requestSubmit()}
|
|
395
|
+
>
|
|
396
|
+
<Icon icon={IconDeviceFloppy} class="mr-1" />
|
|
397
|
+
저장
|
|
398
|
+
</Button>
|
|
399
|
+
<Button size="sm" theme="info" variant="ghost" onClick={handleRefresh}>
|
|
400
|
+
<Icon icon={IconRefresh} class="mr-1" />
|
|
401
|
+
새로고침
|
|
402
|
+
</Button>
|
|
403
|
+
</div>
|
|
404
|
+
</Show>
|
|
405
|
+
|
|
406
|
+
{/* Header (optional) */}
|
|
407
|
+
<Show when={defs().header}>{(headerDef) => headerDef().children}</Show>
|
|
408
|
+
|
|
409
|
+
{/* Filter */}
|
|
410
|
+
<Show when={defs().filter}>
|
|
411
|
+
{(filterDef) => (
|
|
412
|
+
<form class="p-2" onSubmit={handleFilterSubmit}>
|
|
413
|
+
<FormGroup inline>
|
|
414
|
+
<FormGroup.Item>
|
|
415
|
+
<Button type="submit" theme="info" variant="solid">
|
|
416
|
+
<Icon icon={IconSearch} class="mr-1" />
|
|
417
|
+
조회
|
|
418
|
+
</Button>
|
|
419
|
+
</FormGroup.Item>
|
|
420
|
+
{filterDef().children(filter, setFilter)}
|
|
421
|
+
</FormGroup>
|
|
422
|
+
</form>
|
|
423
|
+
)}
|
|
424
|
+
</Show>
|
|
425
|
+
|
|
426
|
+
{/* Toolbar */}
|
|
427
|
+
<Show when={!isSelectMode()}>
|
|
428
|
+
<div class="flex gap-2 p-2 pb-0">
|
|
429
|
+
<Show when={!local.hideAutoTools}>
|
|
430
|
+
{/* Inline edit buttons */}
|
|
431
|
+
<Show when={canEdit() && local.inlineEdit}>
|
|
432
|
+
<Button size="sm" theme="primary" variant="ghost" onClick={handleAddRow}>
|
|
433
|
+
<Icon icon={IconPlus} class="mr-1" />행 추가
|
|
434
|
+
</Button>
|
|
435
|
+
</Show>
|
|
436
|
+
|
|
437
|
+
{/* Modal edit buttons */}
|
|
438
|
+
<Show when={canEdit() && local.modalEdit}>
|
|
439
|
+
<Button
|
|
440
|
+
size="sm"
|
|
441
|
+
theme="primary"
|
|
442
|
+
variant="ghost"
|
|
443
|
+
onClick={() => void handleEditItem()}
|
|
444
|
+
>
|
|
445
|
+
<Icon icon={IconPlus} class="mr-1" />
|
|
446
|
+
등록
|
|
447
|
+
</Button>
|
|
448
|
+
</Show>
|
|
449
|
+
<Show when={canEdit() && local.modalEdit?.deleteItems}>
|
|
450
|
+
<Button
|
|
451
|
+
size="sm"
|
|
452
|
+
theme="danger"
|
|
453
|
+
variant="ghost"
|
|
454
|
+
onClick={handleDeleteItems}
|
|
455
|
+
disabled={
|
|
456
|
+
selectedItems().length === 0 ||
|
|
457
|
+
!selectedItems().some((item) => local.itemDeletable?.(item) ?? true)
|
|
458
|
+
}
|
|
459
|
+
>
|
|
460
|
+
<Icon icon={IconTrash} class="mr-1" />
|
|
461
|
+
선택 삭제
|
|
462
|
+
</Button>
|
|
463
|
+
</Show>
|
|
464
|
+
|
|
465
|
+
{/* Excel buttons */}
|
|
466
|
+
<Show when={canEdit() && local.excel?.upload}>
|
|
467
|
+
<Button size="sm" theme="success" variant="ghost" onClick={handleExcelUpload}>
|
|
468
|
+
<Icon icon={IconUpload} class="mr-1" />
|
|
469
|
+
엑셀 업로드
|
|
470
|
+
</Button>
|
|
471
|
+
</Show>
|
|
472
|
+
<Show when={local.excel}>
|
|
473
|
+
<Button size="sm" theme="success" variant="ghost" onClick={handleExcelDownload}>
|
|
474
|
+
<Icon icon={IconFileExcel} class="mr-1" />
|
|
475
|
+
엑셀 다운로드
|
|
476
|
+
</Button>
|
|
477
|
+
</Show>
|
|
478
|
+
</Show>
|
|
479
|
+
|
|
480
|
+
{/* Custom tools */}
|
|
481
|
+
<Show when={defs().tools}>{(toolsDef) => toolsDef().children(ctx)}</Show>
|
|
482
|
+
</div>
|
|
483
|
+
</Show>
|
|
484
|
+
|
|
485
|
+
{/* DataSheet */}
|
|
486
|
+
<form ref={formRef} class="flex-1 overflow-hidden p-2 pt-1" onSubmit={handleFormSubmit}>
|
|
487
|
+
<DataSheet
|
|
488
|
+
class="h-full"
|
|
489
|
+
items={items}
|
|
490
|
+
persistKey={local.persistKey != null ? `${local.persistKey}-sheet` : undefined}
|
|
491
|
+
page={local.itemsPerPage != null ? page() : undefined}
|
|
492
|
+
onPageChange={setPage}
|
|
493
|
+
totalPageCount={totalPageCount()}
|
|
494
|
+
itemsPerPage={local.itemsPerPage}
|
|
495
|
+
sorts={sorts()}
|
|
496
|
+
onSortsChange={setSorts}
|
|
497
|
+
selectMode={
|
|
498
|
+
isSelectMode()
|
|
499
|
+
? local.selectMode === "multi"
|
|
500
|
+
? "multiple"
|
|
501
|
+
: "single"
|
|
502
|
+
: local.modalEdit?.deleteItems != null
|
|
503
|
+
? "multiple"
|
|
504
|
+
: undefined
|
|
505
|
+
}
|
|
506
|
+
selectedItems={selectedItems()}
|
|
507
|
+
onSelectedItemsChange={setSelectedItems}
|
|
508
|
+
autoSelect={isSelectMode() && local.selectMode === "single" ? "click" : undefined}
|
|
509
|
+
cellClass={(item, _colKey) => {
|
|
510
|
+
const dp = deleteProp();
|
|
511
|
+
if (dp != null && Boolean((item as Record<string, unknown>)[dp])) {
|
|
512
|
+
return clsx("line-through");
|
|
513
|
+
}
|
|
514
|
+
return undefined;
|
|
515
|
+
}}
|
|
516
|
+
>
|
|
517
|
+
{/* Auto delete column */}
|
|
518
|
+
<Show when={deleteProp() != null && canEdit() ? deleteProp() : undefined}>
|
|
519
|
+
{(dp) => (
|
|
520
|
+
<DataSheetColumn<TItem>
|
|
521
|
+
key="__delete"
|
|
522
|
+
header=""
|
|
523
|
+
fixed
|
|
524
|
+
sortable={false}
|
|
525
|
+
resizable={false}
|
|
526
|
+
>
|
|
527
|
+
{(dsCtx) => (
|
|
528
|
+
<div class="flex items-center justify-center px-1 py-0.5">
|
|
529
|
+
<Link
|
|
530
|
+
theme="danger"
|
|
531
|
+
disabled={!(local.itemDeletable?.(dsCtx.item) ?? true)}
|
|
532
|
+
onClick={() => handleToggleDelete(dsCtx.item, dsCtx.index)}
|
|
533
|
+
>
|
|
534
|
+
<Icon
|
|
535
|
+
icon={
|
|
536
|
+
Boolean((dsCtx.item as Record<string, unknown>)[dp()])
|
|
537
|
+
? IconTrashOff
|
|
538
|
+
: IconTrash
|
|
539
|
+
}
|
|
540
|
+
/>
|
|
541
|
+
</Link>
|
|
542
|
+
</div>
|
|
543
|
+
)}
|
|
544
|
+
</DataSheetColumn>
|
|
545
|
+
)}
|
|
546
|
+
</Show>
|
|
547
|
+
|
|
548
|
+
{/* User-defined columns -- map CrudSheetColumn to DataSheetColumn */}
|
|
549
|
+
<For each={defs().columns}>
|
|
550
|
+
{(col) => (
|
|
551
|
+
<DataSheetColumn<TItem>
|
|
552
|
+
key={col.key}
|
|
553
|
+
header={col.header}
|
|
554
|
+
headerContent={col.headerContent}
|
|
555
|
+
headerStyle={col.headerStyle}
|
|
556
|
+
summary={col.summary}
|
|
557
|
+
tooltip={col.tooltip}
|
|
558
|
+
fixed={col.fixed}
|
|
559
|
+
hidden={col.hidden}
|
|
560
|
+
collapse={col.collapse}
|
|
561
|
+
width={col.width}
|
|
562
|
+
class={col.class}
|
|
563
|
+
sortable={col.sortable}
|
|
564
|
+
resizable={col.resizable}
|
|
565
|
+
>
|
|
566
|
+
{(dsCtx) => {
|
|
567
|
+
const crudCtx = {
|
|
568
|
+
...dsCtx,
|
|
569
|
+
setItem: <TKey extends keyof TItem>(key: TKey, value: TItem[TKey]) => {
|
|
570
|
+
setItems(dsCtx.index as any, key as any, value as any);
|
|
571
|
+
},
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
// modalEdit editable column -- wrap with edit link
|
|
575
|
+
if (
|
|
576
|
+
local.modalEdit &&
|
|
577
|
+
col.editable &&
|
|
578
|
+
canEdit() &&
|
|
579
|
+
(local.itemEditable?.(dsCtx.item) ?? true)
|
|
580
|
+
) {
|
|
581
|
+
return (
|
|
582
|
+
<Link
|
|
583
|
+
class="flex w-full"
|
|
584
|
+
onClick={(e) => {
|
|
585
|
+
e.preventDefault();
|
|
586
|
+
e.stopPropagation();
|
|
587
|
+
void handleEditItem(dsCtx.item);
|
|
588
|
+
}}
|
|
589
|
+
>
|
|
590
|
+
{col.cell(crudCtx)}
|
|
591
|
+
</Link>
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return col.cell(crudCtx);
|
|
596
|
+
}}
|
|
597
|
+
</DataSheetColumn>
|
|
598
|
+
)}
|
|
599
|
+
</For>
|
|
600
|
+
</DataSheet>
|
|
601
|
+
</form>
|
|
602
|
+
|
|
603
|
+
{/* Select mode bottom bar */}
|
|
604
|
+
<Show when={isModal && isSelectMode()}>
|
|
605
|
+
<div class="flex gap-2 border-t p-2">
|
|
606
|
+
<div class="flex-1" />
|
|
607
|
+
<Show when={selectedItems().length > 0}>
|
|
608
|
+
<Button size="sm" theme="danger" onClick={handleSelectCancel}>
|
|
609
|
+
{local.selectMode === "multi" ? "모두" : "선택"} 해제
|
|
610
|
+
</Button>
|
|
611
|
+
</Show>
|
|
612
|
+
<Show when={local.selectMode === "multi"}>
|
|
613
|
+
<Button size="sm" theme="primary" onClick={handleSelectConfirm}>
|
|
614
|
+
확인({selectedItems().length})
|
|
615
|
+
</Button>
|
|
616
|
+
</Show>
|
|
617
|
+
</div>
|
|
618
|
+
</Show>
|
|
619
|
+
</BusyContainer>
|
|
620
|
+
</>
|
|
621
|
+
);
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
export const CrudSheet = CrudSheetBase as unknown as CrudSheetComponent;
|
|
625
|
+
CrudSheet.Column = CrudSheetColumn;
|
|
626
|
+
CrudSheet.Filter = CrudSheetFilter;
|
|
627
|
+
CrudSheet.Tools = CrudSheetTools;
|
|
628
|
+
CrudSheet.Header = CrudSheetHeader;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { JSX } from "solid-js";
|
|
2
|
+
import type { CrudSheetColumnDef, CrudSheetColumnProps } from "./types";
|
|
3
|
+
import { normalizeHeader } from "../sheet/sheetUtils";
|
|
4
|
+
|
|
5
|
+
export function isCrudSheetColumnDef(value: unknown): value is CrudSheetColumnDef<unknown> {
|
|
6
|
+
return (
|
|
7
|
+
value != null &&
|
|
8
|
+
typeof value === "object" &&
|
|
9
|
+
(value as Record<string, unknown>)["__type"] === "crud-sheet-column"
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* eslint-disable solid/reactivity -- plain object 반환 패턴으로 reactive context 불필요 */
|
|
14
|
+
export function CrudSheetColumn<TItem>(props: CrudSheetColumnProps<TItem>): JSX.Element {
|
|
15
|
+
return {
|
|
16
|
+
__type: "crud-sheet-column",
|
|
17
|
+
key: props.key,
|
|
18
|
+
header: normalizeHeader(props.header),
|
|
19
|
+
headerContent: props.headerContent,
|
|
20
|
+
headerStyle: props.headerStyle,
|
|
21
|
+
summary: props.summary,
|
|
22
|
+
tooltip: props.tooltip,
|
|
23
|
+
cell: props.children,
|
|
24
|
+
class: props.class,
|
|
25
|
+
fixed: props.fixed ?? false,
|
|
26
|
+
hidden: props.hidden ?? false,
|
|
27
|
+
collapse: props.collapse ?? false,
|
|
28
|
+
width: props.width,
|
|
29
|
+
sortable: props.sortable ?? true,
|
|
30
|
+
resizable: props.resizable ?? true,
|
|
31
|
+
editable: props.editable ?? false,
|
|
32
|
+
} as unknown as JSX.Element;
|
|
33
|
+
}
|
|
34
|
+
/* eslint-enable solid/reactivity */
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { JSX } from "solid-js";
|
|
2
|
+
import type { CrudSheetFilterDef } from "./types";
|
|
3
|
+
|
|
4
|
+
export function isCrudSheetFilterDef(value: unknown): value is CrudSheetFilterDef<any> {
|
|
5
|
+
return (
|
|
6
|
+
value != null &&
|
|
7
|
+
typeof value === "object" &&
|
|
8
|
+
(value as Record<string, unknown>)["__type"] === "crud-sheet-filter"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* eslint-disable solid/reactivity -- plain object 반환 패턴으로 reactive context 불필요 */
|
|
13
|
+
export function CrudSheetFilter<TFilter>(props: {
|
|
14
|
+
children: (filter: TFilter, setFilter: any) => JSX.Element;
|
|
15
|
+
}): JSX.Element {
|
|
16
|
+
return {
|
|
17
|
+
__type: "crud-sheet-filter",
|
|
18
|
+
children: props.children,
|
|
19
|
+
} as unknown as JSX.Element;
|
|
20
|
+
}
|
|
21
|
+
/* eslint-enable solid/reactivity */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { JSX } from "solid-js";
|
|
2
|
+
import type { CrudSheetHeaderDef } from "./types";
|
|
3
|
+
|
|
4
|
+
export function isCrudSheetHeaderDef(value: unknown): value is CrudSheetHeaderDef {
|
|
5
|
+
return (
|
|
6
|
+
value != null &&
|
|
7
|
+
typeof value === "object" &&
|
|
8
|
+
(value as Record<string, unknown>)["__type"] === "crud-sheet-header"
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* eslint-disable solid/reactivity -- plain object 반환 패턴으로 reactive context 불필요 */
|
|
13
|
+
export function CrudSheetHeader(props: { children: JSX.Element }): JSX.Element {
|
|
14
|
+
return {
|
|
15
|
+
__type: "crud-sheet-header",
|
|
16
|
+
children: props.children,
|
|
17
|
+
} as unknown as JSX.Element;
|
|
18
|
+
}
|
|
19
|
+
/* eslint-enable solid/reactivity */
|