@iyulab/flex-table 0.45.0 → 0.47.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,5 @@
1
1
  import { LitElement, type PropertyValues } from 'lit';
2
+ import '@iyulab/components/dist/components/date-picker/UDatePicker.js';
2
3
  import type { ExportFormat } from './export/export.js';
3
4
  import type { CellPosition, CellRange } from './core/selection.js';
4
5
  import type { SortCriteria } from './core/sorting.js';
@@ -395,6 +396,12 @@ export declare class FlexTable extends LitElement {
395
396
  private _handleCopy;
396
397
  private _handlePaste;
397
398
  private _readClipboardText;
399
+ /**
400
+ * After a paste, the cells it wrote are the selection — as in spreadsheets — so the reader sees
401
+ * what changed and can copy, clear or undo it as one block. The active cell stays where the
402
+ * paste began (the same shape as selecting a column: range to the far corner, focus at the start).
403
+ */
404
+ private _selectPastedRange;
398
405
  private _expandRowsForPaste;
399
406
  private _applyPasteData;
400
407
  private _handleFillDown;
@@ -485,6 +492,12 @@ export declare class FlexTable extends LitElement {
485
492
  private _numberFilterState;
486
493
  private _dateFilterState;
487
494
  private _emptyFilterState;
495
+ /**
496
+ * From/to bounds as two `u-date-picker`s — the cell editor's control, for the same reason: the native
497
+ * date inputs show the browser's UI language (`10/02/2026` in an English browser) while the table
498
+ * shows ISO. Two pickers, not one `u-date-range-picker`: either bound may be left open ("from this
499
+ * day on"), which a range value cannot say. Each bound's calendar stops at the other bound.
500
+ */
488
501
  private _renderDateFilter;
489
502
  private _applyDateFilter;
490
503
  private _renderBooleanFilter;
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./flex-table-Bv41ls8b.js";
1
+ import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./flex-table-h2noRChg.js";
2
2
  export { o as FlexTable, e as RowSelectionState, t as UndoStack, n as effectiveAlign, i as exportData, r as flexTableLocale, a as renderCell };
@@ -1,4 +1,5 @@
1
1
  import type { SortCriteria } from '../core/sorting.js';
2
+ import type { SourceError } from '../core/source-error.js';
2
3
  export interface UseODataSourceOptions {
3
4
  pageSize?: number;
4
5
  defaultOrderBy?: string;
@@ -43,7 +44,11 @@ export interface UseODataSourceResult<T> {
43
44
  data: T[];
44
45
  totalCount: number;
45
46
  loading: boolean;
46
- error: string | null;
47
+ /**
48
+ * 마지막 요청의 실패, 없으면 `null`. 화면에는 `error.message` 를 그린다. «어떤 실패인가» 는
49
+ * `status`(HTTP) · `code`(서버의 거절 코드) · `details` 로 가른다 — 401 은 `onUnauthorized` 도 부른다.
50
+ */
51
+ error: SourceError | null;
47
52
  page: number;
48
53
  setPage: (page: number) => void;
49
54
  sortCriteria: SortCriteria[];
package/dist/react.d.ts CHANGED
@@ -69,3 +69,4 @@ export { useODataSource } from './odata/use-odata-source.js';
69
69
  export type { UseODataSourceOptions, UseODataSourceResult } from './odata/types.js';
70
70
  export { useArraySource } from './array/use-array-source.js';
71
71
  export type { UseArraySourceOptions, UseArraySourceResult } from './array/types.js';
72
+ export type { SourceError, SourceErrorDetail } from './core/source-error.js';
package/dist/react.js CHANGED
@@ -1,47 +1,74 @@
1
- import { t as e } from "./flex-table-Bv41ls8b.js";
1
+ import { t as e } from "./flex-table-h2noRChg.js";
2
2
  import { i as t, t as n } from "./query-Dw4iO0ir.js";
3
3
  import { t as r } from "./view-DiLr-TdY.js";
4
4
  import i, { useCallback as a, useEffect as o, useMemo as s, useRef as c, useState as l } from "react";
5
5
  import { createComponent as u } from "@lit/react";
6
+ //#region src/core/source-error.ts
7
+ async function d(e) {
8
+ let t = await e.text().catch(() => ""), n = {
9
+ message: `Request failed (${e.status})`,
10
+ status: e.status
11
+ };
12
+ if (!t) return n;
13
+ let r = t;
14
+ try {
15
+ r = JSON.parse(t);
16
+ } catch {}
17
+ if (n.body = r, !r || typeof r != "object") return n;
18
+ let i = r, a = i.error && typeof i.error == "object" ? i.error : i;
19
+ typeof a.message == "string" && a.message ? n.message = a.message : typeof i.message == "string" && i.message && (n.message = i.message), typeof a.code == "string" && a.code && (n.code = a.code);
20
+ let o = f(a.details);
21
+ return o && (n.details = o), n;
22
+ }
23
+ function f(e) {
24
+ if (!Array.isArray(e)) return;
25
+ let t = e.filter((e) => {
26
+ if (!e || typeof e != "object") return !1;
27
+ let t = e;
28
+ return typeof t.code == "string" && typeof t.message == "string";
29
+ });
30
+ return t.length > 0 ? t : void 0;
31
+ }
32
+ var p = class extends Error {
33
+ constructor(e) {
34
+ super(e.message), this.name = "SourceRequestError", this.failure = e;
35
+ }
36
+ };
37
+ function m(e) {
38
+ return e instanceof p ? e.failure : e instanceof Error ? { message: e.message } : { message: String(e) };
39
+ }
40
+ //#endregion
6
41
  //#region src/odata/use-odata-source.ts
7
- function d(e, r = {}) {
8
- let { pageSize: i = 20, defaultOrderBy: s, initialPage: u = 0, initialSearch: d = "", initialSort: f, fixedFilter: p, baseUrl: m, fetcher: h = fetch, onUnauthorized: g, enabled: _ = !0 } = r, v = p ? JSON.stringify(p) : "", [y, b] = l([]), [x, S] = l(0), [C, w] = l(!1), [T, E] = l(null), D = t({
42
+ function h(e, r = {}) {
43
+ let { pageSize: i = 20, defaultOrderBy: s, initialPage: u = 0, initialSearch: f = "", initialSort: h, fixedFilter: g, baseUrl: _, fetcher: v = fetch, onUnauthorized: y, enabled: b = !0 } = r, x = g ? JSON.stringify(g) : "", [S, C] = l([]), [w, T] = l(0), [E, D] = l(!1), [O, k] = l(null), A = t({
9
44
  defaultOrderBy: s,
10
45
  initialPage: u,
11
- initialSearch: d,
12
- initialSort: f
13
- }), [O, k] = l(D.page), [A, j] = l(D.sortCriteria), [M, N] = l(D.search), [P, F] = l(0), [I, L] = l(v);
14
- v !== I && (L(v), k(0));
15
- let R = c(null), z = a(() => {
16
- F((e) => e + 1);
17
- }, []), B = a((e) => {
18
- N(e), k(0);
19
- }, []), V = a((e) => {
46
+ initialSearch: f,
47
+ initialSort: h
48
+ }), [j, M] = l(A.page), [N, P] = l(A.sortCriteria), [F, I] = l(A.search), [L, R] = l(0), [z, B] = l(x);
49
+ x !== z && (B(x), M(0));
50
+ let V = c(null), H = a(() => {
51
+ R((e) => e + 1);
52
+ }, []), U = a((e) => {
53
+ I(e), M(0);
54
+ }, []), W = a((e) => {
20
55
  let t = e.detail?.criteria;
21
- t && (j(t), k(0));
56
+ t && (P(t), M(0));
22
57
  }, []);
23
58
  return o(() => {
24
- if (R.current?.abort(), !_) return;
59
+ if (V.current?.abort(), !b) return;
25
60
  let t = new AbortController();
26
- R.current = t, w(!0), E(null);
61
+ V.current = t, D(!0), k(null);
27
62
  let r = n({
28
- page: O,
63
+ page: j,
29
64
  pageSize: i,
30
- sortCriteria: A,
65
+ sortCriteria: N,
31
66
  defaultOrderBy: s,
32
- search: M,
33
- fixedFilter: p
34
- }), a = `${m ?? window.location.origin}${e}${r}`, o = async (e) => {
35
- let n = await h(e, { signal: t.signal });
36
- if (!n.ok) {
37
- n.status === 401 && g && g(n);
38
- let e = await n.text().catch(() => ""), t = `Request failed (${n.status})`;
39
- try {
40
- let n = JSON.parse(e);
41
- t = n?.error?.message ?? n?.message ?? t;
42
- } catch {}
43
- throw Error(t);
44
- }
67
+ search: F,
68
+ fixedFilter: g
69
+ }), a = `${_ ?? window.location.origin}${e}${r}`, o = async (e) => {
70
+ let n = await v(e, { signal: t.signal });
71
+ if (!n.ok) throw n.status === 401 && y && y(n), new p(await d(n));
45
72
  return n.json();
46
73
  };
47
74
  return (async () => {
@@ -61,42 +88,42 @@ function d(e, r = {}) {
61
88
  })().then(({ count: e, rows: n }) => {
62
89
  if (t.signal.aborted) return;
63
90
  let r = e ?? 0;
64
- b(n), S(r), E(null);
91
+ C(n), T(r), k(null);
65
92
  let a = r === 0 ? 0 : Math.ceil(r / i) - 1;
66
- O > a && k(a);
93
+ j > a && M(a);
67
94
  }).catch((e) => {
68
- t.signal.aborted || e.name !== "AbortError" && (E(e.message), b([]), S(0));
95
+ t.signal.aborted || e.name !== "AbortError" && (k(m(e)), C([]), T(0));
69
96
  }).finally(() => {
70
- t.signal.aborted || w(!1);
97
+ t.signal.aborted || D(!1);
71
98
  }), () => t.abort();
72
99
  }, [
73
- _,
100
+ b,
74
101
  e,
75
- O,
102
+ j,
76
103
  i,
77
- A,
78
- M,
79
- v,
104
+ N,
105
+ F,
106
+ x,
80
107
  s,
81
- P,
82
- m
108
+ L,
109
+ _
83
110
  ]), {
84
- data: y,
85
- totalCount: x,
86
- loading: C || !_,
87
- error: T,
88
- page: O,
89
- setPage: k,
90
- sortCriteria: A,
91
- onSortChange: V,
92
- search: M,
93
- setSearch: B,
94
- refresh: z
111
+ data: S,
112
+ totalCount: w,
113
+ loading: E || !b,
114
+ error: O,
115
+ page: j,
116
+ setPage: M,
117
+ sortCriteria: N,
118
+ onSortChange: W,
119
+ search: F,
120
+ setSearch: U,
121
+ refresh: H
95
122
  };
96
123
  }
97
124
  //#endregion
98
125
  //#region src/array/use-array-source.ts
99
- function f(e, n = {}) {
126
+ function g(e, n = {}) {
100
127
  let { pageSize: i = 20, defaultOrderBy: o, initialPage: c = 0, initialSearch: u = "", initialSort: d, columns: f, searchFields: p } = n, m = t({
101
128
  defaultOrderBy: o,
102
129
  initialPage: c,
@@ -137,7 +164,7 @@ function f(e, n = {}) {
137
164
  refresh: C
138
165
  };
139
166
  }
140
- var p = u({
167
+ var _ = u({
141
168
  tagName: "flex-table",
142
169
  elementClass: e,
143
170
  react: i,
@@ -176,4 +203,4 @@ var p = u({
176
203
  }
177
204
  });
178
205
  //#endregion
179
- export { p as FlexTableReact, f as useArraySource, d as useODataSource };
206
+ export { _ as FlexTableReact, g as useArraySource, h as useODataSource };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.45.0",
3
+ "version": "0.47.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -45,6 +45,7 @@
45
45
  "build": "npm run typecheck && vite build && tsc --emitDeclarationOnly",
46
46
  "build:demo": "vite build --config vite.config.demo.ts",
47
47
  "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
48
+ "pretest": "node -e \"if(require('fs').existsSync('../../scripts/sibling-dist-check.js'))require('child_process').execFileSync('node',['../../scripts/sibling-dist-check.js'],{stdio:'inherit'})\"",
48
49
  "test": "vitest run",
49
50
  "test:watch": "vitest",
50
51
  "lint": "eslint src/",
@@ -72,7 +73,7 @@
72
73
  "odata-query": "^8.1.0"
73
74
  },
74
75
  "peerDependencies": {
75
- "@iyulab/components": ">=1.56.0",
76
+ "@iyulab/components": ">=2.0.1",
76
77
  "@lit/react": "^1.0.8",
77
78
  "react": "^18.0.0 || ^19.0.0"
78
79
  },
@@ -66,7 +66,7 @@ as `text`). See `references/api.md` for every field.
66
66
  - `editor` returns a Lit template containing an element with class `ft-editor`; its `.value` is committed.
67
67
  - `validator` returns `null` when valid, or an error message (the edit is rejected and `validation-error` fires).
68
68
  - `number` cells, the number filter and paste read numbers in the active `Locale` (`1,5` is 1.5 on a comma-decimal page); an edit that is not a number is rejected with `validation-error`.
69
- - `date` cells edit as a `YYYY-MM-DD` text box in every browser language (also `20261002`, `2026/10/2`, `10-02`); pasted dates are normalized to ISO. `datetime` cells edit as `YYYY-MM-DD HH:mm` (local) and store `YYYY-MM-DDTHH:mm`.
69
+ - `date` cells edit with `u-date-picker` (a `YYYY-MM-DD` text box in every browser language plus a calendar) (also `20261002`, `2026/10/2`, `10-02`); pasted dates are normalized to ISO. `datetime` cells edit as `YYYY-MM-DD HH:mm` (local) and store `YYYY-MM-DDTHH:mm`.
70
70
  - `conditionalRules` applies `{ when, style }` rules in order; later matches override earlier ones.
71
71
  - `pinned: 'left' | 'right'` freezes a column during horizontal scroll.
72
72
 
@@ -127,7 +127,7 @@ Server paging from OData or an in-memory array uses the same binding — see `re
127
127
  - Styles for elements returned by `render` must be passed through the `stylesheets` property
128
128
  (constructable `CSSStyleSheet[]`); document CSS does not cross the shadow boundary.
129
129
  - `useODataSource` `fetcher` / `onUnauthorized` must be stable references (`useCallback`).
130
- - Always render `error` from `useODataSource`; a failed request otherwise leaves the grid empty.
130
+ - Always render `error.message` from `useODataSource`; a failed request otherwise leaves the grid empty. Branch on `error.status` / `error.code` rather than parsing the message.
131
131
 
132
132
  ## References
133
133
 
@@ -73,7 +73,7 @@ function Orders() {
73
73
  return (
74
74
  <>
75
75
  <input value={source.search} onChange={(e) => source.setSearch(e.target.value)} />
76
- {source.error && <p role="alert">{source.error}</p>}
76
+ {source.error && <p role="alert">{source.error.message}</p>}
77
77
  <FlexTableReact<Order>
78
78
  dataMode="server"
79
79
  columns={columns}
@@ -111,7 +111,7 @@ function Orders() {
111
111
  |---|---|
112
112
  | `data`, `totalCount` | Current page and `@odata.count`; `@odata.nextLink` is followed to fill a page |
113
113
  | `loading` | Request in flight |
114
- | `error` | `string \| null` — render it |
114
+ | `error` | `SourceError \| null` — `{ message, status?, code?, details?, body? }`; render `error.message`, branch on `status` / `code` (OData `error.code`) |
115
115
  | `page`, `setPage` | Zero-based |
116
116
  | `sortCriteria`, `onSortChange` | Pass `onSortChange` to the table's `sort-change` (resets to page 0) |
117
117
  | `search`, `setSearch` | Literal text (resets to page 0) |