@wipco/sdui 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +27 -0
- package/dist/chunk-4SU2KYPU.js +59 -0
- package/dist/chunk-56KKWLNI.js +186 -0
- package/dist/chunk-7Y4J7EOK.js +115 -0
- package/dist/chunk-APCXOCAO.js +110 -0
- package/dist/chunk-AROOQ5W6.js +119 -0
- package/dist/chunk-DGPRX4MU.js +251 -0
- package/dist/chunk-FGRR3ROJ.js +497 -0
- package/dist/chunk-IL2KS4WS.js +25 -0
- package/dist/chunk-KADFN76X.js +541 -0
- package/dist/chunk-KBHI3IUR.js +40 -0
- package/dist/chunk-LZJTKVMO.js +767 -0
- package/dist/chunk-MOIU7MUA.js +282 -0
- package/dist/chunk-MSAV4J4I.js +77 -0
- package/dist/chunk-OOHHW4K4.js +159 -0
- package/dist/chunk-Q6SBH63R.js +190 -0
- package/dist/chunk-QZQDHSMD.js +78 -0
- package/dist/chunk-RH7ESUE6.js +309 -0
- package/dist/chunk-SEBPBUZR.js +260 -0
- package/dist/chunk-USKRM22W.js +39 -0
- package/dist/chunk-WNYWTFGW.js +61 -0
- package/dist/chunk-WVRCIVAD.js +72 -0
- package/dist/chunk-ZNITJ4KK.js +433 -0
- package/dist/chunk-ZQR3ULVH.js +193 -0
- package/dist/{chunk-QJHD6HVQ.js → chunk-ZUCI34QY.js} +106 -390
- package/dist/index.d.ts +419 -56
- package/dist/index.js +412 -803
- package/dist/meta.d.ts +188 -2
- package/dist/meta.js +1754 -9
- package/dist/rich.js +19 -4
- package/dist/styles.css +246 -0
- package/dist/widgets/AutocompleteField.d.ts +96 -0
- package/dist/widgets/AutocompleteField.js +3 -0
- package/dist/widgets/ColumnBoard.d.ts +47 -0
- package/dist/widgets/ColumnBoard.js +2 -0
- package/dist/widgets/DiffView.d.ts +30 -0
- package/dist/widgets/DiffView.js +2 -0
- package/dist/widgets/Disclosure.d.ts +20 -0
- package/dist/widgets/Disclosure.js +2 -0
- package/dist/widgets/EventCalendar.d.ts +59 -0
- package/dist/widgets/EventCalendar.js +2 -0
- package/dist/widgets/ImageLightbox.d.ts +25 -0
- package/dist/widgets/ImageLightbox.js +2 -0
- package/dist/widgets/InlineEdit.d.ts +23 -0
- package/dist/widgets/InlineEdit.js +2 -0
- package/dist/widgets/JsonField.d.ts +24 -0
- package/dist/widgets/JsonField.js +2 -0
- package/dist/widgets/LiveClock.d.ts +21 -0
- package/dist/widgets/LiveClock.js +1 -0
- package/dist/widgets/OrgChart.d.ts +32 -0
- package/dist/widgets/OrgChart.js +2 -0
- package/dist/widgets/PeriodFilter.d.ts +48 -0
- package/dist/widgets/PeriodFilter.js +2 -0
- package/dist/widgets/PrintSheet.d.ts +23 -0
- package/dist/widgets/PrintSheet.js +2 -0
- package/dist/widgets/SearchableMultiPicker.d.ts +29 -0
- package/dist/widgets/SearchableMultiPicker.js +3 -0
- package/dist/widgets/SecureScreen.d.ts +25 -0
- package/dist/widgets/SecureScreen.js +2 -0
- package/dist/widgets/StackedMeter.d.ts +26 -0
- package/dist/widgets/StackedMeter.js +1 -0
- package/dist/widgets/Strip.d.ts +20 -0
- package/dist/widgets/Strip.js +1 -0
- package/dist/widgets/SummaryTable.d.ts +31 -0
- package/dist/widgets/SummaryTable.js +1 -0
- package/dist/widgets/TagMultiSelect.d.ts +25 -0
- package/dist/widgets/TagMultiSelect.js +2 -0
- package/dist/widgets/Timeline.d.ts +30 -0
- package/dist/widgets/Timeline.js +3 -0
- package/dist/widgets/TreePicker.d.ts +21 -0
- package/dist/widgets/TreePicker.js +3 -0
- package/dist/widgets/TreeView.d.ts +26 -0
- package/dist/widgets/TreeView.js +2 -0
- package/dist/widgets/index.d.ts +76 -0
- package/dist/widgets/index.js +69 -0
- package/package.json +93 -3
package/dist/index.js
CHANGED
|
@@ -1,74 +1,105 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { DEV, DateRangePicker, DatePicker, isSafeNavigationTarget, useSduiAdapter, hostFetchOf, HoistedStyle, resolveAdapter, assetResolverOf, SduiAdapterContext } from './chunk-FGRR3ROJ.js';
|
|
2
|
+
export { DatePicker, DateRangePicker, defaultAdapter, useSduiAssetResolver, useSduiSandbox } from './chunk-FGRR3ROJ.js';
|
|
3
|
+
import { searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-KADFN76X.js';
|
|
4
|
+
export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-KADFN76X.js';
|
|
5
|
+
import { Toast, Textarea, Tag, Tabs, Table, Switch, StatCard, Spinner, Select, ProgressBar, Pagination, Input, IconButton, Icon, Field, EmptyState, Checkbox, Card, Button, Badge, Alert, SegmentedControl, Dialog } from './chunk-ZUCI34QY.js';
|
|
6
|
+
export { Alert, Avatar, Badge, Breadcrumb, Button, Card, Checkbox, Dialog, EmptyState, Field, Icon, IconButton, Input, Pagination, ProgressBar, Radio, SegmentedControl, Select, SidebarNav, Spinner, StatCard, Switch, Table, Tabs, Tag, Textarea, Toast, Tooltip } from './chunk-ZUCI34QY.js';
|
|
3
7
|
import React, { createContext, useContext, useMemo, useState, useRef, useEffect, useCallback } from 'react';
|
|
4
8
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
5
9
|
|
|
6
|
-
// src/
|
|
7
|
-
var
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
credentials: "include",
|
|
30
|
-
body: init.body === void 0 ? void 0 : JSON.stringify(init.body)
|
|
31
|
-
});
|
|
32
|
-
const text = await response.text();
|
|
33
|
-
let json = null;
|
|
34
|
-
if (text) {
|
|
35
|
-
try {
|
|
36
|
-
json = JSON.parse(text);
|
|
37
|
-
} catch {
|
|
38
|
-
json = null;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (!response.ok) {
|
|
42
|
-
const message = json && typeof json === "object" && typeof json.message === "string" ? json.message : `\uC694\uCCAD\uC774 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4 (${response.status})`;
|
|
43
|
-
throw new Error(message);
|
|
10
|
+
// src/asset-url.ts
|
|
11
|
+
var FETCHING_URL_PROPS = /* @__PURE__ */ new Set([
|
|
12
|
+
"src",
|
|
13
|
+
"srcSet",
|
|
14
|
+
"imageSrcSet",
|
|
15
|
+
"poster",
|
|
16
|
+
"data",
|
|
17
|
+
"background",
|
|
18
|
+
// SVG `<use href>`/`<image xlink:href>`는 실제로 문서를 받아 온다. 어휘에 `<use>`가
|
|
19
|
+
// 없어 오늘은 도달 불가지만, 성질상 이쪽이다.
|
|
20
|
+
"xlinkHref"
|
|
21
|
+
]);
|
|
22
|
+
var NAVIGATION_URL_PROPS = /* @__PURE__ */ new Set(["href", "action", "formAction"]);
|
|
23
|
+
var URL_PROPS = /* @__PURE__ */ new Set([
|
|
24
|
+
...FETCHING_URL_PROPS,
|
|
25
|
+
...NAVIGATION_URL_PROPS
|
|
26
|
+
]);
|
|
27
|
+
function isDangerousUrlValue(value) {
|
|
28
|
+
const normalized = value.replace(/[\u0000-\u0020]/g, "").toLowerCase();
|
|
29
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(normalized);
|
|
30
|
+
if (!scheme) return false;
|
|
31
|
+
if (scheme[1] === "data") {
|
|
32
|
+
return !/^data:image\/(?!svg\+xml)[a-z0-9.+-]+[;,]/.test(normalized);
|
|
44
33
|
}
|
|
45
|
-
return
|
|
34
|
+
return !["http", "https", "mailto", "tel", "blob"].includes(scheme[1]);
|
|
46
35
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
36
|
+
function isStyleProp(name) {
|
|
37
|
+
return name === "style" || name.length > 5 && name.endsWith("Style");
|
|
38
|
+
}
|
|
39
|
+
var CSS_LOADING_VALUE = /(?:url|image-set)\s*\(/i;
|
|
40
|
+
var CSS_URL = /url\(\s*(?:"((?:[^"\\]|\\.)*)"|'((?:[^'\\]|\\.)*)'|([^)'"]*))\s*\)/gi;
|
|
41
|
+
var CSS_BARE_IMAGE_SET = /image-set\(\s*["']/i;
|
|
42
|
+
function cssUrlLiteral(url) {
|
|
43
|
+
return `url("${url.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}")`;
|
|
44
|
+
}
|
|
45
|
+
function resolveStyleAssets(style, resolve, ctx) {
|
|
46
|
+
let out = null;
|
|
47
|
+
for (const [property, value] of Object.entries(style)) {
|
|
48
|
+
if (typeof value !== "string" || !CSS_LOADING_VALUE.test(value)) continue;
|
|
49
|
+
let dropped = false;
|
|
50
|
+
const rewritten = value.replace(CSS_URL, (whole, dq, sq, bare) => {
|
|
51
|
+
const url = (dq ?? sq ?? bare ?? "").trim();
|
|
52
|
+
if (url === "") return whole;
|
|
53
|
+
const next = resolve(url, { ...ctx, kind: "css", cssProperty: property });
|
|
54
|
+
if (next === null) {
|
|
55
|
+
dropped = true;
|
|
56
|
+
return whole;
|
|
57
|
+
}
|
|
58
|
+
return next === url ? whole : cssUrlLiteral(next);
|
|
59
|
+
});
|
|
60
|
+
const unsafeRemains = CSS_BARE_IMAGE_SET.test(rewritten);
|
|
61
|
+
if (!dropped && !unsafeRemains && rewritten === value) continue;
|
|
62
|
+
out ??= { ...style };
|
|
63
|
+
if (dropped || unsafeRemains) delete out[property];
|
|
64
|
+
else out[property] = rewritten;
|
|
62
65
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
return out ?? style;
|
|
67
|
+
}
|
|
68
|
+
var MAX_DEPTH = 8;
|
|
69
|
+
function resolveAssetsIn(propName, value, resolve, component) {
|
|
70
|
+
return walk(propName, value, 0);
|
|
71
|
+
function walk(key, node, depth) {
|
|
72
|
+
if (depth > MAX_DEPTH) return node;
|
|
73
|
+
if (typeof node === "string") {
|
|
74
|
+
if (!FETCHING_URL_PROPS.has(key) || node === "") return node;
|
|
75
|
+
const next = resolve(node, { component, prop: key, kind: "attribute" });
|
|
76
|
+
return next === null ? void 0 : next;
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(node)) {
|
|
79
|
+
let changed = false;
|
|
80
|
+
const next = node.map((entry) => {
|
|
81
|
+
const resolved = walk(key, entry, depth + 1);
|
|
82
|
+
if (resolved !== entry) changed = true;
|
|
83
|
+
return resolved;
|
|
84
|
+
});
|
|
85
|
+
return changed ? next : node;
|
|
86
|
+
}
|
|
87
|
+
if (typeof node !== "object" || node === null) return node;
|
|
88
|
+
if (Object.getPrototypeOf(node) !== Object.prototype && Object.getPrototypeOf(node) !== null) {
|
|
89
|
+
return node;
|
|
69
90
|
}
|
|
91
|
+
const record = node;
|
|
92
|
+
if (isStyleProp(key)) return resolveStyleAssets(record, resolve, { component, prop: key });
|
|
93
|
+
let out = null;
|
|
94
|
+
for (const [childKey, childValue] of Object.entries(record)) {
|
|
95
|
+
const resolved = walk(childKey, childValue, depth + 1);
|
|
96
|
+
if (resolved === childValue) continue;
|
|
97
|
+
out ??= { ...record };
|
|
98
|
+
if (resolved === void 0) delete out[childKey];
|
|
99
|
+
else out[childKey] = resolved;
|
|
100
|
+
}
|
|
101
|
+
return out ?? record;
|
|
70
102
|
}
|
|
71
|
-
return merged;
|
|
72
103
|
}
|
|
73
104
|
var SduiDispatchContext = createContext(null);
|
|
74
105
|
function useSduiDispatch() {
|
|
@@ -229,10 +260,120 @@ function Show({ when, children }) {
|
|
|
229
260
|
function Divider({ style }) {
|
|
230
261
|
return /* @__PURE__ */ jsx("hr", { className: "wp-divider", style });
|
|
231
262
|
}
|
|
232
|
-
|
|
263
|
+
function toNumber(value) {
|
|
264
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
265
|
+
if (typeof value === "string") {
|
|
266
|
+
const plain = value.replace(/,/g, "").trim();
|
|
267
|
+
if (plain !== "" && /^-?\d+(\.\d+)?$/.test(plain)) return Number(plain);
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
function fillTemplate(template, row) {
|
|
272
|
+
return template.replace(
|
|
273
|
+
/\{([\w.-]+)\}/g,
|
|
274
|
+
(_match, key) => encodeURIComponent(String(row[key] ?? ""))
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
function formatDate(raw, format) {
|
|
278
|
+
const date = new Date(raw);
|
|
279
|
+
if (Number.isNaN(date.getTime())) return raw;
|
|
280
|
+
if (format === "datetime") return date.toLocaleString("ko-KR");
|
|
281
|
+
if (format === "time") return date.toLocaleTimeString("ko-KR");
|
|
282
|
+
return date.toLocaleDateString("ko-KR");
|
|
283
|
+
}
|
|
284
|
+
function cellRenderer(cell, key, dispatch) {
|
|
285
|
+
if (!cell || typeof cell !== "object" || typeof cell.type !== "string") return void 0;
|
|
286
|
+
switch (cell.type) {
|
|
287
|
+
case "badge":
|
|
288
|
+
return (row) => {
|
|
289
|
+
const value = String(row[key] ?? "");
|
|
290
|
+
if (!value) return null;
|
|
291
|
+
return /* @__PURE__ */ jsx(Badge, { tone: cell.toneMap?.[value] ?? "neutral", children: value });
|
|
292
|
+
};
|
|
293
|
+
case "tag":
|
|
294
|
+
return (row) => {
|
|
295
|
+
const value = String(row[key] ?? "");
|
|
296
|
+
if (!value) return null;
|
|
297
|
+
return /* @__PURE__ */ jsx(Tag, { color: cell.colorMap?.[value], filled: cell.filled, children: value });
|
|
298
|
+
};
|
|
299
|
+
case "link":
|
|
300
|
+
return (row) => {
|
|
301
|
+
const label = String(row[cell.labelKey ?? key] ?? "");
|
|
302
|
+
if (!label) return null;
|
|
303
|
+
const to = fillTemplate(cell.to ?? "", row);
|
|
304
|
+
return (
|
|
305
|
+
// Button이 아니라 <a>인 이유: 링크는 마우스 오른쪽 클릭·새 탭이 되어야 하는데
|
|
306
|
+
// ButtonProps에 href가 없다. 클래스는 DS의 링크 버튼(.wp-btn--link)을 그대로 쓴다.
|
|
307
|
+
/* @__PURE__ */ jsxs(
|
|
308
|
+
"a",
|
|
309
|
+
{
|
|
310
|
+
className: "wp-btn wp-btn--link",
|
|
311
|
+
href: isSafeNavigationTarget(to) ? to : void 0,
|
|
312
|
+
onClick: (e) => {
|
|
313
|
+
e.preventDefault();
|
|
314
|
+
e.stopPropagation();
|
|
315
|
+
dispatch({ action: "navigate", to });
|
|
316
|
+
},
|
|
317
|
+
children: [
|
|
318
|
+
cell.icon ? /* @__PURE__ */ jsx(Icon, { name: cell.icon, size: 14 }) : null,
|
|
319
|
+
label
|
|
320
|
+
]
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
);
|
|
324
|
+
};
|
|
325
|
+
case "progress":
|
|
326
|
+
return (row) => {
|
|
327
|
+
const value = toNumber(row[key]);
|
|
328
|
+
if (value === null) return String(row[key] ?? "");
|
|
329
|
+
const max = (cell.maxKey ? toNumber(row[cell.maxKey]) : null) ?? cell.max ?? 100;
|
|
330
|
+
return /* @__PURE__ */ jsx(ProgressBar, { value, max: max || 100, tone: cell.tone, showValue: cell.showValue, size: "sm" });
|
|
331
|
+
};
|
|
332
|
+
case "icon":
|
|
333
|
+
return (row) => {
|
|
334
|
+
const value = String(row[key] ?? "");
|
|
335
|
+
const name = cell.iconMap?.[value] ?? cell.icon;
|
|
336
|
+
if (!name) return value;
|
|
337
|
+
return (
|
|
338
|
+
// .wp-row 기본 간격(space-2)은 아이콘+글자 한 덩어리로 보이기엔 넓어 한 단계 좁힌다
|
|
339
|
+
/* @__PURE__ */ jsxs("span", { className: "wp-row", style: { gap: "var(--wp-space-1)" }, children: [
|
|
340
|
+
/* @__PURE__ */ jsx(Icon, { name, size: 14, title: cell.withLabel ? void 0 : value }),
|
|
341
|
+
cell.withLabel ? value : null
|
|
342
|
+
] })
|
|
343
|
+
);
|
|
344
|
+
};
|
|
345
|
+
case "number":
|
|
346
|
+
return (row) => {
|
|
347
|
+
const value = toNumber(row[key]);
|
|
348
|
+
if (value === null) return String(row[key] ?? "");
|
|
349
|
+
const digits = Number.isFinite(cell.digits) ? Math.max(0, Math.min(6, Number(cell.digits))) : 0;
|
|
350
|
+
const text = new Intl.NumberFormat("ko-KR", {
|
|
351
|
+
minimumFractionDigits: digits,
|
|
352
|
+
maximumFractionDigits: digits
|
|
353
|
+
}).format(value);
|
|
354
|
+
return `${cell.prefix ?? ""}${text}${cell.suffix ?? ""}`;
|
|
355
|
+
};
|
|
356
|
+
case "date":
|
|
357
|
+
return (row) => {
|
|
358
|
+
const raw = String(row[key] ?? "");
|
|
359
|
+
return raw ? formatDate(raw, cell.format) : "";
|
|
360
|
+
};
|
|
361
|
+
case "text":
|
|
362
|
+
return void 0;
|
|
363
|
+
default:
|
|
364
|
+
return void 0;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function searchUrl(endpoint, params) {
|
|
233
368
|
const query = params.toString();
|
|
234
|
-
|
|
235
|
-
|
|
369
|
+
return `${endpoint}${query ? endpoint.includes("?") ? "&" : "?" : ""}${query}`;
|
|
370
|
+
}
|
|
371
|
+
function toPage(body) {
|
|
372
|
+
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
373
|
+
return { data: rows, total: typeof body?.total === "number" ? body.total : rows.length };
|
|
374
|
+
}
|
|
375
|
+
var defaultFetcher = async (endpoint, params, signal) => {
|
|
376
|
+
const response = await fetch(searchUrl(endpoint, params), {
|
|
236
377
|
headers: { accept: "application/json" },
|
|
237
378
|
credentials: "include",
|
|
238
379
|
signal
|
|
@@ -241,9 +382,14 @@ var defaultFetcher = async (endpoint, params, signal) => {
|
|
|
241
382
|
if (!response.ok) {
|
|
242
383
|
throw new Error(body?.message ?? `\uBAA9\uB85D\uC744 \uBD88\uB7EC\uC624\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4 (${response.status})`);
|
|
243
384
|
}
|
|
244
|
-
|
|
245
|
-
return { data: rows, total: typeof body?.total === "number" ? body.total : rows.length };
|
|
385
|
+
return toPage(body);
|
|
246
386
|
};
|
|
387
|
+
function adapterFetcher(hostFetch) {
|
|
388
|
+
return async (endpoint, params, signal) => {
|
|
389
|
+
const body = await hostFetch(searchUrl(endpoint, params), { method: "GET", signal });
|
|
390
|
+
return toPage(body);
|
|
391
|
+
};
|
|
392
|
+
}
|
|
247
393
|
var SearchDataContext = createContext(null);
|
|
248
394
|
function SduiSearchDataProvider({
|
|
249
395
|
fetcher,
|
|
@@ -252,7 +398,13 @@ function SduiSearchDataProvider({
|
|
|
252
398
|
return /* @__PURE__ */ jsx(SearchDataContext.Provider, { value: fetcher, children });
|
|
253
399
|
}
|
|
254
400
|
function useSearchFetcher() {
|
|
255
|
-
|
|
401
|
+
const injected = useContext(SearchDataContext);
|
|
402
|
+
const adapter = useSduiAdapter();
|
|
403
|
+
return useMemo(() => {
|
|
404
|
+
if (injected) return injected;
|
|
405
|
+
const hostFetch = hostFetchOf(adapter);
|
|
406
|
+
return hostFetch ? adapterFetcher(hostFetch) : defaultFetcher;
|
|
407
|
+
}, [injected, adapter]);
|
|
256
408
|
}
|
|
257
409
|
var EMPTY_PAGE = { data: [], total: 0 };
|
|
258
410
|
function useRemoteSearchPage(endpoint, params) {
|
|
@@ -289,78 +441,8 @@ function useRemoteSearchPage(endpoint, params) {
|
|
|
289
441
|
const reload = useCallback(() => setReloadKey((value) => value + 1), []);
|
|
290
442
|
return { page, isLoading, error, reload };
|
|
291
443
|
}
|
|
292
|
-
var HOST_STYLE = { position: "relative", display: "inline-flex" };
|
|
293
|
-
var PANEL_STYLE = {
|
|
294
|
-
position: "absolute",
|
|
295
|
-
top: "calc(100% + var(--wp-space-2))",
|
|
296
|
-
minWidth: "100%",
|
|
297
|
-
background: "var(--wp-surface-card)",
|
|
298
|
-
border: "1px solid var(--wp-border-subtle)",
|
|
299
|
-
borderRadius: "var(--wp-radius-lg)",
|
|
300
|
-
boxShadow: "var(--wp-shadow-lg)",
|
|
301
|
-
// 다이얼로그(--wp-z-dialog: 100)보다는 아래 — 모달 위로 삐져나오면 안 된다
|
|
302
|
-
zIndex: 90
|
|
303
|
-
};
|
|
304
|
-
var BODY_STYLE = {
|
|
305
|
-
maxHeight: "60vh",
|
|
306
|
-
overflow: "auto",
|
|
307
|
-
padding: "var(--wp-space-4)"
|
|
308
|
-
};
|
|
309
|
-
var FOOT_STYLE = {
|
|
310
|
-
display: "flex",
|
|
311
|
-
alignItems: "center",
|
|
312
|
-
justifyContent: "space-between",
|
|
313
|
-
gap: "var(--wp-space-2)",
|
|
314
|
-
padding: "var(--wp-space-3)",
|
|
315
|
-
borderTop: "1px solid var(--wp-divider)"
|
|
316
|
-
};
|
|
317
|
-
function Popover({
|
|
318
|
-
open,
|
|
319
|
-
onClose,
|
|
320
|
-
trigger,
|
|
321
|
-
footer,
|
|
322
|
-
alignLeft = true,
|
|
323
|
-
width,
|
|
324
|
-
children
|
|
325
|
-
}) {
|
|
326
|
-
const hostRef = useRef(null);
|
|
327
|
-
const closeRef = useRef(onClose);
|
|
328
|
-
closeRef.current = onClose;
|
|
329
|
-
useEffect(() => {
|
|
330
|
-
if (!open) return;
|
|
331
|
-
const closeOnOutside = (event) => {
|
|
332
|
-
if (event.target instanceof Node && hostRef.current?.contains(event.target)) return;
|
|
333
|
-
closeRef.current();
|
|
334
|
-
};
|
|
335
|
-
const closeOnEscape = (event) => {
|
|
336
|
-
if (event.key === "Escape") closeRef.current();
|
|
337
|
-
};
|
|
338
|
-
document.addEventListener("pointerdown", closeOnOutside);
|
|
339
|
-
document.addEventListener("keydown", closeOnEscape);
|
|
340
|
-
return () => {
|
|
341
|
-
document.removeEventListener("pointerdown", closeOnOutside);
|
|
342
|
-
document.removeEventListener("keydown", closeOnEscape);
|
|
343
|
-
};
|
|
344
|
-
}, [open]);
|
|
345
|
-
return /* @__PURE__ */ jsxs("div", { ref: hostRef, style: HOST_STYLE, children: [
|
|
346
|
-
trigger,
|
|
347
|
-
open ? /* @__PURE__ */ jsxs(
|
|
348
|
-
"div",
|
|
349
|
-
{
|
|
350
|
-
style: {
|
|
351
|
-
...PANEL_STYLE,
|
|
352
|
-
...alignLeft ? { left: 0 } : { right: 0 },
|
|
353
|
-
...width ? { width } : null
|
|
354
|
-
},
|
|
355
|
-
children: [
|
|
356
|
-
/* @__PURE__ */ jsx("div", { style: BODY_STYLE, children }),
|
|
357
|
-
footer ? /* @__PURE__ */ jsx("div", { style: FOOT_STYLE, children: footer }) : null
|
|
358
|
-
]
|
|
359
|
-
}
|
|
360
|
-
) : null
|
|
361
|
-
] });
|
|
362
|
-
}
|
|
363
444
|
var OVERSCAN = 10;
|
|
445
|
+
var CLIP_CSS = `.wp-table--clip tbody td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}`;
|
|
364
446
|
function ResultTable({
|
|
365
447
|
columns,
|
|
366
448
|
rows,
|
|
@@ -395,47 +477,25 @@ function ResultTable({
|
|
|
395
477
|
const table = /* @__PURE__ */ jsxs(
|
|
396
478
|
"table",
|
|
397
479
|
{
|
|
398
|
-
className:
|
|
480
|
+
className: `wp-table wp-table--hover${virtual ? " wp-table--sticky wp-table--clip" : ""}`,
|
|
399
481
|
style: virtual ? { tableLayout: "fixed" } : void 0,
|
|
400
482
|
children: [
|
|
401
483
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((column) => /* @__PURE__ */ jsx(
|
|
402
484
|
"th",
|
|
403
485
|
{
|
|
404
|
-
style: {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
alignItems: "center",
|
|
418
|
-
gap: 3,
|
|
419
|
-
background: "none",
|
|
420
|
-
border: 0,
|
|
421
|
-
padding: 0,
|
|
422
|
-
cursor: "pointer",
|
|
423
|
-
color: "inherit",
|
|
424
|
-
font: "inherit",
|
|
425
|
-
letterSpacing: "inherit",
|
|
426
|
-
textTransform: "inherit"
|
|
427
|
-
},
|
|
428
|
-
children: [
|
|
429
|
-
column.header,
|
|
430
|
-
/* @__PURE__ */ jsx(
|
|
431
|
-
Icon,
|
|
432
|
-
{
|
|
433
|
-
name: sort && sort.key === column.key ? sort.dir === "asc" ? "chevron-up" : "chevron-down" : "chevrons-up-down",
|
|
434
|
-
size: 12
|
|
435
|
-
}
|
|
436
|
-
)
|
|
437
|
-
]
|
|
438
|
-
}
|
|
486
|
+
style: { width: column.width, textAlign: column.align === "right" ? "right" : void 0 },
|
|
487
|
+
children: column.sortable && onSort ? (
|
|
488
|
+
// 인라인 style로는 :hover를 표현할 수 없다 — 색 피드백은 .wp-table__sort:hover가 낸다
|
|
489
|
+
/* @__PURE__ */ jsxs("button", { type: "button", className: "wp-table__sort", onClick: () => onSort(column.key), children: [
|
|
490
|
+
column.header,
|
|
491
|
+
/* @__PURE__ */ jsx(
|
|
492
|
+
Icon,
|
|
493
|
+
{
|
|
494
|
+
name: sort && sort.key === column.key ? sort.dir === "asc" ? "chevron-up" : "chevron-down" : "chevrons-up-down",
|
|
495
|
+
size: 12
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
] })
|
|
439
499
|
) : column.header
|
|
440
500
|
},
|
|
441
501
|
column.key
|
|
@@ -451,24 +511,13 @@ function ResultTable({
|
|
|
451
511
|
ref: offset === 0 ? firstRowRef : void 0,
|
|
452
512
|
"aria-selected": selected || void 0,
|
|
453
513
|
onClick: onRowClick ? () => onRowClick(row) : void 0,
|
|
454
|
-
style: {
|
|
455
|
-
...onRowClick ? { cursor: "pointer" } : null,
|
|
456
|
-
...selected ? { background: "var(--wp-surface-selected)" } : null
|
|
457
|
-
},
|
|
514
|
+
style: onRowClick ? { cursor: "pointer" } : void 0,
|
|
458
515
|
children: columns.map((column) => {
|
|
459
516
|
const cls = [];
|
|
460
517
|
if (column.align === "right") cls.push("wp-table__num");
|
|
461
518
|
if (column.mono) cls.push("wp-table__mono");
|
|
462
519
|
if (column.strong) cls.push("wp-table__strong");
|
|
463
|
-
return /* @__PURE__ */ jsx(
|
|
464
|
-
"td",
|
|
465
|
-
{
|
|
466
|
-
className: cls.join(" ") || void 0,
|
|
467
|
-
style: virtual ? { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } : void 0,
|
|
468
|
-
children: column.render ? column.render(row, index) : row[column.key]
|
|
469
|
-
},
|
|
470
|
-
column.key
|
|
471
|
-
);
|
|
520
|
+
return /* @__PURE__ */ jsx("td", { className: cls.join(" ") || void 0, children: column.render ? column.render(row, index) : row[column.key] }, column.key);
|
|
472
521
|
})
|
|
473
522
|
},
|
|
474
523
|
row.id ?? index
|
|
@@ -480,494 +529,40 @@ function ResultTable({
|
|
|
480
529
|
}
|
|
481
530
|
);
|
|
482
531
|
if (!virtual) return table;
|
|
483
|
-
return /* @__PURE__ */
|
|
532
|
+
return /* @__PURE__ */ jsxs("div", { ref: scrollRef, onScroll: handleScroll, style: { height, overflowY: "auto" }, children: [
|
|
533
|
+
/* @__PURE__ */ jsx(HoistedStyle, { id: "wp-table-clip", css: CLIP_CSS }),
|
|
534
|
+
table
|
|
535
|
+
] });
|
|
484
536
|
}
|
|
485
537
|
function ResultEmptyState({ title }) {
|
|
486
538
|
return /* @__PURE__ */ jsx(EmptyState, { icon: "scan-search", title: `${title}\uC774(\uAC00) \uC5C6\uC2B5\uB2C8\uB2E4`, tight: true, children: "\uAC80\uC0C9 \uC870\uAC74\uC744 \uBC14\uAFB8\uAC70\uB098 \uCD08\uAE30\uD654\uD574 \uBCF4\uC138\uC694." });
|
|
487
539
|
}
|
|
488
|
-
|
|
489
|
-
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
490
|
-
}
|
|
491
|
-
function joinMultiValue(values) {
|
|
492
|
-
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).sort().join(",");
|
|
493
|
-
}
|
|
494
|
-
function normalizeSearchText(value) {
|
|
495
|
-
return value.toLowerCase().replace(/\s+/g, "");
|
|
496
|
-
}
|
|
497
|
-
function includesLooseSearch(text, keyword) {
|
|
498
|
-
const normalizedKeyword = normalizeSearchText(keyword);
|
|
499
|
-
return normalizedKeyword.length === 0 || normalizeSearchText(text).includes(normalizedKeyword);
|
|
500
|
-
}
|
|
501
|
-
function searchFieldKeys(fields) {
|
|
502
|
-
return fields.flatMap((field) => {
|
|
503
|
-
if (field.type === "dateRange") {
|
|
504
|
-
return [field.fromKey ?? `${field.key}From`, field.toKey ?? `${field.key}To`];
|
|
505
|
-
}
|
|
506
|
-
if (field.type === "numberRange") {
|
|
507
|
-
return [field.fromKey ?? `${field.key}Min`, field.toKey ?? `${field.key}Max`];
|
|
508
|
-
}
|
|
509
|
-
return [field.key];
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
function searchValuesFromParams(fields, params) {
|
|
513
|
-
const values = {};
|
|
514
|
-
for (const key of searchFieldKeys(fields)) {
|
|
515
|
-
const value = params.get(key);
|
|
516
|
-
if (value) values[key] = value;
|
|
517
|
-
}
|
|
518
|
-
return values;
|
|
519
|
-
}
|
|
520
|
-
function cleanSearchValues(fields, values) {
|
|
521
|
-
const allowed = new Set(searchFieldKeys(fields));
|
|
522
|
-
const fieldsByKey = new Map(fields.map((field) => [field.key, field]));
|
|
523
|
-
return Object.fromEntries(
|
|
524
|
-
Object.entries(values).map(([key, value]) => {
|
|
525
|
-
const field = fieldsByKey.get(key);
|
|
526
|
-
const cleaned = field?.type === "select" || field?.type === "cascading" ? joinMultiValue(splitMultiValue(value)) : value.trim();
|
|
527
|
-
return [key, cleaned];
|
|
528
|
-
}).filter(([key, value]) => allowed.has(key) && value)
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
function cascadingPathMap(options) {
|
|
532
|
-
const paths = /* @__PURE__ */ new Map();
|
|
533
|
-
const visit = (option, parents) => {
|
|
534
|
-
const path = [...parents, option];
|
|
535
|
-
paths.set(option.value, path);
|
|
536
|
-
option.children?.forEach((child) => visit(child, path));
|
|
537
|
-
};
|
|
538
|
-
options.forEach((option) => visit(option, []));
|
|
539
|
-
return paths;
|
|
540
|
-
}
|
|
541
|
-
function cascadingValuePaths(options) {
|
|
542
|
-
const map = /* @__PURE__ */ new Map();
|
|
543
|
-
for (const [value, path] of cascadingPathMap(options)) {
|
|
544
|
-
map.set(
|
|
545
|
-
value,
|
|
546
|
-
path.map((option) => option.value)
|
|
547
|
-
);
|
|
548
|
-
}
|
|
549
|
-
return map;
|
|
550
|
-
}
|
|
551
|
-
function cascadingPathLabel(pathMap, value) {
|
|
552
|
-
const path = pathMap.get(value);
|
|
553
|
-
return path?.length ? path.map((option) => option.label).join(" > ") : value;
|
|
554
|
-
}
|
|
555
|
-
function activeSearchCriteria(fields, values) {
|
|
556
|
-
const criteria = [];
|
|
557
|
-
for (const field of fields) {
|
|
558
|
-
if (field.type === "select") {
|
|
559
|
-
for (const value2 of splitMultiValue(values[field.key] ?? "")) {
|
|
560
|
-
const label = field.options.find((o) => o.value === value2)?.label ?? value2;
|
|
561
|
-
criteria.push({
|
|
562
|
-
key: `${field.key}:${value2}`,
|
|
563
|
-
label: `${field.label}: ${label}`,
|
|
564
|
-
clearKeys: [field.key],
|
|
565
|
-
valueKey: field.key,
|
|
566
|
-
value: value2
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
if (field.type === "cascading") {
|
|
572
|
-
const pathMap = cascadingPathMap(field.options);
|
|
573
|
-
for (const value2 of splitMultiValue(values[field.key] ?? "")) {
|
|
574
|
-
criteria.push({
|
|
575
|
-
key: `${field.key}:${value2}`,
|
|
576
|
-
label: `${field.label}: ${cascadingPathLabel(pathMap, value2)}`,
|
|
577
|
-
clearKeys: [field.key],
|
|
578
|
-
valueKey: field.key,
|
|
579
|
-
value: value2
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
if (field.type === "dateRange" || field.type === "numberRange") {
|
|
585
|
-
const fromKey = field.fromKey ?? `${field.key}${field.type === "dateRange" ? "From" : "Min"}`;
|
|
586
|
-
const toKey = field.toKey ?? `${field.key}${field.type === "dateRange" ? "To" : "Max"}`;
|
|
587
|
-
const from = values[fromKey];
|
|
588
|
-
const to = values[toKey];
|
|
589
|
-
if (from || to) {
|
|
590
|
-
criteria.push({
|
|
591
|
-
key: field.key,
|
|
592
|
-
label: `${field.label}: ${from ?? ""}~${to ?? ""}`,
|
|
593
|
-
clearKeys: [fromKey, toKey]
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
const value = values[field.key];
|
|
599
|
-
if (value) {
|
|
600
|
-
criteria.push({
|
|
601
|
-
key: field.key,
|
|
602
|
-
label: `${field.label}: ${value}`,
|
|
603
|
-
clearKeys: [field.key]
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
return criteria;
|
|
608
|
-
}
|
|
540
|
+
var EMPTY_FILTERS = [];
|
|
609
541
|
var EMPTY_FIELDS = [];
|
|
610
|
-
var
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
hideSearchInput = false
|
|
620
|
-
}) {
|
|
621
|
-
const [open, setOpen] = useState(false);
|
|
622
|
-
const [draft, setDraft] = useState(advancedValues);
|
|
623
|
-
const hasAdvanced = advancedFields.length > 0;
|
|
624
|
-
useEffect(() => {
|
|
625
|
-
if (!open) setDraft(advancedValues);
|
|
626
|
-
}, [advancedValues, open]);
|
|
627
|
-
const activeCriteria = useMemo(
|
|
628
|
-
() => activeSearchCriteria(advancedFields, advancedValues),
|
|
629
|
-
[advancedFields, advancedValues]
|
|
630
|
-
);
|
|
631
|
-
const submit = (event) => {
|
|
632
|
-
event.preventDefault();
|
|
633
|
-
onSearch(search.trim(), cleanSearchValues(advancedFields, advancedValues));
|
|
634
|
-
setOpen(false);
|
|
635
|
-
};
|
|
636
|
-
const applyAdvancedSearch = () => {
|
|
637
|
-
onSearch(search.trim(), cleanSearchValues(advancedFields, draft));
|
|
638
|
-
setOpen(false);
|
|
639
|
-
};
|
|
640
|
-
const resetAdvancedSearch = () => {
|
|
641
|
-
setDraft({});
|
|
642
|
-
onSearch(search.trim(), {});
|
|
643
|
-
setOpen(false);
|
|
644
|
-
};
|
|
645
|
-
const removeCriterion = (criterion) => {
|
|
646
|
-
const nextValues = { ...advancedValues };
|
|
647
|
-
if (criterion.valueKey && criterion.value !== void 0) {
|
|
648
|
-
const remaining = splitMultiValue(nextValues[criterion.valueKey] ?? "").filter(
|
|
649
|
-
(value) => value !== criterion.value
|
|
650
|
-
);
|
|
651
|
-
if (remaining.length > 0) nextValues[criterion.valueKey] = joinMultiValue(remaining);
|
|
652
|
-
else delete nextValues[criterion.valueKey];
|
|
653
|
-
} else {
|
|
654
|
-
criterion.clearKeys.forEach((key) => delete nextValues[key]);
|
|
655
|
-
}
|
|
656
|
-
setDraft(nextValues);
|
|
657
|
-
onSearch(search.trim(), cleanSearchValues(advancedFields, nextValues));
|
|
658
|
-
};
|
|
659
|
-
return /* @__PURE__ */ jsxs("form", { className: ["wp-row", className].filter(Boolean).join(" "), onSubmit: submit, children: [
|
|
660
|
-
activeCriteria.length > 0 ? /* @__PURE__ */ jsx("div", { className: "wp-row", style: { flexWrap: "wrap" }, children: activeCriteria.map((criterion) => /* @__PURE__ */ jsx(Tag, { onRemove: () => removeCriterion(criterion), children: criterion.label }, criterion.key)) }) : null,
|
|
661
|
-
!hideSearchInput ? /* @__PURE__ */ jsx(
|
|
662
|
-
Input,
|
|
663
|
-
{
|
|
664
|
-
icon: "search",
|
|
665
|
-
value: search,
|
|
666
|
-
onChange: (event) => onSearchChange(event.target.value),
|
|
667
|
-
placeholder
|
|
668
|
-
}
|
|
669
|
-
) : null,
|
|
670
|
-
hasAdvanced ? /* @__PURE__ */ jsx(
|
|
671
|
-
Popover,
|
|
672
|
-
{
|
|
673
|
-
open,
|
|
674
|
-
onClose: () => setOpen(false),
|
|
675
|
-
alignLeft: false,
|
|
676
|
-
width: "min(560px, 80vw)",
|
|
677
|
-
trigger: /* @__PURE__ */ jsx(
|
|
678
|
-
IconButton,
|
|
679
|
-
{
|
|
680
|
-
type: "button",
|
|
681
|
-
icon: "sliders-horizontal",
|
|
682
|
-
label: "\uC0C1\uC138\uAC80\uC0C9",
|
|
683
|
-
"aria-expanded": open,
|
|
684
|
-
style: open ? { background: "var(--wp-surface-active)" } : void 0,
|
|
685
|
-
onClick: () => setOpen((value) => !value)
|
|
686
|
-
}
|
|
687
|
-
),
|
|
688
|
-
footer: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
689
|
-
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", icon: "refresh-cw", onClick: resetAdvancedSearch, children: "\uCD08\uAE30\uD654" }),
|
|
690
|
-
/* @__PURE__ */ jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: applyAdvancedSearch, children: "\uC801\uC6A9" })
|
|
691
|
-
] }),
|
|
692
|
-
children: /* @__PURE__ */ jsx("div", { className: "wp-grid wp-grid--2", children: advancedFields.map((field) => /* @__PURE__ */ jsx(
|
|
693
|
-
SearchFieldInput,
|
|
694
|
-
{
|
|
695
|
-
field,
|
|
696
|
-
values: draft,
|
|
697
|
-
onChange: (patch) => setDraft((current) => ({ ...current, ...patch }))
|
|
698
|
-
},
|
|
699
|
-
field.key
|
|
700
|
-
)) })
|
|
701
|
-
}
|
|
702
|
-
) : null
|
|
703
|
-
] });
|
|
704
|
-
}
|
|
705
|
-
function SearchFieldInput({
|
|
706
|
-
field,
|
|
707
|
-
values,
|
|
708
|
-
onChange
|
|
709
|
-
}) {
|
|
710
|
-
if (field.type === "select") {
|
|
711
|
-
return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsx(MultiSelectInput, { field, values, onChange }) });
|
|
712
|
-
}
|
|
713
|
-
if (field.type === "cascading") {
|
|
714
|
-
return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsx(CascadingInput, { field, values, onChange }) });
|
|
715
|
-
}
|
|
716
|
-
if (field.type === "dateRange") {
|
|
717
|
-
const fromKey = field.fromKey ?? `${field.key}From`;
|
|
718
|
-
const toKey = field.toKey ?? `${field.key}To`;
|
|
719
|
-
return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsxs("div", { className: "wp-row", children: [
|
|
720
|
-
/* @__PURE__ */ jsx(
|
|
721
|
-
Input,
|
|
722
|
-
{
|
|
723
|
-
type: "date",
|
|
724
|
-
mono: true,
|
|
725
|
-
value: values[fromKey] ?? "",
|
|
726
|
-
onChange: (e) => onChange({ [fromKey]: e.target.value })
|
|
727
|
-
}
|
|
728
|
-
),
|
|
729
|
-
/* @__PURE__ */ jsx("span", { className: "wp-caption", children: "~" }),
|
|
730
|
-
/* @__PURE__ */ jsx(
|
|
731
|
-
Input,
|
|
732
|
-
{
|
|
733
|
-
type: "date",
|
|
734
|
-
mono: true,
|
|
735
|
-
value: values[toKey] ?? "",
|
|
736
|
-
onChange: (e) => onChange({ [toKey]: e.target.value })
|
|
737
|
-
}
|
|
738
|
-
)
|
|
739
|
-
] }) });
|
|
740
|
-
}
|
|
741
|
-
if (field.type === "numberRange") {
|
|
742
|
-
const fromKey = field.fromKey ?? `${field.key}Min`;
|
|
743
|
-
const toKey = field.toKey ?? `${field.key}Max`;
|
|
744
|
-
return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsxs("div", { className: "wp-row", children: [
|
|
745
|
-
/* @__PURE__ */ jsx(
|
|
746
|
-
Input,
|
|
747
|
-
{
|
|
748
|
-
type: "number",
|
|
749
|
-
mono: true,
|
|
750
|
-
placeholder: field.minPlaceholder ?? "\uCD5C\uC18C",
|
|
751
|
-
value: values[fromKey] ?? "",
|
|
752
|
-
onChange: (e) => onChange({ [fromKey]: e.target.value })
|
|
753
|
-
}
|
|
754
|
-
),
|
|
755
|
-
/* @__PURE__ */ jsx("span", { className: "wp-caption", children: "~" }),
|
|
756
|
-
/* @__PURE__ */ jsx(
|
|
757
|
-
Input,
|
|
758
|
-
{
|
|
759
|
-
type: "number",
|
|
760
|
-
mono: true,
|
|
761
|
-
placeholder: field.maxPlaceholder ?? "\uCD5C\uB300",
|
|
762
|
-
value: values[toKey] ?? "",
|
|
763
|
-
onChange: (e) => onChange({ [toKey]: e.target.value })
|
|
764
|
-
}
|
|
765
|
-
)
|
|
766
|
-
] }) });
|
|
767
|
-
}
|
|
768
|
-
return /* @__PURE__ */ jsx(Field, { label: field.label, children: /* @__PURE__ */ jsx(
|
|
769
|
-
Input,
|
|
770
|
-
{
|
|
771
|
-
placeholder: field.placeholder,
|
|
772
|
-
value: values[field.key] ?? "",
|
|
773
|
-
onChange: (e) => onChange({ [field.key]: e.target.value })
|
|
774
|
-
}
|
|
775
|
-
) });
|
|
776
|
-
}
|
|
777
|
-
function multiSelectLabel(field, values) {
|
|
778
|
-
if (values.length === 0) return field.allLabel ?? "\uC804\uCCB4";
|
|
779
|
-
const labels = values.map((value) => field.options.find((item) => item.value === value)?.label ?? value);
|
|
780
|
-
if (labels.length <= 2) return labels.join(", ");
|
|
781
|
-
return `${labels[0]} \uC678 ${labels.length - 1}`;
|
|
782
|
-
}
|
|
783
|
-
function MultiSelectInput({
|
|
784
|
-
field,
|
|
785
|
-
values,
|
|
786
|
-
onChange
|
|
787
|
-
}) {
|
|
788
|
-
const [open, setOpen] = useState(false);
|
|
789
|
-
const selected = splitMultiValue(values[field.key] ?? "");
|
|
790
|
-
const toggle = (value, checked) => {
|
|
791
|
-
const next = checked ? [...selected, value] : selected.filter((item) => item !== value);
|
|
792
|
-
onChange({ [field.key]: joinMultiValue(next) });
|
|
793
|
-
};
|
|
794
|
-
return /* @__PURE__ */ jsx(
|
|
795
|
-
Popover,
|
|
796
|
-
{
|
|
797
|
-
open,
|
|
798
|
-
onClose: () => setOpen(false),
|
|
799
|
-
trigger: /* @__PURE__ */ jsx(
|
|
800
|
-
Button,
|
|
801
|
-
{
|
|
802
|
-
type: "button",
|
|
803
|
-
variant: "secondary",
|
|
804
|
-
block: true,
|
|
805
|
-
iconEnd: "chevron-down",
|
|
806
|
-
onClick: () => setOpen((value) => !value),
|
|
807
|
-
children: multiSelectLabel(field, selected)
|
|
808
|
-
}
|
|
809
|
-
),
|
|
810
|
-
children: /* @__PURE__ */ jsx("div", { className: "wp-col", children: field.options.map((option) => /* @__PURE__ */ jsx(
|
|
811
|
-
Checkbox,
|
|
812
|
-
{
|
|
813
|
-
label: option.label,
|
|
814
|
-
checked: selected.includes(option.value),
|
|
815
|
-
onChange: (e) => toggle(option.value, e.target.checked)
|
|
816
|
-
},
|
|
817
|
-
option.value
|
|
818
|
-
)) })
|
|
819
|
-
}
|
|
542
|
+
var PAGE_SIZE_CHOICES = [10, 20, 50, 100];
|
|
543
|
+
var DEFAULT_VIEWS = [
|
|
544
|
+
{ value: "list", icon: "list", label: "\uBAA9\uB85D" },
|
|
545
|
+
{ value: "grid", icon: "layout-grid", label: "\uCE74\uB4DC" }
|
|
546
|
+
];
|
|
547
|
+
function normalizePageSizes(sizes) {
|
|
548
|
+
if (!Array.isArray(sizes)) return void 0;
|
|
549
|
+
const clean = [...new Set(sizes.map(Number).filter((n) => Number.isFinite(n) && n > 0).map(Math.floor))].sort(
|
|
550
|
+
(a, b) => a - b
|
|
820
551
|
);
|
|
552
|
+
return clean.length > 0 ? clean : void 0;
|
|
821
553
|
}
|
|
822
|
-
function
|
|
554
|
+
function resolveViewItems(views, disableGrid, disableList) {
|
|
555
|
+
const source = Array.isArray(views) && views.length > 0 ? views : DEFAULT_VIEWS.filter((v) => !(v.value === "grid" && disableGrid) && !(v.value === "list" && disableList));
|
|
823
556
|
const seen = /* @__PURE__ */ new Set();
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
}
|
|
830
|
-
function cascadingLevels(options, selectedPaths) {
|
|
831
|
-
const levels = [];
|
|
832
|
-
let currentOptions = options;
|
|
833
|
-
let depth = 0;
|
|
834
|
-
while (currentOptions.length > 0) {
|
|
835
|
-
levels.push(currentOptions);
|
|
836
|
-
const selectedAtDepth = uniqueCascadingOptions(
|
|
837
|
-
selectedPaths.map((path) => path[depth]).filter(
|
|
838
|
-
(option) => Boolean(option && currentOptions.some((current) => current.value === option.value))
|
|
839
|
-
)
|
|
840
|
-
);
|
|
841
|
-
const nextOptions = uniqueCascadingOptions(selectedAtDepth.flatMap((option) => option.children ?? []));
|
|
842
|
-
if (nextOptions.length === 0) break;
|
|
843
|
-
currentOptions = nextOptions;
|
|
844
|
-
depth += 1;
|
|
557
|
+
const items = [];
|
|
558
|
+
for (const view of source) {
|
|
559
|
+
const base = DEFAULT_VIEWS.find((d) => d.value === view?.value);
|
|
560
|
+
if (!base || seen.has(base.value)) continue;
|
|
561
|
+
seen.add(base.value);
|
|
562
|
+
items.push({ value: base.value, icon: view.icon ?? base.icon, label: view.label ?? base.label });
|
|
845
563
|
}
|
|
846
|
-
return
|
|
847
|
-
}
|
|
848
|
-
function selectedValuesForCascadingLevel(selectedPaths, depth, options) {
|
|
849
|
-
const optionValues = new Set(options.map((option) => option.value));
|
|
850
|
-
return Array.from(
|
|
851
|
-
new Set(
|
|
852
|
-
selectedPaths.map((path) => path[depth]?.value).filter((value) => Boolean(value) && optionValues.has(value))
|
|
853
|
-
)
|
|
854
|
-
);
|
|
855
|
-
}
|
|
856
|
-
function cascadingPathIncludes(pathMap, value, maybeAncestor) {
|
|
857
|
-
return pathMap.get(value)?.some((option) => option.value === maybeAncestor) ?? false;
|
|
858
|
-
}
|
|
859
|
-
function cascadingValuesOverlap(leftValue, rightValue, pathMap) {
|
|
860
|
-
return cascadingPathIncludes(pathMap, leftValue, rightValue) || cascadingPathIncludes(pathMap, rightValue, leftValue);
|
|
861
|
-
}
|
|
862
|
-
function cascadingLevelLabel(values, allLabel, pathMap, showPathLabels) {
|
|
863
|
-
if (values.length === 0) return allLabel;
|
|
864
|
-
const labels = values.map((value) => {
|
|
865
|
-
if (showPathLabels) return cascadingPathLabel(pathMap, value);
|
|
866
|
-
return pathMap.get(value)?.at(-1)?.label ?? value;
|
|
867
|
-
});
|
|
868
|
-
if (labels.length <= 2) return labels.join(", ");
|
|
869
|
-
return `${labels[0]} \uC678 ${labels.length - 1}`;
|
|
870
|
-
}
|
|
871
|
-
function cascadingOptionLabel(option, pathMap, showPathLabels) {
|
|
872
|
-
return showPathLabels ? cascadingPathLabel(pathMap, option.value) : option.label;
|
|
873
|
-
}
|
|
874
|
-
function CascadingInput({
|
|
875
|
-
field,
|
|
876
|
-
values,
|
|
877
|
-
onChange
|
|
878
|
-
}) {
|
|
879
|
-
const pathMap = useMemo(() => cascadingPathMap(field.options), [field.options]);
|
|
880
|
-
const selectedValues = useMemo(
|
|
881
|
-
() => splitMultiValue(values[field.key] ?? "").filter((value) => pathMap.has(value)),
|
|
882
|
-
[field.key, pathMap, values]
|
|
883
|
-
);
|
|
884
|
-
const selectedPaths = useMemo(
|
|
885
|
-
() => selectedValues.map((value) => pathMap.get(value)).filter((path) => Boolean(path)),
|
|
886
|
-
[pathMap, selectedValues]
|
|
887
|
-
);
|
|
888
|
-
const levels = useMemo(() => cascadingLevels(field.options, selectedPaths), [field.options, selectedPaths]);
|
|
889
|
-
const setSelectedValues = (nextValues) => {
|
|
890
|
-
onChange({ [field.key]: joinMultiValue(nextValues) });
|
|
891
|
-
};
|
|
892
|
-
const toggleOption = (option, nextChecked) => {
|
|
893
|
-
const withoutOverlap = selectedValues.filter(
|
|
894
|
-
(value) => !cascadingValuesOverlap(option.value, value, pathMap)
|
|
895
|
-
);
|
|
896
|
-
setSelectedValues(nextChecked ? [...withoutOverlap, option.value] : withoutOverlap);
|
|
897
|
-
};
|
|
898
|
-
const clearLevel = (depth) => {
|
|
899
|
-
if (depth === 0) {
|
|
900
|
-
setSelectedValues([]);
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
|
-
const parents = selectedValuesForCascadingLevel(selectedPaths, depth - 1, levels[depth - 1] ?? []);
|
|
904
|
-
const rest = selectedValues.filter((value) => {
|
|
905
|
-
const parentValue = pathMap.get(value)?.[depth - 1]?.value;
|
|
906
|
-
return !parentValue || !parents.includes(parentValue);
|
|
907
|
-
});
|
|
908
|
-
setSelectedValues([...rest, ...parents]);
|
|
909
|
-
};
|
|
910
|
-
return /* @__PURE__ */ jsx("div", { className: "wp-col", children: levels.map((options, depth) => {
|
|
911
|
-
const levelValues = selectedValuesForCascadingLevel(selectedPaths, depth, options);
|
|
912
|
-
const showPathLabels = depth > 0 && selectedValuesForCascadingLevel(selectedPaths, depth - 1, levels[depth - 1] ?? []).length > 1;
|
|
913
|
-
return /* @__PURE__ */ jsx(
|
|
914
|
-
CascadingLevelSelect,
|
|
915
|
-
{
|
|
916
|
-
allLabel: depth === 0 ? field.allLabel ?? "\uC804\uCCB4" : "\uC804\uCCB4",
|
|
917
|
-
options,
|
|
918
|
-
pathMap,
|
|
919
|
-
selectedValues: levelValues,
|
|
920
|
-
showPathLabels,
|
|
921
|
-
onClear: () => clearLevel(depth),
|
|
922
|
-
onToggle: toggleOption
|
|
923
|
-
},
|
|
924
|
-
depth
|
|
925
|
-
);
|
|
926
|
-
}) });
|
|
564
|
+
return items.length > 0 ? items : [DEFAULT_VIEWS[0]];
|
|
927
565
|
}
|
|
928
|
-
function CascadingLevelSelect({
|
|
929
|
-
options,
|
|
930
|
-
pathMap,
|
|
931
|
-
selectedValues,
|
|
932
|
-
allLabel,
|
|
933
|
-
showPathLabels,
|
|
934
|
-
onClear,
|
|
935
|
-
onToggle
|
|
936
|
-
}) {
|
|
937
|
-
const [open, setOpen] = useState(false);
|
|
938
|
-
const selectedSet = new Set(selectedValues);
|
|
939
|
-
return /* @__PURE__ */ jsx(
|
|
940
|
-
Popover,
|
|
941
|
-
{
|
|
942
|
-
open,
|
|
943
|
-
onClose: () => setOpen(false),
|
|
944
|
-
trigger: /* @__PURE__ */ jsx(
|
|
945
|
-
Button,
|
|
946
|
-
{
|
|
947
|
-
type: "button",
|
|
948
|
-
variant: "secondary",
|
|
949
|
-
block: true,
|
|
950
|
-
iconEnd: "chevron-down",
|
|
951
|
-
onClick: () => setOpen((value) => !value),
|
|
952
|
-
children: cascadingLevelLabel(selectedValues, allLabel, pathMap, showPathLabels)
|
|
953
|
-
}
|
|
954
|
-
),
|
|
955
|
-
footer: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "xs", icon: "refresh-cw", onClick: onClear, children: allLabel }),
|
|
956
|
-
children: /* @__PURE__ */ jsx("div", { className: "wp-col", style: { minWidth: 200 }, children: options.map((option) => /* @__PURE__ */ jsx(
|
|
957
|
-
Checkbox,
|
|
958
|
-
{
|
|
959
|
-
label: cascadingOptionLabel(option, pathMap, showPathLabels),
|
|
960
|
-
checked: selectedSet.has(option.value),
|
|
961
|
-
onChange: (e) => onToggle(option, e.target.checked)
|
|
962
|
-
},
|
|
963
|
-
option.value
|
|
964
|
-
)) })
|
|
965
|
-
}
|
|
966
|
-
);
|
|
967
|
-
}
|
|
968
|
-
var EMPTY_FILTERS = [];
|
|
969
|
-
var EMPTY_FIELDS2 = [];
|
|
970
|
-
var PAGE_SIZE_CHOICES = [10, 20, 50, 100];
|
|
971
566
|
function SearchResults({
|
|
972
567
|
item,
|
|
973
568
|
columns,
|
|
@@ -975,6 +570,8 @@ function SearchResults({
|
|
|
975
570
|
leftFilters = EMPTY_FILTERS,
|
|
976
571
|
rightFilters = EMPTY_FILTERS,
|
|
977
572
|
initialView = "list",
|
|
573
|
+
views,
|
|
574
|
+
pageSizes,
|
|
978
575
|
className,
|
|
979
576
|
searchParams,
|
|
980
577
|
onParamsChange,
|
|
@@ -986,26 +583,28 @@ function SearchResults({
|
|
|
986
583
|
headerActions,
|
|
987
584
|
isLoading = false,
|
|
988
585
|
hideHeader = false,
|
|
989
|
-
advancedSearchFields =
|
|
586
|
+
advancedSearchFields = EMPTY_FIELDS,
|
|
990
587
|
searchPlaceholder,
|
|
991
588
|
virtual
|
|
992
589
|
}) {
|
|
993
590
|
const hasActions = actions.length > 0;
|
|
591
|
+
const sizeChoices = normalizePageSizes(pageSizes) ?? PAGE_SIZE_CHOICES;
|
|
592
|
+
const defaultPageSize = sizeChoices[0];
|
|
994
593
|
const page = Number(searchParams.get("page")) || 1;
|
|
995
|
-
const pageSize = Number(searchParams.get("limit")) ||
|
|
996
|
-
const pageSizeChoices =
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
};
|
|
594
|
+
const pageSize = Number(searchParams.get("limit")) || defaultPageSize;
|
|
595
|
+
const pageSizeChoices = sizeChoices.includes(pageSize) ? sizeChoices : [...sizeChoices, pageSize].sort((a, b) => a - b);
|
|
596
|
+
const viewItems = useMemo(
|
|
597
|
+
() => resolveViewItems(views, disableGrid, disableList),
|
|
598
|
+
[views, disableGrid, disableList]
|
|
599
|
+
);
|
|
600
|
+
const applyViewConstraints = (candidate) => viewItems.some((v) => v.value === candidate) ? candidate : viewItems[0].value;
|
|
1003
601
|
const appliedSearch = searchParams.get("search") ?? "";
|
|
1004
602
|
const [view, setView] = useState(() => applyViewConstraints(initialView));
|
|
1005
603
|
const [search, setSearch] = useState(appliedSearch);
|
|
1006
604
|
const [sort, setSort] = useState(null);
|
|
1007
605
|
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
1008
606
|
const selectedRowsRef = useRef(/* @__PURE__ */ new Map());
|
|
607
|
+
const activeView = applyViewConstraints(view);
|
|
1009
608
|
useEffect(() => {
|
|
1010
609
|
setSearch(appliedSearch);
|
|
1011
610
|
}, [appliedSearch]);
|
|
@@ -1094,7 +693,7 @@ function SearchResults({
|
|
|
1094
693
|
const safePage = Math.min(Math.max(1, page), pageCount);
|
|
1095
694
|
const virtualThreshold = typeof virtual === "number" ? virtual : 150;
|
|
1096
695
|
const virtualEnabled = virtual === true || typeof virtual === "number";
|
|
1097
|
-
const virtualListActive = virtualEnabled &&
|
|
696
|
+
const virtualListActive = virtualEnabled && activeView === "list" && processed.length > virtualThreshold;
|
|
1098
697
|
const visibleItems = clientPaginated && !virtualListActive ? processed.slice((safePage - 1) * pageSize, safePage * pageSize) : processed;
|
|
1099
698
|
const visibleIds = visibleItems.map((row) => row.id);
|
|
1100
699
|
const allSelected = visibleIds.length > 0 && visibleIds.every((id) => selected.has(id));
|
|
@@ -1125,12 +724,12 @@ function SearchResults({
|
|
|
1125
724
|
{
|
|
1126
725
|
key: "__select",
|
|
1127
726
|
header: /* @__PURE__ */ jsx(
|
|
1128
|
-
|
|
727
|
+
Checkbox,
|
|
1129
728
|
{
|
|
1130
729
|
checked: allSelected,
|
|
1131
730
|
indeterminate: !allSelected && someSelected,
|
|
1132
|
-
label: `${title} \uC804\uCCB4 \uC120\uD0DD`,
|
|
1133
|
-
onChange: (
|
|
731
|
+
"aria-label": `${title} \uC804\uCCB4 \uC120\uD0DD`,
|
|
732
|
+
onChange: (e) => toggleSelectAll(e.target.checked)
|
|
1134
733
|
}
|
|
1135
734
|
),
|
|
1136
735
|
width: "36px",
|
|
@@ -1186,7 +785,14 @@ function SearchResults({
|
|
|
1186
785
|
placeholder: searchPlaceholder
|
|
1187
786
|
}
|
|
1188
787
|
),
|
|
1189
|
-
|
|
788
|
+
viewItems.length > 1 ? /* @__PURE__ */ jsx(
|
|
789
|
+
SegmentedControl,
|
|
790
|
+
{
|
|
791
|
+
items: viewItems,
|
|
792
|
+
value: activeView,
|
|
793
|
+
onChange: (next) => setView(applyViewConstraints(next))
|
|
794
|
+
}
|
|
795
|
+
) : null,
|
|
1190
796
|
headerActions
|
|
1191
797
|
] })
|
|
1192
798
|
] }) : null,
|
|
@@ -1211,21 +817,16 @@ function SearchResults({
|
|
|
1211
817
|
)) })
|
|
1212
818
|
] }) : null,
|
|
1213
819
|
hasActions && selectedItems.length > 0 ? (
|
|
1214
|
-
// 플로팅 벌크 액션 바 — 스크롤 위치와 무관하게 화면 하단 중앙에
|
|
1215
|
-
//
|
|
820
|
+
// 플로팅 벌크 액션 바 — 스크롤 위치와 무관하게 화면 하단 중앙에 뜬다.
|
|
821
|
+
// 생김새는 DS의 .wp-toolbar가 맡고, 여기서는 "화면 하단 중앙 고정"만 얹는다
|
|
822
|
+
// (--floating은 absolute라 스크롤을 따라오지 않는다).
|
|
1216
823
|
/* @__PURE__ */ jsxs(
|
|
1217
824
|
"div",
|
|
1218
825
|
{
|
|
1219
826
|
role: "toolbar",
|
|
827
|
+
className: "wp-toolbar",
|
|
1220
828
|
"aria-label": `${title} \uC120\uD0DD \uD56D\uBAA9 \uC77C\uAD04 \uC791\uC5C5`,
|
|
1221
829
|
style: {
|
|
1222
|
-
display: "flex",
|
|
1223
|
-
alignItems: "center",
|
|
1224
|
-
gap: 6,
|
|
1225
|
-
padding: 5,
|
|
1226
|
-
background: "var(--wp-surface-card)",
|
|
1227
|
-
border: "1px solid var(--wp-border-subtle)",
|
|
1228
|
-
borderRadius: "var(--wp-radius-md)",
|
|
1229
830
|
position: "fixed",
|
|
1230
831
|
bottom: "var(--wp-space-6)",
|
|
1231
832
|
left: "50%",
|
|
@@ -1241,7 +842,7 @@ function SearchResults({
|
|
|
1241
842
|
selectedItems.length,
|
|
1242
843
|
"\uAC74 \uC120\uD0DD\uB428"
|
|
1243
844
|
] }),
|
|
1244
|
-
/* @__PURE__ */ jsx(
|
|
845
|
+
/* @__PURE__ */ jsx("span", { className: "wp-toolbar__sep" }),
|
|
1245
846
|
actions.map((action) => /* @__PURE__ */ jsx(
|
|
1246
847
|
Button,
|
|
1247
848
|
{
|
|
@@ -1253,13 +854,13 @@ function SearchResults({
|
|
|
1253
854
|
},
|
|
1254
855
|
action.label
|
|
1255
856
|
)),
|
|
1256
|
-
/* @__PURE__ */ jsx(
|
|
857
|
+
/* @__PURE__ */ jsx("span", { className: "wp-toolbar__sep" }),
|
|
1257
858
|
/* @__PURE__ */ jsx(Button, { size: "sm", variant: "ghost", icon: "x", onClick: () => setSelected(/* @__PURE__ */ new Set()), children: "\uC120\uD0DD \uD574\uC81C" })
|
|
1258
859
|
]
|
|
1259
860
|
}
|
|
1260
861
|
)
|
|
1261
862
|
) : null,
|
|
1262
|
-
isLoading ? /* @__PURE__ */ jsx("div", { className: "wp-row", style: { justifyContent: "center", padding: "var(--wp-space-8) 0" }, children: /* @__PURE__ */ jsx(Spinner, { size: "lg", label: `${title} \uBD88\uB7EC\uC624\uB294 \uC911` }) }) :
|
|
863
|
+
isLoading ? /* @__PURE__ */ jsx("div", { className: "wp-row", style: { justifyContent: "center", padding: "var(--wp-space-8) 0" }, children: /* @__PURE__ */ jsx(Spinner, { size: "lg", label: `${title} \uBD88\uB7EC\uC624\uB294 \uC911` }) }) : activeView === "list" ? /* @__PURE__ */ jsx(
|
|
1263
864
|
ResultTable,
|
|
1264
865
|
{
|
|
1265
866
|
columns: tableColumns,
|
|
@@ -1283,8 +884,11 @@ function SearchResults({
|
|
|
1283
884
|
options: pageSizeChoices.map((size) => ({ value: String(size), label: `${size}\uAC74` })),
|
|
1284
885
|
value: String(pageSize),
|
|
1285
886
|
onChange: (e) => (
|
|
1286
|
-
// 기본값
|
|
1287
|
-
updateParams({
|
|
887
|
+
// 기본값(첫 후보)은 파라미터를 지워 상태를 깔끔하게 유지하고, 페이지는 1로 되돌린다
|
|
888
|
+
updateParams({
|
|
889
|
+
limit: e.target.value === String(defaultPageSize) ? null : e.target.value,
|
|
890
|
+
page: null
|
|
891
|
+
})
|
|
1288
892
|
)
|
|
1289
893
|
}
|
|
1290
894
|
)
|
|
@@ -1316,65 +920,6 @@ function formatCell(value) {
|
|
|
1316
920
|
if (value instanceof Date) return value.toLocaleString("ko-KR");
|
|
1317
921
|
return value;
|
|
1318
922
|
}
|
|
1319
|
-
function ToolbarSeparator() {
|
|
1320
|
-
return /* @__PURE__ */ jsx("span", { style: { width: 1, height: 20, background: "var(--wp-border-subtle)", flex: "none" } });
|
|
1321
|
-
}
|
|
1322
|
-
function SelectCheckbox({
|
|
1323
|
-
checked,
|
|
1324
|
-
indeterminate,
|
|
1325
|
-
label,
|
|
1326
|
-
onChange
|
|
1327
|
-
}) {
|
|
1328
|
-
const ref = useRef(null);
|
|
1329
|
-
useEffect(() => {
|
|
1330
|
-
if (ref.current) ref.current.indeterminate = indeterminate;
|
|
1331
|
-
}, [indeterminate]);
|
|
1332
|
-
return /* @__PURE__ */ jsxs("label", { className: "wp-check", children: [
|
|
1333
|
-
/* @__PURE__ */ jsx(
|
|
1334
|
-
"input",
|
|
1335
|
-
{
|
|
1336
|
-
ref,
|
|
1337
|
-
type: "checkbox",
|
|
1338
|
-
checked,
|
|
1339
|
-
"aria-label": label,
|
|
1340
|
-
onChange: (e) => onChange(e.target.checked)
|
|
1341
|
-
}
|
|
1342
|
-
),
|
|
1343
|
-
/* @__PURE__ */ jsx(
|
|
1344
|
-
"span",
|
|
1345
|
-
{
|
|
1346
|
-
className: "wp-check__box",
|
|
1347
|
-
style: indeterminate && !checked ? {
|
|
1348
|
-
background: "var(--wp-action-primary-bg)",
|
|
1349
|
-
borderColor: "var(--wp-action-primary-bg)",
|
|
1350
|
-
color: "var(--wp-white)"
|
|
1351
|
-
} : void 0,
|
|
1352
|
-
children: /* @__PURE__ */ jsx(Icon, { name: indeterminate && !checked ? "minus" : "check", size: 12, strokeWidth: 2.5 })
|
|
1353
|
-
}
|
|
1354
|
-
)
|
|
1355
|
-
] });
|
|
1356
|
-
}
|
|
1357
|
-
function ViewToggle({ value, onChange }) {
|
|
1358
|
-
const items = [
|
|
1359
|
-
{ value: "list", icon: "list", label: "\uBAA9\uB85D" },
|
|
1360
|
-
{ value: "grid", icon: "layout-grid", label: "\uCE74\uB4DC" }
|
|
1361
|
-
];
|
|
1362
|
-
return /* @__PURE__ */ jsx("div", { className: "wp-seg", role: "tablist", children: items.map((tab) => /* @__PURE__ */ jsxs(
|
|
1363
|
-
"button",
|
|
1364
|
-
{
|
|
1365
|
-
type: "button",
|
|
1366
|
-
role: "tab",
|
|
1367
|
-
className: "wp-seg__item",
|
|
1368
|
-
"aria-selected": tab.value === value,
|
|
1369
|
-
onClick: () => onChange(tab.value),
|
|
1370
|
-
children: [
|
|
1371
|
-
/* @__PURE__ */ jsx(Icon, { name: tab.icon, size: 14 }),
|
|
1372
|
-
tab.label
|
|
1373
|
-
]
|
|
1374
|
-
},
|
|
1375
|
-
tab.value
|
|
1376
|
-
)) });
|
|
1377
|
-
}
|
|
1378
923
|
function resolveRowPart(part, item) {
|
|
1379
924
|
return typeof part === "function" ? part(item) : part;
|
|
1380
925
|
}
|
|
@@ -1434,6 +979,7 @@ function FilterMultiSelect({
|
|
|
1434
979
|
type: "button",
|
|
1435
980
|
size: "sm",
|
|
1436
981
|
variant: values.length > 0 ? "primary" : "secondary",
|
|
982
|
+
icon: "funnel",
|
|
1437
983
|
iconEnd: "chevron-down",
|
|
1438
984
|
onClick: () => setOpen((value) => !value),
|
|
1439
985
|
children: label
|
|
@@ -1457,7 +1003,7 @@ function FilterMultiSelect({
|
|
|
1457
1003
|
function toList(action) {
|
|
1458
1004
|
return Array.isArray(action) ? action : [action];
|
|
1459
1005
|
}
|
|
1460
|
-
function mapColumns(columns) {
|
|
1006
|
+
function mapColumns(columns, dispatch) {
|
|
1461
1007
|
return (columns ?? []).map((column) => ({
|
|
1462
1008
|
key: column.key,
|
|
1463
1009
|
title: column.title,
|
|
@@ -1466,14 +1012,29 @@ function mapColumns(columns) {
|
|
|
1466
1012
|
ratio: column.ratio,
|
|
1467
1013
|
mono: column.mono,
|
|
1468
1014
|
strong: column.strong,
|
|
1469
|
-
render: column.cell
|
|
1470
|
-
const value = String(row[column.key] ?? "");
|
|
1471
|
-
if (!value) return null;
|
|
1472
|
-
return /* @__PURE__ */ jsx(Badge, { tone: column.cell?.toneMap?.[value] ?? "neutral", children: value });
|
|
1473
|
-
} : void 0
|
|
1015
|
+
render: cellRenderer(column.cell, column.key, dispatch)
|
|
1474
1016
|
}));
|
|
1475
1017
|
}
|
|
1476
|
-
function
|
|
1018
|
+
function matchesCondition(condition, row) {
|
|
1019
|
+
if (!condition || typeof condition.key !== "string") return true;
|
|
1020
|
+
const value = row[condition.key];
|
|
1021
|
+
if (condition.equals !== void 0) return String(value ?? "") === String(condition.equals);
|
|
1022
|
+
if (Array.isArray(condition.in)) return condition.in.some((v) => String(v) === String(value ?? ""));
|
|
1023
|
+
if (condition.notEmpty === false) return true;
|
|
1024
|
+
return value !== void 0 && value !== null && String(value) !== "";
|
|
1025
|
+
}
|
|
1026
|
+
function mapCardRows(rows, dispatch) {
|
|
1027
|
+
return rows?.map((row) => {
|
|
1028
|
+
const render = cellRenderer(row.cell, row.valueKey, dispatch);
|
|
1029
|
+
return {
|
|
1030
|
+
icon: row.icon ? /* @__PURE__ */ jsx(Icon, { name: row.icon, size: 14 }) : void 0,
|
|
1031
|
+
label: row.label,
|
|
1032
|
+
value: render ?? ((item) => String(item[row.valueKey] ?? "")),
|
|
1033
|
+
show: row.showWhen ? (item) => matchesCondition(row.showWhen, item) : void 0
|
|
1034
|
+
};
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
function mapGridCard(gridCard, dispatch) {
|
|
1477
1038
|
if (!gridCard) return void 0;
|
|
1478
1039
|
return {
|
|
1479
1040
|
title: (row) => String(row[gridCard.titleKey] ?? ""),
|
|
@@ -1483,12 +1044,34 @@ function mapGridCard(gridCard) {
|
|
|
1483
1044
|
if (!value) return null;
|
|
1484
1045
|
return { text: value, variant: gridCard.badge.toneMap?.[value] ?? "neutral" };
|
|
1485
1046
|
} : void 0,
|
|
1486
|
-
rows: gridCard.rows
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1047
|
+
rows: mapCardRows(gridCard.rows, dispatch),
|
|
1048
|
+
leftRows: mapCardRows(gridCard.leftRows, dispatch),
|
|
1049
|
+
rightRows: mapCardRows(gridCard.rightRows, dispatch),
|
|
1050
|
+
note: gridCard.noteKey ? (row) => String(row[gridCard.noteKey] ?? "") || null : void 0,
|
|
1051
|
+
actions: gridCard.actions?.length ? (row) => (
|
|
1052
|
+
// 카드 전체가 클릭 대상(rowClick)일 수 있으므로 버튼 클릭은 여기서 전파를 끊는다
|
|
1053
|
+
/* @__PURE__ */ jsx("span", { className: "wp-row", onClick: (e) => e.stopPropagation(), children: gridCard.actions.map((action) => /* @__PURE__ */ jsx(
|
|
1054
|
+
Button,
|
|
1055
|
+
{
|
|
1056
|
+
size: "xs",
|
|
1057
|
+
icon: action.icon,
|
|
1058
|
+
variant: action.variant === "destructive" ? "danger" : action.variant === "outline" ? "secondary" : "ghost",
|
|
1059
|
+
onClick: () => toList(action.run).forEach((a) => dispatch(a, row)),
|
|
1060
|
+
children: action.label
|
|
1061
|
+
},
|
|
1062
|
+
action.label
|
|
1063
|
+
)) })
|
|
1064
|
+
) : void 0
|
|
1490
1065
|
};
|
|
1491
1066
|
}
|
|
1067
|
+
function mapViews(views, gridCard, disableGrid, disableList) {
|
|
1068
|
+
if (!Array.isArray(views) || views.length === 0) return void 0;
|
|
1069
|
+
const gridOff = disableGrid ?? !gridCard;
|
|
1070
|
+
const kept = views.filter(
|
|
1071
|
+
(view) => !(view?.value === "grid" && gridOff) && !(view?.value === "list" && disableList === true)
|
|
1072
|
+
);
|
|
1073
|
+
return kept.length > 0 ? kept : void 0;
|
|
1074
|
+
}
|
|
1492
1075
|
function mapActions(actions, dispatch) {
|
|
1493
1076
|
return (actions ?? []).map((action) => ({
|
|
1494
1077
|
label: action.label,
|
|
@@ -1510,6 +1093,8 @@ function SduiSearchResults({
|
|
|
1510
1093
|
gridCard,
|
|
1511
1094
|
actions,
|
|
1512
1095
|
initialView,
|
|
1096
|
+
views,
|
|
1097
|
+
pageSizes,
|
|
1513
1098
|
disableGrid,
|
|
1514
1099
|
disableList,
|
|
1515
1100
|
searchPlaceholder,
|
|
@@ -1525,12 +1110,14 @@ function SduiSearchResults({
|
|
|
1525
1110
|
{
|
|
1526
1111
|
title,
|
|
1527
1112
|
item: { data: rows, total: rows.length },
|
|
1528
|
-
columns: mapColumns(columns),
|
|
1529
|
-
gridConfig: mapGridCard(gridCard),
|
|
1113
|
+
columns: mapColumns(columns, dispatch),
|
|
1114
|
+
gridConfig: mapGridCard(gridCard, dispatch),
|
|
1530
1115
|
leftFilters,
|
|
1531
1116
|
rightFilters,
|
|
1532
1117
|
actions: mapActions(actions, dispatch),
|
|
1533
1118
|
initialView,
|
|
1119
|
+
views: mapViews(views, gridCard, disableGrid, disableList),
|
|
1120
|
+
pageSizes,
|
|
1534
1121
|
disableGrid: disableGrid ?? !gridCard,
|
|
1535
1122
|
disableList,
|
|
1536
1123
|
advancedSearchFields: fields,
|
|
@@ -1553,6 +1140,8 @@ function RemoteSearchResults({
|
|
|
1553
1140
|
gridCard,
|
|
1554
1141
|
actions,
|
|
1555
1142
|
initialView,
|
|
1143
|
+
views,
|
|
1144
|
+
pageSizes,
|
|
1556
1145
|
disableGrid,
|
|
1557
1146
|
disableList,
|
|
1558
1147
|
searchPlaceholder,
|
|
@@ -1567,12 +1156,13 @@ function RemoteSearchResults({
|
|
|
1567
1156
|
initial.set("limit", String(pageSize));
|
|
1568
1157
|
return initial;
|
|
1569
1158
|
});
|
|
1159
|
+
const defaultPageSize = normalizePageSizes(pageSizes)?.[0] ?? 10;
|
|
1570
1160
|
const query = useMemo(() => {
|
|
1571
1161
|
const next = new URLSearchParams(params);
|
|
1572
1162
|
if (!next.has("page")) next.set("page", "1");
|
|
1573
|
-
if (!next.has("limit")) next.set("limit",
|
|
1163
|
+
if (!next.has("limit")) next.set("limit", String(defaultPageSize));
|
|
1574
1164
|
return next;
|
|
1575
|
-
}, [params]);
|
|
1165
|
+
}, [params, defaultPageSize]);
|
|
1576
1166
|
const { page, isLoading, error, reload } = useRemoteSearchPage(endpoint, query);
|
|
1577
1167
|
if (error && page.data.length === 0) {
|
|
1578
1168
|
return /* @__PURE__ */ jsx(
|
|
@@ -1590,12 +1180,14 @@ function RemoteSearchResults({
|
|
|
1590
1180
|
{
|
|
1591
1181
|
title,
|
|
1592
1182
|
item: page,
|
|
1593
|
-
columns: mapColumns(columns),
|
|
1594
|
-
gridConfig: mapGridCard(gridCard),
|
|
1183
|
+
columns: mapColumns(columns, dispatch),
|
|
1184
|
+
gridConfig: mapGridCard(gridCard, dispatch),
|
|
1595
1185
|
leftFilters,
|
|
1596
1186
|
rightFilters,
|
|
1597
1187
|
actions: mapActions(actions, dispatch),
|
|
1598
1188
|
initialView,
|
|
1189
|
+
views: mapViews(views, gridCard, disableGrid, disableList),
|
|
1190
|
+
pageSizes,
|
|
1599
1191
|
disableGrid: disableGrid ?? !gridCard,
|
|
1600
1192
|
disableList,
|
|
1601
1193
|
advancedSearchFields: fields,
|
|
@@ -1768,6 +1360,13 @@ var defaultRegistry = {
|
|
|
1768
1360
|
// 권한 게이트 — 판정은 SduiRenderer의 can 프롭(permission.tsx)에 위임한다.
|
|
1769
1361
|
// 주입 전까지는 통과시키되 console.error를 한 번 낸다(근거는 permission.tsx의 MISSING 주석).
|
|
1770
1362
|
Can,
|
|
1363
|
+
// 0.3.0에서 이관한 것들은 여기 **없다.**
|
|
1364
|
+
// - DS 6종(Avatar·Breadcrumb·Radio·SegmentedControl·SidebarNav·Tooltip)은
|
|
1365
|
+
// components.tsx에 있고 export만 한다. 기본 어휘를 넓히는 것은 별개 결정이라,
|
|
1366
|
+
// 이름을 올리려면 `@wipco/sdui/meta`의 메타데이터를 같은 커밋에서 채워야 한다
|
|
1367
|
+
// (SduiComponentName이 그 Record의 키라 빠지면 컴파일이 막힌다).
|
|
1368
|
+
// - 표시 위젯 20종은 `widgetComponents` 묶음으로 나가 있다. 정적 import로 core에
|
|
1369
|
+
// 붙지 않게 하려는 것이고 이유는 위 Chart·MapPreview와 같다(widgets/index.ts 참고).
|
|
1771
1370
|
// 검색+목록 화면. 스펙 이름은 ERP 어휘를 그대로 쓰고, 구현은 JSON 전용 어댑터를 가리킨다
|
|
1772
1371
|
// (SearchResults 자체는 함수 프롭을 받는 타입드 컴포넌트라 스펙에서 직접 못 쓴다).
|
|
1773
1372
|
SearchResults: SduiSearchResults,
|
|
@@ -1833,6 +1432,11 @@ var DENIED_PROPS = /* @__PURE__ */ new Set([
|
|
|
1833
1432
|
function isPlainObject(v) {
|
|
1834
1433
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1835
1434
|
}
|
|
1435
|
+
function checkUrlValue(key, value, path, issues) {
|
|
1436
|
+
if (typeof value !== "string" || !URL_PROPS.has(key)) return;
|
|
1437
|
+
if (!isDangerousUrlValue(value)) return;
|
|
1438
|
+
issues.push({ path, message: `${key}\uC5D0 \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 URL \uC2A4\uD0B4\uC785\uB2C8\uB2E4: ${value}` });
|
|
1439
|
+
}
|
|
1836
1440
|
function validateSpec(page, knownComponents) {
|
|
1837
1441
|
const issues = [];
|
|
1838
1442
|
if (!isPlainObject(page)) return [{ path: "$", message: "\uC2A4\uD399\uC774 \uAC1D\uCCB4\uAC00 \uC544\uB2D9\uB2C8\uB2E4" }];
|
|
@@ -1871,6 +1475,7 @@ function walkChild(child, path, issues, known) {
|
|
|
1871
1475
|
});
|
|
1872
1476
|
continue;
|
|
1873
1477
|
}
|
|
1478
|
+
checkUrlValue(name, value, `${path}.props.${name}`, issues);
|
|
1874
1479
|
walkValue(value, `${path}.props.${name}`, issues, name.startsWith("on") || name === "rowClick");
|
|
1875
1480
|
}
|
|
1876
1481
|
if (node.component === "Form" && isPlainObject(node.props) && node.props.rules !== void 0) {
|
|
@@ -1958,6 +1563,7 @@ function walkValue(value, path, issues, actionEligible) {
|
|
|
1958
1563
|
}
|
|
1959
1564
|
for (const [k, v] of Object.entries(value)) {
|
|
1960
1565
|
if (k === "$calc" || k === "action") continue;
|
|
1566
|
+
checkUrlValue(k, v, `${path}.${k}`, issues);
|
|
1961
1567
|
walkValue(v, `${path}.${k}`, issues, k === "run" || k === "rowClick" || k.startsWith("on"));
|
|
1962
1568
|
}
|
|
1963
1569
|
}
|
|
@@ -1995,7 +1601,7 @@ function eventValue(arg) {
|
|
|
1995
1601
|
}
|
|
1996
1602
|
return arg;
|
|
1997
1603
|
}
|
|
1998
|
-
function
|
|
1604
|
+
function toNumber2(v) {
|
|
1999
1605
|
if (typeof v === "number") return v;
|
|
2000
1606
|
if (typeof v === "string") {
|
|
2001
1607
|
const n = Number(v.replace(/,/g, ""));
|
|
@@ -2040,9 +1646,9 @@ function ruleError(rule, value, values, label, customValidators) {
|
|
|
2040
1646
|
case "required":
|
|
2041
1647
|
return empty || value === false ? rule.message ?? `${label}\uC744(\uB97C) \uC785\uB825\uD558\uC138\uC694` : null;
|
|
2042
1648
|
case "min":
|
|
2043
|
-
return !empty &&
|
|
1649
|
+
return !empty && toNumber2(value) < rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
|
|
2044
1650
|
case "max":
|
|
2045
|
-
return !empty &&
|
|
1651
|
+
return !empty && toNumber2(value) > rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value} \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4` : null;
|
|
2046
1652
|
case "minLength":
|
|
2047
1653
|
return !empty && String(value).length < rule.value ? rule.message ?? `${label}\uC740(\uB294) ${rule.value}\uC790 \uC774\uC0C1\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4` : null;
|
|
2048
1654
|
case "maxLength":
|
|
@@ -2093,13 +1699,6 @@ var DENIED_PROPS2 = /* @__PURE__ */ new Set([
|
|
|
2093
1699
|
"constructor",
|
|
2094
1700
|
"prototype"
|
|
2095
1701
|
]);
|
|
2096
|
-
var URL_PROPS = /* @__PURE__ */ new Set(["href", "src", "action", "formAction", "xlinkHref", "poster"]);
|
|
2097
|
-
function isDangerousUrlValue(value) {
|
|
2098
|
-
const normalized = value.replace(/[\u0000-\u0020]/g, "").toLowerCase();
|
|
2099
|
-
const scheme = /^([a-z][a-z0-9+.-]*):/.exec(normalized);
|
|
2100
|
-
if (!scheme) return false;
|
|
2101
|
-
return !["http", "https", "mailto", "tel", "blob"].includes(scheme[1]);
|
|
2102
|
-
}
|
|
2103
1702
|
function childrenOf(node) {
|
|
2104
1703
|
if (node.children === void 0) return void 0;
|
|
2105
1704
|
if (Array.isArray(node.children)) return node.children;
|
|
@@ -2122,7 +1721,7 @@ function SduiNodeRenderer({
|
|
|
2122
1721
|
scope,
|
|
2123
1722
|
engine
|
|
2124
1723
|
}) {
|
|
2125
|
-
const { registry, resolveValue, dispatch } = engine;
|
|
1724
|
+
const { registry, resolveValue, dispatch, resolveAsset } = engine;
|
|
2126
1725
|
const slots = isPlainObject2(node.slots) ? node.slots : void 0;
|
|
2127
1726
|
const childList = childrenOf(node);
|
|
2128
1727
|
if (node.component === "ForEach") {
|
|
@@ -2173,6 +1772,11 @@ function SduiNodeRenderer({
|
|
|
2173
1772
|
warnOnce2(`url:${name}:${resolved}`, `[sdui] \uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 ${name} \uAC12: ${resolved} \u2014 \uBB34\uC2DC\uD569\uB2C8\uB2E4`);
|
|
2174
1773
|
continue;
|
|
2175
1774
|
}
|
|
1775
|
+
if (resolveAsset) {
|
|
1776
|
+
const gated = resolveAssetsIn(name, resolved, resolveAsset, String(node.component));
|
|
1777
|
+
if (gated === void 0) continue;
|
|
1778
|
+
resolved = gated;
|
|
1779
|
+
}
|
|
2176
1780
|
props[name] = resolved;
|
|
2177
1781
|
}
|
|
2178
1782
|
}
|
|
@@ -2194,6 +1798,7 @@ function SduiRenderer({
|
|
|
2194
1798
|
registry = defaultRegistry,
|
|
2195
1799
|
adapter,
|
|
2196
1800
|
syncUrlState = true,
|
|
1801
|
+
can,
|
|
2197
1802
|
onCustomAction,
|
|
2198
1803
|
customValidators
|
|
2199
1804
|
}) {
|
|
@@ -2338,8 +1943,8 @@ function SduiRenderer({
|
|
|
2338
1943
|
const resolved = args.map((arg) => resolveValue(arg, scope));
|
|
2339
1944
|
const nums = (v) => {
|
|
2340
1945
|
if (Array.isArray(v))
|
|
2341
|
-
return v.map((row) => field && isPlainObject2(row) ?
|
|
2342
|
-
return [
|
|
1946
|
+
return v.map((row) => field && isPlainObject2(row) ? toNumber2(row[field]) : toNumber2(row));
|
|
1947
|
+
return [toNumber2(v)];
|
|
2343
1948
|
};
|
|
2344
1949
|
switch (op) {
|
|
2345
1950
|
case "sum":
|
|
@@ -2347,17 +1952,17 @@ function SduiRenderer({
|
|
|
2347
1952
|
case "count":
|
|
2348
1953
|
return resolved.reduce((a, v) => a + (Array.isArray(v) ? v.length : v == null ? 0 : 1), 0);
|
|
2349
1954
|
case "sub": {
|
|
2350
|
-
const list = resolved.map((v) =>
|
|
1955
|
+
const list = resolved.map((v) => toNumber2(v));
|
|
2351
1956
|
return list.length === 0 ? 0 : list.reduce((a, b) => a - b);
|
|
2352
1957
|
}
|
|
2353
1958
|
case "mul":
|
|
2354
|
-
return resolved.map((v) =>
|
|
1959
|
+
return resolved.map((v) => toNumber2(v)).reduce((a, b) => a * b, 1);
|
|
2355
1960
|
case "div": {
|
|
2356
|
-
const [a, b] = resolved.map((v) =>
|
|
1961
|
+
const [a, b] = resolved.map((v) => toNumber2(v));
|
|
2357
1962
|
return b === 0 ? 0 : a / b;
|
|
2358
1963
|
}
|
|
2359
1964
|
case "percent": {
|
|
2360
|
-
const [a, b] = resolved.map((v) =>
|
|
1965
|
+
const [a, b] = resolved.map((v) => toNumber2(v));
|
|
2361
1966
|
return b === 0 ? 0 : Math.round(a / b * 1e3) / 10;
|
|
2362
1967
|
}
|
|
2363
1968
|
case "concat":
|
|
@@ -2370,13 +1975,13 @@ function SduiRenderer({
|
|
|
2370
1975
|
}
|
|
2371
1976
|
// 수량 비교 — 정원 초과 차단(Button.disabled), 임계값 경고(Show.when) 등
|
|
2372
1977
|
case "gt":
|
|
2373
|
-
return
|
|
1978
|
+
return toNumber2(resolved[0]) > toNumber2(resolved[1]);
|
|
2374
1979
|
case "gte":
|
|
2375
|
-
return
|
|
1980
|
+
return toNumber2(resolved[0]) >= toNumber2(resolved[1]);
|
|
2376
1981
|
case "lt":
|
|
2377
|
-
return
|
|
1982
|
+
return toNumber2(resolved[0]) < toNumber2(resolved[1]);
|
|
2378
1983
|
case "lte":
|
|
2379
|
-
return
|
|
1984
|
+
return toNumber2(resolved[0]) <= toNumber2(resolved[1]);
|
|
2380
1985
|
/** 부정 — 조건을 뒤집어 "없을 때만" 렌더링하는 데 쓴다 */
|
|
2381
1986
|
case "not":
|
|
2382
1987
|
return !resolved[0];
|
|
@@ -2554,8 +2159,8 @@ function SduiRenderer({
|
|
|
2554
2159
|
);
|
|
2555
2160
|
dispatchRef.current = dispatch;
|
|
2556
2161
|
const engine = useMemo(
|
|
2557
|
-
() => ({ registry, resolveValue, dispatch }),
|
|
2558
|
-
[registry, resolveValue, dispatch]
|
|
2162
|
+
() => ({ registry, resolveValue, dispatch, resolveAsset: assetResolverOf(host) }),
|
|
2163
|
+
[registry, resolveValue, dispatch, host]
|
|
2559
2164
|
);
|
|
2560
2165
|
const runtime = useMemo(
|
|
2561
2166
|
() => ({
|
|
@@ -2570,14 +2175,18 @@ function SduiRenderer({
|
|
|
2570
2175
|
}),
|
|
2571
2176
|
[state, setEntries]
|
|
2572
2177
|
);
|
|
2573
|
-
return
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2178
|
+
return (
|
|
2179
|
+
// 해석된 어댑터를 트리에 흘려보낸다 — dispatch를 거치지 않고 스스로 요청을 내는
|
|
2180
|
+
// 컴포넌트(목록 조회 등)도 호스트가 건 잠금 안에서 움직이게 하기 위해서다(adapter.ts 주석).
|
|
2181
|
+
/* @__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: [
|
|
2182
|
+
page.title || page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head", children: /* @__PURE__ */ jsxs("div", { className: "wp-page-head__text", children: [
|
|
2183
|
+
page.title ? /* @__PURE__ */ jsx("h1", { className: "wp-page-head__title", children: page.title }) : null,
|
|
2184
|
+
page.subtitle ? /* @__PURE__ */ jsx("div", { className: "wp-page-head__sub", children: page.subtitle }) : null
|
|
2185
|
+
] }) }) : null,
|
|
2186
|
+
/* @__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" }) }),
|
|
2187
|
+
toasts.length > 0 ? /* @__PURE__ */ jsx("div", { className: "wp-toast-region", children: toasts.map((t) => /* @__PURE__ */ jsx(Toast, { tone: t.tone, title: t.title, onClose: () => closeToast(t.id), children: t.message }, t.id)) }) : null
|
|
2188
|
+
] }) }) }) }) })
|
|
2189
|
+
);
|
|
2581
2190
|
}
|
|
2582
2191
|
|
|
2583
|
-
export { Actions, Can, Col, Divider, Grid, KeyValueList, NodeErrorBoundary, PageHead, RemoteSearchResults, ResultTable, Row, SduiDialog, SduiForm, SduiPermissionProvider, SduiRenderer, SduiSearchDataProvider, SduiSearchResults, SearchResults,
|
|
2192
|
+
export { Actions, Can, Col, Divider, Grid, KeyValueList, NodeErrorBoundary, PageHead, RemoteSearchResults, ResultTable, Row, SduiDialog, SduiForm, SduiPermissionProvider, SduiRenderer, SduiSearchDataProvider, SduiSearchResults, SearchResults, Section, Show, Spacer, Text, cellRenderer, createRegistry, defaultRegistry, normalizePageSizes, useRemoteSearchPage, useSduiCan, useSduiDispatch, useSduiRuntime, useSearchFetcher, validateSpec };
|