@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.
- package/CHANGELOG.md +26 -0
- package/README.md +46 -3
- package/dist/{flex-table-CtlqiwD8.js → flex-table-DlvIBu4z.js} +487 -627
- package/dist/flex-table.d.ts +8 -0
- package/dist/flex-table.js +3 -2
- package/dist/locale-B1SiVDS1.js +147 -0
- package/dist/odata/controller.d.ts +29 -0
- package/dist/odata/index.d.ts +3 -0
- package/dist/odata/index.js +18 -2
- package/dist/odata/source.d.ts +79 -0
- package/dist/odata/types.d.ts +7 -57
- package/dist/odata/use-odata-source.d.ts +5 -1
- package/dist/react.js +34 -145
- package/dist/source-CwbD-6ea.js +247 -0
- package/package.json +1 -1
- package/skills/iyulab-flex-table/SKILL.md +2 -3
- package/skills/iyulab-flex-table/references/api.md +1 -0
- package/skills/iyulab-flex-table/references/react.md +26 -7
- package/dist/query-Dw4iO0ir.js +0 -37
|
@@ -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
|
@@ -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` |
|
|
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
|
-
-
|
|
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
|
|
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>`;
|
|
105
|
-
| `onUnauthorized` | — | `(response) => void` on 401 only (403 surfaces as `error`);
|
|
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
|
|
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';
|
package/dist/query-Dw4iO0ir.js
DELETED
|
@@ -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 };
|