@iyulab/flex-table 0.51.0 → 0.53.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.51.0",
3
+ "version": "0.53.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -74,7 +74,7 @@
74
74
  "odata-query": "^8.1.0"
75
75
  },
76
76
  "peerDependencies": {
77
- "@iyulab/components": ">=2.2.0",
77
+ "@iyulab/components": ">=2.8.0",
78
78
  "@lit/react": "^1.0.8",
79
79
  "react": "^18.0.0 || ^19.0.0"
80
80
  },
@@ -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,7 +126,6 @@ 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
129
  - 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
130
 
132
131
  ## References
@@ -63,12 +63,10 @@ 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
  <>
@@ -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
@@ -159,6 +157,28 @@ 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`${error ? html`<p role="alert">${error.message}</p>` : ''}
174
+ <flex-table data-mode="server" .data=${data} .loading=${loading}
175
+ @sort-change=${(e: CustomEvent) => this.orders.source.setSort(e.detail.criteria)}></flex-table>`;
176
+ }
177
+ }
178
+ ```
179
+
180
+ Pure helpers:
181
+
162
182
  ```ts
163
183
  import { buildODataQuery, buildSearchExpression, parseOrderBy } from '@iyulab/flex-table/odata';
164
184
  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 };