@wipco/sdui 0.9.0 → 0.10.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.
@@ -45,6 +45,7 @@ function describeParseError(err, text) {
45
45
  function JsonFieldImpl({
46
46
  value,
47
47
  onChange,
48
+ emptyAsNull = false,
48
49
  rows = 6,
49
50
  invalid = false,
50
51
  disabled = false,
@@ -67,6 +68,10 @@ function JsonFieldImpl({
67
68
  focusedRef.current = false;
68
69
  if (text.trim() === "") {
69
70
  setParseError(null);
71
+ if (emptyAsNull) {
72
+ lastFormattedRef.current = "";
73
+ onChange?.(null);
74
+ }
70
75
  return;
71
76
  }
72
77
  try {
@@ -79,7 +84,7 @@ function JsonFieldImpl({
79
84
  } catch (err) {
80
85
  setParseError(describeParseError(err, text));
81
86
  }
82
- }, [text, onChange]);
87
+ }, [text, onChange, emptyAsNull]);
83
88
  return /* @__PURE__ */ jsxs(
84
89
  "div",
85
90
  {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { CSSProperties, ReactNode, ComponentType } from 'react';
3
- import { S as SduiAction, a as SduiPropValue, b as SduiRule, c as SduiPage } from './types-Dl4rPyan.js';
4
- export { d as SduiCalcRef, e as SduiChild, f as SduiIndexRef, g as SduiItemRef, h as SduiNode, i as SduiPageQuery, j as SduiScalar, k as SduiStateRef } from './types-Dl4rPyan.js';
3
+ import { S as SduiAction, a as SduiPropValue, b as SduiRule, c as SduiPage } from './types-DO8_hC6A.js';
4
+ export { d as SduiCalcRef, e as SduiChild, f as SduiIndexRef, g as SduiItemRef, h as SduiNode, i as SduiPageQuery, j as SduiScalar, k as SduiStateRef } from './types-DO8_hC6A.js';
5
5
  import { S as SduiBoxProps, a as SduiContainerLayoutProps, b as SduiColumnsScale, c as SduiGapScale, d as SduiWeightScale, e as SduiTextAlignScale } from './layout-B1QrIBm6.js';
6
6
  export { C as CHILD_LAYOUT_PROPS, L as LAYOUT_CSS, f as SduiAlignScale, g as SduiChildLayoutProps, h as SduiSizeScale, i as SduiSpanScale, j as childLayoutFrom, k as columnsClass, l as containerLayoutStyle, m as joinClass, n as mergeStyle, t as typographyStyle } from './layout-B1QrIBm6.js';
7
7
  import { D as DatePickerProps, a as DateRangePickerProps } from './DatePicker-BndreCPl.js';
package/dist/index.js CHANGED
@@ -688,47 +688,52 @@ function SearchResults({
688
688
  }
689
689
  return entries;
690
690
  }, [leftFilters, rightFilters, searchParams]);
691
+ const hasAllRows = item.data.length >= item.total;
691
692
  const processed = useMemo(() => {
692
693
  const cell = (row, key) => row[key];
693
694
  let rows = item.data;
694
- if (appliedSearch) {
695
- rows = rows.filter(
696
- (row) => includesLooseSearch(
697
- Object.values(row).filter((v) => typeof v === "string" || typeof v === "number").join(" "),
698
- appliedSearch
699
- )
700
- );
701
- }
702
- for (const { key, values } of activeFilters) {
703
- rows = rows.filter((row) => values.includes(String(cell(row, key))));
704
- }
705
- for (const field of advancedSearchFields) {
706
- if (field.type === "select") {
707
- const wanted = splitMultiValue(advancedValues[field.key] ?? "");
708
- if (wanted.length > 0) rows = rows.filter((row) => wanted.includes(String(cell(row, field.key))));
709
- } else if (field.type === "cascading") {
710
- const wanted = splitMultiValue(advancedValues[field.key] ?? "");
711
- if (wanted.length > 0) {
712
- const lineages = cascadingValuePaths(field.options);
713
- rows = rows.filter((row) => {
714
- const own = String(cell(row, field.key));
715
- const lineage = lineages.get(own) ?? [own];
716
- return lineage.some((value) => wanted.includes(value));
717
- });
695
+ if (hasAllRows) {
696
+ if (appliedSearch) {
697
+ rows = rows.filter(
698
+ (row) => includesLooseSearch(
699
+ Object.values(row).filter((v) => typeof v === "string" || typeof v === "number").join(" "),
700
+ appliedSearch
701
+ )
702
+ );
703
+ }
704
+ for (const { key, values } of activeFilters) {
705
+ rows = rows.filter((row) => values.includes(String(cell(row, key))));
706
+ }
707
+ for (const field of advancedSearchFields) {
708
+ if (field.type === "select") {
709
+ const wanted = splitMultiValue(advancedValues[field.key] ?? "");
710
+ if (wanted.length > 0) rows = rows.filter((row) => wanted.includes(String(cell(row, field.key))));
711
+ } else if (field.type === "cascading") {
712
+ const wanted = splitMultiValue(advancedValues[field.key] ?? "");
713
+ if (wanted.length > 0) {
714
+ const lineages = cascadingValuePaths(field.options);
715
+ rows = rows.filter((row) => {
716
+ const own = String(cell(row, field.key));
717
+ const lineage = lineages.get(own) ?? [own];
718
+ return lineage.some((value) => wanted.includes(value));
719
+ });
720
+ }
721
+ } else if (field.type === "dateRange") {
722
+ const from = advancedValues[field.fromKey ?? `${field.key}From`];
723
+ const to = advancedValues[field.toKey ?? `${field.key}To`];
724
+ const datePart = (v) => String(v ?? "").slice(0, 10);
725
+ if (from) rows = rows.filter((row) => datePart(cell(row, field.key)) >= from.slice(0, 10));
726
+ if (to) rows = rows.filter((row) => datePart(cell(row, field.key)) <= to.slice(0, 10));
727
+ } else if (field.type === "numberRange") {
728
+ const min = numericValue(advancedValues[field.fromKey ?? `${field.key}Min`]);
729
+ const max = numericValue(advancedValues[field.toKey ?? `${field.key}Max`]);
730
+ if (min !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? -Infinity) >= min);
731
+ if (max !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? Infinity) <= max);
732
+ } else {
733
+ const keyword = advancedValues[field.key];
734
+ const baseKey = field.key.endsWith("Contains") ? field.key.slice(0, -"Contains".length) : field.key;
735
+ if (keyword) rows = rows.filter((row) => includesLooseSearch(String(cell(row, baseKey) ?? ""), keyword));
718
736
  }
719
- } else if (field.type === "dateRange") {
720
- const from = advancedValues[field.fromKey ?? `${field.key}From`];
721
- const to = advancedValues[field.toKey ?? `${field.key}To`];
722
- if (from) rows = rows.filter((row) => String(cell(row, field.key) ?? "") >= from);
723
- if (to) rows = rows.filter((row) => String(cell(row, field.key) ?? "") <= to);
724
- } else if (field.type === "numberRange") {
725
- const min = numericValue(advancedValues[field.fromKey ?? `${field.key}Min`]);
726
- const max = numericValue(advancedValues[field.toKey ?? `${field.key}Max`]);
727
- if (min !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? -Infinity) >= min);
728
- if (max !== null) rows = rows.filter((row) => (numericValue(cell(row, field.key)) ?? Infinity) <= max);
729
- } else {
730
- const keyword = advancedValues[field.key];
731
- if (keyword) rows = rows.filter((row) => includesLooseSearch(String(cell(row, field.key) ?? ""), keyword));
732
737
  }
733
738
  }
734
739
  if (sort) {
@@ -746,8 +751,8 @@ function SearchResults({
746
751
  });
747
752
  }
748
753
  return rows;
749
- }, [item.data, appliedSearch, activeFilters, advancedSearchFields, advancedValues, sort, columns]);
750
- const clientPaginated = item.data.length >= item.total;
754
+ }, [item.data, hasAllRows, appliedSearch, activeFilters, advancedSearchFields, advancedValues, sort, columns]);
755
+ const clientPaginated = hasAllRows;
751
756
  const totalCount = clientPaginated ? processed.length : item.total;
752
757
  const pageCount = Math.max(1, Math.ceil(totalCount / pageSize));
753
758
  const safePage = Math.min(Math.max(1, page), pageCount);
@@ -1808,8 +1813,11 @@ function validatePageData(data, issues) {
1808
1813
  if (raw.select !== void 0 && typeof raw.select !== "string") {
1809
1814
  issues.push({ path: `${path}.select`, message: "select\uB294 \uBB38\uC790\uC5F4 \uACBD\uB85C\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
1810
1815
  }
1816
+ if (raw.debounceMs !== void 0 && typeof raw.debounceMs !== "number") {
1817
+ issues.push({ path: `${path}.debounceMs`, message: "debounceMs\uB294 \uC22B\uC790(ms)\uC5EC\uC57C \uD569\uB2C8\uB2E4" });
1818
+ }
1811
1819
  for (const [k, v] of Object.entries(raw)) {
1812
- if (k === "endpoint" || k === "select") continue;
1820
+ if (k === "endpoint" || k === "select" || k === "debounceMs") continue;
1813
1821
  if (k !== "when" && k !== "fallback") {
1814
1822
  issues.push({ path: `${path}.${k}`, message: `\uC54C \uC218 \uC5C6\uB294 \uD56D\uBAA9: ${k}` });
1815
1823
  continue;
@@ -1960,12 +1968,14 @@ function planPageData(data, resolve) {
1960
1968
  const resolved = resolve(query.endpoint);
1961
1969
  const endpoint = typeof resolved === "string" ? resolved.trim() : "";
1962
1970
  const enabled = "when" in query ? Boolean(resolve(query.when)) : true;
1971
+ const debounceMs = typeof query.debounceMs === "number" && Number.isFinite(query.debounceMs) && query.debounceMs > 0 ? query.debounceMs : void 0;
1963
1972
  plans.push({
1964
1973
  key,
1965
1974
  endpoint,
1966
1975
  enabled,
1967
1976
  select: typeof query.select === "string" ? query.select : void 0,
1968
- fallback: query.fallback
1977
+ fallback: query.fallback,
1978
+ debounceMs
1969
1979
  });
1970
1980
  }
1971
1981
  return plans;
@@ -2185,6 +2195,8 @@ function SduiNodeRenderer({
2185
2195
  props[name] = (arg) => actions.forEach(
2186
2196
  (a) => dispatch(resolveValue(a, scope), arg)
2187
2197
  );
2198
+ } else if (name === "rowClick" && (isAction(raw) || isActionList(raw))) {
2199
+ props[name] = raw;
2188
2200
  } else {
2189
2201
  let resolved = resolveValue(raw, scope);
2190
2202
  if (resolved === null) {
@@ -2456,12 +2468,18 @@ function SduiRenderer({
2456
2468
  if (runnable.length === 0) return;
2457
2469
  const controller = new AbortController();
2458
2470
  let cancelled = false;
2471
+ const timers = [];
2459
2472
  setEntries(
2460
2473
  Object.fromEntries(runnable.map((plan) => [`${plan.key}.$loading`, true]))
2461
2474
  );
2475
+ const afterDebounce = (plan) => plan.debounceMs === void 0 ? Promise.resolve() : new Promise((resolveWait) => {
2476
+ timers.push(setTimeout(resolveWait, plan.debounceMs));
2477
+ });
2462
2478
  void Promise.all(
2463
2479
  runnable.map(async (plan) => {
2464
2480
  try {
2481
+ await afterDebounce(plan);
2482
+ if (cancelled) return;
2465
2483
  const response = await hostRef.current.fetch(plan.endpoint, {
2466
2484
  method: "GET",
2467
2485
  signal: controller.signal
@@ -2494,12 +2512,17 @@ function SduiRenderer({
2494
2512
  return () => {
2495
2513
  cancelled = true;
2496
2514
  controller.abort();
2515
+ timers.forEach(clearTimeout);
2497
2516
  };
2498
2517
  }, [dataPlanKey, dataEpochKey, setEntries]);
2499
2518
  const dispatchRef = useRef(() => {
2500
2519
  });
2501
2520
  const dispatch = useCallback(
2502
- (action, arg) => {
2521
+ (rawAction, arg) => {
2522
+ const action = resolveValue(
2523
+ rawAction,
2524
+ EMPTY_SCOPE
2525
+ );
2503
2526
  switch (action.action) {
2504
2527
  case "navigate":
2505
2528
  if (!isSafeNavigationTarget(action.to)) {
@@ -2668,7 +2691,7 @@ function SduiRenderer({
2668
2691
  break;
2669
2692
  }
2670
2693
  },
2671
- [pushToast, onCustomAction, customValidators, setEntries]
2694
+ [pushToast, onCustomAction, customValidators, setEntries, resolveValue]
2672
2695
  );
2673
2696
  dispatchRef.current = dispatch;
2674
2697
  const engine = useMemo(
package/dist/meta.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as SduiPropValue, h as SduiNode } from './types-Dl4rPyan.js';
1
+ import { a as SduiPropValue, h as SduiNode } from './types-DO8_hC6A.js';
2
2
 
3
3
  /**
4
4
  * 시각 빌더용 컴포넌트 메타데이터 스키마.
package/dist/meta.js CHANGED
@@ -3308,6 +3308,15 @@ var JsonFieldMeta = {
3308
3308
  { name: "rows", kind: "number", label: "\uC904 \uC218", default: 6, group: "appearance" },
3309
3309
  { name: "placeholder", kind: "string", label: "\uC548\uB0B4 \uBB38\uAD6C", default: "JSON\uC744 \uC785\uB825\uD558\uC138\uC694", group: "content" },
3310
3310
  { name: "invalid", kind: "boolean", label: "\uC624\uB958 \uD45C\uC2DC", default: false, bindable: true, group: "validation" },
3311
+ {
3312
+ name: "emptyAsNull",
3313
+ kind: "boolean",
3314
+ label: "\uBE44\uC6B0\uBA74 null \uC800\uC7A5",
3315
+ default: false,
3316
+ description: "\uBE48 \uD14D\uC2A4\uD2B8\uB85C blur \uD558\uBA74 onChange(null) \u2014 \uC9C0\uC6CC\uC11C \uAC12\uC744 \uC0AD\uC81C\uD558\uB294 \uD3FC\uC5D0\uC11C \uCF20\uB2E4.",
3317
+ group: "behavior",
3318
+ advanced: true
3319
+ },
3311
3320
  { name: "disabled", kind: "boolean", label: "\uBE44\uD65C\uC131", default: false, bindable: true, group: "behavior" }
3312
3321
  ],
3313
3322
  children: { accepts: false },
@@ -216,6 +216,12 @@ interface SduiPageQuery {
216
216
  when?: SduiPropValue;
217
217
  /** 실패했거나 아직 조회 전일 때 상태에 들어갈 값. 생략하면 `undefined`. */
218
218
  fallback?: SduiPropValue;
219
+ /**
220
+ * 경로가 바뀐 뒤 이 시간(ms)만큼 조용해야 조회를 낸다. 경로가 입력값을 참조하는
221
+ * 조회(관리번호 실시간 검증 등)가 키 입력마다 GET 을 내는 것을 막는 자리다 —
222
+ * v2 는 400ms 를 썼다. 생략하면 즉시.
223
+ */
224
+ debounceMs?: number;
219
225
  }
220
226
  interface SduiPage {
221
227
  version: 1;
@@ -14,6 +14,13 @@ interface JsonFieldProps {
14
14
  /** 객체 또는 JSON 문자열 */
15
15
  value?: unknown;
16
16
  onChange?: (parsed: unknown) => void;
17
+ /**
18
+ * 빈 텍스트로 blur 하면 onChange(null) 을 낸다 — "지워서 값 삭제"용.
19
+ * 기본은 종전대로 무동작이다: 빈 칸이 곧 null 저장이 되면, 잠깐 지우고 다시 쓰려던
20
+ * 사용자의 미완성 상태가 그대로 저장 후보가 되기 때문에 옵트인으로 둔다.
21
+ * (ERP 유닛 수정의 '옵션 비우기 (null 저장)' 버튼이 이 옵션으로 대체된다.)
22
+ */
23
+ emptyAsNull?: boolean;
17
24
  /** 기본 6 */
18
25
  rows?: number;
19
26
  invalid?: boolean;
@@ -1,3 +1,3 @@
1
- export { JsonField } from '../chunk-JONNWN6N.js';
1
+ export { JsonField } from '../chunk-FDVI3PWQ.js';
2
2
  import '../chunk-B5JTXYDB.js';
3
3
  import '../chunk-SAN4IX6N.js';
@@ -22,8 +22,8 @@ import { ImageLightbox } from '../chunk-3PYMVHL3.js';
22
22
  export { ImageLightbox } from '../chunk-3PYMVHL3.js';
23
23
  import { InlineEdit } from '../chunk-7AHNAX6T.js';
24
24
  export { InlineEdit } from '../chunk-7AHNAX6T.js';
25
- import { JsonField } from '../chunk-JONNWN6N.js';
26
- export { JsonField } from '../chunk-JONNWN6N.js';
25
+ import { JsonField } from '../chunk-FDVI3PWQ.js';
26
+ export { JsonField } from '../chunk-FDVI3PWQ.js';
27
27
  import { LiveClock } from '../chunk-Z3WGIFJP.js';
28
28
  export { LiveClock } from '../chunk-Z3WGIFJP.js';
29
29
  import { OrgChart } from '../chunk-BT4HC3WR.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipco/sdui",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "WIPCO 서버 주도 UI(SDUI) 렌더러 — JSON 페이지 스펙을 React 컴포넌트로 렌더링",
5
5
  "keywords": [
6
6
  "sdui",