@wipco/sdui 1.5.0 → 1.6.1
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-7P7N43JV.js → chunk-34BABTH4.js} +2 -2
- package/dist/{chunk-DMLDKW7K.js → chunk-37BNG4DT.js} +1 -1
- package/dist/{chunk-5WW4MBRO.js → chunk-3JTBOABA.js} +6 -2
- package/dist/{chunk-HBIIEYKW.js → chunk-3PCR73OC.js} +4 -4
- package/dist/{chunk-VY7MVBSE.js → chunk-3PT5OBOU.js} +2 -2
- package/dist/{chunk-YVTV4ZV6.js → chunk-4YPPL7JK.js} +1 -1
- package/dist/{chunk-E6ISNQK3.js → chunk-6YBSBC77.js} +31 -8
- package/dist/{chunk-YWUDBLO6.js → chunk-7G6J6QFC.js} +2 -2
- package/dist/{chunk-UV6HHFEW.js → chunk-A57GKV5F.js} +129 -68
- package/dist/{chunk-I45EUZC6.js → chunk-AJ3KDBEE.js} +4 -2
- package/dist/chunk-BITS7UJY.js +333 -0
- package/dist/{chunk-DOL57OOP.js → chunk-BV37UCDF.js} +3 -3
- package/dist/chunk-CSZCWBOE.js +256 -0
- package/dist/{chunk-XGFDY43I.js → chunk-CUFGH3LB.js} +88 -18
- package/dist/{chunk-A6LAIGUS.js → chunk-EGFDELSF.js} +7 -4
- package/dist/{chunk-JIV4YHCZ.js → chunk-EJIWOD7W.js} +1 -1
- package/dist/{chunk-CXW6KT6X.js → chunk-ESAFQXEU.js} +3 -3
- package/dist/{chunk-N5RKM6IC.js → chunk-EUSC5NMZ.js} +4 -4
- package/dist/{chunk-RWSH2MBU.js → chunk-FP32LNFK.js} +73 -19
- package/dist/{chunk-44TDRCKR.js → chunk-GKYUDTMI.js} +5 -5
- package/dist/chunk-GSM664TX.js +38 -0
- package/dist/{chunk-I5SD7XRA.js → chunk-GSXD52XG.js} +3 -3
- package/dist/{chunk-O4IISNE7.js → chunk-IB2ZPUTF.js} +51 -12
- package/dist/{chunk-YOZN35HD.js → chunk-IIRVHSNK.js} +3 -3
- package/dist/{chunk-FNMS7OB7.js → chunk-J7BZF4T4.js} +5 -5
- package/dist/{chunk-KO6OEY3G.js → chunk-JHFA5RDN.js} +3 -13
- package/dist/{chunk-DKVSM5HX.js → chunk-KDKRFWIF.js} +6 -6
- package/dist/chunk-KO3EUNWB.js +380 -0
- package/dist/{chunk-JYAR7D5U.js → chunk-NLSR3JYU.js} +2 -2
- package/dist/{chunk-ZORQPB6B.js → chunk-O4EMQWCV.js} +325 -360
- package/dist/{chunk-2HLXCKK7.js → chunk-OCYJ2ZOV.js} +2 -2
- package/dist/{chunk-OQ52XMGG.js → chunk-OKNF2SBB.js} +4 -4
- package/dist/{chunk-GNA6SX2H.js → chunk-OQ7A3SFM.js} +2 -2
- package/dist/{chunk-A5XQOEVH.js → chunk-PCFH74RD.js} +4 -4
- package/dist/{chunk-BEM2YS7U.js → chunk-SGGIWPP6.js} +3 -3
- package/dist/{chunk-NIJUVTEW.js → chunk-TB7G5DXQ.js} +3 -3
- package/dist/{chunk-GZBY7I4Y.js → chunk-TWUKNUUQ.js} +66 -10
- package/dist/{chunk-WPSK2CB5.js → chunk-UC725BGW.js} +16 -5
- package/dist/{chunk-ZFNFPXPR.js → chunk-US3K6Q5T.js} +2 -2
- package/dist/{chunk-MPT7WJG2.js → chunk-USNPJEXH.js} +5 -5
- package/dist/{chunk-SAPBDJJQ.js → chunk-UZZLMPRW.js} +2 -2
- package/dist/{chunk-Q4SWUINA.js → chunk-VAXRTBJH.js} +1 -1
- package/dist/{chunk-RYBBPFI7.js → chunk-VQKX5PJ4.js} +4 -4
- package/dist/chunk-VR5MFUD2.js +106 -0
- package/dist/{chunk-JZXU22PE.js → chunk-VSNHDHGS.js} +208 -117
- package/dist/{chunk-2YP5N4VV.js → chunk-VZOQRQAR.js} +1 -1
- package/dist/{chunk-AXGF4M57.js → chunk-XAXROQKB.js} +1 -1
- package/dist/{chunk-IZ3JA2BR.js → chunk-XFKNU6QN.js} +3 -3
- package/dist/{chunk-GTGS2QRL.js → chunk-XUVWIRZJ.js} +3 -3
- package/dist/{chunk-RNOUXUBO.js → chunk-YYAGE733.js} +323 -59
- package/dist/index.d.ts +449 -91
- package/dist/index.js +1060 -138
- package/dist/{layout-C3nRXMwb.d.ts → layout-DmRikwZb.d.ts} +5 -4
- package/dist/meta.d.ts +1 -1
- package/dist/meta.js +351 -25
- package/dist/rich/ExcelUploadCard.d.ts +1 -1
- package/dist/rich/ExcelUploadCard.js +5 -4
- package/dist/rich.d.ts +22 -3
- package/dist/rich.js +304 -35
- package/dist/styles.css +119 -4
- package/dist/{types-COjGKgmd.d.ts → types-CMikpoN5.d.ts} +65 -2
- 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 +17 -3
- 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-4YPPL7JK.js';
|
|
2
|
+
import { useSduiDispatch, SduiDialog, useSduiCan, useSduiRuntime, SduiDirtyRegistryContext, SduiLeaveRegistryContext, SduiFormScopeContext, SduiDispatchContext, SduiRuntimeContext, SduiPermissionProvider } from './chunk-VR5MFUD2.js';
|
|
3
|
+
export { SduiDialog, SduiPermissionProvider, useSduiCan, useSduiDispatch, useSduiRuntime } from './chunk-VR5MFUD2.js';
|
|
4
|
+
import { Text, Spacer, Show, Section, Row, PageHead, KeyValueList, Grid, Divider, Col, Actions } from './chunk-UC725BGW.js';
|
|
5
|
+
export { Actions, Col, Divider, Grid, KeyValueList, PageHead, Row, Section, Show, Spacer, Text } from './chunk-UC725BGW.js';
|
|
6
|
+
import { ImageViewer } from './chunk-UZZLMPRW.js';
|
|
7
|
+
export { ImageViewer } from './chunk-UZZLMPRW.js';
|
|
8
|
+
import { normalizeSearchFields, searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-A57GKV5F.js';
|
|
9
|
+
export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-A57GKV5F.js';
|
|
10
|
+
import { parseYmd, YMD_RE, TimePicker, DateRangePicker, DatePicker } from './chunk-FP32LNFK.js';
|
|
11
|
+
export { DatePicker, DateRangePicker, TimePicker } from './chunk-FP32LNFK.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-JHFA5RDN.js';
|
|
16
|
+
export { defaultAdapter, useSduiAssetResolver, useSduiSandbox } from './chunk-JHFA5RDN.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-YYAGE733.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-YYAGE733.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,7 +659,7 @@ 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 };
|
|
473
663
|
}
|
|
474
664
|
function useRemoteSearchSummary(endpoint, params) {
|
|
475
665
|
const adapter = useSduiAdapter();
|
|
@@ -523,6 +713,8 @@ function summaryParams(params) {
|
|
|
523
713
|
const next = new URLSearchParams(params);
|
|
524
714
|
next.delete("page");
|
|
525
715
|
next.delete("limit");
|
|
716
|
+
next.delete("sort");
|
|
717
|
+
next.delete("order");
|
|
526
718
|
return next;
|
|
527
719
|
}
|
|
528
720
|
function plainValue(value) {
|
|
@@ -545,6 +737,9 @@ function SearchSummaryCards({ items, values, dispatch }) {
|
|
|
545
737
|
}
|
|
546
738
|
var OVERSCAN = 10;
|
|
547
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
|
+
}
|
|
548
743
|
function ResultTable({
|
|
549
744
|
columns,
|
|
550
745
|
rows,
|
|
@@ -552,10 +747,12 @@ function ResultTable({
|
|
|
552
747
|
onSort,
|
|
553
748
|
onRowClick,
|
|
554
749
|
selectedIds,
|
|
750
|
+
pickedIds,
|
|
555
751
|
empty,
|
|
556
752
|
virtual = false,
|
|
557
753
|
height = 560,
|
|
558
|
-
rowHeight = 41
|
|
754
|
+
rowHeight = 41,
|
|
755
|
+
stackable = true
|
|
559
756
|
}) {
|
|
560
757
|
const scrollRef = useRef(null);
|
|
561
758
|
const firstRowRef = useRef(null);
|
|
@@ -571,6 +768,7 @@ function ResultTable({
|
|
|
571
768
|
});
|
|
572
769
|
if (!rows.length && empty) return /* @__PURE__ */ jsx(Fragment, { children: empty });
|
|
573
770
|
const selectedSet = selectedIds && selectedIds.length > 0 ? new Set(selectedIds) : null;
|
|
771
|
+
const pickedSet = pickedIds && pickedIds.length > 0 ? new Set(pickedIds) : null;
|
|
574
772
|
const unit = measured ?? rowHeight;
|
|
575
773
|
const start = virtual ? Math.max(0, Math.floor(scrollTop / unit) - OVERSCAN) : 0;
|
|
576
774
|
const end = virtual ? Math.min(rows.length, Math.ceil((scrollTop + height) / unit) + OVERSCAN) : rows.length;
|
|
@@ -579,13 +777,14 @@ function ResultTable({
|
|
|
579
777
|
const table = /* @__PURE__ */ jsxs(
|
|
580
778
|
"table",
|
|
581
779
|
{
|
|
582
|
-
className: `wp-table wp-table--hover${virtual ? " wp-table--sticky wp-table--clip" : ""}`,
|
|
780
|
+
className: `wp-table wp-table--hover${virtual ? " wp-table--sticky wp-table--clip" : stackable ? " wp-table--stackable" : ""}`,
|
|
583
781
|
style: virtual ? { tableLayout: "fixed" } : void 0,
|
|
584
782
|
children: [
|
|
585
783
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((column) => /* @__PURE__ */ jsx(
|
|
586
784
|
"th",
|
|
587
785
|
{
|
|
588
786
|
style: { width: column.width, textAlign: column.align === "right" ? "right" : void 0 },
|
|
787
|
+
"data-mobile": column.mobile,
|
|
589
788
|
children: column.sortable && onSort ? (
|
|
590
789
|
// 인라인 style로는 :hover를 표현할 수 없다 — 색 피드백은 .wp-table__sort:hover가 낸다
|
|
591
790
|
/* @__PURE__ */ jsxs("button", { type: "button", className: "wp-table__sort", onClick: () => onSort(column.key), children: [
|
|
@@ -607,11 +806,14 @@ function ResultTable({
|
|
|
607
806
|
rows.slice(start, end).map((row, offset) => {
|
|
608
807
|
const index = start + offset;
|
|
609
808
|
const selected = selectedSet ? selectedSet.has(row.id) : false;
|
|
809
|
+
const picked = pickedSet ? pickedSet.has(String(row.id)) : false;
|
|
610
810
|
return /* @__PURE__ */ jsx(
|
|
611
811
|
"tr",
|
|
612
812
|
{
|
|
613
813
|
ref: offset === 0 ? firstRowRef : void 0,
|
|
614
814
|
"aria-selected": selected || void 0,
|
|
815
|
+
"aria-current": picked ? "true" : void 0,
|
|
816
|
+
className: picked ? "wp-row--selected" : void 0,
|
|
615
817
|
onClick: onRowClick ? () => onRowClick(row) : void 0,
|
|
616
818
|
style: onRowClick ? { cursor: "pointer" } : void 0,
|
|
617
819
|
children: columns.map((column) => {
|
|
@@ -619,10 +821,20 @@ function ResultTable({
|
|
|
619
821
|
if (column.align === "right") cls.push("wp-table__num");
|
|
620
822
|
if (column.mono) cls.push("wp-table__mono");
|
|
621
823
|
if (column.strong) cls.push("wp-table__strong");
|
|
622
|
-
|
|
824
|
+
const value = column.render ? column.render(row, index) : (
|
|
623
825
|
// 평면 키 우선, 없으면 점 경로 — 셀 렌더러(cells.tsx)와 같은 조회 규칙이다
|
|
624
826
|
fieldOf(row, column.key)
|
|
625
|
-
)
|
|
827
|
+
);
|
|
828
|
+
return /* @__PURE__ */ jsx(
|
|
829
|
+
"td",
|
|
830
|
+
{
|
|
831
|
+
className: cls.join(" ") || void 0,
|
|
832
|
+
"data-label": labelOf(column),
|
|
833
|
+
"data-mobile": column.mobile,
|
|
834
|
+
children: isBlankCell(value) ? /* @__PURE__ */ jsx("span", { style: { color: "var(--wp-text-subtle)" }, children: "\u2014" }) : value
|
|
835
|
+
},
|
|
836
|
+
column.key
|
|
837
|
+
);
|
|
626
838
|
})
|
|
627
839
|
},
|
|
628
840
|
row.id ?? index
|
|
@@ -633,14 +845,42 @@ function ResultTable({
|
|
|
633
845
|
]
|
|
634
846
|
}
|
|
635
847
|
);
|
|
636
|
-
if (!virtual)
|
|
848
|
+
if (!virtual) {
|
|
849
|
+
return /* @__PURE__ */ jsx("div", { className: "wp-table-scroll", children: table });
|
|
850
|
+
}
|
|
637
851
|
return /* @__PURE__ */ jsxs("div", { ref: scrollRef, onScroll: handleScroll, style: { height, overflowY: "auto" }, children: [
|
|
638
852
|
/* @__PURE__ */ jsx(HoistedStyle, { id: "wp-table-clip", css: CLIP_CSS }),
|
|
639
853
|
table
|
|
640
854
|
] });
|
|
641
855
|
}
|
|
642
|
-
function
|
|
643
|
-
|
|
856
|
+
function labelOf(column) {
|
|
857
|
+
if (typeof column.label === "string") return column.label;
|
|
858
|
+
return typeof column.header === "string" ? column.header : "";
|
|
859
|
+
}
|
|
860
|
+
function ResultEmptyState({ title, filtered = false, emptyTitle, emptyText, scoped = false, onReset }) {
|
|
861
|
+
const name = String(title ?? "").trim() || "\uD56D\uBAA9";
|
|
862
|
+
if (filtered) {
|
|
863
|
+
return /* @__PURE__ */ jsx(
|
|
864
|
+
EmptyState,
|
|
865
|
+
{
|
|
866
|
+
icon: "scan-search",
|
|
867
|
+
title: `\uC870\uAC74\uC5D0 \uB9DE\uB294 ${name}${josaOf(name, "\uC774/\uAC00")} \uC5C6\uC2B5\uB2C8\uB2E4`,
|
|
868
|
+
tight: true,
|
|
869
|
+
actions: onReset ? /* @__PURE__ */ jsx(Button, { size: "sm", variant: "secondary", icon: "refresh-cw", onClick: onReset, children: "\uC870\uAC74 \uCD08\uAE30\uD654" }) : void 0,
|
|
870
|
+
children: "\uAC80\uC0C9\uC5B4\uB098 \uC870\uAC74\uC744 \uBC14\uAFD4 \uBCF4\uC138\uC694."
|
|
871
|
+
}
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
return /* @__PURE__ */ jsx(EmptyState, { title: emptyTitle || `${scoped ? "" : "\uC544\uC9C1 "}${name}${josaOf(name, "\uC774/\uAC00")} \uC5C6\uC2B5\uB2C8\uB2E4`, tight: true, children: emptyText || void 0 });
|
|
875
|
+
}
|
|
876
|
+
function normalizeSelectedIds(ids) {
|
|
877
|
+
if (!Array.isArray(ids)) return [];
|
|
878
|
+
const out = [];
|
|
879
|
+
for (const id of ids) {
|
|
880
|
+
if (typeof id === "string" && id !== "") out.push(id);
|
|
881
|
+
else if (typeof id === "number" && Number.isFinite(id)) out.push(String(id));
|
|
882
|
+
}
|
|
883
|
+
return out;
|
|
644
884
|
}
|
|
645
885
|
var EMPTY_FILTERS = [];
|
|
646
886
|
var EMPTY_FIELDS = [];
|
|
@@ -649,6 +889,29 @@ var DEFAULT_VIEWS = [
|
|
|
649
889
|
{ value: "list", icon: "list", label: "\uBAA9\uB85D" },
|
|
650
890
|
{ value: "grid", icon: "layout-grid", label: "\uCE74\uB4DC" }
|
|
651
891
|
];
|
|
892
|
+
var VIEW_STORAGE_PREFIX = "wp-sdui:view:";
|
|
893
|
+
var viewMemory = /* @__PURE__ */ new Map();
|
|
894
|
+
function readStoredView(title) {
|
|
895
|
+
const inMemory = viewMemory.get(title);
|
|
896
|
+
if (inMemory) return inMemory;
|
|
897
|
+
try {
|
|
898
|
+
if (typeof window === "undefined") return null;
|
|
899
|
+
const stored = window.sessionStorage.getItem(VIEW_STORAGE_PREFIX + title);
|
|
900
|
+
return stored === "grid" || stored === "list" ? stored : null;
|
|
901
|
+
} catch {
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function writeStoredView(title, view) {
|
|
906
|
+
viewMemory.set(title, view);
|
|
907
|
+
try {
|
|
908
|
+
if (typeof window !== "undefined") window.sessionStorage.setItem(VIEW_STORAGE_PREFIX + title, view);
|
|
909
|
+
} catch {
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function looksLikeLocator(value) {
|
|
913
|
+
return /^(https?:)?\/\//.test(value) || value.startsWith("/") || value.startsWith("data:");
|
|
914
|
+
}
|
|
652
915
|
function normalizePageSizes(sizes) {
|
|
653
916
|
if (!Array.isArray(sizes)) return void 0;
|
|
654
917
|
const clean = [...new Set(sizes.map(Number).filter((n) => Number.isFinite(n) && n > 0).map(Math.floor))].sort(
|
|
@@ -697,9 +960,18 @@ function SearchResults({
|
|
|
697
960
|
hideHeader = false,
|
|
698
961
|
advancedSearchFields: rawAdvancedSearchFields = EMPTY_FIELDS,
|
|
699
962
|
searchPlaceholder,
|
|
700
|
-
virtual
|
|
963
|
+
virtual,
|
|
964
|
+
serverUnsortable,
|
|
965
|
+
primaryKey,
|
|
966
|
+
emptyTitle,
|
|
967
|
+
emptyText,
|
|
968
|
+
serverFiltered = false,
|
|
969
|
+
selectedIds
|
|
701
970
|
}) {
|
|
702
971
|
const hasActions = actions.length > 0;
|
|
972
|
+
const isMobile = useIsMobile();
|
|
973
|
+
const pickedIds = normalizeSelectedIds(selectedIds);
|
|
974
|
+
const pickedSet = pickedIds.length > 0 ? new Set(pickedIds) : null;
|
|
703
975
|
const advancedSearchFields = useMemo(
|
|
704
976
|
() => normalizeSearchFields(rawAdvancedSearchFields),
|
|
705
977
|
[rawAdvancedSearchFields]
|
|
@@ -715,12 +987,18 @@ function SearchResults({
|
|
|
715
987
|
);
|
|
716
988
|
const applyViewConstraints = (candidate) => viewItems.some((v) => v.value === candidate) ? candidate : viewItems[0].value;
|
|
717
989
|
const appliedSearch = searchParams.get("search") ?? "";
|
|
718
|
-
const [
|
|
990
|
+
const [chosenView, setChosenView] = useState(() => readStoredView(title));
|
|
719
991
|
const [search, setSearch] = useState(appliedSearch);
|
|
720
|
-
const [sort, setSort] = useState(null);
|
|
721
992
|
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
722
993
|
const selectedRowsRef = useRef(/* @__PURE__ */ new Map());
|
|
723
|
-
const
|
|
994
|
+
const hasGridView = viewItems.some((v) => v.value === "grid");
|
|
995
|
+
const defaultView = isMobile && hasGridView ? "grid" : initialView;
|
|
996
|
+
const activeView = applyViewConstraints(chosenView ?? defaultView);
|
|
997
|
+
const chooseView = (next) => {
|
|
998
|
+
const view = applyViewConstraints(next);
|
|
999
|
+
writeStoredView(title, view);
|
|
1000
|
+
setChosenView(view);
|
|
1001
|
+
};
|
|
724
1002
|
useEffect(() => {
|
|
725
1003
|
setSearch(appliedSearch);
|
|
726
1004
|
}, [appliedSearch]);
|
|
@@ -737,33 +1015,43 @@ function SearchResults({
|
|
|
737
1015
|
onParamsChange(next);
|
|
738
1016
|
};
|
|
739
1017
|
const activeFilters = useMemo(() => {
|
|
740
|
-
const
|
|
1018
|
+
const entries2 = [];
|
|
741
1019
|
for (const filter of [...leftFilters, ...rightFilters]) {
|
|
742
1020
|
const values = splitMultiValue(searchParams.get(filter.key) ?? "");
|
|
743
|
-
if (values.length > 0)
|
|
1021
|
+
if (values.length > 0) entries2.push({ key: filter.key, values });
|
|
744
1022
|
}
|
|
745
1023
|
if (tabs && typeof tabs.key === "string") {
|
|
746
1024
|
const values = splitMultiValue(searchParams.get(tabs.key) ?? "");
|
|
747
|
-
if (values.length > 0)
|
|
1025
|
+
if (values.length > 0) entries2.push({ key: tabs.key, values });
|
|
748
1026
|
}
|
|
749
|
-
return
|
|
1027
|
+
return entries2;
|
|
750
1028
|
}, [leftFilters, rightFilters, tabs, searchParams]);
|
|
751
1029
|
const hasAllRows = item.data.length >= item.total;
|
|
1030
|
+
const sortAllowed = (key) => hasAllRows || !(serverUnsortable === "all" || Array.isArray(serverUnsortable) && serverUnsortable.includes(key));
|
|
1031
|
+
const requestedSort = sortFromParams(searchParams);
|
|
1032
|
+
const sortKey = requestedSort && sortAllowed(requestedSort.key) ? requestedSort.key : null;
|
|
1033
|
+
const sortDir = requestedSort?.dir ?? "asc";
|
|
1034
|
+
const sort = useMemo(() => sortKey ? { key: sortKey, dir: sortDir } : null, [sortKey, sortDir]);
|
|
752
1035
|
const processed = useMemo(() => {
|
|
753
1036
|
const cell = (row, key) => fieldOf(row, key);
|
|
754
1037
|
let rows = item.data;
|
|
755
|
-
if (hasAllRows) {
|
|
1038
|
+
if (hasAllRows && !serverFiltered) {
|
|
1039
|
+
const source = item.data;
|
|
1040
|
+
const known = (key) => source.some((row) => cell(row, key) !== void 0);
|
|
756
1041
|
if (appliedSearch) {
|
|
757
1042
|
rows = rows.filter((row) => includesLooseSearch(localSearchText(row), appliedSearch));
|
|
758
1043
|
}
|
|
759
1044
|
for (const { key, values } of activeFilters) {
|
|
1045
|
+
if (!known(key)) continue;
|
|
760
1046
|
rows = rows.filter((row) => values.includes(String(cell(row, key))));
|
|
761
1047
|
}
|
|
762
1048
|
for (const field of advancedSearchFields) {
|
|
763
1049
|
if (field.type === "select") {
|
|
1050
|
+
if (!known(field.key)) continue;
|
|
764
1051
|
const wanted = splitMultiValue(advancedValues[field.key] ?? "");
|
|
765
1052
|
if (wanted.length > 0) rows = rows.filter((row) => wanted.includes(String(cell(row, field.key))));
|
|
766
1053
|
} else if (field.type === "cascading") {
|
|
1054
|
+
if (!known(field.key)) continue;
|
|
767
1055
|
const wanted = splitMultiValue(advancedValues[field.key] ?? "");
|
|
768
1056
|
if (wanted.length > 0) {
|
|
769
1057
|
const lineages = cascadingValuePaths(field.options);
|
|
@@ -774,12 +1062,14 @@ function SearchResults({
|
|
|
774
1062
|
});
|
|
775
1063
|
}
|
|
776
1064
|
} else if (field.type === "dateRange") {
|
|
1065
|
+
if (!known(field.key)) continue;
|
|
777
1066
|
const from = advancedValues[field.fromKey ?? `${field.key}From`];
|
|
778
1067
|
const to = advancedValues[field.toKey ?? `${field.key}To`];
|
|
779
|
-
const datePart = (v) => String(v ?? "").slice(0, 10);
|
|
1068
|
+
const datePart = (v) => isZonedIsoDateTime(v) ? formatDateValue(v, "date") : String(v ?? "").slice(0, 10);
|
|
780
1069
|
if (from) rows = rows.filter((row) => datePart(cell(row, field.key)) >= from.slice(0, 10));
|
|
781
1070
|
if (to) rows = rows.filter((row) => datePart(cell(row, field.key)) <= to.slice(0, 10));
|
|
782
1071
|
} else if (field.type === "numberRange") {
|
|
1072
|
+
if (!known(field.key)) continue;
|
|
783
1073
|
const min = numericValue(advancedValues[field.fromKey ?? `${field.key}Min`]);
|
|
784
1074
|
const max = numericValue(advancedValues[field.toKey ?? `${field.key}Max`]);
|
|
785
1075
|
if (min !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? -Infinity) >= min);
|
|
@@ -788,11 +1078,13 @@ function SearchResults({
|
|
|
788
1078
|
const keyword = advancedValues[field.key];
|
|
789
1079
|
const suffix = ["_contains", "Contains"].find((s) => field.key.endsWith(s));
|
|
790
1080
|
const baseKey = suffix ? field.key.slice(0, -suffix.length) : field.key;
|
|
791
|
-
if (keyword
|
|
1081
|
+
if (keyword && known(baseKey)) {
|
|
1082
|
+
rows = rows.filter((row) => includesLooseSearch(String(cell(row, baseKey) ?? ""), keyword));
|
|
1083
|
+
}
|
|
792
1084
|
}
|
|
793
1085
|
}
|
|
794
1086
|
}
|
|
795
|
-
if (sort) {
|
|
1087
|
+
if (sort && hasAllRows) {
|
|
796
1088
|
const column = columns.find((c) => c.key === sort.key);
|
|
797
1089
|
const dirFactor = sort.dir === "asc" ? 1 : -1;
|
|
798
1090
|
rows = [...rows].sort((a, b) => {
|
|
@@ -807,7 +1099,7 @@ function SearchResults({
|
|
|
807
1099
|
});
|
|
808
1100
|
}
|
|
809
1101
|
return rows;
|
|
810
|
-
}, [item.data, hasAllRows, appliedSearch, activeFilters, advancedSearchFields, advancedValues, sort, columns]);
|
|
1102
|
+
}, [item.data, hasAllRows, serverFiltered, appliedSearch, activeFilters, advancedSearchFields, advancedValues, sort, columns]);
|
|
811
1103
|
const clientPaginated = hasAllRows;
|
|
812
1104
|
const totalCount = clientPaginated ? processed.length : item.total;
|
|
813
1105
|
const pageCount = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
@@ -839,7 +1131,50 @@ function SearchResults({
|
|
|
839
1131
|
});
|
|
840
1132
|
};
|
|
841
1133
|
const selectedItems = Array.from(selected).map((id) => selectedRowsRef.current.get(id)).filter((row) => row !== void 0);
|
|
1134
|
+
const paramsKey = searchParams.toString();
|
|
1135
|
+
const seenRef = useRef(null);
|
|
1136
|
+
const answersRef = useRef(/* @__PURE__ */ new Map());
|
|
1137
|
+
useEffect(() => {
|
|
1138
|
+
const settle = (key, data) => {
|
|
1139
|
+
const answers = answersRef.current;
|
|
1140
|
+
const nextIds = new Set(data.map((row) => row.id));
|
|
1141
|
+
const prev = answers.get(key);
|
|
1142
|
+
answers.delete(key);
|
|
1143
|
+
answers.set(key, nextIds);
|
|
1144
|
+
if (answers.size > 64) answers.delete(answers.keys().next().value);
|
|
1145
|
+
if (!prev) return;
|
|
1146
|
+
const vanished = [...prev].filter((id) => !nextIds.has(id));
|
|
1147
|
+
if (vanished.length === 0) return;
|
|
1148
|
+
vanished.forEach((id) => selectedRowsRef.current.delete(id));
|
|
1149
|
+
setSelected((current) => {
|
|
1150
|
+
let changed = false;
|
|
1151
|
+
const next = new Set(current);
|
|
1152
|
+
for (const id of vanished) if (next.delete(id)) changed = true;
|
|
1153
|
+
return changed ? next : current;
|
|
1154
|
+
});
|
|
1155
|
+
};
|
|
1156
|
+
const seen = seenRef.current;
|
|
1157
|
+
seenRef.current = { key: paramsKey, data: item.data };
|
|
1158
|
+
if (!seen) {
|
|
1159
|
+
settle(paramsKey, item.data);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (seen.data === item.data) return;
|
|
1163
|
+
settle(paramsKey, item.data);
|
|
1164
|
+
}, [paramsKey, item.data]);
|
|
842
1165
|
const ratioSum = columns.reduce((sum, c) => sum + (c.ratio ?? 0), 0);
|
|
1166
|
+
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;
|
|
1167
|
+
const rowLabelOf = (row) => {
|
|
1168
|
+
const keys = [primaryKey, titleColumnKey, ...columns.map((c) => c.key)].filter(
|
|
1169
|
+
(key) => typeof key === "string" && key.length > 0
|
|
1170
|
+
);
|
|
1171
|
+
for (const key of keys) {
|
|
1172
|
+
const value = fieldOf(row, key);
|
|
1173
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
1174
|
+
if (typeof value === "string" && value.trim() && !looksLikeLocator(value.trim())) return value.trim();
|
|
1175
|
+
}
|
|
1176
|
+
return "\uD589";
|
|
1177
|
+
};
|
|
843
1178
|
const tableColumns = [
|
|
844
1179
|
...hasActions ? [
|
|
845
1180
|
{
|
|
@@ -854,6 +1189,7 @@ function SearchResults({
|
|
|
854
1189
|
}
|
|
855
1190
|
),
|
|
856
1191
|
width: "36px",
|
|
1192
|
+
label: "",
|
|
857
1193
|
render: (row) => (
|
|
858
1194
|
// 셀 어디를 눌러도 행 클릭(onRowClick)으로 번지지 않게 래퍼에서 전파를 끊는다
|
|
859
1195
|
/* @__PURE__ */ jsx("span", { onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsx(
|
|
@@ -861,7 +1197,7 @@ function SearchResults({
|
|
|
861
1197
|
{
|
|
862
1198
|
checked: selected.has(row.id),
|
|
863
1199
|
onChange: (e) => toggleSelect(row, e.target.checked),
|
|
864
|
-
"aria-label": `${row
|
|
1200
|
+
"aria-label": `${rowLabelOf(row)} \uC120\uD0DD`
|
|
865
1201
|
}
|
|
866
1202
|
) })
|
|
867
1203
|
)
|
|
@@ -870,7 +1206,9 @@ function SearchResults({
|
|
|
870
1206
|
...columns.map((column) => ({
|
|
871
1207
|
key: column.key,
|
|
872
1208
|
header: column.title,
|
|
873
|
-
|
|
1209
|
+
label: column.title,
|
|
1210
|
+
mobile: column.mobile ?? (column.key === titleColumnKey ? "title" : void 0),
|
|
1211
|
+
sortable: column.sortable && sortAllowed(column.key),
|
|
874
1212
|
align: column.textAlign === "right" ? "right" : "left",
|
|
875
1213
|
mono: column.mono,
|
|
876
1214
|
strong: column.strong,
|
|
@@ -879,9 +1217,51 @@ function SearchResults({
|
|
|
879
1217
|
}))
|
|
880
1218
|
];
|
|
881
1219
|
const handleSort = (key) => {
|
|
882
|
-
|
|
1220
|
+
const next = nextSort(sort, key);
|
|
1221
|
+
updateParams({ [SORT_PARAM]: next?.key ?? null, [ORDER_PARAM]: next?.dir ?? null, page: null });
|
|
883
1222
|
};
|
|
884
|
-
const
|
|
1223
|
+
const sortChoices = columns.filter((column) => column.sortable && sortAllowed(column.key));
|
|
1224
|
+
const mobileSort = isMobile && sortChoices.length > 0 ? /* @__PURE__ */ jsx(
|
|
1225
|
+
Select,
|
|
1226
|
+
{
|
|
1227
|
+
size: "sm",
|
|
1228
|
+
"aria-label": `${title} \uC815\uB82C`,
|
|
1229
|
+
value: sort ? `${sort.key}:${sort.dir}` : "",
|
|
1230
|
+
options: [
|
|
1231
|
+
{ value: "", label: "\uAE30\uBCF8 \uC815\uB82C" },
|
|
1232
|
+
...sortChoices.flatMap((column) => [
|
|
1233
|
+
{ value: `${column.key}:asc`, label: `${column.title} \uC624\uB984\uCC28\uC21C` },
|
|
1234
|
+
{ value: `${column.key}:desc`, label: `${column.title} \uB0B4\uB9BC\uCC28\uC21C` }
|
|
1235
|
+
])
|
|
1236
|
+
],
|
|
1237
|
+
onChange: (e) => {
|
|
1238
|
+
const cut = e.target.value.lastIndexOf(":");
|
|
1239
|
+
const key = cut > 0 ? e.target.value.slice(0, cut) : "";
|
|
1240
|
+
const dir = e.target.value.slice(cut + 1) === "desc" ? "desc" : "asc";
|
|
1241
|
+
updateParams({ [SORT_PARAM]: key || null, [ORDER_PARAM]: key ? dir : null, page: null });
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
) : null;
|
|
1245
|
+
const chipKeys = [...leftFilters, ...rightFilters].map((filter) => filter.key);
|
|
1246
|
+
const filtered = Boolean(appliedSearch) || Object.values(advancedValues).some((value) => Boolean(value)) || chipKeys.some((key) => Boolean(searchParams.get(key)));
|
|
1247
|
+
const scoped = Boolean(tabs && typeof tabs.key === "string" && searchParams.get(tabs.key));
|
|
1248
|
+
const resetConditions = () => {
|
|
1249
|
+
const patch = { search: null, page: null };
|
|
1250
|
+
for (const key of searchFieldKeys(advancedSearchFields)) patch[key] = null;
|
|
1251
|
+
for (const key of chipKeys) patch[key] = null;
|
|
1252
|
+
updateParams(patch);
|
|
1253
|
+
};
|
|
1254
|
+
const empty = /* @__PURE__ */ jsx(
|
|
1255
|
+
ResultEmptyState,
|
|
1256
|
+
{
|
|
1257
|
+
title,
|
|
1258
|
+
filtered,
|
|
1259
|
+
scoped,
|
|
1260
|
+
emptyTitle,
|
|
1261
|
+
emptyText,
|
|
1262
|
+
onReset: resetConditions
|
|
1263
|
+
}
|
|
1264
|
+
);
|
|
885
1265
|
return /* @__PURE__ */ jsxs("div", { className: ["wp-col", className].filter(Boolean).join(" "), style, "aria-label": title, children: [
|
|
886
1266
|
summary ?? null,
|
|
887
1267
|
tabs && Array.isArray(tabs.items) && tabs.items.length > 0 ? /* @__PURE__ */ jsx(
|
|
@@ -921,9 +1301,10 @@ function SearchResults({
|
|
|
921
1301
|
{
|
|
922
1302
|
items: viewItems,
|
|
923
1303
|
value: activeView,
|
|
924
|
-
onChange: (next) =>
|
|
1304
|
+
onChange: (next) => chooseView(next)
|
|
925
1305
|
}
|
|
926
1306
|
) : null,
|
|
1307
|
+
mobileSort,
|
|
927
1308
|
headerActions
|
|
928
1309
|
] })
|
|
929
1310
|
] }) : null,
|
|
@@ -1000,11 +1381,21 @@ function SearchResults({
|
|
|
1000
1381
|
onSort: handleSort,
|
|
1001
1382
|
onRowClick,
|
|
1002
1383
|
selectedIds: hasActions ? Array.from(selected) : void 0,
|
|
1384
|
+
pickedIds: pickedSet ? pickedIds : void 0,
|
|
1003
1385
|
empty,
|
|
1004
1386
|
virtual: virtualListActive
|
|
1005
1387
|
}
|
|
1006
|
-
) : visibleItems.length === 0 ? empty : /* @__PURE__ */ jsx("div", { className: "wp-grid wp-grid--3", children: visibleItems.map((row) => /* @__PURE__ */ jsx(
|
|
1007
|
-
|
|
1388
|
+
) : visibleItems.length === 0 ? empty : /* @__PURE__ */ jsx("div", { className: "wp-grid wp-grid--3", children: visibleItems.map((row) => /* @__PURE__ */ jsx(
|
|
1389
|
+
GridCard,
|
|
1390
|
+
{
|
|
1391
|
+
item: row,
|
|
1392
|
+
config: gridConfig,
|
|
1393
|
+
onClick: onRowClick,
|
|
1394
|
+
picked: pickedSet ? pickedSet.has(String(row.id)) : false
|
|
1395
|
+
},
|
|
1396
|
+
row.id
|
|
1397
|
+
)) }),
|
|
1398
|
+
virtualListActive && clientPaginated || totalCount === 0 && !isLoading ? null : /* @__PURE__ */ jsxs("div", { className: "wp-row wp-row--between", children: [
|
|
1008
1399
|
/* @__PURE__ */ jsxs("div", { className: "wp-row", style: { flex: "none" }, children: [
|
|
1009
1400
|
/* @__PURE__ */ jsx("span", { className: "wp-caption", style: { whiteSpace: "nowrap" }, children: "\uD398\uC774\uC9C0\uB2F9" }),
|
|
1010
1401
|
/* @__PURE__ */ jsx(
|
|
@@ -1046,13 +1437,17 @@ function formatCell(value) {
|
|
|
1046
1437
|
if (value instanceof Date) return formatDateCellValue(value);
|
|
1047
1438
|
return value;
|
|
1048
1439
|
}
|
|
1440
|
+
function blankAsDash(value) {
|
|
1441
|
+
return value === null || value === void 0 || value === "" ? /* @__PURE__ */ jsx("span", { style: { color: "var(--wp-text-subtle)" }, children: "\u2014" }) : value;
|
|
1442
|
+
}
|
|
1049
1443
|
function resolveRowPart(part, item) {
|
|
1050
1444
|
return typeof part === "function" ? part(item) : part;
|
|
1051
1445
|
}
|
|
1052
1446
|
function GridCard({
|
|
1053
1447
|
item,
|
|
1054
1448
|
config,
|
|
1055
|
-
onClick
|
|
1449
|
+
onClick,
|
|
1450
|
+
picked = false
|
|
1056
1451
|
}) {
|
|
1057
1452
|
if (!config) return null;
|
|
1058
1453
|
const badge = config.badge?.(item) ?? null;
|
|
@@ -1061,7 +1456,7 @@ function GridCard({
|
|
|
1061
1456
|
return /* @__PURE__ */ jsxs("div", { className: "wp-row", children: [
|
|
1062
1457
|
resolveRowPart(row.icon, item),
|
|
1063
1458
|
row.label != null ? /* @__PURE__ */ jsx("span", { className: "wp-caption", children: resolveRowPart(row.label, item) }) : null,
|
|
1064
|
-
/* @__PURE__ */ jsx("span", { className: "wp-body-sm", children: resolveRowPart(row.value, item) })
|
|
1459
|
+
/* @__PURE__ */ jsx("span", { className: "wp-body-sm", children: blankAsDash(resolveRowPart(row.value, item)) })
|
|
1065
1460
|
] }, idx);
|
|
1066
1461
|
}) });
|
|
1067
1462
|
const note = config.note?.(item);
|
|
@@ -1069,6 +1464,7 @@ function GridCard({
|
|
|
1069
1464
|
Card,
|
|
1070
1465
|
{
|
|
1071
1466
|
interactive: Boolean(onClick),
|
|
1467
|
+
...picked ? { selected: true, className: "is-selected", "aria-current": "true" } : {},
|
|
1072
1468
|
cover: config.cover?.(item),
|
|
1073
1469
|
title: config.title(item),
|
|
1074
1470
|
subtitle: config.subtitle?.(item),
|
|
@@ -1130,6 +1526,22 @@ function FilterMultiSelect({
|
|
|
1130
1526
|
function toList(action) {
|
|
1131
1527
|
return Array.isArray(action) ? action : [action];
|
|
1132
1528
|
}
|
|
1529
|
+
function conditionParams(params) {
|
|
1530
|
+
const next = new URLSearchParams(params);
|
|
1531
|
+
next.delete("page");
|
|
1532
|
+
next.delete("limit");
|
|
1533
|
+
return next.toString();
|
|
1534
|
+
}
|
|
1535
|
+
function useQueryStateMirror(dispatch, key, params) {
|
|
1536
|
+
const value = conditionParams(params);
|
|
1537
|
+
const lastRef = useRef(null);
|
|
1538
|
+
useEffect(() => {
|
|
1539
|
+
if (typeof key !== "string" || key === "") return;
|
|
1540
|
+
if (lastRef.current === value) return;
|
|
1541
|
+
lastRef.current = value;
|
|
1542
|
+
dispatch({ action: "setState", key, value });
|
|
1543
|
+
}, [dispatch, key, value]);
|
|
1544
|
+
}
|
|
1133
1545
|
function mapColumns(columns, dispatch) {
|
|
1134
1546
|
return (columns ?? []).map((column) => ({
|
|
1135
1547
|
key: column.key,
|
|
@@ -1139,6 +1551,7 @@ function mapColumns(columns, dispatch) {
|
|
|
1139
1551
|
ratio: column.ratio,
|
|
1140
1552
|
mono: column.mono,
|
|
1141
1553
|
strong: column.strong,
|
|
1554
|
+
mobile: column.mobile === "title" || column.mobile === "hide" ? column.mobile : void 0,
|
|
1142
1555
|
render: cellRenderer(column.cell, column.key, dispatch)
|
|
1143
1556
|
}));
|
|
1144
1557
|
}
|
|
@@ -1240,11 +1653,16 @@ function SduiSearchResultsImpl({
|
|
|
1240
1653
|
searchPlaceholder,
|
|
1241
1654
|
rowClick,
|
|
1242
1655
|
headerActions,
|
|
1243
|
-
virtual
|
|
1656
|
+
virtual,
|
|
1657
|
+
emptyTitle,
|
|
1658
|
+
emptyText,
|
|
1659
|
+
queryStateKey,
|
|
1660
|
+
selectedIds
|
|
1244
1661
|
}) {
|
|
1245
1662
|
const dispatch = useSduiDispatch();
|
|
1246
1663
|
const [params, setParams] = useState(() => new URLSearchParams());
|
|
1247
1664
|
const rows = Array.isArray(data) ? data : [];
|
|
1665
|
+
useQueryStateMirror(dispatch, queryStateKey, params);
|
|
1248
1666
|
return /* @__PURE__ */ jsx(
|
|
1249
1667
|
SearchResults,
|
|
1250
1668
|
{
|
|
@@ -1265,6 +1683,10 @@ function SduiSearchResultsImpl({
|
|
|
1265
1683
|
searchPlaceholder,
|
|
1266
1684
|
headerActions,
|
|
1267
1685
|
virtual,
|
|
1686
|
+
primaryKey: gridCard?.titleKey,
|
|
1687
|
+
emptyTitle,
|
|
1688
|
+
emptyText,
|
|
1689
|
+
selectedIds,
|
|
1268
1690
|
onRowClick: rowClick ? (row) => toList(rowClick).forEach((a) => dispatch(a, row)) : void 0,
|
|
1269
1691
|
searchParams: params,
|
|
1270
1692
|
onParamsChange: setParams
|
|
@@ -1293,7 +1715,11 @@ function RemoteSearchResultsImpl({
|
|
|
1293
1715
|
pageSize,
|
|
1294
1716
|
searchParams: controlledParams,
|
|
1295
1717
|
onParamsChange,
|
|
1296
|
-
summary
|
|
1718
|
+
summary,
|
|
1719
|
+
emptyTitle,
|
|
1720
|
+
emptyText,
|
|
1721
|
+
queryStateKey,
|
|
1722
|
+
selectedIds
|
|
1297
1723
|
}) {
|
|
1298
1724
|
const dispatch = useSduiDispatch();
|
|
1299
1725
|
const defaultPageSize = normalizePageSizes(pageSizes)?.[0] ?? 10;
|
|
@@ -1313,8 +1739,18 @@ function RemoteSearchResultsImpl({
|
|
|
1313
1739
|
if (!next.has("limit")) next.set("limit", String(defaultPageSize));
|
|
1314
1740
|
return next;
|
|
1315
1741
|
}, [params, defaultPageSize]);
|
|
1316
|
-
const { page, isLoading, error, reload } = useRemoteSearchPage(endpoint, query);
|
|
1742
|
+
const { page, pageQuery, isLoading, error, reload } = useRemoteSearchPage(endpoint, query);
|
|
1743
|
+
const requestedSort = sortFromParams(params);
|
|
1744
|
+
const queryString = query.toString();
|
|
1745
|
+
const settledForQuery = pageQuery === queryString;
|
|
1746
|
+
useEffect(() => {
|
|
1747
|
+
if (!requestedSort || !settledForQuery || page.data.length >= page.total) return;
|
|
1748
|
+
if (!serverSortMemory.record(endpoint, requestedSort, page.sort ?? null)) {
|
|
1749
|
+
handleParamsChange(withoutSort(params));
|
|
1750
|
+
}
|
|
1751
|
+
}, [page, settledForQuery, endpoint, requestedSort?.key, requestedSort?.dir]);
|
|
1317
1752
|
const summaryQuery = useMemo(() => summaryParams(params), [params]);
|
|
1753
|
+
useQueryStateMirror(dispatch, queryStateKey, params);
|
|
1318
1754
|
const summaryState = useRemoteSearchSummary(
|
|
1319
1755
|
summary && typeof summary.endpoint === "string" ? summary.endpoint : null,
|
|
1320
1756
|
summaryQuery
|
|
@@ -1351,6 +1787,12 @@ function RemoteSearchResultsImpl({
|
|
|
1351
1787
|
headerActions,
|
|
1352
1788
|
virtual,
|
|
1353
1789
|
isLoading,
|
|
1790
|
+
serverUnsortable: serverSortMemory.unsortable(endpoint),
|
|
1791
|
+
serverFiltered: true,
|
|
1792
|
+
primaryKey: gridCard?.titleKey,
|
|
1793
|
+
emptyTitle,
|
|
1794
|
+
emptyText,
|
|
1795
|
+
selectedIds,
|
|
1354
1796
|
summary: summary ? /* @__PURE__ */ jsx(SearchSummaryCards, { items: summary.items, values: summaryState.values, dispatch }) : void 0,
|
|
1355
1797
|
onRowClick: rowClick ? (row) => toList(rowClick).forEach((a) => dispatch(a, row)) : void 0,
|
|
1356
1798
|
searchParams: params,
|
|
@@ -1360,12 +1802,6 @@ function RemoteSearchResultsImpl({
|
|
|
1360
1802
|
}
|
|
1361
1803
|
var SduiSearchResults = boxed(SduiSearchResultsImpl, "SduiSearchResults");
|
|
1362
1804
|
var RemoteSearchResults = boxed(RemoteSearchResultsImpl, "RemoteSearchResults");
|
|
1363
|
-
var SduiRuntimeContext = createContext(null);
|
|
1364
|
-
function useSduiRuntime() {
|
|
1365
|
-
const runtime = useContext(SduiRuntimeContext);
|
|
1366
|
-
if (!runtime) throw new Error("useSduiRuntime\uC740 SduiRenderer \uB0B4\uBD80\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
1367
|
-
return runtime;
|
|
1368
|
-
}
|
|
1369
1805
|
function prefillParams(search, hash) {
|
|
1370
1806
|
const params = new URLSearchParams(search);
|
|
1371
1807
|
const q = hash.indexOf("?");
|
|
@@ -1402,21 +1838,21 @@ function SduiForm({
|
|
|
1402
1838
|
return JSON.stringify(keys.map((key) => [key, state[key]]));
|
|
1403
1839
|
};
|
|
1404
1840
|
useEffect(() => {
|
|
1405
|
-
const
|
|
1841
|
+
const entries2 = {};
|
|
1406
1842
|
for (const [field, value] of Object.entries(initial ?? {})) {
|
|
1407
|
-
if (runtime.state[`${id}.${field}`] === void 0)
|
|
1843
|
+
if (runtime.state[`${id}.${field}`] === void 0) entries2[`${id}.${field}`] = value;
|
|
1408
1844
|
}
|
|
1409
1845
|
if (initialFromUrl && typeof window !== "undefined") {
|
|
1410
1846
|
const params = prefillParams(window.location.search, window.location.hash);
|
|
1411
1847
|
for (const [field, param] of Object.entries(initialFromUrl)) {
|
|
1412
1848
|
const raw = params.get(param);
|
|
1413
1849
|
if (raw !== null && runtime.state[`${id}.${field}`] === void 0) {
|
|
1414
|
-
|
|
1850
|
+
entries2[`${id}.${field}`] = raw;
|
|
1415
1851
|
}
|
|
1416
1852
|
}
|
|
1417
1853
|
}
|
|
1418
|
-
if (Object.keys(
|
|
1419
|
-
initialSnapshot.current = sliceOf({ ...runtime.state, ...
|
|
1854
|
+
if (Object.keys(entries2).length > 0) runtime.setEntries(entries2);
|
|
1855
|
+
initialSnapshot.current = sliceOf({ ...runtime.state, ...entries2 });
|
|
1420
1856
|
}, [id]);
|
|
1421
1857
|
useEffect(() => {
|
|
1422
1858
|
return () => {
|
|
@@ -1437,6 +1873,25 @@ function SduiForm({
|
|
|
1437
1873
|
}
|
|
1438
1874
|
}, [slice, id, runtime]);
|
|
1439
1875
|
const dirty = Boolean(runtime.state[`${id}.$dirty`]);
|
|
1876
|
+
const latestState = useRef(runtime.state);
|
|
1877
|
+
latestState.current = runtime.state;
|
|
1878
|
+
const dirtyRegistry = useContext(SduiDirtyRegistryContext);
|
|
1879
|
+
useEffect(
|
|
1880
|
+
() => dirtyRegistry?.register(
|
|
1881
|
+
() => initialSnapshot.current !== null && sliceOf(latestState.current) !== initialSnapshot.current
|
|
1882
|
+
),
|
|
1883
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1884
|
+
[dirtyRegistry, id]
|
|
1885
|
+
);
|
|
1886
|
+
const leaveRegistry = useContext(SduiLeaveRegistryContext);
|
|
1887
|
+
useEffect(() => {
|
|
1888
|
+
if (!guard || !leaveRegistry) return;
|
|
1889
|
+
return leaveRegistry.register(
|
|
1890
|
+
id,
|
|
1891
|
+
() => initialSnapshot.current !== null && sliceOf(latestState.current) !== initialSnapshot.current
|
|
1892
|
+
);
|
|
1893
|
+
}, [guard, leaveRegistry, id]);
|
|
1894
|
+
const scope = useMemo(() => ({ id, rules: JSON.parse(rulesJson) }), [id, rulesJson]);
|
|
1440
1895
|
useEffect(() => {
|
|
1441
1896
|
if (!guard || !dirty) return;
|
|
1442
1897
|
const onBeforeUnload = (event) => {
|
|
@@ -1451,12 +1906,13 @@ function SduiForm({
|
|
|
1451
1906
|
{
|
|
1452
1907
|
className: ["wp-col", className].filter(Boolean).join(" "),
|
|
1453
1908
|
style,
|
|
1909
|
+
"data-sdui-form": id,
|
|
1454
1910
|
onSubmit: (event) => {
|
|
1455
1911
|
event.preventDefault();
|
|
1456
1912
|
onSubmit?.(event);
|
|
1457
1913
|
},
|
|
1458
1914
|
noValidate: true,
|
|
1459
|
-
children
|
|
1915
|
+
children: /* @__PURE__ */ jsx(SduiFormScopeContext.Provider, { value: scope, children })
|
|
1460
1916
|
}
|
|
1461
1917
|
);
|
|
1462
1918
|
}
|
|
@@ -1798,6 +2254,9 @@ function withProps(Component, defaults) {
|
|
|
1798
2254
|
function deferChainEntry(container, key) {
|
|
1799
2255
|
return typeof container.action === "string" && (key === "onSuccess" || key === "onError");
|
|
1800
2256
|
}
|
|
2257
|
+
function deferEachTarget(container, key) {
|
|
2258
|
+
return container.action === "request" && container.each !== void 0 && (key === "endpoint" || key === "body");
|
|
2259
|
+
}
|
|
1801
2260
|
function failureDetailOf(error) {
|
|
1802
2261
|
const message = error instanceof Error ? error.message : "\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4";
|
|
1803
2262
|
const carrier = error;
|
|
@@ -1817,12 +2276,14 @@ function runSuccessChain(kind, id, run) {
|
|
|
1817
2276
|
);
|
|
1818
2277
|
}
|
|
1819
2278
|
}
|
|
2279
|
+
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";
|
|
1820
2280
|
async function runRequestAction(action, deps) {
|
|
2281
|
+
if (action.each !== void 0) return runRequestEach(action, deps);
|
|
1821
2282
|
const pendingKey = typeof action.pendingKey === "string" && action.pendingKey !== "" ? action.pendingKey : null;
|
|
1822
2283
|
let pendingOn = false;
|
|
1823
2284
|
try {
|
|
1824
2285
|
if (typeof action.endpoint !== "string" || action.endpoint === "") {
|
|
1825
|
-
throw new Error(
|
|
2286
|
+
throw new Error(EMPTY_ENDPOINT_MESSAGE);
|
|
1826
2287
|
}
|
|
1827
2288
|
if (pendingKey) {
|
|
1828
2289
|
pendingOn = true;
|
|
@@ -1856,6 +2317,91 @@ async function runRequestAction(action, deps) {
|
|
|
1856
2317
|
if (pendingKey && pendingOn) deps.dispatch({ action: "setState", key: pendingKey, value: false });
|
|
1857
2318
|
}
|
|
1858
2319
|
}
|
|
2320
|
+
var REQUEST_EACH_CONCURRENCY = 3;
|
|
2321
|
+
async function mapWithLimit(items, limit, fn) {
|
|
2322
|
+
const results = new Array(items.length);
|
|
2323
|
+
let next = 0;
|
|
2324
|
+
const worker = async () => {
|
|
2325
|
+
while (next < items.length) {
|
|
2326
|
+
const index = next;
|
|
2327
|
+
next += 1;
|
|
2328
|
+
results[index] = await fn(items[index], index);
|
|
2329
|
+
}
|
|
2330
|
+
};
|
|
2331
|
+
const width = Math.max(1, Math.min(limit, items.length));
|
|
2332
|
+
await Promise.all(Array.from({ length: width }, () => worker()));
|
|
2333
|
+
return results;
|
|
2334
|
+
}
|
|
2335
|
+
function eachFailureMessage(total, failed, first) {
|
|
2336
|
+
const head = `${total}\uAC74 \uC911 ${failed}\uAC74 \uC2E4\uD328`;
|
|
2337
|
+
return first ? `${head} \u2014 ${first}` : head;
|
|
2338
|
+
}
|
|
2339
|
+
async function runRequestEach(action, deps) {
|
|
2340
|
+
const list = action.each;
|
|
2341
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
2342
|
+
warnOnce(
|
|
2343
|
+
"request:each:empty",
|
|
2344
|
+
"[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)",
|
|
2345
|
+
"warn"
|
|
2346
|
+
);
|
|
2347
|
+
return;
|
|
2348
|
+
}
|
|
2349
|
+
const pendingKey = typeof action.pendingKey === "string" && action.pendingKey !== "" ? action.pendingKey : null;
|
|
2350
|
+
const resolveFor = deps.resolveFor ?? ((value) => value);
|
|
2351
|
+
const method = action.method ?? "POST";
|
|
2352
|
+
if (pendingKey) deps.dispatch({ action: "setState", key: pendingKey, value: true });
|
|
2353
|
+
try {
|
|
2354
|
+
const outcomes = await mapWithLimit(list, REQUEST_EACH_CONCURRENCY, async (item, index) => {
|
|
2355
|
+
try {
|
|
2356
|
+
const endpoint = resolveFor(action.endpoint, item, index);
|
|
2357
|
+
if (typeof endpoint !== "string" || endpoint === "") throw new Error(EMPTY_ENDPOINT_MESSAGE);
|
|
2358
|
+
const body = action.body === void 0 ? void 0 : resolveFor(action.body, item, index);
|
|
2359
|
+
const value = await deps.fetch(endpoint, { method, ...body === void 0 ? {} : { body } });
|
|
2360
|
+
return { ok: true, value };
|
|
2361
|
+
} catch (error) {
|
|
2362
|
+
return { ok: false, error };
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
const errors = [];
|
|
2366
|
+
outcomes.forEach((outcome, index) => {
|
|
2367
|
+
if (!outcome.ok) errors.push({ index, item: list[index], ...failureDetailOf(outcome.error) });
|
|
2368
|
+
});
|
|
2369
|
+
const succeeded = outcomes.length - errors.length;
|
|
2370
|
+
const invalidate = typeof action.invalidate === "string" && action.invalidate !== "" ? action.invalidate : null;
|
|
2371
|
+
if (errors.length === 0) {
|
|
2372
|
+
runSuccessChain("request", `each:${list.length}`, () => {
|
|
2373
|
+
if (invalidate) deps.dispatch({ action: "invalidate", prefix: invalidate });
|
|
2374
|
+
const responses = outcomes.map((outcome) => outcome.ok ? outcome.value : null);
|
|
2375
|
+
(action.onSuccess ?? []).forEach((a) => deps.dispatch(a, responses));
|
|
2376
|
+
});
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
if (succeeded > 0 && invalidate) {
|
|
2380
|
+
runSuccessChain("request", `each:${list.length}`, () => deps.dispatch({ action: "invalidate", prefix: invalidate }));
|
|
2381
|
+
}
|
|
2382
|
+
const failure = {
|
|
2383
|
+
failed: errors.length,
|
|
2384
|
+
succeeded,
|
|
2385
|
+
total: list.length,
|
|
2386
|
+
message: eachFailureMessage(list.length, errors.length, errors[0]?.message),
|
|
2387
|
+
errors
|
|
2388
|
+
};
|
|
2389
|
+
if (typeof action.errorKey === "string" && action.errorKey !== "") {
|
|
2390
|
+
deps.dispatch({ action: "setState", key: action.errorKey, value: failure });
|
|
2391
|
+
}
|
|
2392
|
+
if (action.onError?.length) {
|
|
2393
|
+
action.onError.forEach((a) => deps.dispatch(a, failure));
|
|
2394
|
+
} else {
|
|
2395
|
+
deps.toast({
|
|
2396
|
+
tone: "danger",
|
|
2397
|
+
title: `${list.length}\uAC74 \uC911 ${errors.length}\uAC74 \uC2E4\uD328`,
|
|
2398
|
+
message: errors[0]?.message
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
} finally {
|
|
2402
|
+
if (pendingKey) deps.dispatch({ action: "setState", key: pendingKey, value: false });
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
1859
2405
|
|
|
1860
2406
|
// src/state-path.ts
|
|
1861
2407
|
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
@@ -2292,6 +2838,33 @@ function nearestArgsFrame(scope) {
|
|
|
2292
2838
|
}
|
|
2293
2839
|
return void 0;
|
|
2294
2840
|
}
|
|
2841
|
+
function toastText(value) {
|
|
2842
|
+
if (typeof value === "string") return value;
|
|
2843
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
2844
|
+
if (isPlainObject(value) && typeof value.message === "string") return value.message;
|
|
2845
|
+
return void 0;
|
|
2846
|
+
}
|
|
2847
|
+
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.";
|
|
2848
|
+
function leaveMessageOf(value) {
|
|
2849
|
+
if (typeof value === "string") return value.trim() === "" ? null : value;
|
|
2850
|
+
return value ? DEFAULT_LEAVE_MESSAGE : null;
|
|
2851
|
+
}
|
|
2852
|
+
function titleText(value) {
|
|
2853
|
+
if (typeof value === "string") return value.trim() === "" ? null : value;
|
|
2854
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
2855
|
+
return null;
|
|
2856
|
+
}
|
|
2857
|
+
function collectFormValues(state, formId) {
|
|
2858
|
+
const prefix = `${formId}.`;
|
|
2859
|
+
const values = {};
|
|
2860
|
+
for (const [key, value] of Object.entries(state)) {
|
|
2861
|
+
if (key.startsWith(prefix) && !key.slice(prefix.length).startsWith("$") && value !== void 0) {
|
|
2862
|
+
values[key.slice(prefix.length)] = value;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
return values;
|
|
2866
|
+
}
|
|
2867
|
+
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}}";
|
|
2295
2868
|
function hasExplicitPageHead(body) {
|
|
2296
2869
|
return Array.isArray(body) && body.some((child) => isPlainObject(child) && child.component === "PageHead");
|
|
2297
2870
|
}
|
|
@@ -2369,45 +2942,76 @@ function toIndex(v) {
|
|
|
2369
2942
|
function inRange(index, length) {
|
|
2370
2943
|
return index !== null && index >= 0 && index < length;
|
|
2371
2944
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2945
|
+
var CHOICE_KINDS = /* @__PURE__ */ new Set([
|
|
2946
|
+
"Select",
|
|
2947
|
+
"AutocompleteField",
|
|
2948
|
+
"PositionSelect",
|
|
2949
|
+
"TagMultiSelect",
|
|
2950
|
+
"TreePicker",
|
|
2951
|
+
"SearchableMultiPicker",
|
|
2952
|
+
"RoleMemberPicker",
|
|
2953
|
+
"SegmentedControl",
|
|
2954
|
+
"Radio",
|
|
2955
|
+
"DatePicker",
|
|
2956
|
+
"DateRangePicker",
|
|
2957
|
+
"TimePicker"
|
|
2958
|
+
]);
|
|
2959
|
+
function requiredVerb(kind) {
|
|
2960
|
+
if (kind && CHOICE_KINDS.has(kind)) return "\uC120\uD0DD\uD558\uC138\uC694";
|
|
2961
|
+
if (kind === "FileDropzone") return "\uCCA8\uBD80\uD558\uC138\uC694";
|
|
2962
|
+
if (kind === "Checkbox" || kind === "Switch") return "\uD655\uC778\uD558\uC138\uC694";
|
|
2963
|
+
return "\uC785\uB825\uD558\uC138\uC694";
|
|
2964
|
+
}
|
|
2965
|
+
function validateForm(values, config, customValidators, fieldInfo) {
|
|
2966
|
+
const out = { errors: {}, failed: {} };
|
|
2967
|
+
if (!config) return out;
|
|
2968
|
+
const labelOf2 = (field) => config.labels[field] ?? fieldInfo?.get(field)?.label;
|
|
2375
2969
|
for (const [field, rules] of Object.entries(config.rules)) {
|
|
2376
2970
|
const value = values[field];
|
|
2377
|
-
const
|
|
2971
|
+
const context = {
|
|
2972
|
+
label: labelOf2(field),
|
|
2973
|
+
kind: fieldInfo?.get(field)?.kind,
|
|
2974
|
+
labelOf: labelOf2
|
|
2975
|
+
};
|
|
2378
2976
|
for (const rule of rules) {
|
|
2379
|
-
const message = ruleError(rule, value, values,
|
|
2977
|
+
const message = ruleError(rule, value, values, context, customValidators);
|
|
2380
2978
|
if (message) {
|
|
2381
|
-
errors[field] = message;
|
|
2979
|
+
out.errors[field] = message;
|
|
2980
|
+
out.failed[field] = rule.rule;
|
|
2382
2981
|
break;
|
|
2383
2982
|
}
|
|
2384
2983
|
}
|
|
2385
2984
|
}
|
|
2386
|
-
return
|
|
2985
|
+
return out;
|
|
2387
2986
|
}
|
|
2388
|
-
function ruleError(rule, value, values, label, customValidators) {
|
|
2987
|
+
function ruleError(rule, value, values, { label, kind, labelOf: labelOf2 }, customValidators) {
|
|
2389
2988
|
const empty = value === void 0 || value === null || value === "" || Array.isArray(value) && value.length === 0 || // DateRangePicker의 {} 같은 "값 없는 객체"도 미입력으로 본다
|
|
2390
2989
|
typeof value === "object" && !Array.isArray(value) && Object.values(value).every((v) => v === void 0 || v === null || v === "");
|
|
2990
|
+
const subject = (pair) => label ? `${josa(label, pair)} ` : "";
|
|
2391
2991
|
switch (rule.rule) {
|
|
2392
2992
|
case "required":
|
|
2393
|
-
return empty || value === false ? rule.message ?? `${label
|
|
2993
|
+
return empty || value === false ? rule.message ?? (label ? `${josa(label, "\uC744/\uB97C")} ${requiredVerb(kind)}` : "\uD544\uC218 \uD56D\uBAA9\uC785\uB2C8\uB2E4") : null;
|
|
2394
2994
|
case "min":
|
|
2395
|
-
return !empty && toNumber(value) < rule.value ? rule.message ?? `${
|
|
2995
|
+
return !empty && toNumber(value) < rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
|
|
2396
2996
|
case "max":
|
|
2397
|
-
return !empty && toNumber(value) > rule.value ? rule.message ?? `${
|
|
2997
|
+
return !empty && toNumber(value) > rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value} \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
|
|
2398
2998
|
case "minLength":
|
|
2399
|
-
return !empty && String(value).length < rule.value ? rule.message ?? `${
|
|
2999
|
+
return !empty && String(value).length < rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value}\uC790 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
|
|
2400
3000
|
case "maxLength":
|
|
2401
|
-
return !empty && String(value).length > rule.value ? rule.message ?? `${
|
|
3001
|
+
return !empty && String(value).length > rule.value ? rule.message ?? `${subject("\uC740/\uB294")}${rule.value}\uC790 \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
|
|
2402
3002
|
case "pattern":
|
|
2403
|
-
return !empty && !new RegExp(rule.value).test(String(value)) ? rule.message ?? `${label} \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4` : null;
|
|
3003
|
+
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;
|
|
2404
3004
|
case "notBeforeField": {
|
|
2405
3005
|
const other = values[rule.field];
|
|
2406
3006
|
if (empty || other === void 0 || other === null || other === "") return null;
|
|
2407
3007
|
const a = Number(String(value).replace(/,/g, ""));
|
|
2408
3008
|
const b = Number(String(other).replace(/,/g, ""));
|
|
2409
3009
|
const before = Number.isFinite(a) && Number.isFinite(b) && String(value).trim() !== "" ? a < b : String(value) < String(other);
|
|
2410
|
-
|
|
3010
|
+
if (!before) return null;
|
|
3011
|
+
if (rule.message) return rule.message;
|
|
3012
|
+
const otherLabel = labelOf2(rule.field);
|
|
3013
|
+
if (label && otherLabel) return `${josa(label, "\uC740/\uB294")} ${otherLabel}\uBCF4\uB2E4 \uC55E\uC124 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`;
|
|
3014
|
+
return `${subject("\uC774/\uAC00")}\uC21C\uC11C\uC5D0 \uB9DE\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4`;
|
|
2411
3015
|
}
|
|
2412
3016
|
case "custom": {
|
|
2413
3017
|
const validator = customValidators?.[rule.name];
|
|
@@ -2420,6 +3024,122 @@ function ruleError(rule, value, values, label, customValidators) {
|
|
|
2420
3024
|
}
|
|
2421
3025
|
}
|
|
2422
3026
|
}
|
|
3027
|
+
function invalidSummary(result, labelOf2, order = []) {
|
|
3028
|
+
const rank = (field) => {
|
|
3029
|
+
const index = order.indexOf(field);
|
|
3030
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
3031
|
+
};
|
|
3032
|
+
const fields = Object.keys(result.errors).sort((a, b) => rank(a) - rank(b));
|
|
3033
|
+
const count = fields.length;
|
|
3034
|
+
const allRequired = fields.every((field) => result.failed[field] === "required");
|
|
3035
|
+
const labels = fields.map(labelOf2).filter((l) => typeof l === "string" && l !== "");
|
|
3036
|
+
if (count === 1 && labels.length === 1) return { title: result.errors[fields[0]] };
|
|
3037
|
+
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`;
|
|
3038
|
+
if (labels.length === 0) return { title, message: count === 1 ? void 0 : result.errors[fields[0]] };
|
|
3039
|
+
const shown = labels.slice(0, 4).join(", ");
|
|
3040
|
+
const rest = labels.length - 4;
|
|
3041
|
+
return { title, message: rest > 0 ? `${shown} \uC678 ${rest}\uAC1C` : shown };
|
|
3042
|
+
}
|
|
3043
|
+
var FOCUSABLE_IN_FIELD = 'input:not([type="hidden"]):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
3044
|
+
function formElementOf(formId) {
|
|
3045
|
+
if (typeof document === "undefined") return void 0;
|
|
3046
|
+
return Array.from(document.querySelectorAll("form[data-sdui-form]")).find(
|
|
3047
|
+
(el) => el.getAttribute("data-sdui-form") === formId && el.isConnected
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
function fieldOrderOf(formId) {
|
|
3051
|
+
const form = formElementOf(formId);
|
|
3052
|
+
if (!form) return [];
|
|
3053
|
+
return Array.from(form.querySelectorAll("[data-sdui-field]")).map(
|
|
3054
|
+
(el) => el.getAttribute("data-sdui-field") ?? ""
|
|
3055
|
+
);
|
|
3056
|
+
}
|
|
3057
|
+
function focusFirstInvalid(formId, fields) {
|
|
3058
|
+
const form = formElementOf(formId);
|
|
3059
|
+
if (!form) return;
|
|
3060
|
+
const wanted = new Set(fields);
|
|
3061
|
+
const candidates = form.querySelectorAll('[data-sdui-field], [aria-invalid]:not([aria-invalid="false"])');
|
|
3062
|
+
const hit = Array.from(candidates).find((el) => {
|
|
3063
|
+
const field = el.getAttribute("data-sdui-field");
|
|
3064
|
+
return field !== null ? wanted.has(field) : true;
|
|
3065
|
+
});
|
|
3066
|
+
if (!hit) return;
|
|
3067
|
+
const target = hit.matches(FOCUSABLE_IN_FIELD) ? hit : hit.querySelector(FOCUSABLE_IN_FIELD);
|
|
3068
|
+
hit.scrollIntoView?.({ block: "center", inline: "nearest" });
|
|
3069
|
+
target?.focus({ preventScroll: true });
|
|
3070
|
+
}
|
|
3071
|
+
var INVALID_PROP_COMPONENTS = /* @__PURE__ */ new Set([
|
|
3072
|
+
"Input",
|
|
3073
|
+
"Textarea",
|
|
3074
|
+
"Select",
|
|
3075
|
+
"DatePicker",
|
|
3076
|
+
"DateRangePicker",
|
|
3077
|
+
"TimePicker",
|
|
3078
|
+
"AutocompleteField",
|
|
3079
|
+
"JsonField",
|
|
3080
|
+
"PositionSelect"
|
|
3081
|
+
]);
|
|
3082
|
+
var BINDING_PROPS = ["value", "checked", "values"];
|
|
3083
|
+
function formKeyOf(value, formId) {
|
|
3084
|
+
if (!isStateRef(value)) return null;
|
|
3085
|
+
const prefix = `${formId}.`;
|
|
3086
|
+
if (!value.$state.startsWith(prefix)) return null;
|
|
3087
|
+
const key = value.$state.slice(prefix.length);
|
|
3088
|
+
return key === "" || key.startsWith("$") ? null : key;
|
|
3089
|
+
}
|
|
3090
|
+
function boundInputOf(node, formId, depth = 0) {
|
|
3091
|
+
if (!Array.isArray(node.children)) return null;
|
|
3092
|
+
for (const child of node.children) {
|
|
3093
|
+
if (!isPlainObject(child)) continue;
|
|
3094
|
+
const childNode = child;
|
|
3095
|
+
const props = isPlainObject(childNode.props) ? childNode.props : {};
|
|
3096
|
+
for (const name of BINDING_PROPS) {
|
|
3097
|
+
const key = formKeyOf(props[name], formId);
|
|
3098
|
+
if (key) return { key, kind: String(childNode.component) };
|
|
3099
|
+
}
|
|
3100
|
+
const component = childNode.component;
|
|
3101
|
+
if (component === "Field" || component === "Form" || component === "ForEach" || component === "Fragment") continue;
|
|
3102
|
+
if (depth < 4) {
|
|
3103
|
+
const found = boundInputOf(childNode, formId, depth + 1);
|
|
3104
|
+
if (found) return found;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
return null;
|
|
3108
|
+
}
|
|
3109
|
+
function hasRequiredRule(rules) {
|
|
3110
|
+
return Array.isArray(rules) && rules.some((rule) => isPlainObject(rule) && rule.rule === "required");
|
|
3111
|
+
}
|
|
3112
|
+
function applyFormBinding(node, props, form, engine) {
|
|
3113
|
+
const raw = isPlainObject(node.props) ? node.props : {};
|
|
3114
|
+
const errorOf = (key) => {
|
|
3115
|
+
const errors = engine.resolveValue({ $state: `${form.id}.$errors` }, EMPTY_SCOPE);
|
|
3116
|
+
const message = isPlainObject(errors) ? errors[key] : void 0;
|
|
3117
|
+
return typeof message === "string" && message !== "" ? message : void 0;
|
|
3118
|
+
};
|
|
3119
|
+
if (node.component === "Field") {
|
|
3120
|
+
const bound = boundInputOf(node, form.id);
|
|
3121
|
+
if (!bound) return;
|
|
3122
|
+
if (!Object.hasOwn(raw, "required") && hasRequiredRule(form.rules[bound.key])) props.required = true;
|
|
3123
|
+
if (!Object.hasOwn(raw, "error")) {
|
|
3124
|
+
const message = errorOf(bound.key);
|
|
3125
|
+
if (message) props.error = message;
|
|
3126
|
+
}
|
|
3127
|
+
props["data-sdui-field"] = bound.key;
|
|
3128
|
+
engine.noteField(form.id, bound.key, {
|
|
3129
|
+
label: typeof props.label === "string" && props.label !== "" ? props.label : void 0,
|
|
3130
|
+
kind: bound.kind
|
|
3131
|
+
});
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
if (typeof node.component !== "string" || !INVALID_PROP_COMPONENTS.has(node.component)) return;
|
|
3135
|
+
if (Object.hasOwn(raw, "invalid")) return;
|
|
3136
|
+
for (const name of BINDING_PROPS) {
|
|
3137
|
+
const key = formKeyOf(raw[name], form.id);
|
|
3138
|
+
if (!key) continue;
|
|
3139
|
+
if (errorOf(key)) props.invalid = true;
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
2423
3143
|
var EMPTY_SCOPE = [];
|
|
2424
3144
|
var DENIED_PROPS2 = /* @__PURE__ */ new Set([
|
|
2425
3145
|
"dangerouslySetInnerHTML",
|
|
@@ -2452,6 +3172,7 @@ function SduiNodeRenderer({
|
|
|
2452
3172
|
engine
|
|
2453
3173
|
}) {
|
|
2454
3174
|
const { registry, resolveValue, dispatch, resolveAsset } = engine;
|
|
3175
|
+
const formScope = React9.useContext(SduiFormScopeContext);
|
|
2455
3176
|
const slots = isPlainObject(node.slots) ? node.slots : void 0;
|
|
2456
3177
|
const childList = childrenOf(node);
|
|
2457
3178
|
if (node.component === "ForEach") {
|
|
@@ -2516,6 +3237,10 @@ function SduiNodeRenderer({
|
|
|
2516
3237
|
props[name] = resolved;
|
|
2517
3238
|
}
|
|
2518
3239
|
}
|
|
3240
|
+
if (formScope) applyFormBinding(node, props, formScope, engine);
|
|
3241
|
+
if (typeof node.component === "string" && INVALID_PROP_COMPONENTS.has(node.component) && props.invalid !== void 0 && typeof props.invalid !== "boolean") {
|
|
3242
|
+
props.invalid = Boolean(props.invalid);
|
|
3243
|
+
}
|
|
2519
3244
|
const childLayout = childLayoutFrom(props);
|
|
2520
3245
|
if (childLayout) {
|
|
2521
3246
|
for (const name of CHILD_LAYOUT_PROPS) delete props[name];
|
|
@@ -2612,19 +3337,53 @@ function SduiFragmentBody({
|
|
|
2612
3337
|
return null;
|
|
2613
3338
|
}
|
|
2614
3339
|
const page = spec;
|
|
2615
|
-
if (page.data || page.urlState || page.subscribe) {
|
|
3340
|
+
if (page.data || page.urlState || page.subscribe || page.leaveGuard !== void 0 || page.titleFrom !== void 0) {
|
|
2616
3341
|
warnOnce(
|
|
2617
3342
|
`fragment:page-features:${id}`,
|
|
2618
|
-
`[sdui] Fragment \uC2A4\uD399 ${id}\uC758 data/urlState/subscribe\
|
|
3343
|
+
`[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`
|
|
2619
3344
|
);
|
|
2620
3345
|
}
|
|
2621
3346
|
return /* @__PURE__ */ jsx(SduiFragmentChainContext.Provider, { value: nextChain, children: page.body.map((child, i) => renderChild(child, i, bodyScope, engine)) });
|
|
2622
3347
|
}
|
|
3348
|
+
function SduiToastRegion({ hostId }) {
|
|
3349
|
+
const board = useSyncExternalStore(subscribeToasts, toastSnapshot, toastServerSnapshot);
|
|
3350
|
+
const toasts = useMemo(
|
|
3351
|
+
() => board.entries.filter((entry) => toastHostOf(entry, board.hosts) === hostId),
|
|
3352
|
+
[board, hostId]
|
|
3353
|
+
);
|
|
3354
|
+
useEffect(() => {
|
|
3355
|
+
if (toasts.length > 0) markToastsShown(toasts.map((t) => t.id));
|
|
3356
|
+
}, [toasts]);
|
|
3357
|
+
const modalOpen = useAnyModalOpen();
|
|
3358
|
+
if (toasts.length === 0) return null;
|
|
3359
|
+
return /* @__PURE__ */ jsxs("div", { className: modalOpen ? "wp-toast-region wp-toast-region--modal" : "wp-toast-region", children: [
|
|
3360
|
+
/* @__PURE__ */ jsx(HoistedStyle, { id: "wp-sdui-toast-modal", css: TOAST_MODAL_CSS }),
|
|
3361
|
+
toasts.map((t) => /* @__PURE__ */ jsx(Toast, { tone: t.tone, title: t.title, onClose: () => closeToastEntry(t.id), children: t.message }, t.id))
|
|
3362
|
+
] });
|
|
3363
|
+
}
|
|
2623
3364
|
function urlSeedSettled(state, seeded, baseline) {
|
|
2624
3365
|
return Object.keys(seeded).every(
|
|
2625
3366
|
(key) => Object.is(state[key], seeded[key]) || !Object.is(state[key], baseline[key])
|
|
2626
3367
|
);
|
|
2627
3368
|
}
|
|
3369
|
+
function encodeUrlStateValue(value) {
|
|
3370
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
3371
|
+
if (typeof value !== "string") return JSON.stringify(value);
|
|
3372
|
+
if (/^["[{]/.test(value)) return JSON.stringify(value);
|
|
3373
|
+
try {
|
|
3374
|
+
JSON.parse(value);
|
|
3375
|
+
return JSON.stringify(value);
|
|
3376
|
+
} catch {
|
|
3377
|
+
return value;
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
function decodeUrlStateValue(raw) {
|
|
3381
|
+
try {
|
|
3382
|
+
return JSON.parse(raw);
|
|
3383
|
+
} catch {
|
|
3384
|
+
return raw;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
2628
3387
|
function SduiRenderer({
|
|
2629
3388
|
page,
|
|
2630
3389
|
registry = defaultRegistry,
|
|
@@ -2644,24 +3403,43 @@ function SduiRenderer({
|
|
|
2644
3403
|
const urlSeeded = useRef(false);
|
|
2645
3404
|
const urlSeedPending = useRef(null);
|
|
2646
3405
|
const urlHomePath = useRef(typeof window !== "undefined" ? window.location.pathname : "");
|
|
2647
|
-
const [toasts, setToasts] = useState([]);
|
|
2648
|
-
const toastSeq = useRef(0);
|
|
2649
|
-
const toastTimers = useRef([]);
|
|
2650
3406
|
const stateRef = useRef(state);
|
|
2651
3407
|
stateRef.current = state;
|
|
2652
3408
|
const formsRef = useRef(/* @__PURE__ */ new Map());
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
3409
|
+
const fieldInfoRef = useRef(/* @__PURE__ */ new Map());
|
|
3410
|
+
const noteField = useCallback((formId, key, info) => {
|
|
3411
|
+
let fields = fieldInfoRef.current.get(formId);
|
|
3412
|
+
if (!fields) {
|
|
3413
|
+
fields = /* @__PURE__ */ new Map();
|
|
3414
|
+
fieldInfoRef.current.set(formId, fields);
|
|
3415
|
+
}
|
|
3416
|
+
fields.set(key, info);
|
|
2656
3417
|
}, []);
|
|
3418
|
+
const [toastHostId] = useState(allocateToastHostId);
|
|
3419
|
+
const toastChannel = React9.useContext(OverlayContainerContext);
|
|
3420
|
+
useEffect(() => registerToastHost(toastHostId, toastChannel), [toastHostId, toastChannel]);
|
|
2657
3421
|
useEffect(() => {
|
|
2658
3422
|
if (!DEV) return;
|
|
2659
3423
|
const issues = validateSpec(page, new Set(Object.keys(registry)));
|
|
2660
3424
|
issues.forEach((issue) => console.warn(`[sdui:spec] ${issue.path} \u2014 ${issue.message}`));
|
|
2661
3425
|
}, [page, registry]);
|
|
2662
|
-
const
|
|
2663
|
-
|
|
2664
|
-
|
|
3426
|
+
const leaveFormsRef = useRef(/* @__PURE__ */ new Map());
|
|
3427
|
+
const leaveRegistry = useMemo(
|
|
3428
|
+
() => ({
|
|
3429
|
+
register: (formId, isDirty) => {
|
|
3430
|
+
leaveFormsRef.current.set(formId, isDirty);
|
|
3431
|
+
return () => {
|
|
3432
|
+
if (leaveFormsRef.current.get(formId) === isDirty) leaveFormsRef.current.delete(formId);
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
}),
|
|
3436
|
+
[]
|
|
3437
|
+
);
|
|
3438
|
+
const leavingRef = useRef(false);
|
|
3439
|
+
const setEntries = useCallback((entries2) => {
|
|
3440
|
+
stateRef.current = { ...stateRef.current, ...entries2 };
|
|
3441
|
+
leavingRef.current = false;
|
|
3442
|
+
setState((prev) => ({ ...prev, ...entries2 }));
|
|
2665
3443
|
}, []);
|
|
2666
3444
|
useEffect(() => {
|
|
2667
3445
|
if (!syncUrlState || urlSeeded.current || !page.urlState?.length || typeof window === "undefined") return;
|
|
@@ -2670,13 +3448,7 @@ function SduiRenderer({
|
|
|
2670
3448
|
const seeded = {};
|
|
2671
3449
|
for (const key of page.urlState) {
|
|
2672
3450
|
const raw = params.get(key);
|
|
2673
|
-
if (raw !== null)
|
|
2674
|
-
try {
|
|
2675
|
-
seeded[key] = JSON.parse(raw);
|
|
2676
|
-
} catch {
|
|
2677
|
-
seeded[key] = raw;
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
3451
|
+
if (raw !== null) seeded[key] = decodeUrlStateValue(raw);
|
|
2680
3452
|
}
|
|
2681
3453
|
if (Object.keys(seeded).length > 0) {
|
|
2682
3454
|
const baseline = {};
|
|
@@ -2685,6 +3457,16 @@ function SduiRenderer({
|
|
|
2685
3457
|
setEntries(seeded);
|
|
2686
3458
|
}
|
|
2687
3459
|
}, [page.urlState, setEntries, syncUrlState]);
|
|
3460
|
+
const [urlResync, setUrlResync] = useState(0);
|
|
3461
|
+
const hasUrlState = Boolean(page.urlState?.length);
|
|
3462
|
+
useEffect(() => {
|
|
3463
|
+
if (!syncUrlState || !hasUrlState || typeof window === "undefined") return;
|
|
3464
|
+
const onPop = () => {
|
|
3465
|
+
if (window.location.pathname === urlHomePath.current) setUrlResync((n) => n + 1);
|
|
3466
|
+
};
|
|
3467
|
+
window.addEventListener("popstate", onPop);
|
|
3468
|
+
return () => window.removeEventListener("popstate", onPop);
|
|
3469
|
+
}, [syncUrlState, hasUrlState]);
|
|
2688
3470
|
useEffect(() => {
|
|
2689
3471
|
if (!syncUrlState || !page.urlState?.length || !urlSeeded.current || typeof window === "undefined") return;
|
|
2690
3472
|
if (window.location.pathname !== urlHomePath.current) return;
|
|
@@ -2696,16 +3478,19 @@ function SduiRenderer({
|
|
|
2696
3478
|
const params = new URLSearchParams(window.location.search);
|
|
2697
3479
|
let changed = false;
|
|
2698
3480
|
for (const key of page.urlState) {
|
|
2699
|
-
const
|
|
2700
|
-
const
|
|
3481
|
+
const serialized = encodeUrlStateValue(state[key]);
|
|
3482
|
+
const current = params.get(key);
|
|
2701
3483
|
if (serialized === null) {
|
|
2702
|
-
if (
|
|
3484
|
+
if (current !== null) {
|
|
2703
3485
|
params.delete(key);
|
|
2704
3486
|
changed = true;
|
|
2705
3487
|
}
|
|
2706
|
-
} else
|
|
2707
|
-
|
|
2708
|
-
|
|
3488
|
+
} else {
|
|
3489
|
+
const sameValue = current !== null && (current === serialized || encodeUrlStateValue(decodeUrlStateValue(current)) === serialized);
|
|
3490
|
+
if (!sameValue) {
|
|
3491
|
+
params.set(key, serialized);
|
|
3492
|
+
changed = true;
|
|
3493
|
+
}
|
|
2709
3494
|
}
|
|
2710
3495
|
}
|
|
2711
3496
|
if (changed) {
|
|
@@ -2720,7 +3505,7 @@ function SduiRenderer({
|
|
|
2720
3505
|
);
|
|
2721
3506
|
}
|
|
2722
3507
|
}
|
|
2723
|
-
}, [state, page.urlState, syncUrlState]);
|
|
3508
|
+
}, [state, page.urlState, syncUrlState, urlResync]);
|
|
2724
3509
|
const subscribeChannels = page.subscribe?.join(",") ?? "";
|
|
2725
3510
|
const hasSubscribe = typeof host.subscribe === "function";
|
|
2726
3511
|
useEffect(() => {
|
|
@@ -2741,16 +3526,11 @@ function SduiRenderer({
|
|
|
2741
3526
|
unsubscribe();
|
|
2742
3527
|
};
|
|
2743
3528
|
}, [subscribeChannels, hasSubscribe]);
|
|
2744
|
-
const closeToast = useCallback((id) => {
|
|
2745
|
-
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
2746
|
-
}, []);
|
|
2747
3529
|
const pushToast = useCallback(
|
|
2748
3530
|
(toast) => {
|
|
2749
|
-
|
|
2750
|
-
setToasts((prev) => [...prev, { ...toast, id }]);
|
|
2751
|
-
toastTimers.current.push(setTimeout(() => closeToast(id), 4e3));
|
|
3531
|
+
pushToastEntry(toastHostId, toastChannel, toast);
|
|
2752
3532
|
},
|
|
2753
|
-
[
|
|
3533
|
+
[toastHostId, toastChannel]
|
|
2754
3534
|
);
|
|
2755
3535
|
const lookupState = useCallback((key) => {
|
|
2756
3536
|
const current = stateRef.current;
|
|
@@ -2811,7 +3591,7 @@ function SduiRenderer({
|
|
|
2811
3591
|
if (isPlainObject(value)) {
|
|
2812
3592
|
const out = {};
|
|
2813
3593
|
for (const [k, v] of Object.entries(value)) {
|
|
2814
|
-
out[k] = deferChainEntry(value, k) || deferRunEntry(k, v) ? v : resolveValue(v, scope);
|
|
3594
|
+
out[k] = deferChainEntry(value, k) || deferRunEntry(k, v) || deferEachTarget(value, k) ? v : resolveValue(v, scope);
|
|
2815
3595
|
}
|
|
2816
3596
|
return out;
|
|
2817
3597
|
}
|
|
@@ -2819,6 +3599,76 @@ function SduiRenderer({
|
|
|
2819
3599
|
},
|
|
2820
3600
|
[lookupState]
|
|
2821
3601
|
);
|
|
3602
|
+
const leaveMessageNow = useCallback(() => {
|
|
3603
|
+
if (leavingRef.current) return null;
|
|
3604
|
+
if (page.leaveGuard !== void 0) {
|
|
3605
|
+
const message = leaveMessageOf(resolveValue(page.leaveGuard, EMPTY_SCOPE));
|
|
3606
|
+
if (message) return message;
|
|
3607
|
+
}
|
|
3608
|
+
for (const [formId, isDirty] of leaveFormsRef.current) {
|
|
3609
|
+
if (stateRef.current[`${formId}.$resetDirty`] === true) continue;
|
|
3610
|
+
if (isDirty()) return DEFAULT_LEAVE_MESSAGE;
|
|
3611
|
+
}
|
|
3612
|
+
return null;
|
|
3613
|
+
}, [page.leaveGuard, resolveValue]);
|
|
3614
|
+
const leaveSentRef = useRef(null);
|
|
3615
|
+
const unloadHandlerRef = useRef(null);
|
|
3616
|
+
const syncLeaveGuard = useCallback(() => {
|
|
3617
|
+
const message = leaveMessageNow();
|
|
3618
|
+
if (message === leaveSentRef.current) return message;
|
|
3619
|
+
leaveSentRef.current = message;
|
|
3620
|
+
hostRef.current.setLeaveGuard?.(message);
|
|
3621
|
+
if (typeof window === "undefined") return message;
|
|
3622
|
+
if (message !== null && !unloadHandlerRef.current) {
|
|
3623
|
+
const handler = (event) => {
|
|
3624
|
+
event.preventDefault();
|
|
3625
|
+
event.returnValue = "";
|
|
3626
|
+
};
|
|
3627
|
+
window.addEventListener("beforeunload", handler);
|
|
3628
|
+
unloadHandlerRef.current = handler;
|
|
3629
|
+
} else if (message === null && unloadHandlerRef.current) {
|
|
3630
|
+
window.removeEventListener("beforeunload", unloadHandlerRef.current);
|
|
3631
|
+
unloadHandlerRef.current = null;
|
|
3632
|
+
}
|
|
3633
|
+
return message;
|
|
3634
|
+
}, [leaveMessageNow]);
|
|
3635
|
+
useEffect(() => {
|
|
3636
|
+
syncLeaveGuard();
|
|
3637
|
+
});
|
|
3638
|
+
useEffect(
|
|
3639
|
+
() => () => {
|
|
3640
|
+
if (leaveSentRef.current !== null) {
|
|
3641
|
+
leaveSentRef.current = null;
|
|
3642
|
+
hostRef.current.setLeaveGuard?.(null);
|
|
3643
|
+
}
|
|
3644
|
+
if (unloadHandlerRef.current && typeof window !== "undefined") {
|
|
3645
|
+
window.removeEventListener("beforeunload", unloadHandlerRef.current);
|
|
3646
|
+
unloadHandlerRef.current = null;
|
|
3647
|
+
}
|
|
3648
|
+
},
|
|
3649
|
+
[]
|
|
3650
|
+
);
|
|
3651
|
+
const [pendingLeave, setPendingLeave] = useState(null);
|
|
3652
|
+
const titleFrom = page.titleFrom;
|
|
3653
|
+
const resolvedTitle = useMemo(
|
|
3654
|
+
() => titleFrom === void 0 ? null : titleText(resolveValue(titleFrom, EMPTY_SCOPE)),
|
|
3655
|
+
// state 가 바뀌어야 다시 푼다 — resolveValue 는 ref 를 읽어 정체성이 안 바뀐다.
|
|
3656
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3657
|
+
[titleFrom, resolveValue, state]
|
|
3658
|
+
);
|
|
3659
|
+
const effectiveTitle = resolvedTitle ?? page.title;
|
|
3660
|
+
useEffect(() => {
|
|
3661
|
+
if (titleFrom === void 0) return;
|
|
3662
|
+
hostRef.current.setTitle?.(effectiveTitle ?? null);
|
|
3663
|
+
}, [titleFrom, effectiveTitle]);
|
|
3664
|
+
useEffect(
|
|
3665
|
+
() => () => {
|
|
3666
|
+
if (titleFrom !== void 0) hostRef.current.setTitle?.(null);
|
|
3667
|
+
},
|
|
3668
|
+
// 마운트 1회 계약 — 라우트가 바뀌면 호스트가 key 로 갈아 끼운다.
|
|
3669
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
3670
|
+
[]
|
|
3671
|
+
);
|
|
2822
3672
|
const dataPlan = useMemo(
|
|
2823
3673
|
() => planPageData(page.data, (value) => resolveValue(value, EMPTY_SCOPE)),
|
|
2824
3674
|
// state를 의존성에 두어야 $state를 참조하는 경로가 값 변화를 따라간다.
|
|
@@ -2908,6 +3758,34 @@ function SduiRenderer({
|
|
|
2908
3758
|
revertUnsettledStamps(fetchedRef.current, runnable, epochs, settled);
|
|
2909
3759
|
};
|
|
2910
3760
|
}, [dataPlanKey, dataEpochKey, setEntries]);
|
|
3761
|
+
const revalidateField = useCallback(
|
|
3762
|
+
(key) => {
|
|
3763
|
+
for (const [formId, config] of formsRef.current) {
|
|
3764
|
+
const prefix = `${formId}.`;
|
|
3765
|
+
if (!key.startsWith(prefix)) continue;
|
|
3766
|
+
const field = key.slice(prefix.length);
|
|
3767
|
+
if (field === "" || field.startsWith("$")) continue;
|
|
3768
|
+
const errors = stateRef.current[`${formId}.$errors`];
|
|
3769
|
+
if (!isPlainObject(errors) || !Object.hasOwn(errors, field)) continue;
|
|
3770
|
+
const next = { ...errors };
|
|
3771
|
+
try {
|
|
3772
|
+
const result = validateForm(
|
|
3773
|
+
collectFormValues(stateRef.current, formId),
|
|
3774
|
+
config,
|
|
3775
|
+
customValidators,
|
|
3776
|
+
fieldInfoRef.current.get(formId)
|
|
3777
|
+
);
|
|
3778
|
+
const message = result.errors[field];
|
|
3779
|
+
if (message) next[field] = message;
|
|
3780
|
+
else delete next[field];
|
|
3781
|
+
} catch {
|
|
3782
|
+
delete next[field];
|
|
3783
|
+
}
|
|
3784
|
+
setEntries({ [`${formId}.$errors`]: next });
|
|
3785
|
+
}
|
|
3786
|
+
},
|
|
3787
|
+
[customValidators, setEntries]
|
|
3788
|
+
);
|
|
2911
3789
|
const dispatchRef = useRef(() => {
|
|
2912
3790
|
});
|
|
2913
3791
|
const dispatch = useCallback(
|
|
@@ -2924,17 +3802,24 @@ function SduiRenderer({
|
|
|
2924
3802
|
eventScope
|
|
2925
3803
|
);
|
|
2926
3804
|
switch (action.action) {
|
|
2927
|
-
case "navigate":
|
|
3805
|
+
case "navigate": {
|
|
2928
3806
|
if (!isSafeNavigationTarget(action.to)) {
|
|
2929
3807
|
warnOnce(`navigate:${String(action.to)}`, `[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 navigate \uB300\uC0C1: ${String(action.to)}`);
|
|
2930
3808
|
break;
|
|
2931
3809
|
}
|
|
3810
|
+
const leaveMessage = syncLeaveGuard();
|
|
3811
|
+
if (leaveMessage !== null && typeof hostRef.current.setLeaveGuard !== "function") {
|
|
3812
|
+
setPendingLeave({ to: action.to, message: leaveMessage });
|
|
3813
|
+
break;
|
|
3814
|
+
}
|
|
2932
3815
|
hostRef.current.navigate(action.to);
|
|
2933
3816
|
break;
|
|
3817
|
+
}
|
|
2934
3818
|
case "setState": {
|
|
2935
3819
|
const nextValue = action.fromEvent ? eventValue(arg) : action.value;
|
|
2936
3820
|
if (!Array.isArray(action.path) || action.path.length === 0) {
|
|
2937
3821
|
setEntries({ [action.key]: nextValue });
|
|
3822
|
+
revalidateField(action.key);
|
|
2938
3823
|
break;
|
|
2939
3824
|
}
|
|
2940
3825
|
const written = setAtPath(
|
|
@@ -2950,6 +3835,7 @@ function SduiRenderer({
|
|
|
2950
3835
|
break;
|
|
2951
3836
|
}
|
|
2952
3837
|
setEntries({ [action.key]: written.next });
|
|
3838
|
+
revalidateField(action.key);
|
|
2953
3839
|
break;
|
|
2954
3840
|
}
|
|
2955
3841
|
case "open":
|
|
@@ -2959,7 +3845,7 @@ function SduiRenderer({
|
|
|
2959
3845
|
setEntries({ [action.key]: false });
|
|
2960
3846
|
break;
|
|
2961
3847
|
case "toast":
|
|
2962
|
-
pushToast({ tone: action.tone ?? "info", title: action.title, message: action.message });
|
|
3848
|
+
pushToast({ tone: action.tone ?? "info", title: toastText(action.title), message: toastText(action.message) });
|
|
2963
3849
|
break;
|
|
2964
3850
|
case "arrayAppend": {
|
|
2965
3851
|
const current = stateRef.current[action.key];
|
|
@@ -3026,16 +3912,21 @@ function SduiRenderer({
|
|
|
3026
3912
|
let phase = "validate";
|
|
3027
3913
|
let pendingOn = false;
|
|
3028
3914
|
try {
|
|
3029
|
-
const
|
|
3030
|
-
const
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
}
|
|
3035
|
-
}
|
|
3036
|
-
const errors = validateForm(values, formsRef.current.get(formId), customValidators);
|
|
3915
|
+
const values = collectFormValues(stateRef.current, formId);
|
|
3916
|
+
const config = formsRef.current.get(formId);
|
|
3917
|
+
const fieldInfo = fieldInfoRef.current.get(formId);
|
|
3918
|
+
const result = validateForm(values, config, customValidators, fieldInfo);
|
|
3919
|
+
const errors = result.errors;
|
|
3037
3920
|
if (Object.keys(errors).length > 0) {
|
|
3038
3921
|
setEntries({ [`${formId}.$errors`]: errors, [`${formId}.$submitting`]: false });
|
|
3922
|
+
const summary = invalidSummary(
|
|
3923
|
+
result,
|
|
3924
|
+
(field) => config?.labels[field] ?? fieldInfo?.get(field)?.label,
|
|
3925
|
+
fieldOrderOf(formId)
|
|
3926
|
+
);
|
|
3927
|
+
pushToast({ tone: "warning", title: summary.title, message: summary.message });
|
|
3928
|
+
const fields = Object.keys(errors);
|
|
3929
|
+
setTimeout(() => focusFirstInvalid(formId, fields), 0);
|
|
3039
3930
|
return;
|
|
3040
3931
|
}
|
|
3041
3932
|
setEntries({ [`${formId}.$errors`]: {}, [`${formId}.$submitting`]: true });
|
|
@@ -3080,7 +3971,10 @@ function SduiRenderer({
|
|
|
3080
3971
|
// scope 를 잇는다 — request 의 onSuccess/onError 도 같은 자리의 텍스트다.
|
|
3081
3972
|
// {"$arg"} 체인 항목은 dispatch 진입부가 이 스코프로 펼친다(submitForm과 동일).
|
|
3082
3973
|
dispatch: (a, value) => dispatchRef.current(a, value, scope),
|
|
3083
|
-
toast: pushToast
|
|
3974
|
+
toast: pushToast,
|
|
3975
|
+
// each: 원소를 행 프레임으로 얹어 endpoint·body 를 원소마다 푼다 — 같은 자리의
|
|
3976
|
+
// $state·$arg·$event 는 그대로 보이고, $item/$index 만 이 원소를 가리킨다.
|
|
3977
|
+
resolveFor: (value, item, index) => resolveValue(value, [...eventScope, { item, index }])
|
|
3084
3978
|
});
|
|
3085
3979
|
break;
|
|
3086
3980
|
case "invalidate": {
|
|
@@ -3127,12 +4021,12 @@ function SduiRenderer({
|
|
|
3127
4021
|
break;
|
|
3128
4022
|
}
|
|
3129
4023
|
},
|
|
3130
|
-
[pushToast, onCustomAction, customValidators, setEntries, resolveValue]
|
|
4024
|
+
[pushToast, onCustomAction, customValidators, setEntries, resolveValue, revalidateField, syncLeaveGuard]
|
|
3131
4025
|
);
|
|
3132
4026
|
dispatchRef.current = dispatch;
|
|
3133
4027
|
const engine = useMemo(
|
|
3134
|
-
() => ({ registry, resolveValue, dispatch, resolveAsset: assetResolverOf(host) }),
|
|
3135
|
-
[registry, resolveValue, dispatch, host]
|
|
4028
|
+
() => ({ registry, resolveValue, dispatch, resolveAsset: assetResolverOf(host), noteField }),
|
|
4029
|
+
[registry, resolveValue, dispatch, host, noteField]
|
|
3136
4030
|
);
|
|
3137
4031
|
const runtime = useMemo(
|
|
3138
4032
|
() => ({
|
|
@@ -3150,15 +4044,43 @@ function SduiRenderer({
|
|
|
3150
4044
|
return (
|
|
3151
4045
|
// 해석된 어댑터를 트리에 흘려보낸다 — dispatch를 거치지 않고 스스로 요청을 내는
|
|
3152
4046
|
// 컴포넌트(목록 조회 등)도 호스트가 건 잠금 안에서 움직이게 하기 위해서다(adapter.ts 주석).
|
|
3153
|
-
/* @__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: [
|
|
4047
|
+
/* @__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: [
|
|
3154
4048
|
/* @__PURE__ */ jsx(HoistedStyle, { id: "wp-sdui-layout", css: LAYOUT_CSS }),
|
|
3155
|
-
!hasExplicitPageHead(page.body) && (
|
|
3156
|
-
|
|
4049
|
+
!hasExplicitPageHead(page.body) && (effectiveTitle || page.subtitle) ? /* @__PURE__ */ jsx("div", { className: "wp-page-head", children: /* @__PURE__ */ jsxs("div", { className: "wp-page-head__text", children: [
|
|
4050
|
+
effectiveTitle ? /* @__PURE__ */ jsx("h1", { className: "wp-page-head__title", children: effectiveTitle }) : null,
|
|
3157
4051
|
page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head__sub", children: page.subtitle }) : null
|
|
3158
4052
|
] }) }) : null,
|
|
3159
4053
|
/* @__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" }) }),
|
|
3160
|
-
|
|
3161
|
-
|
|
4054
|
+
/* @__PURE__ */ jsx(SduiToastRegion, { hostId: toastHostId }),
|
|
4055
|
+
pendingLeave ? /* @__PURE__ */ jsx(OverlayLayer, { onClose: () => setPendingLeave(null), children: /* @__PURE__ */ jsx(
|
|
4056
|
+
Dialog,
|
|
4057
|
+
{
|
|
4058
|
+
size: "sm",
|
|
4059
|
+
icon: "triangle-alert",
|
|
4060
|
+
tone: "danger",
|
|
4061
|
+
title: "\uC800\uC7A5\uD558\uC9C0 \uC54A\uACE0 \uB098\uAC08\uAE4C\uC694?",
|
|
4062
|
+
onClose: () => setPendingLeave(null),
|
|
4063
|
+
footer: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4064
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", "data-autofocus": "", onClick: () => setPendingLeave(null), children: "\uACC4\uC18D \uC791\uC131" }),
|
|
4065
|
+
/* @__PURE__ */ jsx(
|
|
4066
|
+
Button,
|
|
4067
|
+
{
|
|
4068
|
+
variant: "danger",
|
|
4069
|
+
onClick: () => {
|
|
4070
|
+
const { to } = pendingLeave;
|
|
4071
|
+
setPendingLeave(null);
|
|
4072
|
+
leavingRef.current = true;
|
|
4073
|
+
syncLeaveGuard();
|
|
4074
|
+
hostRef.current.navigate(to);
|
|
4075
|
+
},
|
|
4076
|
+
children: "\uB098\uAC00\uAE30"
|
|
4077
|
+
}
|
|
4078
|
+
)
|
|
4079
|
+
] }),
|
|
4080
|
+
children: pendingLeave.message
|
|
4081
|
+
}
|
|
4082
|
+
) }) : null
|
|
4083
|
+
] }) }) }) }) }) })
|
|
3162
4084
|
);
|
|
3163
4085
|
}
|
|
3164
4086
|
|
|
@@ -3219,4 +4141,4 @@ async function resolvePatternSpec(id, load) {
|
|
|
3219
4141
|
// src/icon-names.ts
|
|
3220
4142
|
var ICON_NAMES = Object.keys(ICON_NODES);
|
|
3221
4143
|
|
|
3222
|
-
export { Can, ICON_NAMES, NodeErrorBoundary, RemoteSearchResults, ResultTable, SduiForm, SduiRenderer, SduiSearchDataProvider, SduiSearchInvalidationProvider, SduiSearchResults, SearchResults, SearchSummaryCards, SortHandle, SortableList, cellRenderer, createRegistry, createSearchInvalidator, defaultRegistry, imageRenderer, isValidSpecId, mapActions, mapCardRows, mapColumns, mapGridCard, mapViews, matchesCondition, normalizePageSizes, patternSpecCandidates, resolvePatternSpec, sortableDropIndex, sortableMove, summaryParams, useRemoteSearchPage, useRemoteSearchSummary,
|
|
4144
|
+
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 };
|