@wipco/sdui 1.4.2 → 1.6.0
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/{DatePicker-C_TT_SOw.d.ts → DatePicker-DX9juElE.d.ts} +10 -1
- package/dist/{chunk-GZBY7I4Y.js → chunk-2KQL6DYG.js} +3 -3
- package/dist/{chunk-RYBBPFI7.js → chunk-2QXA52DG.js} +4 -4
- package/dist/{chunk-DMLDKW7K.js → chunk-37BNG4DT.js} +1 -1
- package/dist/{chunk-5WW4MBRO.js → chunk-3JTBOABA.js} +6 -2
- package/dist/{chunk-UV6HHFEW.js → chunk-4OABUPIU.js} +129 -68
- package/dist/{chunk-ZORQPB6B.js → chunk-53KS4E5N.js} +318 -359
- package/dist/{chunk-I5SD7XRA.js → chunk-6TI4WF5Y.js} +3 -3
- package/dist/{chunk-44TDRCKR.js → chunk-6XI2VF2O.js} +5 -5
- package/dist/{chunk-HBIIEYKW.js → chunk-7C54CKFZ.js} +4 -4
- package/dist/{chunk-IZ3JA2BR.js → chunk-7JP52QQO.js} +3 -3
- package/dist/{chunk-I45EUZC6.js → chunk-AY4GM6KQ.js} +4 -2
- package/dist/{chunk-SAPBDJJQ.js → chunk-AZSLSAJB.js} +2 -2
- package/dist/chunk-BITS7UJY.js +333 -0
- package/dist/{chunk-RNOUXUBO.js → chunk-BLFJMMBW.js} +306 -57
- package/dist/{chunk-YOZN35HD.js → chunk-CEVZAFPO.js} +3 -3
- package/dist/chunk-CI5OZ2QT.js +106 -0
- package/dist/{chunk-FNMS7OB7.js → chunk-CSE3PZOZ.js} +5 -5
- package/dist/chunk-CSZCWBOE.js +256 -0
- package/dist/{chunk-JIV4YHCZ.js → chunk-EJIWOD7W.js} +1 -1
- package/dist/{chunk-JZXU22PE.js → chunk-FEKKOBPX.js} +208 -117
- package/dist/chunk-GSM664TX.js +38 -0
- package/dist/{chunk-JYAR7D5U.js → chunk-HRHKQOOF.js} +2 -2
- package/dist/{chunk-CXW6KT6X.js → chunk-HXLQSO66.js} +3 -3
- package/dist/{chunk-O4IISNE7.js → chunk-IB2ZPUTF.js} +51 -12
- package/dist/{chunk-MPT7WJG2.js → chunk-IQXLNICY.js} +5 -5
- package/dist/{chunk-OQ52XMGG.js → chunk-K6RE23XX.js} +4 -4
- package/dist/{chunk-VY7MVBSE.js → chunk-KZ6U6FKB.js} +2 -2
- package/dist/{chunk-WPSK2CB5.js → chunk-LIZTK3HO.js} +16 -5
- package/dist/{chunk-Q4SWUINA.js → chunk-LKQJJSVJ.js} +1 -1
- package/dist/{chunk-E6ISNQK3.js → chunk-M6H3SHIS.js} +31 -8
- package/dist/chunk-MN4RZ2WP.js +380 -0
- package/dist/{chunk-GTGS2QRL.js → chunk-NA232CBX.js} +3 -3
- package/dist/{chunk-ZFNFPXPR.js → chunk-NAIWQQPW.js} +2 -2
- package/dist/{chunk-YVTV4ZV6.js → chunk-NWYYNE3Q.js} +1 -1
- package/dist/{chunk-DKVSM5HX.js → chunk-OUEJWC2Z.js} +6 -6
- package/dist/{chunk-DOL57OOP.js → chunk-OYDJHQNZ.js} +3 -3
- package/dist/{chunk-YWUDBLO6.js → chunk-QVS6K4FP.js} +2 -2
- package/dist/{chunk-N5RKM6IC.js → chunk-RU5U2YMT.js} +4 -4
- package/dist/{chunk-A6LAIGUS.js → chunk-V7BWMOHO.js} +7 -4
- package/dist/{chunk-2HLXCKK7.js → chunk-VPOOIEKT.js} +2 -2
- package/dist/{chunk-2YP5N4VV.js → chunk-VZOQRQAR.js} +1 -1
- package/dist/{chunk-GNA6SX2H.js → chunk-W4VA76QO.js} +2 -2
- package/dist/{chunk-BEM2YS7U.js → chunk-WEG3V7ZB.js} +3 -3
- package/dist/{chunk-XGFDY43I.js → chunk-WTIYILY3.js} +88 -18
- package/dist/{chunk-RWSH2MBU.js → chunk-X2NIRVLN.js} +73 -19
- package/dist/{chunk-AXGF4M57.js → chunk-XAXROQKB.js} +1 -1
- package/dist/{chunk-NIJUVTEW.js → chunk-XG22XKKD.js} +3 -3
- package/dist/{chunk-A5XQOEVH.js → chunk-YN7X7BEO.js} +4 -4
- package/dist/{chunk-KO6OEY3G.js → chunk-YNEEJCSZ.js} +3 -13
- package/dist/{chunk-7P7N43JV.js → chunk-YZTTFEL4.js} +2 -2
- package/dist/index.d.ts +494 -89
- package/dist/index.js +1098 -124
- package/dist/{layout-C3nRXMwb.d.ts → layout-DmRikwZb.d.ts} +5 -4
- package/dist/meta.d.ts +1 -1
- package/dist/meta.js +286 -22
- package/dist/rich/ExcelUploadCard.d.ts +1 -1
- package/dist/rich/ExcelUploadCard.js +5 -4
- package/dist/rich.d.ts +21 -3
- package/dist/rich.js +277 -35
- package/dist/styles.css +102 -4
- package/dist/{types-COjGKgmd.d.ts → types-CYfIthux.d.ts} +57 -1
- package/dist/widgets/ApprovalLineEditor.d.ts +1 -1
- package/dist/widgets/ApprovalLineEditor.js +9 -8
- package/dist/widgets/ApprovalOverridesEditor.d.ts +1 -1
- package/dist/widgets/ApprovalOverridesEditor.js +11 -10
- package/dist/widgets/ApprovalStepper.d.ts +29 -4
- package/dist/widgets/ApprovalStepper.js +3 -3
- package/dist/widgets/AutocompleteField.d.ts +1 -1
- package/dist/widgets/AutocompleteField.js +9 -8
- package/dist/widgets/ColumnBoard.d.ts +1 -1
- package/dist/widgets/ColumnBoard.js +3 -3
- package/dist/widgets/DiffView.d.ts +1 -1
- package/dist/widgets/DiffView.js +3 -3
- package/dist/widgets/Disclosure.d.ts +1 -1
- package/dist/widgets/Disclosure.js +3 -3
- package/dist/widgets/EventCalendar.d.ts +5 -1
- package/dist/widgets/EventCalendar.js +5 -3
- package/dist/widgets/FileDropzone.d.ts +1 -1
- package/dist/widgets/FileDropzone.js +9 -7
- package/dist/widgets/ImageLightbox.d.ts +1 -1
- package/dist/widgets/ImageLightbox.js +7 -5
- package/dist/widgets/InlineEdit.d.ts +1 -1
- package/dist/widgets/InlineEdit.js +3 -3
- package/dist/widgets/JsonField.d.ts +1 -1
- package/dist/widgets/JsonField.js +3 -3
- package/dist/widgets/LiveClock.d.ts +1 -1
- package/dist/widgets/LiveClock.js +2 -2
- package/dist/widgets/OptionSchemaEditor.d.ts +1 -1
- package/dist/widgets/OptionSchemaEditor.js +4 -4
- package/dist/widgets/OptionValueFields.d.ts +1 -1
- package/dist/widgets/OptionValueFields.js +4 -4
- package/dist/widgets/OrgChart.d.ts +1 -1
- package/dist/widgets/OrgChart.js +4 -4
- package/dist/widgets/OrgChartBoard.d.ts +15 -33
- package/dist/widgets/OrgChartBoard.js +10 -8
- package/dist/widgets/PeriodFilter.d.ts +1 -1
- package/dist/widgets/PeriodFilter.js +6 -5
- package/dist/widgets/PermissionMatrixEditor.d.ts +34 -2
- package/dist/widgets/PermissionMatrixEditor.js +6 -4
- package/dist/widgets/PositionSelect.d.ts +1 -1
- package/dist/widgets/PositionSelect.js +5 -4
- package/dist/widgets/PrintSheet.d.ts +15 -2
- package/dist/widgets/PrintSheet.js +3 -3
- package/dist/widgets/ProfileEditDiffCard.d.ts +1 -1
- package/dist/widgets/ProfileEditDiffCard.js +5 -4
- package/dist/widgets/QRDisplay.d.ts +1 -1
- package/dist/widgets/QRDisplay.js +5 -4
- package/dist/widgets/RankBoard.d.ts +1 -1
- package/dist/widgets/RankBoard.js +8 -6
- package/dist/widgets/RecordSheet.d.ts +1 -1
- package/dist/widgets/RecordSheet.js +2 -2
- package/dist/widgets/RoleMemberPicker.d.ts +1 -1
- package/dist/widgets/RoleMemberPicker.js +6 -5
- package/dist/widgets/SearchableMultiPicker.d.ts +1 -1
- package/dist/widgets/SearchableMultiPicker.js +7 -6
- package/dist/widgets/SecureScreen.d.ts +1 -1
- package/dist/widgets/SecureScreen.js +3 -3
- package/dist/widgets/StackedMeter.d.ts +1 -1
- package/dist/widgets/StackedMeter.js +2 -2
- package/dist/widgets/StagePeriodEditor.d.ts +1 -1
- package/dist/widgets/StagePeriodEditor.js +6 -5
- package/dist/widgets/Strip.d.ts +1 -1
- package/dist/widgets/Strip.js +2 -2
- package/dist/widgets/SummaryTable.d.ts +1 -1
- package/dist/widgets/SummaryTable.js +2 -2
- package/dist/widgets/TagInput.d.ts +1 -1
- package/dist/widgets/TagInput.js +3 -3
- package/dist/widgets/TagMultiSelect.d.ts +1 -1
- package/dist/widgets/TagMultiSelect.js +5 -4
- package/dist/widgets/Timeline.d.ts +1 -1
- package/dist/widgets/Timeline.js +4 -4
- package/dist/widgets/TreePicker.d.ts +1 -1
- package/dist/widgets/TreePicker.js +8 -7
- package/dist/widgets/TreeView.d.ts +1 -1
- package/dist/widgets/TreeView.js +7 -6
- package/dist/widgets/index.d.ts +3 -3
- package/dist/widgets/index.js +84 -83
- package/package.json +1 -1
- package/dist/chunk-LFEJLOFN.js +0 -352
- package/dist/chunk-MMIGOPO5.js +0 -111
- package/dist/chunk-TFGNKFL2.js +0 -39
- package/dist/chunk-XQPTSEJC.js +0 -135
package/dist/index.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
export { loadOptionalModule } from './chunk-
|
|
2
|
-
import { useSduiDispatch, SduiDialog, useSduiCan, SduiDispatchContext, SduiPermissionProvider } from './chunk-
|
|
3
|
-
export { SduiDialog, SduiPermissionProvider, useSduiCan, useSduiDispatch } from './chunk-
|
|
4
|
-
import { Text, Spacer, Show, Section, Row, PageHead, KeyValueList, Grid, Divider, Col, Actions } from './chunk-
|
|
5
|
-
export { Actions, Col, Divider, Grid, KeyValueList, PageHead, Row, Section, Show, Spacer, Text } from './chunk-
|
|
6
|
-
import { ImageViewer } from './chunk-
|
|
7
|
-
export { ImageViewer } from './chunk-
|
|
8
|
-
import { normalizeSearchFields, searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-
|
|
9
|
-
export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-
|
|
10
|
-
import { parseYmd, YMD_RE, TimePicker, DateRangePicker, DatePicker } from './chunk-
|
|
11
|
-
export { DatePicker, DateRangePicker, TimePicker } from './chunk-
|
|
1
|
+
export { loadOptionalModule } from './chunk-NWYYNE3Q.js';
|
|
2
|
+
import { useSduiDispatch, SduiDialog, useSduiCan, useSduiRuntime, SduiDirtyRegistryContext, SduiLeaveRegistryContext, SduiFormScopeContext, SduiDispatchContext, SduiRuntimeContext, SduiPermissionProvider } from './chunk-CI5OZ2QT.js';
|
|
3
|
+
export { SduiDialog, SduiPermissionProvider, useSduiCan, useSduiDispatch, useSduiRuntime } from './chunk-CI5OZ2QT.js';
|
|
4
|
+
import { Text, Spacer, Show, Section, Row, PageHead, KeyValueList, Grid, Divider, Col, Actions } from './chunk-LIZTK3HO.js';
|
|
5
|
+
export { Actions, Col, Divider, Grid, KeyValueList, PageHead, Row, Section, Show, Spacer, Text } from './chunk-LIZTK3HO.js';
|
|
6
|
+
import { ImageViewer } from './chunk-AZSLSAJB.js';
|
|
7
|
+
export { ImageViewer } from './chunk-AZSLSAJB.js';
|
|
8
|
+
import { normalizeSearchFields, searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-4OABUPIU.js';
|
|
9
|
+
export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-4OABUPIU.js';
|
|
10
|
+
import { parseYmd, YMD_RE, TimePicker, DateRangePicker, DatePicker } from './chunk-X2NIRVLN.js';
|
|
11
|
+
export { DatePicker, DateRangePicker, TimePicker } from './chunk-X2NIRVLN.js';
|
|
12
|
+
import { OverlayContainerContext, OverlayLayer, useAnyModalOpen } from './chunk-BITS7UJY.js';
|
|
13
|
+
export { OverlayContainerContext, OverlayLayer, getOverlayRoot, useAnyModalOpen, useOverlayLayer } from './chunk-BITS7UJY.js';
|
|
12
14
|
import { HoistedStyle } from './chunk-RIZRM7OO.js';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
import { useSduiAdapter, hostFetchOf, isSafeNavigationTarget, useSduiSandbox, resolveAdapter, assetResolverOf, SduiAdapterContext } from './chunk-YNEEJCSZ.js';
|
|
16
|
+
export { defaultAdapter, useSduiAssetResolver, useSduiSandbox } from './chunk-YNEEJCSZ.js';
|
|
17
|
+
import { DEV, useIsMobile } from './chunk-GSM664TX.js';
|
|
18
|
+
export { MOBILE_MEDIA_QUERY, useIsMobile } from './chunk-GSM664TX.js';
|
|
19
|
+
import { EmptyState, Button, Checkbox, Select, Tabs, SegmentedControl, Spinner, Pagination, fieldOf, StatCard, isZonedIsoDateTime, formatDateValue, Icon, Card, Badge, ProgressBar, Tag, Toast, Textarea, Table, Switch, Input, IconButton, Field, Alert, warnOnce, isPlainObject, CALC_OPS, evaluateCalc, Dialog, ICON_NODES, toNumber } from './chunk-BLFJMMBW.js';
|
|
20
|
+
export { Alert, Avatar, Badge, Breadcrumb, Button, CALC_OPS, Card, Checkbox, Dialog, EmptyState, Field, Icon, IconButton, Input, Pagination, ProgressBar, Radio, SegmentedControl, Select, SidebarNav, Spinner, StatCard, Switch, Table, Tabs, Tag, Textarea, Toast, Tooltip, formatDateValue, formatDisplayValue, formatNumberValue, isZonedIsoDateTime, parseNumeric } from './chunk-BLFJMMBW.js';
|
|
21
|
+
import { boxed, LAYOUT_CSS, childLayoutFrom, CHILD_LAYOUT_PROPS } from './chunk-3JTBOABA.js';
|
|
22
|
+
export { CHILD_LAYOUT_PROPS, LAYOUT_CSS, boxed, childLayoutFrom, columnsClass, containerLayoutStyle, joinClass, mergeStyle, typographyStyle } from './chunk-3JTBOABA.js';
|
|
23
|
+
import React9, { createContext, useState, useMemo, useEffect, useRef, useContext, useCallback, useSyncExternalStore } from 'react';
|
|
21
24
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
22
25
|
|
|
23
26
|
// src/asset-url.ts
|
|
@@ -114,6 +117,119 @@ function resolveAssetsIn(propName, value, resolve, component) {
|
|
|
114
117
|
return out ?? record;
|
|
115
118
|
}
|
|
116
119
|
}
|
|
120
|
+
|
|
121
|
+
// src/josa.ts
|
|
122
|
+
var HANGUL_FIRST = 44032;
|
|
123
|
+
var HANGUL_LAST = 55203;
|
|
124
|
+
var JONG_RIEUL = 8;
|
|
125
|
+
var DIGIT_JONG = {
|
|
126
|
+
"0": 21,
|
|
127
|
+
"1": JONG_RIEUL,
|
|
128
|
+
"2": 0,
|
|
129
|
+
"3": 16,
|
|
130
|
+
"4": 0,
|
|
131
|
+
"5": 0,
|
|
132
|
+
"6": 1,
|
|
133
|
+
"7": JONG_RIEUL,
|
|
134
|
+
"8": JONG_RIEUL,
|
|
135
|
+
"9": 0
|
|
136
|
+
};
|
|
137
|
+
function finalConsonant(word) {
|
|
138
|
+
let text = word.trim();
|
|
139
|
+
for (; ; ) {
|
|
140
|
+
const next = text.replace(/\s*[([([][^()[\]()[]]*[)\])]]$/u, "").replace(/["'”’」』]+$/u, "").trim();
|
|
141
|
+
if (next === text) break;
|
|
142
|
+
text = next;
|
|
143
|
+
}
|
|
144
|
+
const last = text.slice(-1);
|
|
145
|
+
if (!last) return null;
|
|
146
|
+
const code = last.charCodeAt(0);
|
|
147
|
+
if (code >= HANGUL_FIRST && code <= HANGUL_LAST) return (code - HANGUL_FIRST) % 28;
|
|
148
|
+
if (last in DIGIT_JONG) return DIGIT_JONG[last];
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function josaOf(word, pair) {
|
|
152
|
+
const [withFinal, withoutFinal] = pair.split("/");
|
|
153
|
+
const jong = finalConsonant(word);
|
|
154
|
+
if (jong === null) return `${withFinal}(${withoutFinal})`;
|
|
155
|
+
if (pair === "\uC73C\uB85C/\uB85C") return jong === 0 || jong === JONG_RIEUL ? withoutFinal : withFinal;
|
|
156
|
+
return jong === 0 ? withoutFinal : withFinal;
|
|
157
|
+
}
|
|
158
|
+
function josa(word, pair) {
|
|
159
|
+
return `${word}${josaOf(word, pair)}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/toast-store.ts
|
|
163
|
+
var TOAST_LIFETIME_MS = 4e3;
|
|
164
|
+
var TOAST_UNSHOWN_TTL_MS = 1e4;
|
|
165
|
+
var entries = [];
|
|
166
|
+
var hosts = [];
|
|
167
|
+
var nextToastId = 0;
|
|
168
|
+
var nextHostId = 0;
|
|
169
|
+
var version = { entries, hosts };
|
|
170
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
171
|
+
function later(fn, ms) {
|
|
172
|
+
const timer = setTimeout(fn, ms);
|
|
173
|
+
timer.unref?.();
|
|
174
|
+
}
|
|
175
|
+
function emit() {
|
|
176
|
+
version = { entries, hosts };
|
|
177
|
+
listeners.forEach((fn) => fn());
|
|
178
|
+
}
|
|
179
|
+
function subscribeToasts(fn) {
|
|
180
|
+
listeners.add(fn);
|
|
181
|
+
return () => {
|
|
182
|
+
listeners.delete(fn);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function toastSnapshot() {
|
|
186
|
+
return version;
|
|
187
|
+
}
|
|
188
|
+
var EMPTY_SNAPSHOT = { entries: [], hosts: [] };
|
|
189
|
+
function toastServerSnapshot() {
|
|
190
|
+
return EMPTY_SNAPSHOT;
|
|
191
|
+
}
|
|
192
|
+
function allocateToastHostId() {
|
|
193
|
+
nextHostId += 1;
|
|
194
|
+
return nextHostId;
|
|
195
|
+
}
|
|
196
|
+
function registerToastHost(id, channel) {
|
|
197
|
+
hosts = [...hosts.filter((h) => h.id !== id), { id, channel }];
|
|
198
|
+
emit();
|
|
199
|
+
return () => {
|
|
200
|
+
hosts = hosts.filter((h) => h.id !== id);
|
|
201
|
+
emit();
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function pushToastEntry(owner, channel, toast) {
|
|
205
|
+
nextToastId += 1;
|
|
206
|
+
const id = nextToastId;
|
|
207
|
+
entries = [...entries, { ...toast, id, owner, channel, shown: false }];
|
|
208
|
+
emit();
|
|
209
|
+
later(() => {
|
|
210
|
+
const entry = entries.find((e) => e.id === id);
|
|
211
|
+
if (entry && !entry.shown) closeToastEntry(id);
|
|
212
|
+
}, TOAST_UNSHOWN_TTL_MS);
|
|
213
|
+
return id;
|
|
214
|
+
}
|
|
215
|
+
function closeToastEntry(id) {
|
|
216
|
+
if (!entries.some((e) => e.id === id)) return;
|
|
217
|
+
entries = entries.filter((e) => e.id !== id);
|
|
218
|
+
emit();
|
|
219
|
+
}
|
|
220
|
+
function markToastsShown(ids) {
|
|
221
|
+
const fresh = ids.filter((id) => entries.some((e) => e.id === id && !e.shown));
|
|
222
|
+
if (fresh.length === 0) return;
|
|
223
|
+
entries = entries.map((e) => fresh.includes(e.id) ? { ...e, shown: true } : e);
|
|
224
|
+
for (const id of fresh) later(() => closeToastEntry(id), TOAST_LIFETIME_MS);
|
|
225
|
+
}
|
|
226
|
+
function toastHostOf(entry, liveHosts) {
|
|
227
|
+
if (liveHosts.some((h) => h.id === entry.owner)) return entry.owner;
|
|
228
|
+
for (let i = liveHosts.length - 1; i >= 0; i -= 1) {
|
|
229
|
+
if (liveHosts[i].channel === entry.channel) return liveHosts[i].id;
|
|
230
|
+
}
|
|
231
|
+
return void 0;
|
|
232
|
+
}
|
|
117
233
|
function SpecErrorCard({ children }) {
|
|
118
234
|
return /* @__PURE__ */ jsx(
|
|
119
235
|
"div",
|
|
@@ -361,13 +477,85 @@ function cellRenderer(cell, key, dispatch) {
|
|
|
361
477
|
return void 0;
|
|
362
478
|
}
|
|
363
479
|
}
|
|
480
|
+
|
|
481
|
+
// src/search/sort.ts
|
|
482
|
+
var SORT_PARAM = "sort";
|
|
483
|
+
var ORDER_PARAM = "order";
|
|
484
|
+
function sortFromParams(params) {
|
|
485
|
+
const key = params.get(SORT_PARAM);
|
|
486
|
+
if (!key) return null;
|
|
487
|
+
return { key, dir: params.get(ORDER_PARAM) === "desc" ? "desc" : "asc" };
|
|
488
|
+
}
|
|
489
|
+
function nextSort(current, key) {
|
|
490
|
+
if (!current || current.key !== key) return { key, dir: "asc" };
|
|
491
|
+
return current.dir === "asc" ? { key, dir: "desc" } : null;
|
|
492
|
+
}
|
|
493
|
+
function sortEchoOf(body) {
|
|
494
|
+
if (!body || typeof body !== "object") return null;
|
|
495
|
+
const sort = body.sort;
|
|
496
|
+
if (!sort || typeof sort !== "object") return null;
|
|
497
|
+
const { field, order } = sort;
|
|
498
|
+
if (typeof field !== "string" || !field) return null;
|
|
499
|
+
return { field, order: order === "desc" ? "desc" : "asc" };
|
|
500
|
+
}
|
|
501
|
+
var NONE = Object.freeze([]);
|
|
502
|
+
function endpointPath(endpoint) {
|
|
503
|
+
const cut = endpoint.indexOf("?");
|
|
504
|
+
return cut >= 0 ? endpoint.slice(0, cut) : endpoint;
|
|
505
|
+
}
|
|
506
|
+
function createServerSortMemory() {
|
|
507
|
+
const entries2 = /* @__PURE__ */ new Map();
|
|
508
|
+
const entry = (endpoint) => {
|
|
509
|
+
const path = endpointPath(endpoint);
|
|
510
|
+
let found = entries2.get(path);
|
|
511
|
+
if (!found) {
|
|
512
|
+
found = { supported: /* @__PURE__ */ new Set(), unsupported: [], all: false };
|
|
513
|
+
entries2.set(path, found);
|
|
514
|
+
}
|
|
515
|
+
return found;
|
|
516
|
+
};
|
|
517
|
+
const unsortable = (endpoint) => {
|
|
518
|
+
const e = entries2.get(endpointPath(endpoint));
|
|
519
|
+
if (!e) return NONE;
|
|
520
|
+
return e.all ? "all" : e.unsupported.length > 0 ? e.unsupported : NONE;
|
|
521
|
+
};
|
|
522
|
+
return {
|
|
523
|
+
record(endpoint, requested, echo) {
|
|
524
|
+
const e = entry(endpoint);
|
|
525
|
+
if (echo) {
|
|
526
|
+
e.supported.add(echo.field);
|
|
527
|
+
e.all = false;
|
|
528
|
+
if (echo.field === requested.key) return true;
|
|
529
|
+
}
|
|
530
|
+
if (e.supported.size === 0) e.all = true;
|
|
531
|
+
else if (!e.unsupported.includes(requested.key)) e.unsupported = [...e.unsupported, requested.key];
|
|
532
|
+
return false;
|
|
533
|
+
},
|
|
534
|
+
unsortable,
|
|
535
|
+
allows(endpoint, key) {
|
|
536
|
+
const verdict = unsortable(endpoint);
|
|
537
|
+
return verdict !== "all" && !verdict.includes(key);
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
var serverSortMemory = createServerSortMemory();
|
|
542
|
+
function withoutSort(params) {
|
|
543
|
+
const next = new URLSearchParams(params);
|
|
544
|
+
next.delete(SORT_PARAM);
|
|
545
|
+
next.delete(ORDER_PARAM);
|
|
546
|
+
return next;
|
|
547
|
+
}
|
|
364
548
|
function searchUrl(endpoint, params) {
|
|
365
549
|
const query = params.toString();
|
|
366
550
|
return `${endpoint}${query ? endpoint.includes("?") ? "&" : "?" : ""}${query}`;
|
|
367
551
|
}
|
|
368
552
|
function toPage(body) {
|
|
369
553
|
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
370
|
-
return {
|
|
554
|
+
return {
|
|
555
|
+
data: rows,
|
|
556
|
+
total: typeof body?.total === "number" ? body.total : rows.length,
|
|
557
|
+
sort: sortEchoOf(body)
|
|
558
|
+
};
|
|
371
559
|
}
|
|
372
560
|
var defaultFetcher = async (endpoint, params, signal) => {
|
|
373
561
|
const response = await fetch(searchUrl(endpoint, params), {
|
|
@@ -398,19 +586,19 @@ function SduiSearchDataProvider({
|
|
|
398
586
|
return /* @__PURE__ */ jsx(SearchDataContext.Provider, { value: fetcher, children });
|
|
399
587
|
}
|
|
400
588
|
function createSearchInvalidator() {
|
|
401
|
-
const
|
|
589
|
+
const listeners2 = /* @__PURE__ */ new Set();
|
|
402
590
|
return {
|
|
403
591
|
invalidate(prefix = "", exact = false) {
|
|
404
|
-
for (const entry of [...
|
|
592
|
+
for (const entry of [...listeners2]) {
|
|
405
593
|
const hit = prefix === "" ? true : exact ? entry.endpoint === prefix : entry.endpoint.startsWith(prefix);
|
|
406
594
|
if (hit) entry.reload();
|
|
407
595
|
}
|
|
408
596
|
},
|
|
409
597
|
subscribe(endpoint, reload) {
|
|
410
598
|
const entry = { endpoint, reload };
|
|
411
|
-
|
|
599
|
+
listeners2.add(entry);
|
|
412
600
|
return () => {
|
|
413
|
-
|
|
601
|
+
listeners2.delete(entry);
|
|
414
602
|
};
|
|
415
603
|
}
|
|
416
604
|
};
|
|
@@ -436,6 +624,7 @@ function useRemoteSearchPage(endpoint, params) {
|
|
|
436
624
|
const fetcher = useSearchFetcher();
|
|
437
625
|
const invalidator = useContext(SearchInvalidationContext);
|
|
438
626
|
const [page, setPage] = useState(EMPTY_PAGE);
|
|
627
|
+
const [pageQuery, setPageQuery] = useState(null);
|
|
439
628
|
const [isLoading, setIsLoading] = useState(true);
|
|
440
629
|
const [error, setError] = useState(null);
|
|
441
630
|
const [reloadKey, setReloadKey] = useState(0);
|
|
@@ -454,6 +643,7 @@ function useRemoteSearchPage(endpoint, params) {
|
|
|
454
643
|
(next) => {
|
|
455
644
|
if (!alive) return;
|
|
456
645
|
setPage(next);
|
|
646
|
+
setPageQuery(query);
|
|
457
647
|
setError(null);
|
|
458
648
|
setIsLoading(false);
|
|
459
649
|
},
|
|
@@ -469,10 +659,87 @@ function useRemoteSearchPage(endpoint, params) {
|
|
|
469
659
|
};
|
|
470
660
|
}, [endpoint, query, reloadKey]);
|
|
471
661
|
const reload = useCallback(() => setReloadKey((value) => value + 1), []);
|
|
472
|
-
return { page, isLoading, error, reload };
|
|
662
|
+
return { page, pageQuery, isLoading, error, reload };
|
|
663
|
+
}
|
|
664
|
+
function useRemoteSearchSummary(endpoint, params) {
|
|
665
|
+
const adapter = useSduiAdapter();
|
|
666
|
+
const invalidator = useContext(SearchInvalidationContext);
|
|
667
|
+
const [values, setValues] = useState(null);
|
|
668
|
+
const [isLoading, setIsLoading] = useState(endpoint !== null);
|
|
669
|
+
const [error, setError] = useState(null);
|
|
670
|
+
const [reloadKey, setReloadKey] = useState(0);
|
|
671
|
+
useEffect(() => {
|
|
672
|
+
if (!invalidator || endpoint === null) return void 0;
|
|
673
|
+
return invalidator.subscribe(endpoint, () => setReloadKey((value) => value + 1));
|
|
674
|
+
}, [invalidator, endpoint]);
|
|
675
|
+
const query = params.toString();
|
|
676
|
+
const hostFetchRef = useRef(hostFetchOf(adapter));
|
|
677
|
+
hostFetchRef.current = hostFetchOf(adapter);
|
|
678
|
+
useEffect(() => {
|
|
679
|
+
if (endpoint === null) return void 0;
|
|
680
|
+
const controller = new AbortController();
|
|
681
|
+
let alive = true;
|
|
682
|
+
const url = searchUrl(endpoint, new URLSearchParams(query));
|
|
683
|
+
setIsLoading(true);
|
|
684
|
+
const hostFetch = hostFetchRef.current;
|
|
685
|
+
const request = hostFetch ? hostFetch(url, { method: "GET", signal: controller.signal }) : fetch(url, { headers: { accept: "application/json" }, credentials: "same-origin", signal: controller.signal }).then(
|
|
686
|
+
async (response) => {
|
|
687
|
+
const body = await response.json().catch(() => null);
|
|
688
|
+
if (!response.ok) throw new Error(body?.message ?? `\uD569\uACC4\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 (${response.status})`);
|
|
689
|
+
return body;
|
|
690
|
+
}
|
|
691
|
+
);
|
|
692
|
+
request.then(
|
|
693
|
+
(body) => {
|
|
694
|
+
if (!alive) return;
|
|
695
|
+
setValues(body && typeof body === "object" && !Array.isArray(body) ? body : null);
|
|
696
|
+
setError(null);
|
|
697
|
+
setIsLoading(false);
|
|
698
|
+
},
|
|
699
|
+
(cause) => {
|
|
700
|
+
if (!alive || controller.signal.aborted) return;
|
|
701
|
+
setError(cause instanceof Error ? cause : new Error("\uD569\uACC4\uB97C \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4"));
|
|
702
|
+
setIsLoading(false);
|
|
703
|
+
}
|
|
704
|
+
);
|
|
705
|
+
return () => {
|
|
706
|
+
alive = false;
|
|
707
|
+
controller.abort();
|
|
708
|
+
};
|
|
709
|
+
}, [endpoint, query, reloadKey]);
|
|
710
|
+
return { values, isLoading, error };
|
|
711
|
+
}
|
|
712
|
+
function summaryParams(params) {
|
|
713
|
+
const next = new URLSearchParams(params);
|
|
714
|
+
next.delete("page");
|
|
715
|
+
next.delete("limit");
|
|
716
|
+
next.delete("sort");
|
|
717
|
+
next.delete("order");
|
|
718
|
+
return next;
|
|
719
|
+
}
|
|
720
|
+
function plainValue(value) {
|
|
721
|
+
if (value === null || value === void 0 || value === "") return "\u2014";
|
|
722
|
+
if (typeof value === "number") return value.toLocaleString("ko-KR");
|
|
723
|
+
if (typeof value === "string" && /^-?\d+(\.\d+)?$/.test(value)) return Number(value).toLocaleString("ko-KR");
|
|
724
|
+
return String(value);
|
|
725
|
+
}
|
|
726
|
+
function SearchSummaryCards({ items, values, dispatch }) {
|
|
727
|
+
const list = Array.isArray(items) ? items.filter((item) => item && typeof item.key === "string") : [];
|
|
728
|
+
if (list.length === 0) return null;
|
|
729
|
+
const columns = Math.min(Math.max(list.length, 2), 4);
|
|
730
|
+
const row = values ?? {};
|
|
731
|
+
return /* @__PURE__ */ jsx("div", { className: `wp-grid wp-grid--${columns}`, "aria-label": "\uAC80\uC0C9 \uACB0\uACFC \uD569\uACC4", children: list.map((item) => {
|
|
732
|
+
const render = item.cell ? cellRenderer(item.cell, item.key, dispatch) : void 0;
|
|
733
|
+
const raw = values ? fieldOf(row, item.key) : void 0;
|
|
734
|
+
const value = values && render && raw !== void 0 && raw !== null ? render(row) : plainValue(raw);
|
|
735
|
+
return /* @__PURE__ */ jsx(StatCard, { size: "sm", label: item.label, icon: item.icon, value, unit: item.unit }, item.key);
|
|
736
|
+
}) });
|
|
473
737
|
}
|
|
474
738
|
var OVERSCAN = 10;
|
|
475
739
|
var CLIP_CSS = `.wp-table--clip tbody td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}`;
|
|
740
|
+
function isBlankCell(value) {
|
|
741
|
+
return value === null || value === void 0 || value === "";
|
|
742
|
+
}
|
|
476
743
|
function ResultTable({
|
|
477
744
|
columns,
|
|
478
745
|
rows,
|
|
@@ -483,7 +750,8 @@ function ResultTable({
|
|
|
483
750
|
empty,
|
|
484
751
|
virtual = false,
|
|
485
752
|
height = 560,
|
|
486
|
-
rowHeight = 41
|
|
753
|
+
rowHeight = 41,
|
|
754
|
+
stackable = true
|
|
487
755
|
}) {
|
|
488
756
|
const scrollRef = useRef(null);
|
|
489
757
|
const firstRowRef = useRef(null);
|
|
@@ -507,13 +775,14 @@ function ResultTable({
|
|
|
507
775
|
const table = /* @__PURE__ */ jsxs(
|
|
508
776
|
"table",
|
|
509
777
|
{
|
|
510
|
-
className: `wp-table wp-table--hover${virtual ? " wp-table--sticky wp-table--clip" : ""}`,
|
|
778
|
+
className: `wp-table wp-table--hover${virtual ? " wp-table--sticky wp-table--clip" : stackable ? " wp-table--stackable" : ""}`,
|
|
511
779
|
style: virtual ? { tableLayout: "fixed" } : void 0,
|
|
512
780
|
children: [
|
|
513
781
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((column) => /* @__PURE__ */ jsx(
|
|
514
782
|
"th",
|
|
515
783
|
{
|
|
516
784
|
style: { width: column.width, textAlign: column.align === "right" ? "right" : void 0 },
|
|
785
|
+
"data-mobile": column.mobile,
|
|
517
786
|
children: column.sortable && onSort ? (
|
|
518
787
|
// 인라인 style로는 :hover를 표현할 수 없다 — 색 피드백은 .wp-table__sort:hover가 낸다
|
|
519
788
|
/* @__PURE__ */ jsxs("button", { type: "button", className: "wp-table__sort", onClick: () => onSort(column.key), children: [
|
|
@@ -547,10 +816,20 @@ function ResultTable({
|
|
|
547
816
|
if (column.align === "right") cls.push("wp-table__num");
|
|
548
817
|
if (column.mono) cls.push("wp-table__mono");
|
|
549
818
|
if (column.strong) cls.push("wp-table__strong");
|
|
550
|
-
|
|
819
|
+
const value = column.render ? column.render(row, index) : (
|
|
551
820
|
// 평면 키 우선, 없으면 점 경로 — 셀 렌더러(cells.tsx)와 같은 조회 규칙이다
|
|
552
821
|
fieldOf(row, column.key)
|
|
553
|
-
)
|
|
822
|
+
);
|
|
823
|
+
return /* @__PURE__ */ jsx(
|
|
824
|
+
"td",
|
|
825
|
+
{
|
|
826
|
+
className: cls.join(" ") || void 0,
|
|
827
|
+
"data-label": labelOf(column),
|
|
828
|
+
"data-mobile": column.mobile,
|
|
829
|
+
children: isBlankCell(value) ? /* @__PURE__ */ jsx("span", { style: { color: "var(--wp-text-subtle)" }, children: "\u2014" }) : value
|
|
830
|
+
},
|
|
831
|
+
column.key
|
|
832
|
+
);
|
|
554
833
|
})
|
|
555
834
|
},
|
|
556
835
|
row.id ?? index
|
|
@@ -561,14 +840,33 @@ function ResultTable({
|
|
|
561
840
|
]
|
|
562
841
|
}
|
|
563
842
|
);
|
|
564
|
-
if (!virtual)
|
|
843
|
+
if (!virtual) {
|
|
844
|
+
return /* @__PURE__ */ jsx("div", { className: "wp-table-scroll", children: table });
|
|
845
|
+
}
|
|
565
846
|
return /* @__PURE__ */ jsxs("div", { ref: scrollRef, onScroll: handleScroll, style: { height, overflowY: "auto" }, children: [
|
|
566
847
|
/* @__PURE__ */ jsx(HoistedStyle, { id: "wp-table-clip", css: CLIP_CSS }),
|
|
567
848
|
table
|
|
568
849
|
] });
|
|
569
850
|
}
|
|
570
|
-
function
|
|
571
|
-
|
|
851
|
+
function labelOf(column) {
|
|
852
|
+
if (typeof column.label === "string") return column.label;
|
|
853
|
+
return typeof column.header === "string" ? column.header : "";
|
|
854
|
+
}
|
|
855
|
+
function ResultEmptyState({ title, filtered = false, emptyTitle, emptyText, scoped = false, onReset }) {
|
|
856
|
+
const name = String(title ?? "").trim() || "\uD56D\uBAA9";
|
|
857
|
+
if (filtered) {
|
|
858
|
+
return /* @__PURE__ */ jsx(
|
|
859
|
+
EmptyState,
|
|
860
|
+
{
|
|
861
|
+
icon: "scan-search",
|
|
862
|
+
title: `\uC870\uAC74\uC5D0 \uB9DE\uB294 ${name}${josaOf(name, "\uC774/\uAC00")} \uC5C6\uC2B5\uB2C8\uB2E4`,
|
|
863
|
+
tight: true,
|
|
864
|
+
actions: onReset ? /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", icon: "refresh-cw", onClick: onReset, children: "\uC870\uAC74 \uCD08\uAE30\uD654" }) : void 0,
|
|
865
|
+
children: "\uAC80\uC0C9\uC5B4\uB098 \uC870\uAC74\uC744 \uBC14\uAFD4 \uBCF4\uC138\uC694."
|
|
866
|
+
}
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
return /* @__PURE__ */ jsx(EmptyState, { title: emptyTitle || `${scoped ? "" : "\uC544\uC9C1 "}${name}${josaOf(name, "\uC774/\uAC00")} \uC5C6\uC2B5\uB2C8\uB2E4`, tight: true, children: emptyText || void 0 });
|
|
572
870
|
}
|
|
573
871
|
var EMPTY_FILTERS = [];
|
|
574
872
|
var EMPTY_FIELDS = [];
|
|
@@ -577,6 +875,29 @@ var DEFAULT_VIEWS = [
|
|
|
577
875
|
{ value: "list", icon: "list", label: "\uBAA9\uB85D" },
|
|
578
876
|
{ value: "grid", icon: "layout-grid", label: "\uCE74\uB4DC" }
|
|
579
877
|
];
|
|
878
|
+
var VIEW_STORAGE_PREFIX = "wp-sdui:view:";
|
|
879
|
+
var viewMemory = /* @__PURE__ */ new Map();
|
|
880
|
+
function readStoredView(title) {
|
|
881
|
+
const inMemory = viewMemory.get(title);
|
|
882
|
+
if (inMemory) return inMemory;
|
|
883
|
+
try {
|
|
884
|
+
if (typeof window === "undefined") return null;
|
|
885
|
+
const stored = window.sessionStorage.getItem(VIEW_STORAGE_PREFIX + title);
|
|
886
|
+
return stored === "grid" || stored === "list" ? stored : null;
|
|
887
|
+
} catch {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function writeStoredView(title, view) {
|
|
892
|
+
viewMemory.set(title, view);
|
|
893
|
+
try {
|
|
894
|
+
if (typeof window !== "undefined") window.sessionStorage.setItem(VIEW_STORAGE_PREFIX + title, view);
|
|
895
|
+
} catch {
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
function looksLikeLocator(value) {
|
|
899
|
+
return /^(https?:)?\/\//.test(value) || value.startsWith("/") || value.startsWith("data:");
|
|
900
|
+
}
|
|
580
901
|
function normalizePageSizes(sizes) {
|
|
581
902
|
if (!Array.isArray(sizes)) return void 0;
|
|
582
903
|
const clean = [...new Set(sizes.map(Number).filter((n) => Number.isFinite(n) && n > 0).map(Math.floor))].sort(
|
|
@@ -606,6 +927,8 @@ function SearchResults({
|
|
|
606
927
|
gridConfig,
|
|
607
928
|
leftFilters = EMPTY_FILTERS,
|
|
608
929
|
rightFilters = EMPTY_FILTERS,
|
|
930
|
+
tabs,
|
|
931
|
+
summary,
|
|
609
932
|
initialView = "list",
|
|
610
933
|
views,
|
|
611
934
|
pageSizes,
|
|
@@ -623,9 +946,15 @@ function SearchResults({
|
|
|
623
946
|
hideHeader = false,
|
|
624
947
|
advancedSearchFields: rawAdvancedSearchFields = EMPTY_FIELDS,
|
|
625
948
|
searchPlaceholder,
|
|
626
|
-
virtual
|
|
949
|
+
virtual,
|
|
950
|
+
serverUnsortable,
|
|
951
|
+
primaryKey,
|
|
952
|
+
emptyTitle,
|
|
953
|
+
emptyText,
|
|
954
|
+
serverFiltered = false
|
|
627
955
|
}) {
|
|
628
956
|
const hasActions = actions.length > 0;
|
|
957
|
+
const isMobile = useIsMobile();
|
|
629
958
|
const advancedSearchFields = useMemo(
|
|
630
959
|
() => normalizeSearchFields(rawAdvancedSearchFields),
|
|
631
960
|
[rawAdvancedSearchFields]
|
|
@@ -641,12 +970,18 @@ function SearchResults({
|
|
|
641
970
|
);
|
|
642
971
|
const applyViewConstraints = (candidate) => viewItems.some((v) => v.value === candidate) ? candidate : viewItems[0].value;
|
|
643
972
|
const appliedSearch = searchParams.get("search") ?? "";
|
|
644
|
-
const [
|
|
973
|
+
const [chosenView, setChosenView] = useState(() => readStoredView(title));
|
|
645
974
|
const [search, setSearch] = useState(appliedSearch);
|
|
646
|
-
const [sort, setSort] = useState(null);
|
|
647
975
|
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
648
976
|
const selectedRowsRef = useRef(/* @__PURE__ */ new Map());
|
|
649
|
-
const
|
|
977
|
+
const hasGridView = viewItems.some((v) => v.value === "grid");
|
|
978
|
+
const defaultView = isMobile && hasGridView ? "grid" : initialView;
|
|
979
|
+
const activeView = applyViewConstraints(chosenView ?? defaultView);
|
|
980
|
+
const chooseView = (next) => {
|
|
981
|
+
const view = applyViewConstraints(next);
|
|
982
|
+
writeStoredView(title, view);
|
|
983
|
+
setChosenView(view);
|
|
984
|
+
};
|
|
650
985
|
useEffect(() => {
|
|
651
986
|
setSearch(appliedSearch);
|
|
652
987
|
}, [appliedSearch]);
|
|
@@ -663,29 +998,43 @@ function SearchResults({
|
|
|
663
998
|
onParamsChange(next);
|
|
664
999
|
};
|
|
665
1000
|
const activeFilters = useMemo(() => {
|
|
666
|
-
const
|
|
1001
|
+
const entries2 = [];
|
|
667
1002
|
for (const filter of [...leftFilters, ...rightFilters]) {
|
|
668
1003
|
const values = splitMultiValue(searchParams.get(filter.key) ?? "");
|
|
669
|
-
if (values.length > 0)
|
|
1004
|
+
if (values.length > 0) entries2.push({ key: filter.key, values });
|
|
1005
|
+
}
|
|
1006
|
+
if (tabs && typeof tabs.key === "string") {
|
|
1007
|
+
const values = splitMultiValue(searchParams.get(tabs.key) ?? "");
|
|
1008
|
+
if (values.length > 0) entries2.push({ key: tabs.key, values });
|
|
670
1009
|
}
|
|
671
|
-
return
|
|
672
|
-
}, [leftFilters, rightFilters, searchParams]);
|
|
1010
|
+
return entries2;
|
|
1011
|
+
}, [leftFilters, rightFilters, tabs, searchParams]);
|
|
673
1012
|
const hasAllRows = item.data.length >= item.total;
|
|
1013
|
+
const sortAllowed = (key) => hasAllRows || !(serverUnsortable === "all" || Array.isArray(serverUnsortable) && serverUnsortable.includes(key));
|
|
1014
|
+
const requestedSort = sortFromParams(searchParams);
|
|
1015
|
+
const sortKey = requestedSort && sortAllowed(requestedSort.key) ? requestedSort.key : null;
|
|
1016
|
+
const sortDir = requestedSort?.dir ?? "asc";
|
|
1017
|
+
const sort = useMemo(() => sortKey ? { key: sortKey, dir: sortDir } : null, [sortKey, sortDir]);
|
|
674
1018
|
const processed = useMemo(() => {
|
|
675
1019
|
const cell = (row, key) => fieldOf(row, key);
|
|
676
1020
|
let rows = item.data;
|
|
677
|
-
if (hasAllRows) {
|
|
1021
|
+
if (hasAllRows && !serverFiltered) {
|
|
1022
|
+
const source = item.data;
|
|
1023
|
+
const known = (key) => source.some((row) => cell(row, key) !== void 0);
|
|
678
1024
|
if (appliedSearch) {
|
|
679
1025
|
rows = rows.filter((row) => includesLooseSearch(localSearchText(row), appliedSearch));
|
|
680
1026
|
}
|
|
681
1027
|
for (const { key, values } of activeFilters) {
|
|
1028
|
+
if (!known(key)) continue;
|
|
682
1029
|
rows = rows.filter((row) => values.includes(String(cell(row, key))));
|
|
683
1030
|
}
|
|
684
1031
|
for (const field of advancedSearchFields) {
|
|
685
1032
|
if (field.type === "select") {
|
|
1033
|
+
if (!known(field.key)) continue;
|
|
686
1034
|
const wanted = splitMultiValue(advancedValues[field.key] ?? "");
|
|
687
1035
|
if (wanted.length > 0) rows = rows.filter((row) => wanted.includes(String(cell(row, field.key))));
|
|
688
1036
|
} else if (field.type === "cascading") {
|
|
1037
|
+
if (!known(field.key)) continue;
|
|
689
1038
|
const wanted = splitMultiValue(advancedValues[field.key] ?? "");
|
|
690
1039
|
if (wanted.length > 0) {
|
|
691
1040
|
const lineages = cascadingValuePaths(field.options);
|
|
@@ -696,24 +1045,29 @@ function SearchResults({
|
|
|
696
1045
|
});
|
|
697
1046
|
}
|
|
698
1047
|
} else if (field.type === "dateRange") {
|
|
1048
|
+
if (!known(field.key)) continue;
|
|
699
1049
|
const from = advancedValues[field.fromKey ?? `${field.key}From`];
|
|
700
1050
|
const to = advancedValues[field.toKey ?? `${field.key}To`];
|
|
701
|
-
const datePart = (v) => String(v ?? "").slice(0, 10);
|
|
1051
|
+
const datePart = (v) => isZonedIsoDateTime(v) ? formatDateValue(v, "date") : String(v ?? "").slice(0, 10);
|
|
702
1052
|
if (from) rows = rows.filter((row) => datePart(cell(row, field.key)) >= from.slice(0, 10));
|
|
703
1053
|
if (to) rows = rows.filter((row) => datePart(cell(row, field.key)) <= to.slice(0, 10));
|
|
704
1054
|
} else if (field.type === "numberRange") {
|
|
1055
|
+
if (!known(field.key)) continue;
|
|
705
1056
|
const min = numericValue(advancedValues[field.fromKey ?? `${field.key}Min`]);
|
|
706
1057
|
const max = numericValue(advancedValues[field.toKey ?? `${field.key}Max`]);
|
|
707
1058
|
if (min !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? -Infinity) >= min);
|
|
708
1059
|
if (max !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? Infinity) <= max);
|
|
709
1060
|
} else {
|
|
710
1061
|
const keyword = advancedValues[field.key];
|
|
711
|
-
const
|
|
712
|
-
|
|
1062
|
+
const suffix = ["_contains", "Contains"].find((s) => field.key.endsWith(s));
|
|
1063
|
+
const baseKey = suffix ? field.key.slice(0, -suffix.length) : field.key;
|
|
1064
|
+
if (keyword && known(baseKey)) {
|
|
1065
|
+
rows = rows.filter((row) => includesLooseSearch(String(cell(row, baseKey) ?? ""), keyword));
|
|
1066
|
+
}
|
|
713
1067
|
}
|
|
714
1068
|
}
|
|
715
1069
|
}
|
|
716
|
-
if (sort) {
|
|
1070
|
+
if (sort && hasAllRows) {
|
|
717
1071
|
const column = columns.find((c) => c.key === sort.key);
|
|
718
1072
|
const dirFactor = sort.dir === "asc" ? 1 : -1;
|
|
719
1073
|
rows = [...rows].sort((a, b) => {
|
|
@@ -728,7 +1082,7 @@ function SearchResults({
|
|
|
728
1082
|
});
|
|
729
1083
|
}
|
|
730
1084
|
return rows;
|
|
731
|
-
}, [item.data, hasAllRows, appliedSearch, activeFilters, advancedSearchFields, advancedValues, sort, columns]);
|
|
1085
|
+
}, [item.data, hasAllRows, serverFiltered, appliedSearch, activeFilters, advancedSearchFields, advancedValues, sort, columns]);
|
|
732
1086
|
const clientPaginated = hasAllRows;
|
|
733
1087
|
const totalCount = clientPaginated ? processed.length : item.total;
|
|
734
1088
|
const pageCount = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
@@ -760,7 +1114,50 @@ function SearchResults({
|
|
|
760
1114
|
});
|
|
761
1115
|
};
|
|
762
1116
|
const selectedItems = Array.from(selected).map((id) => selectedRowsRef.current.get(id)).filter((row) => row !== void 0);
|
|
1117
|
+
const paramsKey = searchParams.toString();
|
|
1118
|
+
const seenRef = useRef(null);
|
|
1119
|
+
const answersRef = useRef(/* @__PURE__ */ new Map());
|
|
1120
|
+
useEffect(() => {
|
|
1121
|
+
const settle = (key, data) => {
|
|
1122
|
+
const answers = answersRef.current;
|
|
1123
|
+
const nextIds = new Set(data.map((row) => row.id));
|
|
1124
|
+
const prev = answers.get(key);
|
|
1125
|
+
answers.delete(key);
|
|
1126
|
+
answers.set(key, nextIds);
|
|
1127
|
+
if (answers.size > 64) answers.delete(answers.keys().next().value);
|
|
1128
|
+
if (!prev) return;
|
|
1129
|
+
const vanished = [...prev].filter((id) => !nextIds.has(id));
|
|
1130
|
+
if (vanished.length === 0) return;
|
|
1131
|
+
vanished.forEach((id) => selectedRowsRef.current.delete(id));
|
|
1132
|
+
setSelected((current) => {
|
|
1133
|
+
let changed = false;
|
|
1134
|
+
const next = new Set(current);
|
|
1135
|
+
for (const id of vanished) if (next.delete(id)) changed = true;
|
|
1136
|
+
return changed ? next : current;
|
|
1137
|
+
});
|
|
1138
|
+
};
|
|
1139
|
+
const seen = seenRef.current;
|
|
1140
|
+
seenRef.current = { key: paramsKey, data: item.data };
|
|
1141
|
+
if (!seen) {
|
|
1142
|
+
settle(paramsKey, item.data);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
if (seen.data === item.data) return;
|
|
1146
|
+
settle(paramsKey, item.data);
|
|
1147
|
+
}, [paramsKey, item.data]);
|
|
763
1148
|
const ratioSum = columns.reduce((sum, c) => sum + (c.ratio ?? 0), 0);
|
|
1149
|
+
const titleColumnKey = columns.find((c) => c.mobile === "title")?.key ?? (primaryKey && columns.some((c) => c.key === primaryKey) ? primaryKey : void 0) ?? columns.find((c) => c.strong && c.mobile !== "hide")?.key;
|
|
1150
|
+
const rowLabelOf = (row) => {
|
|
1151
|
+
const keys = [primaryKey, titleColumnKey, ...columns.map((c) => c.key)].filter(
|
|
1152
|
+
(key) => typeof key === "string" && key.length > 0
|
|
1153
|
+
);
|
|
1154
|
+
for (const key of keys) {
|
|
1155
|
+
const value = fieldOf(row, key);
|
|
1156
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1157
|
+
if (typeof value === "string" && value.trim() && !looksLikeLocator(value.trim())) return value.trim();
|
|
1158
|
+
}
|
|
1159
|
+
return "\uD589";
|
|
1160
|
+
};
|
|
764
1161
|
const tableColumns = [
|
|
765
1162
|
...hasActions ? [
|
|
766
1163
|
{
|
|
@@ -775,6 +1172,7 @@ function SearchResults({
|
|
|
775
1172
|
}
|
|
776
1173
|
),
|
|
777
1174
|
width: "36px",
|
|
1175
|
+
label: "",
|
|
778
1176
|
render: (row) => (
|
|
779
1177
|
// 셀 어디를 눌러도 행 클릭(onRowClick)으로 번지지 않게 래퍼에서 전파를 끊는다
|
|
780
1178
|
/* @__PURE__ */ jsx("span", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
|
|
@@ -782,7 +1180,7 @@ function SearchResults({
|
|
|
782
1180
|
{
|
|
783
1181
|
checked: selected.has(row.id),
|
|
784
1182
|
onChange: (e) => toggleSelect(row, e.target.checked),
|
|
785
|
-
"aria-label": `${row
|
|
1183
|
+
"aria-label": `${rowLabelOf(row)} \uC120\uD0DD`
|
|
786
1184
|
}
|
|
787
1185
|
) })
|
|
788
1186
|
)
|
|
@@ -791,7 +1189,9 @@ function SearchResults({
|
|
|
791
1189
|
...columns.map((column) => ({
|
|
792
1190
|
key: column.key,
|
|
793
1191
|
header: column.title,
|
|
794
|
-
|
|
1192
|
+
label: column.title,
|
|
1193
|
+
mobile: column.mobile ?? (column.key === titleColumnKey ? "title" : void 0),
|
|
1194
|
+
sortable: column.sortable && sortAllowed(column.key),
|
|
795
1195
|
align: column.textAlign === "right" ? "right" : "left",
|
|
796
1196
|
mono: column.mono,
|
|
797
1197
|
strong: column.strong,
|
|
@@ -800,10 +1200,62 @@ function SearchResults({
|
|
|
800
1200
|
}))
|
|
801
1201
|
];
|
|
802
1202
|
const handleSort = (key) => {
|
|
803
|
-
|
|
1203
|
+
const next = nextSort(sort, key);
|
|
1204
|
+
updateParams({ [SORT_PARAM]: next?.key ?? null, [ORDER_PARAM]: next?.dir ?? null, page: null });
|
|
1205
|
+
};
|
|
1206
|
+
const sortChoices = columns.filter((column) => column.sortable && sortAllowed(column.key));
|
|
1207
|
+
const mobileSort = isMobile && sortChoices.length > 0 ? /* @__PURE__ */ jsx(
|
|
1208
|
+
Select,
|
|
1209
|
+
{
|
|
1210
|
+
size: "sm",
|
|
1211
|
+
"aria-label": `${title} \uC815\uB82C`,
|
|
1212
|
+
value: sort ? `${sort.key}:${sort.dir}` : "",
|
|
1213
|
+
options: [
|
|
1214
|
+
{ value: "", label: "\uAE30\uBCF8 \uC815\uB82C" },
|
|
1215
|
+
...sortChoices.flatMap((column) => [
|
|
1216
|
+
{ value: `${column.key}:asc`, label: `${column.title} \uC624\uB984\uCC28\uC21C` },
|
|
1217
|
+
{ value: `${column.key}:desc`, label: `${column.title} \uB0B4\uB9BC\uCC28\uC21C` }
|
|
1218
|
+
])
|
|
1219
|
+
],
|
|
1220
|
+
onChange: (e) => {
|
|
1221
|
+
const cut = e.target.value.lastIndexOf(":");
|
|
1222
|
+
const key = cut > 0 ? e.target.value.slice(0, cut) : "";
|
|
1223
|
+
const dir = e.target.value.slice(cut + 1) === "desc" ? "desc" : "asc";
|
|
1224
|
+
updateParams({ [SORT_PARAM]: key || null, [ORDER_PARAM]: key ? dir : null, page: null });
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
) : null;
|
|
1228
|
+
const chipKeys = [...leftFilters, ...rightFilters].map((filter) => filter.key);
|
|
1229
|
+
const filtered = Boolean(appliedSearch) || Object.values(advancedValues).some((value) => Boolean(value)) || chipKeys.some((key) => Boolean(searchParams.get(key)));
|
|
1230
|
+
const scoped = Boolean(tabs && typeof tabs.key === "string" && searchParams.get(tabs.key));
|
|
1231
|
+
const resetConditions = () => {
|
|
1232
|
+
const patch = { search: null, page: null };
|
|
1233
|
+
for (const key of searchFieldKeys(advancedSearchFields)) patch[key] = null;
|
|
1234
|
+
for (const key of chipKeys) patch[key] = null;
|
|
1235
|
+
updateParams(patch);
|
|
804
1236
|
};
|
|
805
|
-
const empty = /* @__PURE__ */ jsx(
|
|
1237
|
+
const empty = /* @__PURE__ */ jsx(
|
|
1238
|
+
ResultEmptyState,
|
|
1239
|
+
{
|
|
1240
|
+
title,
|
|
1241
|
+
filtered,
|
|
1242
|
+
scoped,
|
|
1243
|
+
emptyTitle,
|
|
1244
|
+
emptyText,
|
|
1245
|
+
onReset: resetConditions
|
|
1246
|
+
}
|
|
1247
|
+
);
|
|
806
1248
|
return /* @__PURE__ */ jsxs("div", { className: ["wp-col", className].filter(Boolean).join(" "), style, "aria-label": title, children: [
|
|
1249
|
+
summary ?? null,
|
|
1250
|
+
tabs && Array.isArray(tabs.items) && tabs.items.length > 0 ? /* @__PURE__ */ jsx(
|
|
1251
|
+
Tabs,
|
|
1252
|
+
{
|
|
1253
|
+
"aria-label": `${title} \uAD6C\uBD84`,
|
|
1254
|
+
items: tabs.items.map((tab) => ({ value: tab.value ?? "", label: tab.label, icon: tab.icon })),
|
|
1255
|
+
value: searchParams.get(tabs.key) ?? "",
|
|
1256
|
+
onChange: (value) => updateParams({ [tabs.key]: value || null, page: null })
|
|
1257
|
+
}
|
|
1258
|
+
) : null,
|
|
807
1259
|
!hideHeader ? /* @__PURE__ */ jsxs("div", { className: "wp-row wp-row--between", style: { flexWrap: "wrap" }, children: [
|
|
808
1260
|
/* @__PURE__ */ jsxs("span", { className: "wp-caption", children: [
|
|
809
1261
|
"\uCD1D ",
|
|
@@ -832,9 +1284,10 @@ function SearchResults({
|
|
|
832
1284
|
{
|
|
833
1285
|
items: viewItems,
|
|
834
1286
|
value: activeView,
|
|
835
|
-
onChange: (next) =>
|
|
1287
|
+
onChange: (next) => chooseView(next)
|
|
836
1288
|
}
|
|
837
1289
|
) : null,
|
|
1290
|
+
mobileSort,
|
|
838
1291
|
headerActions
|
|
839
1292
|
] })
|
|
840
1293
|
] }) : null,
|
|
@@ -915,7 +1368,7 @@ function SearchResults({
|
|
|
915
1368
|
virtual: virtualListActive
|
|
916
1369
|
}
|
|
917
1370
|
) : visibleItems.length === 0 ? empty : /* @__PURE__ */ jsx("div", { className: "wp-grid wp-grid--3", children: visibleItems.map((row) => /* @__PURE__ */ jsx(GridCard, { item: row, config: gridConfig, onClick: onRowClick }, row.id)) }),
|
|
918
|
-
virtualListActive && clientPaginated ? null : /* @__PURE__ */ jsxs("div", { className: "wp-row wp-row--between", children: [
|
|
1371
|
+
virtualListActive && clientPaginated || totalCount === 0 && !isLoading ? null : /* @__PURE__ */ jsxs("div", { className: "wp-row wp-row--between", children: [
|
|
919
1372
|
/* @__PURE__ */ jsxs("div", { className: "wp-row", style: { flex: "none" }, children: [
|
|
920
1373
|
/* @__PURE__ */ jsx("span", { className: "wp-caption", style: { whiteSpace: "nowrap" }, children: "\uD398\uC774\uC9C0\uB2F9" }),
|
|
921
1374
|
/* @__PURE__ */ jsx(
|
|
@@ -957,6 +1410,9 @@ function formatCell(value) {
|
|
|
957
1410
|
if (value instanceof Date) return formatDateCellValue(value);
|
|
958
1411
|
return value;
|
|
959
1412
|
}
|
|
1413
|
+
function blankAsDash(value) {
|
|
1414
|
+
return value === null || value === void 0 || value === "" ? /* @__PURE__ */ jsx("span", { style: { color: "var(--wp-text-subtle)" }, children: "\u2014" }) : value;
|
|
1415
|
+
}
|
|
960
1416
|
function resolveRowPart(part, item) {
|
|
961
1417
|
return typeof part === "function" ? part(item) : part;
|
|
962
1418
|
}
|
|
@@ -972,7 +1428,7 @@ function GridCard({
|
|
|
972
1428
|
return /* @__PURE__ */ jsxs("div", { className: "wp-row", children: [
|
|
973
1429
|
resolveRowPart(row.icon, item),
|
|
974
1430
|
row.label != null ? /* @__PURE__ */ jsx("span", { className: "wp-caption", children: resolveRowPart(row.label, item) }) : null,
|
|
975
|
-
/* @__PURE__ */ jsx("span", { className: "wp-body-sm", children: resolveRowPart(row.value, item) })
|
|
1431
|
+
/* @__PURE__ */ jsx("span", { className: "wp-body-sm", children: blankAsDash(resolveRowPart(row.value, item)) })
|
|
976
1432
|
] }, idx);
|
|
977
1433
|
}) });
|
|
978
1434
|
const note = config.note?.(item);
|
|
@@ -1041,6 +1497,22 @@ function FilterMultiSelect({
|
|
|
1041
1497
|
function toList(action) {
|
|
1042
1498
|
return Array.isArray(action) ? action : [action];
|
|
1043
1499
|
}
|
|
1500
|
+
function conditionParams(params) {
|
|
1501
|
+
const next = new URLSearchParams(params);
|
|
1502
|
+
next.delete("page");
|
|
1503
|
+
next.delete("limit");
|
|
1504
|
+
return next.toString();
|
|
1505
|
+
}
|
|
1506
|
+
function useQueryStateMirror(dispatch, key, params) {
|
|
1507
|
+
const value = conditionParams(params);
|
|
1508
|
+
const lastRef = useRef(null);
|
|
1509
|
+
useEffect(() => {
|
|
1510
|
+
if (typeof key !== "string" || key === "") return;
|
|
1511
|
+
if (lastRef.current === value) return;
|
|
1512
|
+
lastRef.current = value;
|
|
1513
|
+
dispatch({ action: "setState", key, value });
|
|
1514
|
+
}, [dispatch, key, value]);
|
|
1515
|
+
}
|
|
1044
1516
|
function mapColumns(columns, dispatch) {
|
|
1045
1517
|
return (columns ?? []).map((column) => ({
|
|
1046
1518
|
key: column.key,
|
|
@@ -1050,6 +1522,7 @@ function mapColumns(columns, dispatch) {
|
|
|
1050
1522
|
ratio: column.ratio,
|
|
1051
1523
|
mono: column.mono,
|
|
1052
1524
|
strong: column.strong,
|
|
1525
|
+
mobile: column.mobile === "title" || column.mobile === "hide" ? column.mobile : void 0,
|
|
1053
1526
|
render: cellRenderer(column.cell, column.key, dispatch)
|
|
1054
1527
|
}));
|
|
1055
1528
|
}
|
|
@@ -1140,6 +1613,7 @@ function SduiSearchResultsImpl({
|
|
|
1140
1613
|
fields,
|
|
1141
1614
|
leftFilters,
|
|
1142
1615
|
rightFilters,
|
|
1616
|
+
tabs,
|
|
1143
1617
|
gridCard,
|
|
1144
1618
|
actions,
|
|
1145
1619
|
initialView,
|
|
@@ -1150,11 +1624,15 @@ function SduiSearchResultsImpl({
|
|
|
1150
1624
|
searchPlaceholder,
|
|
1151
1625
|
rowClick,
|
|
1152
1626
|
headerActions,
|
|
1153
|
-
virtual
|
|
1627
|
+
virtual,
|
|
1628
|
+
emptyTitle,
|
|
1629
|
+
emptyText,
|
|
1630
|
+
queryStateKey
|
|
1154
1631
|
}) {
|
|
1155
1632
|
const dispatch = useSduiDispatch();
|
|
1156
1633
|
const [params, setParams] = useState(() => new URLSearchParams());
|
|
1157
1634
|
const rows = Array.isArray(data) ? data : [];
|
|
1635
|
+
useQueryStateMirror(dispatch, queryStateKey, params);
|
|
1158
1636
|
return /* @__PURE__ */ jsx(
|
|
1159
1637
|
SearchResults,
|
|
1160
1638
|
{
|
|
@@ -1164,6 +1642,7 @@ function SduiSearchResultsImpl({
|
|
|
1164
1642
|
gridConfig: mapGridCard(gridCard, dispatch),
|
|
1165
1643
|
leftFilters,
|
|
1166
1644
|
rightFilters,
|
|
1645
|
+
tabs,
|
|
1167
1646
|
actions: mapActions(actions, dispatch),
|
|
1168
1647
|
initialView,
|
|
1169
1648
|
views: mapViews(views, gridCard, disableGrid, disableList),
|
|
@@ -1174,6 +1653,9 @@ function SduiSearchResultsImpl({
|
|
|
1174
1653
|
searchPlaceholder,
|
|
1175
1654
|
headerActions,
|
|
1176
1655
|
virtual,
|
|
1656
|
+
primaryKey: gridCard?.titleKey,
|
|
1657
|
+
emptyTitle,
|
|
1658
|
+
emptyText,
|
|
1177
1659
|
onRowClick: rowClick ? (row) => toList(rowClick).forEach((a) => dispatch(a, row)) : void 0,
|
|
1178
1660
|
searchParams: params,
|
|
1179
1661
|
onParamsChange: setParams
|
|
@@ -1187,6 +1669,7 @@ function RemoteSearchResultsImpl({
|
|
|
1187
1669
|
fields,
|
|
1188
1670
|
leftFilters,
|
|
1189
1671
|
rightFilters,
|
|
1672
|
+
tabs,
|
|
1190
1673
|
gridCard,
|
|
1191
1674
|
actions,
|
|
1192
1675
|
initialView,
|
|
@@ -1200,7 +1683,11 @@ function RemoteSearchResultsImpl({
|
|
|
1200
1683
|
virtual,
|
|
1201
1684
|
pageSize,
|
|
1202
1685
|
searchParams: controlledParams,
|
|
1203
|
-
onParamsChange
|
|
1686
|
+
onParamsChange,
|
|
1687
|
+
summary,
|
|
1688
|
+
emptyTitle,
|
|
1689
|
+
emptyText,
|
|
1690
|
+
queryStateKey
|
|
1204
1691
|
}) {
|
|
1205
1692
|
const dispatch = useSduiDispatch();
|
|
1206
1693
|
const defaultPageSize = normalizePageSizes(pageSizes)?.[0] ?? 10;
|
|
@@ -1220,7 +1707,22 @@ function RemoteSearchResultsImpl({
|
|
|
1220
1707
|
if (!next.has("limit")) next.set("limit", String(defaultPageSize));
|
|
1221
1708
|
return next;
|
|
1222
1709
|
}, [params, defaultPageSize]);
|
|
1223
|
-
const { page, isLoading, error, reload } = useRemoteSearchPage(endpoint, query);
|
|
1710
|
+
const { page, pageQuery, isLoading, error, reload } = useRemoteSearchPage(endpoint, query);
|
|
1711
|
+
const requestedSort = sortFromParams(params);
|
|
1712
|
+
const queryString = query.toString();
|
|
1713
|
+
const settledForQuery = pageQuery === queryString;
|
|
1714
|
+
useEffect(() => {
|
|
1715
|
+
if (!requestedSort || !settledForQuery || page.data.length >= page.total) return;
|
|
1716
|
+
if (!serverSortMemory.record(endpoint, requestedSort, page.sort ?? null)) {
|
|
1717
|
+
handleParamsChange(withoutSort(params));
|
|
1718
|
+
}
|
|
1719
|
+
}, [page, settledForQuery, endpoint, requestedSort?.key, requestedSort?.dir]);
|
|
1720
|
+
const summaryQuery = useMemo(() => summaryParams(params), [params]);
|
|
1721
|
+
useQueryStateMirror(dispatch, queryStateKey, params);
|
|
1722
|
+
const summaryState = useRemoteSearchSummary(
|
|
1723
|
+
summary && typeof summary.endpoint === "string" ? summary.endpoint : null,
|
|
1724
|
+
summaryQuery
|
|
1725
|
+
);
|
|
1224
1726
|
if (error && page.data.length === 0) {
|
|
1225
1727
|
return /* @__PURE__ */ jsx(
|
|
1226
1728
|
EmptyState,
|
|
@@ -1241,6 +1743,7 @@ function RemoteSearchResultsImpl({
|
|
|
1241
1743
|
gridConfig: mapGridCard(gridCard, dispatch),
|
|
1242
1744
|
leftFilters,
|
|
1243
1745
|
rightFilters,
|
|
1746
|
+
tabs,
|
|
1244
1747
|
actions: mapActions(actions, dispatch),
|
|
1245
1748
|
initialView,
|
|
1246
1749
|
views: mapViews(views, gridCard, disableGrid, disableList),
|
|
@@ -1252,6 +1755,12 @@ function RemoteSearchResultsImpl({
|
|
|
1252
1755
|
headerActions,
|
|
1253
1756
|
virtual,
|
|
1254
1757
|
isLoading,
|
|
1758
|
+
serverUnsortable: serverSortMemory.unsortable(endpoint),
|
|
1759
|
+
serverFiltered: true,
|
|
1760
|
+
primaryKey: gridCard?.titleKey,
|
|
1761
|
+
emptyTitle,
|
|
1762
|
+
emptyText,
|
|
1763
|
+
summary: summary ? /* @__PURE__ */ jsx(SearchSummaryCards, { items: summary.items, values: summaryState.values, dispatch }) : void 0,
|
|
1255
1764
|
onRowClick: rowClick ? (row) => toList(rowClick).forEach((a) => dispatch(a, row)) : void 0,
|
|
1256
1765
|
searchParams: params,
|
|
1257
1766
|
onParamsChange: handleParamsChange
|
|
@@ -1260,12 +1769,6 @@ function RemoteSearchResultsImpl({
|
|
|
1260
1769
|
}
|
|
1261
1770
|
var SduiSearchResults = boxed(SduiSearchResultsImpl, "SduiSearchResults");
|
|
1262
1771
|
var RemoteSearchResults = boxed(RemoteSearchResultsImpl, "RemoteSearchResults");
|
|
1263
|
-
var SduiRuntimeContext = createContext(null);
|
|
1264
|
-
function useSduiRuntime() {
|
|
1265
|
-
const runtime = useContext(SduiRuntimeContext);
|
|
1266
|
-
if (!runtime) throw new Error("useSduiRuntime\uC740 SduiRenderer \uB0B4\uBD80\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
1267
|
-
return runtime;
|
|
1268
|
-
}
|
|
1269
1772
|
function prefillParams(search, hash) {
|
|
1270
1773
|
const params = new URLSearchParams(search);
|
|
1271
1774
|
const q = hash.indexOf("?");
|
|
@@ -1302,21 +1805,21 @@ function SduiForm({
|
|
|
1302
1805
|
return JSON.stringify(keys.map((key) => [key, state[key]]));
|
|
1303
1806
|
};
|
|
1304
1807
|
useEffect(() => {
|
|
1305
|
-
const
|
|
1808
|
+
const entries2 = {};
|
|
1306
1809
|
for (const [field, value] of Object.entries(initial ?? {})) {
|
|
1307
|
-
if (runtime.state[`${id}.${field}`] === void 0)
|
|
1810
|
+
if (runtime.state[`${id}.${field}`] === void 0) entries2[`${id}.${field}`] = value;
|
|
1308
1811
|
}
|
|
1309
1812
|
if (initialFromUrl && typeof window !== "undefined") {
|
|
1310
1813
|
const params = prefillParams(window.location.search, window.location.hash);
|
|
1311
1814
|
for (const [field, param] of Object.entries(initialFromUrl)) {
|
|
1312
1815
|
const raw = params.get(param);
|
|
1313
1816
|
if (raw !== null && runtime.state[`${id}.${field}`] === void 0) {
|
|
1314
|
-
|
|
1817
|
+
entries2[`${id}.${field}`] = raw;
|
|
1315
1818
|
}
|
|
1316
1819
|
}
|
|
1317
1820
|
}
|
|
1318
|
-
if (Object.keys(
|
|
1319
|
-
initialSnapshot.current = sliceOf({ ...runtime.state, ...
|
|
1821
|
+
if (Object.keys(entries2).length > 0) runtime.setEntries(entries2);
|
|
1822
|
+
initialSnapshot.current = sliceOf({ ...runtime.state, ...entries2 });
|
|
1320
1823
|
}, [id]);
|
|
1321
1824
|
useEffect(() => {
|
|
1322
1825
|
return () => {
|
|
@@ -1337,6 +1840,25 @@ function SduiForm({
|
|
|
1337
1840
|
}
|
|
1338
1841
|
}, [slice, id, runtime]);
|
|
1339
1842
|
const dirty = Boolean(runtime.state[`${id}.$dirty`]);
|
|
1843
|
+
const latestState = useRef(runtime.state);
|
|
1844
|
+
latestState.current = runtime.state;
|
|
1845
|
+
const dirtyRegistry = useContext(SduiDirtyRegistryContext);
|
|
1846
|
+
useEffect(
|
|
1847
|
+
() => dirtyRegistry?.register(
|
|
1848
|
+
() => initialSnapshot.current !== null && sliceOf(latestState.current) !== initialSnapshot.current
|
|
1849
|
+
),
|
|
1850
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1851
|
+
[dirtyRegistry, id]
|
|
1852
|
+
);
|
|
1853
|
+
const leaveRegistry = useContext(SduiLeaveRegistryContext);
|
|
1854
|
+
useEffect(() => {
|
|
1855
|
+
if (!guard || !leaveRegistry) return;
|
|
1856
|
+
return leaveRegistry.register(
|
|
1857
|
+
id,
|
|
1858
|
+
() => initialSnapshot.current !== null && sliceOf(latestState.current) !== initialSnapshot.current
|
|
1859
|
+
);
|
|
1860
|
+
}, [guard, leaveRegistry, id]);
|
|
1861
|
+
const scope = useMemo(() => ({ id, rules: JSON.parse(rulesJson) }), [id, rulesJson]);
|
|
1340
1862
|
useEffect(() => {
|
|
1341
1863
|
if (!guard || !dirty) return;
|
|
1342
1864
|
const onBeforeUnload = (event) => {
|
|
@@ -1351,12 +1873,13 @@ function SduiForm({
|
|
|
1351
1873
|
{
|
|
1352
1874
|
className: ["wp-col", className].filter(Boolean).join(" "),
|
|
1353
1875
|
style,
|
|
1876
|
+
"data-sdui-form": id,
|
|
1354
1877
|
onSubmit: (event) => {
|
|
1355
1878
|
event.preventDefault();
|
|
1356
1879
|
onSubmit?.(event);
|
|
1357
1880
|
},
|
|
1358
1881
|
noValidate: true,
|
|
1359
|
-
children
|
|
1882
|
+
children: /* @__PURE__ */ jsx(SduiFormScopeContext.Provider, { value: scope, children })
|
|
1360
1883
|
}
|
|
1361
1884
|
);
|
|
1362
1885
|
}
|
|
@@ -1698,6 +2221,9 @@ function withProps(Component, defaults) {
|
|
|
1698
2221
|
function deferChainEntry(container, key) {
|
|
1699
2222
|
return typeof container.action === "string" && (key === "onSuccess" || key === "onError");
|
|
1700
2223
|
}
|
|
2224
|
+
function deferEachTarget(container, key) {
|
|
2225
|
+
return container.action === "request" && container.each !== void 0 && (key === "endpoint" || key === "body");
|
|
2226
|
+
}
|
|
1701
2227
|
function failureDetailOf(error) {
|
|
1702
2228
|
const message = error instanceof Error ? error.message : "\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4";
|
|
1703
2229
|
const carrier = error;
|
|
@@ -1717,12 +2243,14 @@ function runSuccessChain(kind, id, run) {
|
|
|
1717
2243
|
);
|
|
1718
2244
|
}
|
|
1719
2245
|
}
|
|
2246
|
+
var EMPTY_ENDPOINT_MESSAGE = "request \uC561\uC158\uC758 endpoint\uAC00 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4 \u2014 $state \uCC38\uC870\uC5D0 \uC544\uC9C1 \uAC12\uC774 \uC5C6\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694";
|
|
1720
2247
|
async function runRequestAction(action, deps) {
|
|
2248
|
+
if (action.each !== void 0) return runRequestEach(action, deps);
|
|
1721
2249
|
const pendingKey = typeof action.pendingKey === "string" && action.pendingKey !== "" ? action.pendingKey : null;
|
|
1722
2250
|
let pendingOn = false;
|
|
1723
2251
|
try {
|
|
1724
2252
|
if (typeof action.endpoint !== "string" || action.endpoint === "") {
|
|
1725
|
-
throw new Error(
|
|
2253
|
+
throw new Error(EMPTY_ENDPOINT_MESSAGE);
|
|
1726
2254
|
}
|
|
1727
2255
|
if (pendingKey) {
|
|
1728
2256
|
pendingOn = true;
|
|
@@ -1756,6 +2284,91 @@ async function runRequestAction(action, deps) {
|
|
|
1756
2284
|
if (pendingKey && pendingOn) deps.dispatch({ action: "setState", key: pendingKey, value: false });
|
|
1757
2285
|
}
|
|
1758
2286
|
}
|
|
2287
|
+
var REQUEST_EACH_CONCURRENCY = 3;
|
|
2288
|
+
async function mapWithLimit(items, limit, fn) {
|
|
2289
|
+
const results = new Array(items.length);
|
|
2290
|
+
let next = 0;
|
|
2291
|
+
const worker = async () => {
|
|
2292
|
+
while (next < items.length) {
|
|
2293
|
+
const index = next;
|
|
2294
|
+
next += 1;
|
|
2295
|
+
results[index] = await fn(items[index], index);
|
|
2296
|
+
}
|
|
2297
|
+
};
|
|
2298
|
+
const width = Math.max(1, Math.min(limit, items.length));
|
|
2299
|
+
await Promise.all(Array.from({ length: width }, () => worker()));
|
|
2300
|
+
return results;
|
|
2301
|
+
}
|
|
2302
|
+
function eachFailureMessage(total, failed, first) {
|
|
2303
|
+
const head = `${total}\uAC74 \uC911 ${failed}\uAC74 \uC2E4\uD328`;
|
|
2304
|
+
return first ? `${head} \u2014 ${first}` : head;
|
|
2305
|
+
}
|
|
2306
|
+
async function runRequestEach(action, deps) {
|
|
2307
|
+
const list = action.each;
|
|
2308
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
2309
|
+
warnOnce(
|
|
2310
|
+
"request:each:empty",
|
|
2311
|
+
"[sdui] request \uC561\uC158\uC758 each \uAC00 \uBC30\uC5F4\uC774 \uC544\uB2C8\uAC70\uB098 \uBE44\uC5B4 \uC788\uC2B5\uB2C8\uB2E4 \u2014 \uC694\uCCAD\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (\uBC84\uD2BC\uC744 \uC120\uD0DD \uC218\uB85C \uC7A0\uADF8\uC138\uC694)",
|
|
2312
|
+
"warn"
|
|
2313
|
+
);
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
const pendingKey = typeof action.pendingKey === "string" && action.pendingKey !== "" ? action.pendingKey : null;
|
|
2317
|
+
const resolveFor = deps.resolveFor ?? ((value) => value);
|
|
2318
|
+
const method = action.method ?? "POST";
|
|
2319
|
+
if (pendingKey) deps.dispatch({ action: "setState", key: pendingKey, value: true });
|
|
2320
|
+
try {
|
|
2321
|
+
const outcomes = await mapWithLimit(list, REQUEST_EACH_CONCURRENCY, async (item, index) => {
|
|
2322
|
+
try {
|
|
2323
|
+
const endpoint = resolveFor(action.endpoint, item, index);
|
|
2324
|
+
if (typeof endpoint !== "string" || endpoint === "") throw new Error(EMPTY_ENDPOINT_MESSAGE);
|
|
2325
|
+
const body = action.body === void 0 ? void 0 : resolveFor(action.body, item, index);
|
|
2326
|
+
const value = await deps.fetch(endpoint, { method, ...body === void 0 ? {} : { body } });
|
|
2327
|
+
return { ok: true, value };
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
return { ok: false, error };
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2332
|
+
const errors = [];
|
|
2333
|
+
outcomes.forEach((outcome, index) => {
|
|
2334
|
+
if (!outcome.ok) errors.push({ index, item: list[index], ...failureDetailOf(outcome.error) });
|
|
2335
|
+
});
|
|
2336
|
+
const succeeded = outcomes.length - errors.length;
|
|
2337
|
+
const invalidate = typeof action.invalidate === "string" && action.invalidate !== "" ? action.invalidate : null;
|
|
2338
|
+
if (errors.length === 0) {
|
|
2339
|
+
runSuccessChain("request", `each:${list.length}`, () => {
|
|
2340
|
+
if (invalidate) deps.dispatch({ action: "invalidate", prefix: invalidate });
|
|
2341
|
+
const responses = outcomes.map((outcome) => outcome.ok ? outcome.value : null);
|
|
2342
|
+
(action.onSuccess ?? []).forEach((a) => deps.dispatch(a, responses));
|
|
2343
|
+
});
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
if (succeeded > 0 && invalidate) {
|
|
2347
|
+
runSuccessChain("request", `each:${list.length}`, () => deps.dispatch({ action: "invalidate", prefix: invalidate }));
|
|
2348
|
+
}
|
|
2349
|
+
const failure = {
|
|
2350
|
+
failed: errors.length,
|
|
2351
|
+
succeeded,
|
|
2352
|
+
total: list.length,
|
|
2353
|
+
message: eachFailureMessage(list.length, errors.length, errors[0]?.message),
|
|
2354
|
+
errors
|
|
2355
|
+
};
|
|
2356
|
+
if (typeof action.errorKey === "string" && action.errorKey !== "") {
|
|
2357
|
+
deps.dispatch({ action: "setState", key: action.errorKey, value: failure });
|
|
2358
|
+
}
|
|
2359
|
+
if (action.onError?.length) {
|
|
2360
|
+
action.onError.forEach((a) => deps.dispatch(a, failure));
|
|
2361
|
+
} else {
|
|
2362
|
+
deps.toast({
|
|
2363
|
+
tone: "danger",
|
|
2364
|
+
title: `${list.length}\uAC74 \uC911 ${errors.length}\uAC74 \uC2E4\uD328`,
|
|
2365
|
+
message: errors[0]?.message
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
} finally {
|
|
2369
|
+
if (pendingKey) deps.dispatch({ action: "setState", key: pendingKey, value: false });
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
1759
2372
|
|
|
1760
2373
|
// src/state-path.ts
|
|
1761
2374
|
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
@@ -2192,6 +2805,33 @@ function nearestArgsFrame(scope) {
|
|
|
2192
2805
|
}
|
|
2193
2806
|
return void 0;
|
|
2194
2807
|
}
|
|
2808
|
+
function toastText(value) {
|
|
2809
|
+
if (typeof value === "string") return value;
|
|
2810
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2811
|
+
if (isPlainObject(value) && typeof value.message === "string") return value.message;
|
|
2812
|
+
return void 0;
|
|
2813
|
+
}
|
|
2814
|
+
var DEFAULT_LEAVE_MESSAGE = "\uC800\uC7A5\uD558\uC9C0 \uC54A\uC740 \uBCC0\uACBD \uC0AC\uD56D\uC774 \uC788\uC2B5\uB2C8\uB2E4. \uC774 \uD654\uBA74\uC744 \uB5A0\uB098\uBA74 \uC0AC\uB77C\uC9D1\uB2C8\uB2E4.";
|
|
2815
|
+
function leaveMessageOf(value) {
|
|
2816
|
+
if (typeof value === "string") return value.trim() === "" ? null : value;
|
|
2817
|
+
return value ? DEFAULT_LEAVE_MESSAGE : null;
|
|
2818
|
+
}
|
|
2819
|
+
function titleText(value) {
|
|
2820
|
+
if (typeof value === "string") return value.trim() === "" ? null : value;
|
|
2821
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
2822
|
+
return null;
|
|
2823
|
+
}
|
|
2824
|
+
function collectFormValues(state, formId) {
|
|
2825
|
+
const prefix = `${formId}.`;
|
|
2826
|
+
const values = {};
|
|
2827
|
+
for (const [key, value] of Object.entries(state)) {
|
|
2828
|
+
if (key.startsWith(prefix) && !key.slice(prefix.length).startsWith("$") && value !== void 0) {
|
|
2829
|
+
values[key.slice(prefix.length)] = value;
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
return values;
|
|
2833
|
+
}
|
|
2834
|
+
var TOAST_MODAL_CSS = ".wp-toast-region.wp-toast-region--modal{top:16px;bottom:auto}@media screen and (max-width:767.98px){.wp-toast-region.wp-toast-region--modal{top:calc(12px + env(safe-area-inset-top));left:12px;right:12px}}";
|
|
2195
2835
|
function hasExplicitPageHead(body) {
|
|
2196
2836
|
return Array.isArray(body) && body.some((child) => isPlainObject(child) && child.component === "PageHead");
|
|
2197
2837
|
}
|
|
@@ -2269,45 +2909,76 @@ function toIndex(v) {
|
|
|
2269
2909
|
function inRange(index, length) {
|
|
2270
2910
|
return index !== null && index >= 0 && index < length;
|
|
2271
2911
|
}
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2912
|
+
var CHOICE_KINDS = /* @__PURE__ */ new Set([
|
|
2913
|
+
"Select",
|
|
2914
|
+
"AutocompleteField",
|
|
2915
|
+
"PositionSelect",
|
|
2916
|
+
"TagMultiSelect",
|
|
2917
|
+
"TreePicker",
|
|
2918
|
+
"SearchableMultiPicker",
|
|
2919
|
+
"RoleMemberPicker",
|
|
2920
|
+
"SegmentedControl",
|
|
2921
|
+
"Radio",
|
|
2922
|
+
"DatePicker",
|
|
2923
|
+
"DateRangePicker",
|
|
2924
|
+
"TimePicker"
|
|
2925
|
+
]);
|
|
2926
|
+
function requiredVerb(kind) {
|
|
2927
|
+
if (kind && CHOICE_KINDS.has(kind)) return "\uC120\uD0DD\uD558\uC138\uC694";
|
|
2928
|
+
if (kind === "FileDropzone") return "\uCCA8\uBD80\uD558\uC138\uC694";
|
|
2929
|
+
if (kind === "Checkbox" || kind === "Switch") return "\uD655\uC778\uD558\uC138\uC694";
|
|
2930
|
+
return "\uC785\uB825\uD558\uC138\uC694";
|
|
2931
|
+
}
|
|
2932
|
+
function validateForm(values, config, customValidators, fieldInfo) {
|
|
2933
|
+
const out = { errors: {}, failed: {} };
|
|
2934
|
+
if (!config) return out;
|
|
2935
|
+
const labelOf2 = (field) => config.labels[field] ?? fieldInfo?.get(field)?.label;
|
|
2275
2936
|
for (const [field, rules] of Object.entries(config.rules)) {
|
|
2276
2937
|
const value = values[field];
|
|
2277
|
-
const
|
|
2938
|
+
const context = {
|
|
2939
|
+
label: labelOf2(field),
|
|
2940
|
+
kind: fieldInfo?.get(field)?.kind,
|
|
2941
|
+
labelOf: labelOf2
|
|
2942
|
+
};
|
|
2278
2943
|
for (const rule of rules) {
|
|
2279
|
-
const message = ruleError(rule, value, values,
|
|
2944
|
+
const message = ruleError(rule, value, values, context, customValidators);
|
|
2280
2945
|
if (message) {
|
|
2281
|
-
errors[field] = message;
|
|
2946
|
+
out.errors[field] = message;
|
|
2947
|
+
out.failed[field] = rule.rule;
|
|
2282
2948
|
break;
|
|
2283
2949
|
}
|
|
2284
2950
|
}
|
|
2285
2951
|
}
|
|
2286
|
-
return
|
|
2952
|
+
return out;
|
|
2287
2953
|
}
|
|
2288
|
-
function ruleError(rule, value, values, label, customValidators) {
|
|
2954
|
+
function ruleError(rule, value, values, { label, kind, labelOf: labelOf2 }, customValidators) {
|
|
2289
2955
|
const empty = value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0 || // DateRangePicker의 {} 같은 "값 없는 객체"도 미입력으로 본다
|
|
2290
2956
|
typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => v === void 0 || v === null || v === "");
|
|
2957
|
+
const subject = (pair) => label ? `${josa(label, pair)} ` : "";
|
|
2291
2958
|
switch (rule.rule) {
|
|
2292
2959
|
case "required":
|
|
2293
|
-
return empty || value === false ? rule.message ?? `${label
|
|
2960
|
+
return empty || value === false ? rule.message ?? (label ? `${josa(label, "\uC744/\uB97C")} ${requiredVerb(kind)}` : "\uD544\uC218 \uD56D\uBAA9\uC785\uB2C8\uB2E4") : null;
|
|
2294
2961
|
case "min":
|
|
2295
|
-
return !empty && toNumber(value) < rule.value ? rule.message ?? `${
|
|
2962
|
+
return !empty && toNumber(value) < rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
|
|
2296
2963
|
case "max":
|
|
2297
|
-
return !empty && toNumber(value) > rule.value ? rule.message ?? `${
|
|
2964
|
+
return !empty && toNumber(value) > rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value} \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
|
|
2298
2965
|
case "minLength":
|
|
2299
|
-
return !empty && String(value).length < rule.value ? rule.message ?? `${
|
|
2966
|
+
return !empty && String(value).length < rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value}\uC790 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
|
|
2300
2967
|
case "maxLength":
|
|
2301
|
-
return !empty && String(value).length > rule.value ? rule.message ?? `${
|
|
2968
|
+
return !empty && String(value).length > rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value}\uC790 \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
|
|
2302
2969
|
case "pattern":
|
|
2303
|
-
return !empty && !new RegExp(rule.value).test(String(value)) ? rule.message ?? `${label} \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4` : null;
|
|
2970
|
+
return !empty && !new RegExp(rule.value).test(String(value)) ? rule.message ?? (label ? `${label} \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4` : "\uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4") : null;
|
|
2304
2971
|
case "notBeforeField": {
|
|
2305
2972
|
const other = values[rule.field];
|
|
2306
2973
|
if (empty || other === void 0 || other === null || other === "") return null;
|
|
2307
2974
|
const a = Number(String(value).replace(/,/g, ""));
|
|
2308
2975
|
const b = Number(String(other).replace(/,/g, ""));
|
|
2309
2976
|
const before = Number.isFinite(a) && Number.isFinite(b) && String(value).trim() !== "" ? a < b : String(value) < String(other);
|
|
2310
|
-
|
|
2977
|
+
if (!before) return null;
|
|
2978
|
+
if (rule.message) return rule.message;
|
|
2979
|
+
const otherLabel = labelOf2(rule.field);
|
|
2980
|
+
if (label && otherLabel) return `${josa(label, "\uC740/\uB294")} ${otherLabel}\uBCF4\uB2E4 \uC55E\uC124 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`;
|
|
2981
|
+
return `${subject("\uC774/\uAC00")}\uC21C\uC11C\uC5D0 \uB9DE\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`;
|
|
2311
2982
|
}
|
|
2312
2983
|
case "custom": {
|
|
2313
2984
|
const validator = customValidators?.[rule.name];
|
|
@@ -2320,6 +2991,122 @@ function ruleError(rule, value, values, label, customValidators) {
|
|
|
2320
2991
|
}
|
|
2321
2992
|
}
|
|
2322
2993
|
}
|
|
2994
|
+
function invalidSummary(result, labelOf2, order = []) {
|
|
2995
|
+
const rank = (field) => {
|
|
2996
|
+
const index = order.indexOf(field);
|
|
2997
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
2998
|
+
};
|
|
2999
|
+
const fields = Object.keys(result.errors).sort((a, b) => rank(a) - rank(b));
|
|
3000
|
+
const count = fields.length;
|
|
3001
|
+
const allRequired = fields.every((field) => result.failed[field] === "required");
|
|
3002
|
+
const labels = fields.map(labelOf2).filter((l) => typeof l === "string" && l !== "");
|
|
3003
|
+
if (count === 1 && labels.length === 1) return { title: result.errors[fields[0]] };
|
|
3004
|
+
const title = allRequired ? `\uD544\uC218 \uD56D\uBAA9 ${count}\uAC1C\uB97C \uCC44\uC6CC \uC8FC\uC138\uC694` : `\uC785\uB825\uAC12 ${count}\uAC1C\uB97C \uD655\uC778\uD574 \uC8FC\uC138\uC694`;
|
|
3005
|
+
if (labels.length === 0) return { title, message: count === 1 ? void 0 : result.errors[fields[0]] };
|
|
3006
|
+
const shown = labels.slice(0, 4).join(", ");
|
|
3007
|
+
const rest = labels.length - 4;
|
|
3008
|
+
return { title, message: rest > 0 ? `${shown} \uC678 ${rest}\uAC1C` : shown };
|
|
3009
|
+
}
|
|
3010
|
+
var FOCUSABLE_IN_FIELD = 'input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
3011
|
+
function formElementOf(formId) {
|
|
3012
|
+
if (typeof document === "undefined") return void 0;
|
|
3013
|
+
return Array.from(document.querySelectorAll("form[data-sdui-form]")).find(
|
|
3014
|
+
(el) => el.getAttribute("data-sdui-form") === formId && el.isConnected
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
3017
|
+
function fieldOrderOf(formId) {
|
|
3018
|
+
const form = formElementOf(formId);
|
|
3019
|
+
if (!form) return [];
|
|
3020
|
+
return Array.from(form.querySelectorAll("[data-sdui-field]")).map(
|
|
3021
|
+
(el) => el.getAttribute("data-sdui-field") ?? ""
|
|
3022
|
+
);
|
|
3023
|
+
}
|
|
3024
|
+
function focusFirstInvalid(formId, fields) {
|
|
3025
|
+
const form = formElementOf(formId);
|
|
3026
|
+
if (!form) return;
|
|
3027
|
+
const wanted = new Set(fields);
|
|
3028
|
+
const candidates = form.querySelectorAll('[data-sdui-field], [aria-invalid]:not([aria-invalid="false"])');
|
|
3029
|
+
const hit = Array.from(candidates).find((el) => {
|
|
3030
|
+
const field = el.getAttribute("data-sdui-field");
|
|
3031
|
+
return field !== null ? wanted.has(field) : true;
|
|
3032
|
+
});
|
|
3033
|
+
if (!hit) return;
|
|
3034
|
+
const target = hit.matches(FOCUSABLE_IN_FIELD) ? hit : hit.querySelector(FOCUSABLE_IN_FIELD);
|
|
3035
|
+
hit.scrollIntoView?.({ block: "center", inline: "nearest" });
|
|
3036
|
+
target?.focus({ preventScroll: true });
|
|
3037
|
+
}
|
|
3038
|
+
var INVALID_PROP_COMPONENTS = /* @__PURE__ */ new Set([
|
|
3039
|
+
"Input",
|
|
3040
|
+
"Textarea",
|
|
3041
|
+
"Select",
|
|
3042
|
+
"DatePicker",
|
|
3043
|
+
"DateRangePicker",
|
|
3044
|
+
"TimePicker",
|
|
3045
|
+
"AutocompleteField",
|
|
3046
|
+
"JsonField",
|
|
3047
|
+
"PositionSelect"
|
|
3048
|
+
]);
|
|
3049
|
+
var BINDING_PROPS = ["value", "checked", "values"];
|
|
3050
|
+
function formKeyOf(value, formId) {
|
|
3051
|
+
if (!isStateRef(value)) return null;
|
|
3052
|
+
const prefix = `${formId}.`;
|
|
3053
|
+
if (!value.$state.startsWith(prefix)) return null;
|
|
3054
|
+
const key = value.$state.slice(prefix.length);
|
|
3055
|
+
return key === "" || key.startsWith("$") ? null : key;
|
|
3056
|
+
}
|
|
3057
|
+
function boundInputOf(node, formId, depth = 0) {
|
|
3058
|
+
if (!Array.isArray(node.children)) return null;
|
|
3059
|
+
for (const child of node.children) {
|
|
3060
|
+
if (!isPlainObject(child)) continue;
|
|
3061
|
+
const childNode = child;
|
|
3062
|
+
const props = isPlainObject(childNode.props) ? childNode.props : {};
|
|
3063
|
+
for (const name of BINDING_PROPS) {
|
|
3064
|
+
const key = formKeyOf(props[name], formId);
|
|
3065
|
+
if (key) return { key, kind: String(childNode.component) };
|
|
3066
|
+
}
|
|
3067
|
+
const component = childNode.component;
|
|
3068
|
+
if (component === "Field" || component === "Form" || component === "ForEach" || component === "Fragment") continue;
|
|
3069
|
+
if (depth < 4) {
|
|
3070
|
+
const found = boundInputOf(childNode, formId, depth + 1);
|
|
3071
|
+
if (found) return found;
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
return null;
|
|
3075
|
+
}
|
|
3076
|
+
function hasRequiredRule(rules) {
|
|
3077
|
+
return Array.isArray(rules) && rules.some((rule) => isPlainObject(rule) && rule.rule === "required");
|
|
3078
|
+
}
|
|
3079
|
+
function applyFormBinding(node, props, form, engine) {
|
|
3080
|
+
const raw = isPlainObject(node.props) ? node.props : {};
|
|
3081
|
+
const errorOf = (key) => {
|
|
3082
|
+
const errors = engine.resolveValue({ $state: `${form.id}.$errors` }, EMPTY_SCOPE);
|
|
3083
|
+
const message = isPlainObject(errors) ? errors[key] : void 0;
|
|
3084
|
+
return typeof message === "string" && message !== "" ? message : void 0;
|
|
3085
|
+
};
|
|
3086
|
+
if (node.component === "Field") {
|
|
3087
|
+
const bound = boundInputOf(node, form.id);
|
|
3088
|
+
if (!bound) return;
|
|
3089
|
+
if (!Object.hasOwn(raw, "required") && hasRequiredRule(form.rules[bound.key])) props.required = true;
|
|
3090
|
+
if (!Object.hasOwn(raw, "error")) {
|
|
3091
|
+
const message = errorOf(bound.key);
|
|
3092
|
+
if (message) props.error = message;
|
|
3093
|
+
}
|
|
3094
|
+
props["data-sdui-field"] = bound.key;
|
|
3095
|
+
engine.noteField(form.id, bound.key, {
|
|
3096
|
+
label: typeof props.label === "string" && props.label !== "" ? props.label : void 0,
|
|
3097
|
+
kind: bound.kind
|
|
3098
|
+
});
|
|
3099
|
+
return;
|
|
3100
|
+
}
|
|
3101
|
+
if (typeof node.component !== "string" || !INVALID_PROP_COMPONENTS.has(node.component)) return;
|
|
3102
|
+
if (Object.hasOwn(raw, "invalid")) return;
|
|
3103
|
+
for (const name of BINDING_PROPS) {
|
|
3104
|
+
const key = formKeyOf(raw[name], form.id);
|
|
3105
|
+
if (!key) continue;
|
|
3106
|
+
if (errorOf(key)) props.invalid = true;
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
2323
3110
|
var EMPTY_SCOPE = [];
|
|
2324
3111
|
var DENIED_PROPS2 = /* @__PURE__ */ new Set([
|
|
2325
3112
|
"dangerouslySetInnerHTML",
|
|
@@ -2352,6 +3139,7 @@ function SduiNodeRenderer({
|
|
|
2352
3139
|
engine
|
|
2353
3140
|
}) {
|
|
2354
3141
|
const { registry, resolveValue, dispatch, resolveAsset } = engine;
|
|
3142
|
+
const formScope = React9.useContext(SduiFormScopeContext);
|
|
2355
3143
|
const slots = isPlainObject(node.slots) ? node.slots : void 0;
|
|
2356
3144
|
const childList = childrenOf(node);
|
|
2357
3145
|
if (node.component === "ForEach") {
|
|
@@ -2416,6 +3204,10 @@ function SduiNodeRenderer({
|
|
|
2416
3204
|
props[name] = resolved;
|
|
2417
3205
|
}
|
|
2418
3206
|
}
|
|
3207
|
+
if (formScope) applyFormBinding(node, props, formScope, engine);
|
|
3208
|
+
if (typeof node.component === "string" && INVALID_PROP_COMPONENTS.has(node.component) && props.invalid !== void 0 && typeof props.invalid !== "boolean") {
|
|
3209
|
+
props.invalid = Boolean(props.invalid);
|
|
3210
|
+
}
|
|
2419
3211
|
const childLayout = childLayoutFrom(props);
|
|
2420
3212
|
if (childLayout) {
|
|
2421
3213
|
for (const name of CHILD_LAYOUT_PROPS) delete props[name];
|
|
@@ -2512,14 +3304,30 @@ function SduiFragmentBody({
|
|
|
2512
3304
|
return null;
|
|
2513
3305
|
}
|
|
2514
3306
|
const page = spec;
|
|
2515
|
-
if (page.data || page.urlState || page.subscribe) {
|
|
3307
|
+
if (page.data || page.urlState || page.subscribe || page.leaveGuard !== void 0 || page.titleFrom !== void 0) {
|
|
2516
3308
|
warnOnce(
|
|
2517
3309
|
`fragment:page-features:${id}`,
|
|
2518
|
-
`[sdui] Fragment \uC2A4\uD399 ${id}\uC758 data/urlState/subscribe\
|
|
3310
|
+
`[sdui] Fragment \uC2A4\uD399 ${id}\uC758 data/urlState/subscribe/leaveGuard/titleFrom\uC740 \uBB34\uC2DC\uB429\uB2C8\uB2E4 \u2014 \uC870\uAC01\uC740 body\uB9CC \uB80C\uB354\uD569\uB2C8\uB2E4`
|
|
2519
3311
|
);
|
|
2520
3312
|
}
|
|
2521
3313
|
return /* @__PURE__ */ jsx(SduiFragmentChainContext.Provider, { value: nextChain, children: page.body.map((child, i) => renderChild(child, i, bodyScope, engine)) });
|
|
2522
3314
|
}
|
|
3315
|
+
function SduiToastRegion({ hostId }) {
|
|
3316
|
+
const board = useSyncExternalStore(subscribeToasts, toastSnapshot, toastServerSnapshot);
|
|
3317
|
+
const toasts = useMemo(
|
|
3318
|
+
() => board.entries.filter((entry) => toastHostOf(entry, board.hosts) === hostId),
|
|
3319
|
+
[board, hostId]
|
|
3320
|
+
);
|
|
3321
|
+
useEffect(() => {
|
|
3322
|
+
if (toasts.length > 0) markToastsShown(toasts.map((t) => t.id));
|
|
3323
|
+
}, [toasts]);
|
|
3324
|
+
const modalOpen = useAnyModalOpen();
|
|
3325
|
+
if (toasts.length === 0) return null;
|
|
3326
|
+
return /* @__PURE__ */ jsxs("div", { className: modalOpen ? "wp-toast-region wp-toast-region--modal" : "wp-toast-region", children: [
|
|
3327
|
+
/* @__PURE__ */ jsx(HoistedStyle, { id: "wp-sdui-toast-modal", css: TOAST_MODAL_CSS }),
|
|
3328
|
+
toasts.map((t) => /* @__PURE__ */ jsx(Toast, { tone: t.tone, title: t.title, onClose: () => closeToastEntry(t.id), children: t.message }, t.id))
|
|
3329
|
+
] });
|
|
3330
|
+
}
|
|
2523
3331
|
function urlSeedSettled(state, seeded, baseline) {
|
|
2524
3332
|
return Object.keys(seeded).every(
|
|
2525
3333
|
(key) => Object.is(state[key], seeded[key]) || !Object.is(state[key], baseline[key])
|
|
@@ -2544,24 +3352,43 @@ function SduiRenderer({
|
|
|
2544
3352
|
const urlSeeded = useRef(false);
|
|
2545
3353
|
const urlSeedPending = useRef(null);
|
|
2546
3354
|
const urlHomePath = useRef(typeof window !== "undefined" ? window.location.pathname : "");
|
|
2547
|
-
const [toasts, setToasts] = useState([]);
|
|
2548
|
-
const toastSeq = useRef(0);
|
|
2549
|
-
const toastTimers = useRef([]);
|
|
2550
3355
|
const stateRef = useRef(state);
|
|
2551
3356
|
stateRef.current = state;
|
|
2552
3357
|
const formsRef = useRef(/* @__PURE__ */ new Map());
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
3358
|
+
const fieldInfoRef = useRef(/* @__PURE__ */ new Map());
|
|
3359
|
+
const noteField = useCallback((formId, key, info) => {
|
|
3360
|
+
let fields = fieldInfoRef.current.get(formId);
|
|
3361
|
+
if (!fields) {
|
|
3362
|
+
fields = /* @__PURE__ */ new Map();
|
|
3363
|
+
fieldInfoRef.current.set(formId, fields);
|
|
3364
|
+
}
|
|
3365
|
+
fields.set(key, info);
|
|
2556
3366
|
}, []);
|
|
3367
|
+
const [toastHostId] = useState(allocateToastHostId);
|
|
3368
|
+
const toastChannel = React9.useContext(OverlayContainerContext);
|
|
3369
|
+
useEffect(() => registerToastHost(toastHostId, toastChannel), [toastHostId, toastChannel]);
|
|
2557
3370
|
useEffect(() => {
|
|
2558
3371
|
if (!DEV) return;
|
|
2559
3372
|
const issues = validateSpec(page, new Set(Object.keys(registry)));
|
|
2560
3373
|
issues.forEach((issue) => console.warn(`[sdui:spec] ${issue.path} \u2014 ${issue.message}`));
|
|
2561
3374
|
}, [page, registry]);
|
|
2562
|
-
const
|
|
2563
|
-
|
|
2564
|
-
|
|
3375
|
+
const leaveFormsRef = useRef(/* @__PURE__ */ new Map());
|
|
3376
|
+
const leaveRegistry = useMemo(
|
|
3377
|
+
() => ({
|
|
3378
|
+
register: (formId, isDirty) => {
|
|
3379
|
+
leaveFormsRef.current.set(formId, isDirty);
|
|
3380
|
+
return () => {
|
|
3381
|
+
if (leaveFormsRef.current.get(formId) === isDirty) leaveFormsRef.current.delete(formId);
|
|
3382
|
+
};
|
|
3383
|
+
}
|
|
3384
|
+
}),
|
|
3385
|
+
[]
|
|
3386
|
+
);
|
|
3387
|
+
const leavingRef = useRef(false);
|
|
3388
|
+
const setEntries = useCallback((entries2) => {
|
|
3389
|
+
stateRef.current = { ...stateRef.current, ...entries2 };
|
|
3390
|
+
leavingRef.current = false;
|
|
3391
|
+
setState((prev) => ({ ...prev, ...entries2 }));
|
|
2565
3392
|
}, []);
|
|
2566
3393
|
useEffect(() => {
|
|
2567
3394
|
if (!syncUrlState || urlSeeded.current || !page.urlState?.length || typeof window === "undefined") return;
|
|
@@ -2585,6 +3412,16 @@ function SduiRenderer({
|
|
|
2585
3412
|
setEntries(seeded);
|
|
2586
3413
|
}
|
|
2587
3414
|
}, [page.urlState, setEntries, syncUrlState]);
|
|
3415
|
+
const [urlResync, setUrlResync] = useState(0);
|
|
3416
|
+
const hasUrlState = Boolean(page.urlState?.length);
|
|
3417
|
+
useEffect(() => {
|
|
3418
|
+
if (!syncUrlState || !hasUrlState || typeof window === "undefined") return;
|
|
3419
|
+
const onPop = () => {
|
|
3420
|
+
if (window.location.pathname === urlHomePath.current) setUrlResync((n) => n + 1);
|
|
3421
|
+
};
|
|
3422
|
+
window.addEventListener("popstate", onPop);
|
|
3423
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
3424
|
+
}, [syncUrlState, hasUrlState]);
|
|
2588
3425
|
useEffect(() => {
|
|
2589
3426
|
if (!syncUrlState || !page.urlState?.length || !urlSeeded.current || typeof window === "undefined") return;
|
|
2590
3427
|
if (window.location.pathname !== urlHomePath.current) return;
|
|
@@ -2620,7 +3457,7 @@ function SduiRenderer({
|
|
|
2620
3457
|
);
|
|
2621
3458
|
}
|
|
2622
3459
|
}
|
|
2623
|
-
}, [state, page.urlState, syncUrlState]);
|
|
3460
|
+
}, [state, page.urlState, syncUrlState, urlResync]);
|
|
2624
3461
|
const subscribeChannels = page.subscribe?.join(",") ?? "";
|
|
2625
3462
|
const hasSubscribe = typeof host.subscribe === "function";
|
|
2626
3463
|
useEffect(() => {
|
|
@@ -2641,16 +3478,11 @@ function SduiRenderer({
|
|
|
2641
3478
|
unsubscribe();
|
|
2642
3479
|
};
|
|
2643
3480
|
}, [subscribeChannels, hasSubscribe]);
|
|
2644
|
-
const closeToast = useCallback((id) => {
|
|
2645
|
-
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
2646
|
-
}, []);
|
|
2647
3481
|
const pushToast = useCallback(
|
|
2648
3482
|
(toast) => {
|
|
2649
|
-
|
|
2650
|
-
setToasts((prev) => [...prev, { ...toast, id }]);
|
|
2651
|
-
toastTimers.current.push(setTimeout(() => closeToast(id), 4e3));
|
|
3483
|
+
pushToastEntry(toastHostId, toastChannel, toast);
|
|
2652
3484
|
},
|
|
2653
|
-
[
|
|
3485
|
+
[toastHostId, toastChannel]
|
|
2654
3486
|
);
|
|
2655
3487
|
const lookupState = useCallback((key) => {
|
|
2656
3488
|
const current = stateRef.current;
|
|
@@ -2711,7 +3543,7 @@ function SduiRenderer({
|
|
|
2711
3543
|
if (isPlainObject(value)) {
|
|
2712
3544
|
const out = {};
|
|
2713
3545
|
for (const [k, v] of Object.entries(value)) {
|
|
2714
|
-
out[k] = deferChainEntry(value, k) || deferRunEntry(k, v) ? v : resolveValue(v, scope);
|
|
3546
|
+
out[k] = deferChainEntry(value, k) || deferRunEntry(k, v) || deferEachTarget(value, k) ? v : resolveValue(v, scope);
|
|
2715
3547
|
}
|
|
2716
3548
|
return out;
|
|
2717
3549
|
}
|
|
@@ -2719,6 +3551,76 @@ function SduiRenderer({
|
|
|
2719
3551
|
},
|
|
2720
3552
|
[lookupState]
|
|
2721
3553
|
);
|
|
3554
|
+
const leaveMessageNow = useCallback(() => {
|
|
3555
|
+
if (leavingRef.current) return null;
|
|
3556
|
+
if (page.leaveGuard !== void 0) {
|
|
3557
|
+
const message = leaveMessageOf(resolveValue(page.leaveGuard, EMPTY_SCOPE));
|
|
3558
|
+
if (message) return message;
|
|
3559
|
+
}
|
|
3560
|
+
for (const [formId, isDirty] of leaveFormsRef.current) {
|
|
3561
|
+
if (stateRef.current[`${formId}.$resetDirty`] === true) continue;
|
|
3562
|
+
if (isDirty()) return DEFAULT_LEAVE_MESSAGE;
|
|
3563
|
+
}
|
|
3564
|
+
return null;
|
|
3565
|
+
}, [page.leaveGuard, resolveValue]);
|
|
3566
|
+
const leaveSentRef = useRef(null);
|
|
3567
|
+
const unloadHandlerRef = useRef(null);
|
|
3568
|
+
const syncLeaveGuard = useCallback(() => {
|
|
3569
|
+
const message = leaveMessageNow();
|
|
3570
|
+
if (message === leaveSentRef.current) return message;
|
|
3571
|
+
leaveSentRef.current = message;
|
|
3572
|
+
hostRef.current.setLeaveGuard?.(message);
|
|
3573
|
+
if (typeof window === "undefined") return message;
|
|
3574
|
+
if (message !== null && !unloadHandlerRef.current) {
|
|
3575
|
+
const handler = (event) => {
|
|
3576
|
+
event.preventDefault();
|
|
3577
|
+
event.returnValue = "";
|
|
3578
|
+
};
|
|
3579
|
+
window.addEventListener("beforeunload", handler);
|
|
3580
|
+
unloadHandlerRef.current = handler;
|
|
3581
|
+
} else if (message === null && unloadHandlerRef.current) {
|
|
3582
|
+
window.removeEventListener("beforeunload", unloadHandlerRef.current);
|
|
3583
|
+
unloadHandlerRef.current = null;
|
|
3584
|
+
}
|
|
3585
|
+
return message;
|
|
3586
|
+
}, [leaveMessageNow]);
|
|
3587
|
+
useEffect(() => {
|
|
3588
|
+
syncLeaveGuard();
|
|
3589
|
+
});
|
|
3590
|
+
useEffect(
|
|
3591
|
+
() => () => {
|
|
3592
|
+
if (leaveSentRef.current !== null) {
|
|
3593
|
+
leaveSentRef.current = null;
|
|
3594
|
+
hostRef.current.setLeaveGuard?.(null);
|
|
3595
|
+
}
|
|
3596
|
+
if (unloadHandlerRef.current && typeof window !== "undefined") {
|
|
3597
|
+
window.removeEventListener("beforeunload", unloadHandlerRef.current);
|
|
3598
|
+
unloadHandlerRef.current = null;
|
|
3599
|
+
}
|
|
3600
|
+
},
|
|
3601
|
+
[]
|
|
3602
|
+
);
|
|
3603
|
+
const [pendingLeave, setPendingLeave] = useState(null);
|
|
3604
|
+
const titleFrom = page.titleFrom;
|
|
3605
|
+
const resolvedTitle = useMemo(
|
|
3606
|
+
() => titleFrom === void 0 ? null : titleText(resolveValue(titleFrom, EMPTY_SCOPE)),
|
|
3607
|
+
// state 가 바뀌어야 다시 푼다 — resolveValue 는 ref 를 읽어 정체성이 안 바뀐다.
|
|
3608
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3609
|
+
[titleFrom, resolveValue, state]
|
|
3610
|
+
);
|
|
3611
|
+
const effectiveTitle = resolvedTitle ?? page.title;
|
|
3612
|
+
useEffect(() => {
|
|
3613
|
+
if (titleFrom === void 0) return;
|
|
3614
|
+
hostRef.current.setTitle?.(effectiveTitle ?? null);
|
|
3615
|
+
}, [titleFrom, effectiveTitle]);
|
|
3616
|
+
useEffect(
|
|
3617
|
+
() => () => {
|
|
3618
|
+
if (titleFrom !== void 0) hostRef.current.setTitle?.(null);
|
|
3619
|
+
},
|
|
3620
|
+
// 마운트 1회 계약 — 라우트가 바뀌면 호스트가 key 로 갈아 끼운다.
|
|
3621
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3622
|
+
[]
|
|
3623
|
+
);
|
|
2722
3624
|
const dataPlan = useMemo(
|
|
2723
3625
|
() => planPageData(page.data, (value) => resolveValue(value, EMPTY_SCOPE)),
|
|
2724
3626
|
// state를 의존성에 두어야 $state를 참조하는 경로가 값 변화를 따라간다.
|
|
@@ -2808,6 +3710,34 @@ function SduiRenderer({
|
|
|
2808
3710
|
revertUnsettledStamps(fetchedRef.current, runnable, epochs, settled);
|
|
2809
3711
|
};
|
|
2810
3712
|
}, [dataPlanKey, dataEpochKey, setEntries]);
|
|
3713
|
+
const revalidateField = useCallback(
|
|
3714
|
+
(key) => {
|
|
3715
|
+
for (const [formId, config] of formsRef.current) {
|
|
3716
|
+
const prefix = `${formId}.`;
|
|
3717
|
+
if (!key.startsWith(prefix)) continue;
|
|
3718
|
+
const field = key.slice(prefix.length);
|
|
3719
|
+
if (field === "" || field.startsWith("$")) continue;
|
|
3720
|
+
const errors = stateRef.current[`${formId}.$errors`];
|
|
3721
|
+
if (!isPlainObject(errors) || !Object.hasOwn(errors, field)) continue;
|
|
3722
|
+
const next = { ...errors };
|
|
3723
|
+
try {
|
|
3724
|
+
const result = validateForm(
|
|
3725
|
+
collectFormValues(stateRef.current, formId),
|
|
3726
|
+
config,
|
|
3727
|
+
customValidators,
|
|
3728
|
+
fieldInfoRef.current.get(formId)
|
|
3729
|
+
);
|
|
3730
|
+
const message = result.errors[field];
|
|
3731
|
+
if (message) next[field] = message;
|
|
3732
|
+
else delete next[field];
|
|
3733
|
+
} catch {
|
|
3734
|
+
delete next[field];
|
|
3735
|
+
}
|
|
3736
|
+
setEntries({ [`${formId}.$errors`]: next });
|
|
3737
|
+
}
|
|
3738
|
+
},
|
|
3739
|
+
[customValidators, setEntries]
|
|
3740
|
+
);
|
|
2811
3741
|
const dispatchRef = useRef(() => {
|
|
2812
3742
|
});
|
|
2813
3743
|
const dispatch = useCallback(
|
|
@@ -2824,17 +3754,24 @@ function SduiRenderer({
|
|
|
2824
3754
|
eventScope
|
|
2825
3755
|
);
|
|
2826
3756
|
switch (action.action) {
|
|
2827
|
-
case "navigate":
|
|
3757
|
+
case "navigate": {
|
|
2828
3758
|
if (!isSafeNavigationTarget(action.to)) {
|
|
2829
3759
|
warnOnce(`navigate:${String(action.to)}`, `[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 navigate \uB300\uC0C1: ${String(action.to)}`);
|
|
2830
3760
|
break;
|
|
2831
3761
|
}
|
|
3762
|
+
const leaveMessage = syncLeaveGuard();
|
|
3763
|
+
if (leaveMessage !== null && typeof hostRef.current.setLeaveGuard !== "function") {
|
|
3764
|
+
setPendingLeave({ to: action.to, message: leaveMessage });
|
|
3765
|
+
break;
|
|
3766
|
+
}
|
|
2832
3767
|
hostRef.current.navigate(action.to);
|
|
2833
3768
|
break;
|
|
3769
|
+
}
|
|
2834
3770
|
case "setState": {
|
|
2835
3771
|
const nextValue = action.fromEvent ? eventValue(arg) : action.value;
|
|
2836
3772
|
if (!Array.isArray(action.path) || action.path.length === 0) {
|
|
2837
3773
|
setEntries({ [action.key]: nextValue });
|
|
3774
|
+
revalidateField(action.key);
|
|
2838
3775
|
break;
|
|
2839
3776
|
}
|
|
2840
3777
|
const written = setAtPath(
|
|
@@ -2850,6 +3787,7 @@ function SduiRenderer({
|
|
|
2850
3787
|
break;
|
|
2851
3788
|
}
|
|
2852
3789
|
setEntries({ [action.key]: written.next });
|
|
3790
|
+
revalidateField(action.key);
|
|
2853
3791
|
break;
|
|
2854
3792
|
}
|
|
2855
3793
|
case "open":
|
|
@@ -2859,7 +3797,7 @@ function SduiRenderer({
|
|
|
2859
3797
|
setEntries({ [action.key]: false });
|
|
2860
3798
|
break;
|
|
2861
3799
|
case "toast":
|
|
2862
|
-
pushToast({ tone: action.tone ?? "info", title: action.title, message: action.message });
|
|
3800
|
+
pushToast({ tone: action.tone ?? "info", title: toastText(action.title), message: toastText(action.message) });
|
|
2863
3801
|
break;
|
|
2864
3802
|
case "arrayAppend": {
|
|
2865
3803
|
const current = stateRef.current[action.key];
|
|
@@ -2926,16 +3864,21 @@ function SduiRenderer({
|
|
|
2926
3864
|
let phase = "validate";
|
|
2927
3865
|
let pendingOn = false;
|
|
2928
3866
|
try {
|
|
2929
|
-
const
|
|
2930
|
-
const
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
|
-
const errors = validateForm(values, formsRef.current.get(formId), customValidators);
|
|
3867
|
+
const values = collectFormValues(stateRef.current, formId);
|
|
3868
|
+
const config = formsRef.current.get(formId);
|
|
3869
|
+
const fieldInfo = fieldInfoRef.current.get(formId);
|
|
3870
|
+
const result = validateForm(values, config, customValidators, fieldInfo);
|
|
3871
|
+
const errors = result.errors;
|
|
2937
3872
|
if (Object.keys(errors).length > 0) {
|
|
2938
3873
|
setEntries({ [`${formId}.$errors`]: errors, [`${formId}.$submitting`]: false });
|
|
3874
|
+
const summary = invalidSummary(
|
|
3875
|
+
result,
|
|
3876
|
+
(field) => config?.labels[field] ?? fieldInfo?.get(field)?.label,
|
|
3877
|
+
fieldOrderOf(formId)
|
|
3878
|
+
);
|
|
3879
|
+
pushToast({ tone: "warning", title: summary.title, message: summary.message });
|
|
3880
|
+
const fields = Object.keys(errors);
|
|
3881
|
+
setTimeout(() => focusFirstInvalid(formId, fields), 0);
|
|
2939
3882
|
return;
|
|
2940
3883
|
}
|
|
2941
3884
|
setEntries({ [`${formId}.$errors`]: {}, [`${formId}.$submitting`]: true });
|
|
@@ -2980,7 +3923,10 @@ function SduiRenderer({
|
|
|
2980
3923
|
// scope 를 잇는다 — request 의 onSuccess/onError 도 같은 자리의 텍스트다.
|
|
2981
3924
|
// {"$arg"} 체인 항목은 dispatch 진입부가 이 스코프로 펼친다(submitForm과 동일).
|
|
2982
3925
|
dispatch: (a, value) => dispatchRef.current(a, value, scope),
|
|
2983
|
-
toast: pushToast
|
|
3926
|
+
toast: pushToast,
|
|
3927
|
+
// each: 원소를 행 프레임으로 얹어 endpoint·body 를 원소마다 푼다 — 같은 자리의
|
|
3928
|
+
// $state·$arg·$event 는 그대로 보이고, $item/$index 만 이 원소를 가리킨다.
|
|
3929
|
+
resolveFor: (value, item, index) => resolveValue(value, [...eventScope, { item, index }])
|
|
2984
3930
|
});
|
|
2985
3931
|
break;
|
|
2986
3932
|
case "invalidate": {
|
|
@@ -3027,12 +3973,12 @@ function SduiRenderer({
|
|
|
3027
3973
|
break;
|
|
3028
3974
|
}
|
|
3029
3975
|
},
|
|
3030
|
-
[pushToast, onCustomAction, customValidators, setEntries, resolveValue]
|
|
3976
|
+
[pushToast, onCustomAction, customValidators, setEntries, resolveValue, revalidateField, syncLeaveGuard]
|
|
3031
3977
|
);
|
|
3032
3978
|
dispatchRef.current = dispatch;
|
|
3033
3979
|
const engine = useMemo(
|
|
3034
|
-
() => ({ registry, resolveValue, dispatch, resolveAsset: assetResolverOf(host) }),
|
|
3035
|
-
[registry, resolveValue, dispatch, host]
|
|
3980
|
+
() => ({ registry, resolveValue, dispatch, resolveAsset: assetResolverOf(host), noteField }),
|
|
3981
|
+
[registry, resolveValue, dispatch, host, noteField]
|
|
3036
3982
|
);
|
|
3037
3983
|
const runtime = useMemo(
|
|
3038
3984
|
() => ({
|
|
@@ -3050,15 +3996,43 @@ function SduiRenderer({
|
|
|
3050
3996
|
return (
|
|
3051
3997
|
// 해석된 어댑터를 트리에 흘려보낸다 — dispatch를 거치지 않고 스스로 요청을 내는
|
|
3052
3998
|
// 컴포넌트(목록 조회 등)도 호스트가 건 잠금 안에서 움직이게 하기 위해서다(adapter.ts 주석).
|
|
3053
|
-
/* @__PURE__ */ jsx(SduiAdapterContext.Provider, { value: host, children: /* @__PURE__ */ jsx(SduiDispatchContext.Provider, { value: dispatch, children: /* @__PURE__ */ jsx(SduiRuntimeContext.Provider, { value: runtime, children: /* @__PURE__ */ jsx(SduiPermissionProvider, { can, children: /* @__PURE__ */ jsxs("div", { className: "sdui-root", children: [
|
|
3999
|
+
/* @__PURE__ */ jsx(SduiAdapterContext.Provider, { value: host, children: /* @__PURE__ */ jsx(SduiDispatchContext.Provider, { value: dispatch, children: /* @__PURE__ */ jsx(SduiRuntimeContext.Provider, { value: runtime, children: /* @__PURE__ */ jsx(SduiPermissionProvider, { can, children: /* @__PURE__ */ jsx(SduiLeaveRegistryContext.Provider, { value: leaveRegistry, children: /* @__PURE__ */ jsxs("div", { className: "sdui-root", children: [
|
|
3054
4000
|
/* @__PURE__ */ jsx(HoistedStyle, { id: "wp-sdui-layout", css: LAYOUT_CSS }),
|
|
3055
|
-
!hasExplicitPageHead(page.body) && (
|
|
3056
|
-
|
|
4001
|
+
!hasExplicitPageHead(page.body) && (effectiveTitle || page.subtitle) ? /* @__PURE__ */ jsx("div", { className: "wp-page-head", children: /* @__PURE__ */ jsxs("div", { className: "wp-page-head__text", children: [
|
|
4002
|
+
effectiveTitle ? /* @__PURE__ */ jsx("h1", { className: "wp-page-head__title", children: effectiveTitle }) : null,
|
|
3057
4003
|
page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head__sub", children: page.subtitle }) : null
|
|
3058
4004
|
] }) }) : null,
|
|
3059
4005
|
/* @__PURE__ */ jsx("div", { className: "wp-col", children: Array.isArray(page.body) ? page.body.map((child, i) => renderChild(child, i, EMPTY_SCOPE, engine)) : /* @__PURE__ */ jsx(SpecErrorCard, { children: "[sdui] page.body\uB294 \uBC30\uC5F4\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4" }) }),
|
|
3060
|
-
|
|
3061
|
-
|
|
4006
|
+
/* @__PURE__ */ jsx(SduiToastRegion, { hostId: toastHostId }),
|
|
4007
|
+
pendingLeave ? /* @__PURE__ */ jsx(OverlayLayer, { onClose: () => setPendingLeave(null), children: /* @__PURE__ */ jsx(
|
|
4008
|
+
Dialog,
|
|
4009
|
+
{
|
|
4010
|
+
size: "sm",
|
|
4011
|
+
icon: "triangle-alert",
|
|
4012
|
+
tone: "danger",
|
|
4013
|
+
title: "\uC800\uC7A5\uD558\uC9C0 \uC54A\uACE0 \uB098\uAC08\uAE4C\uC694?",
|
|
4014
|
+
onClose: () => setPendingLeave(null),
|
|
4015
|
+
footer: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4016
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", "data-autofocus": "", onClick: () => setPendingLeave(null), children: "\uACC4\uC18D \uC791\uC131" }),
|
|
4017
|
+
/* @__PURE__ */ jsx(
|
|
4018
|
+
Button,
|
|
4019
|
+
{
|
|
4020
|
+
variant: "danger",
|
|
4021
|
+
onClick: () => {
|
|
4022
|
+
const { to } = pendingLeave;
|
|
4023
|
+
setPendingLeave(null);
|
|
4024
|
+
leavingRef.current = true;
|
|
4025
|
+
syncLeaveGuard();
|
|
4026
|
+
hostRef.current.navigate(to);
|
|
4027
|
+
},
|
|
4028
|
+
children: "\uB098\uAC00\uAE30"
|
|
4029
|
+
}
|
|
4030
|
+
)
|
|
4031
|
+
] }),
|
|
4032
|
+
children: pendingLeave.message
|
|
4033
|
+
}
|
|
4034
|
+
) }) : null
|
|
4035
|
+
] }) }) }) }) }) })
|
|
3062
4036
|
);
|
|
3063
4037
|
}
|
|
3064
4038
|
|
|
@@ -3119,4 +4093,4 @@ async function resolvePatternSpec(id, load) {
|
|
|
3119
4093
|
// src/icon-names.ts
|
|
3120
4094
|
var ICON_NAMES = Object.keys(ICON_NODES);
|
|
3121
4095
|
|
|
3122
|
-
export { Can, ICON_NAMES, NodeErrorBoundary, RemoteSearchResults, ResultTable, SduiForm, SduiRenderer, SduiSearchDataProvider, SduiSearchInvalidationProvider, SduiSearchResults, SearchResults, SortHandle, SortableList, cellRenderer, createRegistry, createSearchInvalidator, defaultRegistry, imageRenderer, isValidSpecId, mapActions, mapCardRows, mapColumns, mapGridCard, mapViews, matchesCondition, normalizePageSizes, patternSpecCandidates, resolvePatternSpec, sortableDropIndex, sortableMove, useRemoteSearchPage,
|
|
4096
|
+
export { Can, ICON_NAMES, NodeErrorBoundary, ORDER_PARAM, RemoteSearchResults, ResultEmptyState, ResultTable, SORT_PARAM, SduiForm, SduiRenderer, SduiSearchDataProvider, SduiSearchInvalidationProvider, SduiSearchResults, SearchResults, SearchSummaryCards, SortHandle, SortableList, cellRenderer, conditionParams, createRegistry, createSearchInvalidator, createServerSortMemory, defaultRegistry, imageRenderer, isValidSpecId, mapActions, mapCardRows, mapColumns, mapGridCard, mapViews, matchesCondition, nextSort, normalizePageSizes, patternSpecCandidates, resolvePatternSpec, serverSortMemory, sortEchoOf, sortFromParams, sortableDropIndex, sortableMove, summaryParams, useRemoteSearchPage, useRemoteSearchSummary, useSearchFetcher, validateSpec, withProps, withoutSort };
|