@wipco/sdui 0.11.0 → 0.12.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.
@@ -1,4 +1,4 @@
1
- import { includesLooseSearch } from './chunk-BJDWMECO.js';
1
+ import { includesLooseSearch } from './chunk-CDYIUQAD.js';
2
2
  import { Button, Dialog, Input, Tag, Checkbox } from './chunk-B5JTXYDB.js';
3
3
  import { boxed } from './chunk-SAN4IX6N.js';
4
4
  import React, { useMemo, useState, useEffect } from 'react';
@@ -1,4 +1,4 @@
1
- import { Tag, Input, Button, IconButton, Field, Checkbox } from './chunk-B5JTXYDB.js';
1
+ import { Input, Button, IconButton, Tag, Field, Checkbox } from './chunk-B5JTXYDB.js';
2
2
  import { useRef, useEffect, useState, useMemo } from 'react';
3
3
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
4
 
@@ -52,6 +52,57 @@ function Popover({
52
52
  ] })
53
53
  );
54
54
  }
55
+ function normalizeSearchOptions(options, valueKey, labelKey) {
56
+ if (!Array.isArray(options)) return [];
57
+ return options.flatMap((raw) => {
58
+ if (raw === null || typeof raw !== "object") return [];
59
+ const row = raw;
60
+ const value = valueKey ? row[valueKey] : row.value;
61
+ if (value === void 0 || value === null) return [];
62
+ const label = (labelKey ? row[labelKey] : row.label) ?? value;
63
+ return [{ value: String(value), label: String(label) }];
64
+ });
65
+ }
66
+ function normalizeSearchFields(fields) {
67
+ return fields.map((field) => {
68
+ if (field.type === "select") {
69
+ return { ...field, options: normalizeSearchOptions(field.options, field.valueKey, field.labelKey) };
70
+ }
71
+ if (field.type === "cascading" && field.tree) {
72
+ return { ...field, options: buildCascadingTree(field.options, field.tree) };
73
+ }
74
+ return field;
75
+ });
76
+ }
77
+ function buildCascadingTree(rows, tree) {
78
+ if (!Array.isArray(rows)) return [];
79
+ const nodes = /* @__PURE__ */ new Map();
80
+ const parents = /* @__PURE__ */ new Map();
81
+ for (const raw of rows) {
82
+ if (raw === null || typeof raw !== "object") continue;
83
+ const row = raw;
84
+ const id = row[tree.idKey];
85
+ if (id === void 0 || id === null) continue;
86
+ const parent = row[tree.parentIdKey];
87
+ nodes.set(String(id), {
88
+ value: String(id),
89
+ label: String(row[tree.labelKey] ?? id),
90
+ children: []
91
+ });
92
+ parents.set(String(id), parent === void 0 || parent === null ? null : String(parent));
93
+ }
94
+ const roots = [];
95
+ for (const [id, node] of nodes) {
96
+ const parent = parents.get(id);
97
+ const parentNode = parent === null || parent === void 0 ? void 0 : nodes.get(parent);
98
+ if (parentNode) parentNode.children.push(node);
99
+ else roots.push(node);
100
+ }
101
+ for (const node of nodes.values()) {
102
+ if (node.children.length === 0) delete node.children;
103
+ }
104
+ return roots;
105
+ }
55
106
  function splitMultiValue(value) {
56
107
  return value.split(",").map((item) => item.trim()).filter(Boolean);
57
108
  }
@@ -179,7 +230,7 @@ function SearchToolbar({
179
230
  search,
180
231
  onSearchChange,
181
232
  onSearch,
182
- advancedFields = EMPTY_FIELDS,
233
+ advancedFields: rawAdvancedFields = EMPTY_FIELDS,
183
234
  advancedValues = EMPTY_VALUES,
184
235
  placeholder = "\uAC80\uC0C9",
185
236
  className,
@@ -187,6 +238,7 @@ function SearchToolbar({
187
238
  }) {
188
239
  const [open, setOpen] = useState(false);
189
240
  const [draft, setDraft] = useState(advancedValues);
241
+ const advancedFields = useMemo(() => normalizeSearchFields(rawAdvancedFields), [rawAdvancedFields]);
190
242
  const hasAdvanced = advancedFields.length > 0;
191
243
  useEffect(() => {
192
244
  if (!open) setDraft(advancedValues);
@@ -223,59 +275,76 @@ function SearchToolbar({
223
275
  setDraft(nextValues);
224
276
  onSearch(search.trim(), cleanSearchValues(advancedFields, nextValues));
225
277
  };
226
- return /* @__PURE__ */ jsxs("form", { className: ["wp-row", className].filter(Boolean).join(" "), onSubmit: submit, children: [
227
- 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,
228
- !hideSearchInput ? /* @__PURE__ */ jsxs(Fragment, { children: [
229
- /* @__PURE__ */ jsx(
230
- Input,
231
- {
232
- icon: "search",
233
- value: search,
234
- onChange: (event) => onSearchChange(event.target.value),
235
- placeholder
236
- }
237
- ),
238
- /* @__PURE__ */ jsx(Button, { type: "submit", variant: "secondary", icon: "search", children: "\uAC80\uC0C9" })
239
- ] }) : null,
240
- hasAdvanced ? /* @__PURE__ */ jsx(
241
- Popover,
278
+ return (
279
+ /*
280
+ * 세로 축(컨트롤 행 + 칩 행) — 원래 칩이 컨트롤과 같은 행 **앞**에 끼어들어,
281
+ * 조건 몇 개만 적용해도 검색창·보기 전환·등록 버튼이 밀려 헤더가 통째로
282
+ * 무너졌다(실사용 지적, 장비 목록 스크린샷). 칩은 자기 행에서 개행한다.
283
+ */
284
+ /* @__PURE__ */ jsxs(
285
+ "form",
242
286
  {
243
- open,
244
- onClose: () => setOpen(false),
245
- alignLeft: false,
246
- width: "min(560px, 80vw)",
247
- trigger: (
248
- // 검색 입력창 옆에 나란히 서는 자리라 테두리가 있어야 한 덩어리로 읽힌다.
249
- // 열린 상태는 인라인 배경이 아니라 pressed(aria-pressed 틴트)로 알린다.
250
- /* @__PURE__ */ jsx(
251
- IconButton,
252
- {
253
- type: "button",
254
- icon: "sliders-horizontal",
255
- label: "\uC0C1\uC138\uAC80\uC0C9",
256
- variant: "outlined",
257
- pressed: open,
258
- "aria-expanded": open,
259
- onClick: () => setOpen((value) => !value)
260
- }
261
- )
262
- ),
263
- footer: /* @__PURE__ */ jsxs(Fragment, { children: [
264
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", icon: "refresh-cw", onClick: resetAdvancedSearch, children: "\uCD08\uAE30\uD654" }),
265
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: applyAdvancedSearch, children: "\uC801\uC6A9" })
266
- ] }),
267
- children: /* @__PURE__ */ jsx("div", { className: "wp-grid wp-grid--2", children: advancedFields.map((field) => /* @__PURE__ */ jsx(
268
- SearchFieldInput,
269
- {
270
- field,
271
- values: draft,
272
- onChange: (patch) => setDraft((current) => ({ ...current, ...patch }))
273
- },
274
- field.key
275
- )) })
287
+ className: ["wp-col", className].filter(Boolean).join(" "),
288
+ style: { gap: "var(--wp-space-2)" },
289
+ onSubmit: submit,
290
+ children: [
291
+ /* @__PURE__ */ jsxs("div", { className: "wp-row", children: [
292
+ !hideSearchInput ? /* @__PURE__ */ jsxs(Fragment, { children: [
293
+ /* @__PURE__ */ jsx(
294
+ Input,
295
+ {
296
+ icon: "search",
297
+ value: search,
298
+ onChange: (event) => onSearchChange(event.target.value),
299
+ placeholder
300
+ }
301
+ ),
302
+ /* @__PURE__ */ jsx(Button, { type: "submit", variant: "secondary", icon: "search", children: "\uAC80\uC0C9" })
303
+ ] }) : null,
304
+ hasAdvanced ? /* @__PURE__ */ jsx(
305
+ Popover,
306
+ {
307
+ open,
308
+ onClose: () => setOpen(false),
309
+ alignLeft: false,
310
+ width: "min(560px, 80vw)",
311
+ trigger: (
312
+ // 검색 입력창 옆에 나란히 서는 자리라 테두리가 있어야 한 덩어리로 읽힌다.
313
+ // 열린 상태는 인라인 배경이 아니라 pressed(aria-pressed 틴트)로 알린다.
314
+ /* @__PURE__ */ jsx(
315
+ IconButton,
316
+ {
317
+ type: "button",
318
+ icon: "sliders-horizontal",
319
+ label: "\uC0C1\uC138\uAC80\uC0C9",
320
+ variant: "outlined",
321
+ pressed: open,
322
+ "aria-expanded": open,
323
+ onClick: () => setOpen((value) => !value)
324
+ }
325
+ )
326
+ ),
327
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
328
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", icon: "refresh-cw", onClick: resetAdvancedSearch, children: "\uCD08\uAE30\uD654" }),
329
+ /* @__PURE__ */ jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: applyAdvancedSearch, children: "\uC801\uC6A9" })
330
+ ] }),
331
+ children: /* @__PURE__ */ jsx("div", { className: "wp-grid wp-grid--2", children: advancedFields.map((field) => /* @__PURE__ */ jsx(
332
+ SearchFieldInput,
333
+ {
334
+ field,
335
+ values: draft,
336
+ onChange: (patch) => setDraft((current) => ({ ...current, ...patch }))
337
+ },
338
+ field.key
339
+ )) })
340
+ }
341
+ ) : null
342
+ ] }),
343
+ 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
344
+ ]
276
345
  }
277
- ) : null
278
- ] });
346
+ )
347
+ );
279
348
  }
280
349
  function SearchFieldInput({
281
350
  field,
@@ -382,7 +451,7 @@ function MultiSelectInput({
382
451
  children: multiSelectLabel(field, selected)
383
452
  }
384
453
  ),
385
- children: /* @__PURE__ */ jsx("div", { className: "wp-col", children: field.options.map((option) => /* @__PURE__ */ jsx(
454
+ children: /* @__PURE__ */ jsx("div", { className: "wp-col", style: { maxHeight: "min(320px, 40vh)", overflowY: "auto" }, children: field.options.map((option) => /* @__PURE__ */ jsx(
386
455
  Checkbox,
387
456
  {
388
457
  label: option.label,
@@ -528,17 +597,24 @@ function CascadingLevelSelect({
528
597
  }
529
598
  ),
530
599
  footer: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "xs", icon: "refresh-cw", onClick: onClear, children: allLabel }),
531
- children: /* @__PURE__ */ jsx("div", { className: "wp-col", style: { minWidth: 200 }, children: options.map((option) => /* @__PURE__ */ jsx(
532
- Checkbox,
600
+ children: /* @__PURE__ */ jsx(
601
+ "div",
533
602
  {
534
- label: cascadingOptionLabel(option, pathMap, showPathLabels),
535
- checked: selectedSet.has(option.value),
536
- onChange: (e) => onToggle(option, e.target.checked)
537
- },
538
- option.value
539
- )) })
603
+ className: "wp-col",
604
+ style: { minWidth: 200, maxHeight: "min(320px, 40vh)", overflowY: "auto" },
605
+ children: options.map((option) => /* @__PURE__ */ jsx(
606
+ Checkbox,
607
+ {
608
+ label: cascadingOptionLabel(option, pathMap, showPathLabels),
609
+ checked: selectedSet.has(option.value),
610
+ onChange: (e) => onToggle(option, e.target.checked)
611
+ },
612
+ option.value
613
+ ))
614
+ }
615
+ )
540
616
  }
541
617
  );
542
618
  }
543
619
 
544
- export { Popover, SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue };
620
+ export { Popover, SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchFields, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue };
@@ -1,4 +1,4 @@
1
- import { includesLooseSearch } from './chunk-BJDWMECO.js';
1
+ import { includesLooseSearch } from './chunk-CDYIUQAD.js';
2
2
  import { Icon, Button, Input, Spinner } from './chunk-B5JTXYDB.js';
3
3
  import { boxed } from './chunk-SAN4IX6N.js';
4
4
  import { useId, useRef, useState, useEffect, useMemo } from 'react';
package/dist/index.d.ts CHANGED
@@ -221,11 +221,21 @@ type SearchField = {
221
221
  label: string;
222
222
  placeholder?: string;
223
223
  } | {
224
- /** 다중 선택 — 값은 쉼표로 합쳐 저장한다 */
224
+ /**
225
+ * 다중 선택 — 값은 쉼표로 합쳐 저장한다.
226
+ *
227
+ * `valueKey`/`labelKey` 를 주면 options 를 도메인 객체 배열로 받는다
228
+ * (`Select` 0.7.0 과 같은 계약). 스펙에서는 options 에
229
+ * `{"$state":"rt.departments","default":[]}` 를 바인딩하면 page.data 가 채운
230
+ * 실데이터가 그대로 선택지가 된다 — 정적 {label,value}만 받던 것이
231
+ * 예시값 필터가 화면마다 남던 근본 원인이었다(실사용 지적).
232
+ */
225
233
  type: 'select';
226
234
  key: string;
227
235
  label: string;
228
- options: SearchOption[];
236
+ options: Array<SearchOption | Record<string, unknown>>;
237
+ valueKey?: string;
238
+ labelKey?: string;
229
239
  allLabel?: string;
230
240
  } | {
231
241
  type: 'dateRange';
@@ -245,11 +255,20 @@ type SearchField = {
245
255
  /**
246
256
  * 계층 다중 선택 — 단계별 체크박스 목록 체인. 선택된 값들을 쉼표로 합쳐 저장하며,
247
257
  * 부모를 선택하면 그 자손 선택은 제거되고 자손을 선택하면 그 조상 선택이 제거된다.
258
+ *
259
+ * `tree` 를 주면 options 를 **평면 행 배열**로 받아 여기서 계층을 세운다 —
260
+ * `/categories` 처럼 {id, parent_id, name} 평면 목록을 주는 API 를 스펙이
261
+ * 트리로 재조립할 방법이 없어서다($calc 에 그런 연산이 없다).
248
262
  */
249
263
  type: 'cascading';
250
264
  key: string;
251
265
  label: string;
252
- options: CascadingSearchOption[];
266
+ options: Array<CascadingSearchOption | Record<string, unknown>>;
267
+ tree?: {
268
+ idKey: string;
269
+ parentIdKey: string;
270
+ labelKey: string;
271
+ };
253
272
  allLabel?: string;
254
273
  };
255
274
  type SearchValues = Record<string, string>;
@@ -277,7 +296,7 @@ interface SearchToolbarProps {
277
296
  /** 필터 전용 모드: 검색 입력창 없이 상세검색 버튼+칩만 보여준다 */
278
297
  hideSearchInput?: boolean;
279
298
  }
280
- declare function SearchToolbar({ search, onSearchChange, onSearch, advancedFields, advancedValues, placeholder, className, hideSearchInput, }: SearchToolbarProps): React__default.JSX.Element;
299
+ declare function SearchToolbar({ search, onSearchChange, onSearch, advancedFields: rawAdvancedFields, advancedValues, placeholder, className, hideSearchInput, }: SearchToolbarProps): React__default.JSX.Element;
281
300
 
282
301
  /**
283
302
  * 검색 결과 목록 — ERP `app/components/search/SearchResults.tsx`의 계약을 그대로 가져온 포팅.
@@ -403,7 +422,7 @@ interface SearchResultsProps<T> {
403
422
  declare function normalizePageSizes(sizes: number[] | undefined): number[] | undefined;
404
423
  declare function SearchResults<T extends {
405
424
  id: string;
406
- }>({ item, columns, gridConfig, leftFilters, rightFilters, initialView, views, pageSizes, className, style, searchParams, onParamsChange, actions, disableGrid, disableList, onRowClick, title, headerActions, isLoading, hideHeader, advancedSearchFields, searchPlaceholder, virtual, }: SearchResultsProps<T>): React__default.JSX.Element;
425
+ }>({ item, columns, gridConfig, leftFilters, rightFilters, initialView, views, pageSizes, className, style, searchParams, onParamsChange, actions, disableGrid, disableList, onRowClick, title, headerActions, isLoading, hideHeader, advancedSearchFields: rawAdvancedSearchFields, searchPlaceholder, virtual, }: SearchResultsProps<T>): React__default.JSX.Element;
407
426
 
408
427
  /**
409
428
  * 셀 표현 어휘 — 목록 컬럼의 셀과 카드 행의 값이 **같은 규칙**을 쓴다.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { DEV, HoistedStyle, isSafeNavigationTarget, useSduiAdapter, hostFetchOf, DateRangePicker, DatePicker, useSduiSandbox, resolveAdapter, assetResolverOf, SduiAdapterContext } from './chunk-5A7BWIGN.js';
2
2
  export { DatePicker, DateRangePicker, defaultAdapter, useSduiAssetResolver, useSduiSandbox } from './chunk-5A7BWIGN.js';
3
- import { searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-BJDWMECO.js';
4
- export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-BJDWMECO.js';
3
+ import { normalizeSearchFields, searchValuesFromParams, splitMultiValue, includesLooseSearch, cascadingValuePaths, SearchToolbar, searchFieldKeys, Popover } from './chunk-CDYIUQAD.js';
4
+ export { SearchToolbar, cascadingValuePaths, cleanSearchValues, includesLooseSearch, joinMultiValue, normalizeSearchText, searchFieldKeys, searchValuesFromParams, splitMultiValue } from './chunk-CDYIUQAD.js';
5
5
  import { EmptyState, Button, Checkbox, SegmentedControl, Spinner, Select, Pagination, Icon, Card, Badge, ProgressBar, Tag, Toast, Textarea, Tabs, Table, Switch, StatCard, Input, IconButton, Field, Alert, warnOnce, Dialog } from './chunk-B5JTXYDB.js';
6
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-B5JTXYDB.js';
7
7
  import { boxed, mergeStyle, containerLayoutStyle, joinClass, columnsClass, typographyStyle, LAYOUT_CSS, childLayoutFrom, CHILD_LAYOUT_PROPS } from './chunk-SAN4IX6N.js';
@@ -643,11 +643,15 @@ function SearchResults({
643
643
  headerActions,
644
644
  isLoading = false,
645
645
  hideHeader = false,
646
- advancedSearchFields = EMPTY_FIELDS,
646
+ advancedSearchFields: rawAdvancedSearchFields = EMPTY_FIELDS,
647
647
  searchPlaceholder,
648
648
  virtual
649
649
  }) {
650
650
  const hasActions = actions.length > 0;
651
+ const advancedSearchFields = useMemo(
652
+ () => normalizeSearchFields(rawAdvancedSearchFields),
653
+ [rawAdvancedSearchFields]
654
+ );
651
655
  const sizeChoices = normalizePageSizes(pageSizes) ?? PAGE_SIZE_CHOICES;
652
656
  const defaultPageSize = sizeChoices[0];
653
657
  const page = Number(searchParams.get("page")) || 1;
@@ -1,4 +1,4 @@
1
- export { AutocompleteField, toAutocompleteOptions } from '../chunk-3YFDIRCI.js';
2
- import '../chunk-BJDWMECO.js';
1
+ export { AutocompleteField, toAutocompleteOptions } from '../chunk-PEDU6LQK.js';
2
+ import '../chunk-CDYIUQAD.js';
3
3
  import '../chunk-B5JTXYDB.js';
4
4
  import '../chunk-SAN4IX6N.js';
@@ -1,4 +1,4 @@
1
- export { SearchableMultiPicker } from '../chunk-IEUPUFRF.js';
2
- import '../chunk-BJDWMECO.js';
1
+ export { SearchableMultiPicker } from '../chunk-4TS2N5KU.js';
2
+ import '../chunk-CDYIUQAD.js';
3
3
  import '../chunk-B5JTXYDB.js';
4
4
  import '../chunk-SAN4IX6N.js';
@@ -1,5 +1,5 @@
1
- import { SearchableMultiPicker } from '../chunk-IEUPUFRF.js';
2
- export { SearchableMultiPicker } from '../chunk-IEUPUFRF.js';
1
+ import { SearchableMultiPicker } from '../chunk-4TS2N5KU.js';
2
+ export { SearchableMultiPicker } from '../chunk-4TS2N5KU.js';
3
3
  import { SecureScreen } from '../chunk-4CZJF5PR.js';
4
4
  export { SecureScreen } from '../chunk-4CZJF5PR.js';
5
5
  import { StackedMeter } from '../chunk-BMZKZGJF.js';
@@ -32,8 +32,8 @@ import { PeriodFilter } from '../chunk-WJEGCCPY.js';
32
32
  export { PeriodFilter } from '../chunk-WJEGCCPY.js';
33
33
  import { PrintSheet } from '../chunk-JWYBFODH.js';
34
34
  export { PrintSheet } from '../chunk-JWYBFODH.js';
35
- export { AutocompleteField, toAutocompleteOptions } from '../chunk-3YFDIRCI.js';
36
- import '../chunk-BJDWMECO.js';
35
+ export { AutocompleteField, toAutocompleteOptions } from '../chunk-PEDU6LQK.js';
36
+ import '../chunk-CDYIUQAD.js';
37
37
  import { ColumnBoard } from '../chunk-VK6KWB65.js';
38
38
  export { ColumnBoard } from '../chunk-VK6KWB65.js';
39
39
  import { DiffView } from '../chunk-4IRLQWQC.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipco/sdui",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "WIPCO 서버 주도 UI(SDUI) 렌더러 — JSON 페이지 스펙을 React 컴포넌트로 렌더링",
5
5
  "keywords": [
6
6
  "sdui",