@iyulab/flex-table 0.52.0 → 0.54.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.
@@ -0,0 +1,247 @@
1
+ import { n as e } from "./locale-B1SiVDS1.js";
2
+ import t from "odata-query";
3
+ //#region src/odata/query.ts
4
+ function n(e) {
5
+ let t = e.split(/\s+/).map((e) => e.replace(/"/g, "")).filter((e) => e.length > 0);
6
+ if (t.length !== 0) return t.map((e) => `"${e}"`).join(" AND ");
7
+ }
8
+ function r(e) {
9
+ let { defaultOrderBy: t, initialPage: n = 0, initialSearch: r = "", initialSort: a } = e;
10
+ return {
11
+ page: n,
12
+ search: r,
13
+ sortCriteria: a ?? (t ? i(t) : [])
14
+ };
15
+ }
16
+ function i(e) {
17
+ return e.split(",").map((e) => {
18
+ let t = e.trim().split(/\s+/);
19
+ return {
20
+ key: t[0],
21
+ direction: t[1]?.toLowerCase() === "desc" ? "desc" : "asc"
22
+ };
23
+ });
24
+ }
25
+ function a(e) {
26
+ let { page: r, pageSize: i, sortCriteria: a = [], defaultOrderBy: o, search: s, fixedFilter: c } = e, l = a.length > 0 ? a.map((e) => `${e.key} ${e.direction}`).join(", ") : o, u = {
27
+ top: i,
28
+ skip: r * i,
29
+ count: !0
30
+ };
31
+ if (l && (u.orderBy = l), c && (u.filter = c), s) {
32
+ let e = n(s);
33
+ e && (u.search = e);
34
+ }
35
+ return t(u);
36
+ }
37
+ //#endregion
38
+ //#region src/core/source-error.ts
39
+ async function o(t) {
40
+ let n = await t.text().catch(() => ""), r = {
41
+ message: e("requestFailed", { status: t.status }),
42
+ status: t.status
43
+ };
44
+ if (!n) return r;
45
+ let i = n;
46
+ try {
47
+ i = JSON.parse(n);
48
+ } catch {}
49
+ if (r.body = i, !i || typeof i != "object") return r;
50
+ let a = i, o = a.error && typeof a.error == "object" ? a.error : a;
51
+ typeof o.message == "string" && o.message ? r.message = o.message : typeof a.message == "string" && a.message && (r.message = a.message), typeof o.code == "string" && o.code && (r.code = o.code);
52
+ let c = s(o.details);
53
+ return c && (r.details = c), r;
54
+ }
55
+ function s(e) {
56
+ if (!Array.isArray(e)) return;
57
+ let t = e.filter((e) => {
58
+ if (!e || typeof e != "object") return !1;
59
+ let t = e;
60
+ return typeof t.code == "string" && typeof t.message == "string";
61
+ });
62
+ return t.length > 0 ? t : void 0;
63
+ }
64
+ var c = class extends Error {
65
+ constructor(e) {
66
+ super(e.message), this.name = "SourceRequestError", this.failure = e;
67
+ }
68
+ };
69
+ function l(t) {
70
+ return new c({
71
+ message: e("networkFailed"),
72
+ cause: t
73
+ });
74
+ }
75
+ function u(e) {
76
+ return e instanceof c ? e.failure : e instanceof Error ? { message: e.message } : { message: String(e) };
77
+ }
78
+ //#endregion
79
+ //#region src/odata/source.ts
80
+ function d(e, t = {}) {
81
+ let n = e, i = t, s = f(t.fixedFilter), d = r({
82
+ defaultOrderBy: t.defaultOrderBy,
83
+ initialPage: t.initialPage ?? 0,
84
+ initialSearch: t.initialSearch ?? "",
85
+ initialSort: t.initialSort
86
+ }), p = {
87
+ data: [],
88
+ totalCount: 0,
89
+ loading: !0,
90
+ error: null,
91
+ page: d.page,
92
+ sortCriteria: d.sortCriteria,
93
+ search: d.search
94
+ }, m = x(), h = /* @__PURE__ */ new Set(), g = null, _ = !1, v = null;
95
+ function y() {
96
+ return i.enabled ?? !0;
97
+ }
98
+ function b() {
99
+ return i.pageSize ?? 20;
100
+ }
101
+ function x() {
102
+ return {
103
+ ...p,
104
+ loading: p.loading || !y()
105
+ };
106
+ }
107
+ function S(e) {
108
+ p = {
109
+ ...p,
110
+ ...e
111
+ }, C();
112
+ }
113
+ function C() {
114
+ let e = x();
115
+ if (!Object.keys(e).every((t) => e[t] === m[t])) {
116
+ m = e;
117
+ for (let e of [...h]) e();
118
+ }
119
+ }
120
+ function w() {
121
+ return JSON.stringify([
122
+ n,
123
+ i.baseUrl,
124
+ b(),
125
+ i.defaultOrderBy,
126
+ s,
127
+ p.page,
128
+ p.sortCriteria,
129
+ p.search
130
+ ]);
131
+ }
132
+ function T() {
133
+ _ || (_ = !0, queueMicrotask(() => {
134
+ _ = !1, E();
135
+ }));
136
+ }
137
+ function E() {
138
+ if (h.size === 0) return;
139
+ if (!y()) {
140
+ g?.abort(), g = null, v = null, C();
141
+ return;
142
+ }
143
+ let e = w();
144
+ e !== v && (v = e, D());
145
+ }
146
+ function D() {
147
+ g?.abort();
148
+ let e = new AbortController();
149
+ g = e;
150
+ let { page: t, sortCriteria: r, search: s } = p, d = b(), f = i.fetcher ?? fetch, m = i.onUnauthorized;
151
+ S({
152
+ loading: !0,
153
+ error: null
154
+ });
155
+ let h = a({
156
+ page: t,
157
+ pageSize: d,
158
+ sortCriteria: r,
159
+ defaultOrderBy: i.defaultOrderBy,
160
+ search: s,
161
+ fixedFilter: i.fixedFilter
162
+ }), _ = `${i.baseUrl ?? window.location.origin}${n}${h}`, v = async (t) => {
163
+ let n;
164
+ try {
165
+ n = await f(t, { signal: e.signal });
166
+ } catch (e) {
167
+ throw e?.name === "AbortError" ? e : l(e);
168
+ }
169
+ if (!n.ok) throw n.status === 401 && m && m(n), new c(await o(n));
170
+ return n.json();
171
+ };
172
+ (async () => {
173
+ let e = await v(_), t = [...e.value ?? e], n = new URL(_).origin, r = /* @__PURE__ */ new Set([_]), i = _, a = e["@odata.nextLink"];
174
+ for (; typeof a == "string" && a && t.length < d;) {
175
+ let e = new URL(a, i);
176
+ if (e.origin !== n) throw Error(`OData nextLink points outside the source origin (${e.origin})`);
177
+ if (i = e.toString(), r.has(i)) throw Error(`OData nextLink repeats an already-read page (${i})`);
178
+ r.add(i);
179
+ let o = await v(i);
180
+ t.push(...o.value ?? []), a = o["@odata.nextLink"];
181
+ }
182
+ return {
183
+ count: e["@odata.count"],
184
+ rows: t
185
+ };
186
+ })().then(({ count: n, rows: r }) => {
187
+ if (e.signal.aborted) return;
188
+ let i = n ?? 0, a = i === 0 ? 0 : Math.ceil(i / d) - 1;
189
+ t > a ? (S({
190
+ data: r,
191
+ totalCount: i,
192
+ error: null,
193
+ page: a
194
+ }), T()) : S({
195
+ data: r,
196
+ totalCount: i,
197
+ error: null
198
+ });
199
+ }).catch((t) => {
200
+ e.signal.aborted || t?.name !== "AbortError" && S({
201
+ error: u(t),
202
+ data: [],
203
+ totalCount: 0
204
+ });
205
+ }).finally(() => {
206
+ e.signal.aborted || S({ loading: !1 });
207
+ });
208
+ }
209
+ return {
210
+ getState: () => m,
211
+ subscribe(e) {
212
+ return h.add(e), h.size === 1 && T(), () => {
213
+ !h.delete(e) || h.size > 0 || (g?.abort(), g = null, v = null);
214
+ };
215
+ },
216
+ setPage(e) {
217
+ S({ page: e }), T();
218
+ },
219
+ setSort(e) {
220
+ S({
221
+ sortCriteria: e,
222
+ page: 0
223
+ }), T();
224
+ },
225
+ setSearch(e) {
226
+ S({
227
+ search: e,
228
+ page: 0
229
+ }), T();
230
+ },
231
+ refresh() {
232
+ y() && (v = null, T());
233
+ },
234
+ update(e, t = {}) {
235
+ let r = f(t.fixedFilter), a = r !== s;
236
+ n = e, i = t, s = r, a && (p = {
237
+ ...p,
238
+ page: 0
239
+ }), C(), T();
240
+ }
241
+ };
242
+ }
243
+ function f(e) {
244
+ return e ? JSON.stringify(e) : "";
245
+ }
246
+ //#endregion
247
+ export { r as a, i, a as n, n as r, d as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.52.0",
3
+ "version": "0.54.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -27,7 +27,7 @@ npm install @lit/react react
27
27
  |---|---|
28
28
  | `@iyulab/flex-table` | Registers `<flex-table>`; exports `FlexTable`, types, `exportData`, `renderCell`, `flexTableLocale`, `RowSelectionState`, `UndoStack`, `effectiveAlign` |
29
29
  | `@iyulab/flex-table/react` | `FlexTableReact`, `useODataSource`, `useArraySource` |
30
- | `@iyulab/flex-table/odata` | Pure helpers, no React: `buildODataQuery`, `buildSearchExpression`, `parseOrderBy` |
30
+ | `@iyulab/flex-table/odata` | No React: `createODataSource` (the source `useODataSource` adapts), `ODataSourceController` (Lit), `buildODataQuery`, `buildSearchExpression`, `parseOrderBy` |
31
31
  | `@iyulab/flex-table/array` | Pure helper, no React: `computeArrayView` |
32
32
 
33
33
  ## Quick start
@@ -126,8 +126,7 @@ Server paging from OData or an in-memory array uses the same binding — see `re
126
126
  - `--ft-row-height` is read once at first render; change row height later via `rowHeight` / `row-height`.
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
- - `useODataSource` `fetcher` / `onUnauthorized` must be stable references (`useCallback`).
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.
129
+ - Always show `error` from `useODataSource` — pass it to the table (`error={source.error}`); a failed request otherwise leaves the grid empty. Branch on `error.status` / `error.code` rather than parsing the message.
131
130
 
132
131
  ## References
133
132
 
@@ -23,6 +23,7 @@ All names below are members of the `FlexTable` class (`<flex-table>`).
23
23
  | `footerData` | `footer-data` | `Record<string, string \| TemplateResult> \| null` | `null` | Summary row keyed by column key |
24
24
  | `emptyMessage` | `empty-message` | `string` | `'No data'` | |
25
25
  | `noMatchingMessage` | `no-matching-message` | `string` | `'No matching data'` | All rows hidden by filters |
26
+ | `error` | — | `{ message: string } \| null` | `null` | Load failure shown as an alert instead of rows / empty state (a source's `error` as is) |
26
27
  | `loading` | `loading` | `boolean` | `false` | Loading overlay + `aria-busy` |
27
28
  | `importEnabled` | `import-enabled` | `boolean` | `false` | Drag-and-drop `.xlsx` / `.csv` import |
28
29
  | `clearUndoOnDataChange` | `clear-undo-on-data-change` | `boolean` | `false` | Replacing `data` clears undo history |
@@ -63,22 +63,20 @@ Fetches paged, sorted, searched rows from an OData v4 endpoint. Bind the result
63
63
  server-mode table:
64
64
 
65
65
  ```tsx
66
- import { useCallback } from 'react';
67
66
  import { FlexTableReact, useODataSource } from '@iyulab/flex-table/react';
68
67
 
69
68
  function Orders() {
70
- const fetcher = useCallback((input: string, init: RequestInit) => fetch(input, init), []);
71
- const source = useODataSource<Order>('/api/orders', { pageSize: 20, fetcher });
69
+ const source = useODataSource<Order>('/api/orders', { pageSize: 20 });
72
70
 
73
71
  return (
74
72
  <>
75
73
  <input value={source.search} onChange={(e) => source.setSearch(e.target.value)} />
76
- {source.error && <p role="alert">{source.error.message}</p>}
77
74
  <FlexTableReact<Order>
78
75
  dataMode="server"
79
76
  columns={columns}
80
77
  data={source.data}
81
78
  loading={source.loading}
79
+ error={source.error}
82
80
  onSortChange={source.onSortChange}
83
81
  clearSelectionOnDataChange
84
82
  />
@@ -101,8 +99,8 @@ function Orders() {
101
99
  | `initialSort` | — | `SortCriteria[]`; overrides `defaultOrderBy`; `[]` means no sort |
102
100
  | `fixedFilter` | — | odata-query filter object, always applied; value change resets to page 0 (compared by value) |
103
101
  | `baseUrl` | `window.location.origin` | For proxy/BFF setups |
104
- | `fetcher` | global `fetch` | `(input, init) => Promise<Response>`; keep stable |
105
- | `onUnauthorized` | — | `(response) => void` on 401 only (403 surfaces as `error`); keep stable |
102
+ | `fetcher` | global `fetch` | `(input, init) => Promise<Response>`; read when each request starts |
103
+ | `onUnauthorized` | — | `(response) => void` on 401 only (403 surfaces as `error`); read when each request starts |
106
104
  | `enabled` | `true` | While `false`: no request, `loading` stays `true`, `refresh()` is a no-op, in-flight request is cancelled |
107
105
 
108
106
  ### Result
@@ -111,7 +109,7 @@ function Orders() {
111
109
  |---|---|
112
110
  | `data`, `totalCount` | Current page and `@odata.count`; `@odata.nextLink` is followed to fill a page |
113
111
  | `loading` | Request in flight, or no answer yet (true from the first render until the first response settles, and while `enabled: false`) — so `!loading && totalCount === 0` means "no results" |
114
- | `error` | `SourceError \| null` — `{ message, status?, code?, details?, body?, cause? }`; render `error.message`, branch on `status` / `code` (OData `error.code`). `message` is the server's sentence when it sent one; otherwise this package's, in the `flexTableLocale` language — `requestFailed` (`Request failed ({status})`) when a response came back, `networkFailed` when none did (the transport's exception is in `cause`) |
112
+ | `error` | `SourceError \| null` — `{ message, status?, code?, details?, body?, cause? }`; pass it to the table (`error`) or render `error.message`; branch on `status` / `code` (OData `error.code`). `message` is the server's sentence when it sent one; otherwise this package's, in the `flexTableLocale` language — `requestFailed` (`Request failed ({status})`) when a response came back, `networkFailed` when none did (the transport's exception is in `cause`) |
115
113
  | `page`, `setPage` | Zero-based |
116
114
  | `sortCriteria`, `onSortChange` | Pass `onSortChange` to the table's `sort-change` (resets to page 0) |
117
115
  | `search`, `setSearch` | Literal text (resets to page 0) |
@@ -159,6 +157,27 @@ Differences: `totalCount` is the count after search, `loading` is always `false`
159
157
 
160
158
  ## Without React
161
159
 
160
+ `useODataSource` adapts a framework-neutral source with the same options and state:
161
+ `createODataSource(url, options)` → `getState()` · `subscribe(listener)` (the first subscriber starts loading, the last
162
+ unsubscribe cancels) · `setPage` · `setSort(criteria)` · `setSearch` · `refresh` · `update(url, options)`. Changes in one
163
+ tick become one request. For Lit, `ODataSourceController` subscribes on connect, cancels on disconnect and re-renders
164
+ the host:
165
+
166
+ ```ts
167
+ import { ODataSourceController } from '@iyulab/flex-table/odata';
168
+
169
+ class OrdersPage extends LitElement {
170
+ private orders = new ODataSourceController<Order>(this, '/api/orders', { pageSize: 20 });
171
+ render() {
172
+ const { data, loading, error } = this.orders.state;
173
+ return html`<flex-table data-mode="server" .data=${data} .loading=${loading} .error=${error}
174
+ @sort-change=${(e: CustomEvent) => this.orders.source.setSort(e.detail.criteria)}></flex-table>`;
175
+ }
176
+ }
177
+ ```
178
+
179
+ Pure helpers:
180
+
162
181
  ```ts
163
182
  import { buildODataQuery, buildSearchExpression, parseOrderBy } from '@iyulab/flex-table/odata';
164
183
  import { computeArrayView } from '@iyulab/flex-table/array';
@@ -1,37 +0,0 @@
1
- import e from "odata-query";
2
- //#region src/odata/query.ts
3
- function t(e) {
4
- let t = e.split(/\s+/).map((e) => e.replace(/"/g, "")).filter((e) => e.length > 0);
5
- if (t.length !== 0) return t.map((e) => `"${e}"`).join(" AND ");
6
- }
7
- function n(e) {
8
- let { defaultOrderBy: t, initialPage: n = 0, initialSearch: i = "", initialSort: a } = e;
9
- return {
10
- page: n,
11
- search: i,
12
- sortCriteria: a ?? (t ? r(t) : [])
13
- };
14
- }
15
- function r(e) {
16
- return e.split(",").map((e) => {
17
- let t = e.trim().split(/\s+/);
18
- return {
19
- key: t[0],
20
- direction: t[1]?.toLowerCase() === "desc" ? "desc" : "asc"
21
- };
22
- });
23
- }
24
- function i(n) {
25
- let { page: r, pageSize: i, sortCriteria: a = [], defaultOrderBy: o, search: s, fixedFilter: c } = n, l = a.length > 0 ? a.map((e) => `${e.key} ${e.direction}`).join(", ") : o, u = {
26
- top: i,
27
- skip: r * i,
28
- count: !0
29
- };
30
- if (l && (u.orderBy = l), c && (u.filter = c), s) {
31
- let e = t(s);
32
- e && (u.search = e);
33
- }
34
- return e(u);
35
- }
36
- //#endregion
37
- export { n as i, t as n, r, i as t };