@iyulab/flex-table 0.53.0 → 0.55.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 +17 -0
- package/README.md +16 -3
- package/dist/array/index.d.ts +2 -0
- package/dist/array/index.js +71 -2
- package/dist/array/source.d.ts +43 -0
- package/dist/{flex-table-DUAMhXCS.js → flex-table-DlvIBu4z.js} +9 -5
- package/dist/flex-table.d.ts +8 -0
- package/dist/flex-table.js +1 -1
- package/dist/odata/index.js +3 -2
- package/dist/query-Dw4iO0ir.js +37 -0
- package/dist/react.js +3 -2
- package/dist/{source-CwbD-6ea.js → source-r2Pv6Pvo.js} +43 -78
- package/package.json +1 -1
- package/skills/iyulab-flex-table/SKILL.md +2 -2
- package/skills/iyulab-flex-table/references/api.md +1 -0
- package/skills/iyulab-flex-table/references/react.md +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.55.0] - 2026-10-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`createArraySource(rows, options)`** and **`ArraySourceController`** (`@iyulab/flex-table/array`) — the in-memory
|
|
8
|
+
source without React, with the members of `createODataSource` (`getState`, `subscribe`, `setPage`, `setSort`,
|
|
9
|
+
`setSearch`, `refresh`) plus `update(rows, options)`. A custom-element screen can now switch between a server and
|
|
10
|
+
an in-memory source the way a React screen switches between `useODataSource` and `useArraySource`.
|
|
11
|
+
|
|
12
|
+
## [0.54.0] - 2026-10-07
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`error`** property — the last load failure (`{ message }`, so a data source's `SourceError` goes in as is). While
|
|
17
|
+
set, the grid shows `error.message` as an alert where the rows or the empty state would be; a failed query used to
|
|
18
|
+
look like "No data" unless the app drew its own message beside the grid.
|
|
19
|
+
|
|
3
20
|
## [0.53.0] - 2026-10-07
|
|
4
21
|
|
|
5
22
|
### Added
|
package/README.md
CHANGED
|
@@ -113,6 +113,7 @@ guarantee about a *constrained* host. `height-model.browser.test.ts` pins both s
|
|
|
113
113
|
| `footerData` | `footer-data` | `Record<string, string \| TemplateResult> \| null` | `null` | Footer/summary row data (keys match column keys) |
|
|
114
114
|
| `emptyMessage` | `empty-message` | `string` | `'No data'` | Shown when `data` is empty |
|
|
115
115
|
| `noMatchingMessage` | `no-matching-message` | `string` | `'No matching data'` | Shown when `data` has rows but every one is hidden by an active column filter |
|
|
116
|
+
| `error` | — | `{ message: string } \| null` | `null` | The last load failure. While set, `error.message` is shown as an alert where the rows or the empty state would be, so a failed query does not look like "no data". Pass a data source's `error` as is |
|
|
116
117
|
| `stylesheets` | — | `CSSStyleSheet[]` | `[]` | Constructable stylesheets adopted into the shadow root alongside the grid's own styles — the escape hatch for styling content a `render` function inserts, since document CSS doesn't cross the shadow boundary. Reassigning swaps the previous set, it doesn't accumulate |
|
|
117
118
|
|
|
118
119
|
### Read-only Properties
|
|
@@ -816,7 +817,7 @@ The hook returns:
|
|
|
816
817
|
|---|---|
|
|
817
818
|
| `data` / `totalCount` | Current page rows and the server's total (`@odata.count`). When the server pages its response (`@odata.nextLink`, e.g. a page size smaller than `pageSize`), the hook follows the link until the page is filled; a link outside the request's origin, or one that returns to a page already read, is reported through `error` instead of showing a short page |
|
|
818
819
|
| `loading` | A request is in flight, or none has answered yet (true from the first render until the first response settles, and while `enabled: false`) |
|
|
819
|
-
| `error` | The last failed request, or `null`: `{ message, status?, code?, details?, body? }`. **
|
|
820
|
+
| `error` | The last failed request, or `null`: `{ message, status?, code?, details?, body? }`. **Pass it to the table** (`error={source.error}`) or render `error.message` yourself — a failed request otherwise leaves the grid silently empty. Branch on `status` (HTTP), `code` (the server's rejection code, OData `error.code`) and `details` (OData `error.details`); `body` is the parsed response (or its text). A failure with no response — a network error, or a `@odata.nextLink` the hook refused — has a `message` only |
|
|
820
821
|
| `page` / `setPage` | Zero-based page index |
|
|
821
822
|
| `sortCriteria` / `onSortChange` | Bind `onSortChange` to the table's `sort-change` event |
|
|
822
823
|
| `search` / `setSearch` | Current search term and its setter (resets to page 0) |
|
|
@@ -888,8 +889,7 @@ class OrdersPage extends LitElement {
|
|
|
888
889
|
render() {
|
|
889
890
|
const { data, loading, error } = this.orders.state;
|
|
890
891
|
return html`
|
|
891
|
-
|
|
892
|
-
<flex-table data-mode="server" .data=${data} .loading=${loading}
|
|
892
|
+
<flex-table data-mode="server" .data=${data} .loading=${loading} .error=${error}
|
|
893
893
|
@sort-change=${(e: CustomEvent) => this.orders.source.setSort(e.detail.criteria)}></flex-table>`;
|
|
894
894
|
}
|
|
895
895
|
}
|
|
@@ -939,6 +939,19 @@ count after search (not a server-reported total), and `loading`/`error` are alwa
|
|
|
939
939
|
`false`/`null` — there's no request to fail. `refresh` is a no-op kept only so a
|
|
940
940
|
"refresh" button wired unconditionally against either hook doesn't need a branch.
|
|
941
941
|
|
|
942
|
+
Without React, `createArraySource(rows, options)` (`@iyulab/flex-table/array`) is the same source with the same
|
|
943
|
+
members as `createODataSource` — `getState()`, `subscribe()`, `setPage`, `setSort`, `setSearch`, `refresh` (no-op) —
|
|
944
|
+
plus `update(rows, options)` when the rows change. `ArraySourceController` binds it to a Lit element:
|
|
945
|
+
|
|
946
|
+
```ts
|
|
947
|
+
import { ArraySourceController } from '@iyulab/flex-table/array';
|
|
948
|
+
|
|
949
|
+
class PricesPanel extends LitElement {
|
|
950
|
+
private prices = new ArraySourceController(this, [], { pageSize: 20 });
|
|
951
|
+
set rows(rows: DataRow[]) { this.prices.source.update(rows, { pageSize: 20 }); }
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
942
955
|
## Development
|
|
943
956
|
|
|
944
957
|
```bash
|
package/dist/array/index.d.ts
CHANGED
package/dist/array/index.js
CHANGED
|
@@ -1,2 +1,71 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { i as e } from "../query-Dw4iO0ir.js";
|
|
2
|
+
import { t } from "../view-DiLr-TdY.js";
|
|
3
|
+
//#region src/array/source.ts
|
|
4
|
+
function n(n, r = {}) {
|
|
5
|
+
let i = n, a = r, o = e({
|
|
6
|
+
defaultOrderBy: r.defaultOrderBy,
|
|
7
|
+
initialPage: r.initialPage ?? 0,
|
|
8
|
+
initialSearch: r.initialSearch ?? "",
|
|
9
|
+
initialSort: r.initialSort
|
|
10
|
+
}), s = o.page, c = o.sortCriteria, l = o.search, u = f(), d = /* @__PURE__ */ new Set();
|
|
11
|
+
function f() {
|
|
12
|
+
let e = a.pageSize ?? 20, n = t(i, {
|
|
13
|
+
search: l,
|
|
14
|
+
sortCriteria: c,
|
|
15
|
+
page: s,
|
|
16
|
+
pageSize: e,
|
|
17
|
+
columns: a.columns,
|
|
18
|
+
searchFields: a.searchFields
|
|
19
|
+
}), r = n.totalCount === 0 ? 0 : Math.ceil(n.totalCount / e) - 1;
|
|
20
|
+
return s > r && (s = r), {
|
|
21
|
+
data: n.data,
|
|
22
|
+
totalCount: n.totalCount,
|
|
23
|
+
loading: !1,
|
|
24
|
+
error: null,
|
|
25
|
+
page: s,
|
|
26
|
+
sortCriteria: c,
|
|
27
|
+
search: l
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function p() {
|
|
31
|
+
u = f();
|
|
32
|
+
for (let e of [...d]) e();
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
getState: () => u,
|
|
36
|
+
subscribe(e) {
|
|
37
|
+
return d.add(e), () => {
|
|
38
|
+
d.delete(e);
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
setPage(e) {
|
|
42
|
+
s = e, p();
|
|
43
|
+
},
|
|
44
|
+
setSort(e) {
|
|
45
|
+
c = e, s = 0, p();
|
|
46
|
+
},
|
|
47
|
+
setSearch(e) {
|
|
48
|
+
l = e, s = 0, p();
|
|
49
|
+
},
|
|
50
|
+
refresh() {},
|
|
51
|
+
update(e, t = {}) {
|
|
52
|
+
i = e, a = t, p();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
var r = class {
|
|
57
|
+
constructor(e, t, r = {}) {
|
|
58
|
+
this.host = e, this.source = n(t, r), e.addController(this);
|
|
59
|
+
}
|
|
60
|
+
get state() {
|
|
61
|
+
return this.source.getState();
|
|
62
|
+
}
|
|
63
|
+
hostConnected() {
|
|
64
|
+
this.unsubscribe ??= this.source.subscribe(() => this.host.requestUpdate());
|
|
65
|
+
}
|
|
66
|
+
hostDisconnected() {
|
|
67
|
+
this.unsubscribe?.(), this.unsubscribe = void 0;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
//#endregion
|
|
71
|
+
export { r as ArraySourceController, t as computeArrayView, n as createArraySource };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
import type { SortCriteria } from '../core/sorting.js';
|
|
3
|
+
import type { DataRow } from '../models/types.js';
|
|
4
|
+
import type { ODataSourceState } from '../odata/source.js';
|
|
5
|
+
import type { UseArraySourceOptions } from './types.js';
|
|
6
|
+
/** 메모리 소스의 설정 — `useArraySource` 의 옵션과 같다(`initial*` 은 만들 때만 읽는다). */
|
|
7
|
+
export type ArraySourceOptions<T> = UseArraySourceOptions<T>;
|
|
8
|
+
/** 메모리 소스 — `createODataSource` 와 같은 상태·조작 모양이라 같은 표 배선이 두 소스에서 돈다. */
|
|
9
|
+
export interface ArraySource<T> {
|
|
10
|
+
/** 현재 상태(`loading` 은 늘 `false`, `error` 는 늘 `null`). 바뀌지 않았으면 같은 객체다. */
|
|
11
|
+
getState(): ODataSourceState<T>;
|
|
12
|
+
/** 상태가 바뀔 때마다 부른다. 해지 함수를 돌려준다. */
|
|
13
|
+
subscribe(listener: () => void): () => void;
|
|
14
|
+
/** 0-based 페이지로 옮긴다(범위를 넘으면 마지막 페이지). */
|
|
15
|
+
setPage(page: number): void;
|
|
16
|
+
/** 정렬을 바꾸고 첫 장으로 간다. */
|
|
17
|
+
setSort(criteria: SortCriteria[]): void;
|
|
18
|
+
/** 검색어를 바꾸고 첫 장으로 간다. */
|
|
19
|
+
setSearch(term: string): void;
|
|
20
|
+
/** 아무 일도 하지 않는다 — 다시 불러올 원격 상태가 없다(OData 소스와 같은 자리에 이어도 안전하다). */
|
|
21
|
+
refresh(): void;
|
|
22
|
+
/** 행이나 설정이 바뀌었다 — 다시 계산한다. 결과가 줄어 지금 페이지가 없어지면 마지막 페이지로 내려온다. */
|
|
23
|
+
update(data: T[], options?: ArraySourceOptions<T>): void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 이미 메모리에 있는 행의 데이터 소스 — 검색·정렬·페이지 나누기를 로컬에서 한다(`computeArrayView`). React 없이 쓴다;
|
|
27
|
+
* React 는 `useArraySource`, Lit 은 `ArraySourceController`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createArraySource<T extends DataRow = DataRow>(data: T[], options?: ArraySourceOptions<T>): ArraySource<T>;
|
|
30
|
+
/**
|
|
31
|
+
* Lit 어댑터 — 메모리 소스를 호스트에 묶는다. 상태가 바뀌면 호스트를 다시 그린다. 행이 바뀌면 `source.update(rows)`.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ArraySourceController<T extends DataRow = DataRow> implements ReactiveController {
|
|
34
|
+
private readonly host;
|
|
35
|
+
/** 소스 — 조작(`setPage`·`setSort`·`setSearch`·`update`)은 이것으로 한다. */
|
|
36
|
+
readonly source: ArraySource<T>;
|
|
37
|
+
private unsubscribe?;
|
|
38
|
+
constructor(host: ReactiveControllerHost, data: T[], options?: ArraySourceOptions<T>);
|
|
39
|
+
/** 현재 상태. */
|
|
40
|
+
get state(): ODataSourceState<T>;
|
|
41
|
+
hostConnected(): void;
|
|
42
|
+
hostDisconnected(): void;
|
|
43
|
+
}
|
|
@@ -392,6 +392,10 @@ var S = i`
|
|
|
392
392
|
color: var(--ft-empty-color);
|
|
393
393
|
}
|
|
394
394
|
|
|
395
|
+
.ft-error {
|
|
396
|
+
color: var(--ft-invalid-color);
|
|
397
|
+
}
|
|
398
|
+
|
|
395
399
|
.ft-has-comment {
|
|
396
400
|
position: relative;
|
|
397
401
|
}
|
|
@@ -1752,7 +1756,7 @@ var K = {
|
|
|
1752
1756
|
lte: "≤"
|
|
1753
1757
|
}, q = 120, J = 40, Y = 20, X = 32, Z = 5, Xe = 200, Q = class extends r {
|
|
1754
1758
|
constructor(...e) {
|
|
1755
|
-
super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = X, this._targetFloor = 0, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "No data", this.noMatchingMessage = "No matching data", this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._headerCol = null, this._focusedCellEl = null, this._editingCell = null, this._sortCriteria = [], this._selection = new oe(), this._editing = new se(), this._rowSelection = new k(), this._undo = new A(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowIndex = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._warnedUnconstrained = !1, this._onHostFocus = (e) => {
|
|
1759
|
+
super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = X, this._targetFloor = 0, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "No data", this.noMatchingMessage = "No matching data", this.error = null, this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._headerCol = null, this._focusedCellEl = null, this._editingCell = null, this._sortCriteria = [], this._selection = new oe(), this._editing = new se(), this._rowSelection = new k(), this._undo = new A(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowIndex = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._warnedUnconstrained = !1, this._onHostFocus = (e) => {
|
|
1756
1760
|
e.composedPath()[0] !== this || this._activeCell || this._headerCol !== null || this.visibleColumns.length !== 0 && this.matches(":focus-visible") && (this._visibleRowCount > 0 ? (this._selection.setActive(0, 0), this._syncActiveCell()) : this._headerCol = 0);
|
|
1757
1761
|
}, this._onFocusIn = (e) => {
|
|
1758
1762
|
let t = e.composedPath()[0];
|
|
@@ -4153,8 +4157,8 @@ var K = {
|
|
|
4153
4157
|
for (let e of u) h.push(this._renderHeaderCell(t[e], e));
|
|
4154
4158
|
for (let e = c; e < l; e++) h.push(this._renderHeaderCell(t[e], e));
|
|
4155
4159
|
let g = this._colDragIndicatorLeft == null ? "" : a`<div class="ft-drop-indicator" style="left:${this._colDragIndicatorLeft}px"></div>`;
|
|
4156
|
-
if (this.data.length === 0 || this._visibleRowCount === 0) {
|
|
4157
|
-
let e = this.data.length === 0 ? this.emptyMessage : this.noMatchingMessage
|
|
4160
|
+
if (this.error || this.data.length === 0 || this._visibleRowCount === 0) {
|
|
4161
|
+
let e = this.error ? a`<div class="ft-empty ft-error" role="alert">${this.error.message}</div>` : a`<div class="ft-empty">${this.data.length === 0 ? this.emptyMessage : this.noMatchingMessage}</div>`;
|
|
4158
4162
|
return a`
|
|
4159
4163
|
${r}
|
|
4160
4164
|
<div class="ft-header" role="row" aria-rowindex="1" style="width: ${s}px; height: ${i}px;">
|
|
@@ -4162,7 +4166,7 @@ var K = {
|
|
|
4162
4166
|
${h}
|
|
4163
4167
|
${g}
|
|
4164
4168
|
</div>
|
|
4165
|
-
|
|
4169
|
+
${e}
|
|
4166
4170
|
`;
|
|
4167
4171
|
}
|
|
4168
4172
|
let { start: _, end: v } = this.visibleRange, y = this._frozenRowCount, b = [];
|
|
@@ -4449,7 +4453,7 @@ G([c({ type: Array })], Q.prototype, "columns", void 0), G([c({
|
|
|
4449
4453
|
})], Q.prototype, "emptyMessage", void 0), G([c({
|
|
4450
4454
|
type: String,
|
|
4451
4455
|
attribute: "no-matching-message"
|
|
4452
|
-
})], Q.prototype, "noMatchingMessage", void 0), G([c({
|
|
4456
|
+
})], Q.prototype, "noMatchingMessage", void 0), G([c({ attribute: !1 })], Q.prototype, "error", void 0), G([c({
|
|
4453
4457
|
type: Boolean,
|
|
4454
4458
|
reflect: !0
|
|
4455
4459
|
})], Q.prototype, "loading", void 0), G([c({
|
package/dist/flex-table.d.ts
CHANGED
|
@@ -72,6 +72,14 @@ export declare class FlexTable extends LitElement {
|
|
|
72
72
|
* column filters (0 visible rows, non-empty `data`). Default: 'No matching data'.
|
|
73
73
|
*/
|
|
74
74
|
noMatchingMessage: string;
|
|
75
|
+
/**
|
|
76
|
+
* The last load failure, or `null`. While set, the grid shows `error.message` in an alert where rows or the empty
|
|
77
|
+
* state would be — a failed query otherwise looks like "no data". Takes a data source's `error` as is
|
|
78
|
+
* (`useODataSource`/`createODataSource`, a `SourceError`): any `{ message }`.
|
|
79
|
+
*/
|
|
80
|
+
error: {
|
|
81
|
+
message: string;
|
|
82
|
+
} | null;
|
|
75
83
|
/**
|
|
76
84
|
* True일 때 그리드 위에 로딩 오버레이를 표시하고 host에 `aria-busy="true"`를 반영한다.
|
|
77
85
|
* `useODataSource()`가 반환하는 `loading`과 자연 연동하도록 설계됨:
|
package/dist/flex-table.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as e, i as t, n, o as r, r as i, t as a } from "./flex-table-
|
|
1
|
+
import { a as e, i as t, n, o as r, r as i, t as a } from "./flex-table-DlvIBu4z.js";
|
|
2
2
|
import { t as o } from "./locale-B1SiVDS1.js";
|
|
3
3
|
export { a as FlexTable, e as RowSelectionState, t as UndoStack, n as effectiveAlign, i as exportData, o as flexTableLocale, r as renderCell };
|
package/dist/odata/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as e, r as t, t as n } from "../query-Dw4iO0ir.js";
|
|
2
|
+
import { t as r } from "../source-r2Pv6Pvo.js";
|
|
2
3
|
//#region src/odata/controller.ts
|
|
3
4
|
var i = class {
|
|
4
5
|
constructor(e, t, n = {}) {
|
|
@@ -15,4 +16,4 @@ var i = class {
|
|
|
15
16
|
}
|
|
16
17
|
};
|
|
17
18
|
//#endregion
|
|
18
|
-
export { i as ODataSourceController,
|
|
19
|
+
export { i as ODataSourceController, n as buildODataQuery, e as buildSearchExpression, r as createODataSource, t as parseOrderBy };
|
|
@@ -0,0 +1,37 @@
|
|
|
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 };
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { t as e } from "./flex-table-
|
|
2
|
-
import {
|
|
1
|
+
import { t as e } from "./flex-table-DlvIBu4z.js";
|
|
2
|
+
import { i as t } from "./query-Dw4iO0ir.js";
|
|
3
|
+
import { t as n } from "./source-r2Pv6Pvo.js";
|
|
3
4
|
import { t as r } from "./view-DiLr-TdY.js";
|
|
4
5
|
import i, { useCallback as a, useEffect as o, useMemo as s, useState as c, useSyncExternalStore as l } from "react";
|
|
5
6
|
import { createComponent as u } from "@lit/react";
|
|
@@ -1,58 +1,23 @@
|
|
|
1
1
|
import { n as e } from "./locale-B1SiVDS1.js";
|
|
2
|
-
import t from "
|
|
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
|
|
2
|
+
import { i as t, t as n } from "./query-Dw4iO0ir.js";
|
|
38
3
|
//#region src/core/source-error.ts
|
|
39
|
-
async function
|
|
4
|
+
async function r(t) {
|
|
40
5
|
let n = await t.text().catch(() => ""), r = {
|
|
41
6
|
message: e("requestFailed", { status: t.status }),
|
|
42
7
|
status: t.status
|
|
43
8
|
};
|
|
44
9
|
if (!n) return r;
|
|
45
|
-
let
|
|
10
|
+
let a = n;
|
|
46
11
|
try {
|
|
47
|
-
|
|
12
|
+
a = JSON.parse(n);
|
|
48
13
|
} catch {}
|
|
49
|
-
if (r.body =
|
|
50
|
-
let
|
|
51
|
-
typeof
|
|
52
|
-
let c = s
|
|
14
|
+
if (r.body = a, !a || typeof a != "object") return r;
|
|
15
|
+
let o = a, s = o.error && typeof o.error == "object" ? o.error : o;
|
|
16
|
+
typeof s.message == "string" && s.message ? r.message = s.message : typeof o.message == "string" && o.message && (r.message = o.message), typeof s.code == "string" && s.code && (r.code = s.code);
|
|
17
|
+
let c = i(s.details);
|
|
53
18
|
return c && (r.details = c), r;
|
|
54
19
|
}
|
|
55
|
-
function
|
|
20
|
+
function i(e) {
|
|
56
21
|
if (!Array.isArray(e)) return;
|
|
57
22
|
let t = e.filter((e) => {
|
|
58
23
|
if (!e || typeof e != "object") return !1;
|
|
@@ -61,42 +26,42 @@ function s(e) {
|
|
|
61
26
|
});
|
|
62
27
|
return t.length > 0 ? t : void 0;
|
|
63
28
|
}
|
|
64
|
-
var
|
|
29
|
+
var a = class extends Error {
|
|
65
30
|
constructor(e) {
|
|
66
31
|
super(e.message), this.name = "SourceRequestError", this.failure = e;
|
|
67
32
|
}
|
|
68
33
|
};
|
|
69
|
-
function
|
|
70
|
-
return new
|
|
34
|
+
function o(t) {
|
|
35
|
+
return new a({
|
|
71
36
|
message: e("networkFailed"),
|
|
72
37
|
cause: t
|
|
73
38
|
});
|
|
74
39
|
}
|
|
75
|
-
function
|
|
76
|
-
return e instanceof
|
|
40
|
+
function s(e) {
|
|
41
|
+
return e instanceof a ? e.failure : e instanceof Error ? { message: e.message } : { message: String(e) };
|
|
77
42
|
}
|
|
78
43
|
//#endregion
|
|
79
44
|
//#region src/odata/source.ts
|
|
80
|
-
function
|
|
81
|
-
let
|
|
82
|
-
defaultOrderBy:
|
|
83
|
-
initialPage:
|
|
84
|
-
initialSearch:
|
|
85
|
-
initialSort:
|
|
45
|
+
function c(e, i = {}) {
|
|
46
|
+
let c = e, u = i, d = l(i.fixedFilter), f = t({
|
|
47
|
+
defaultOrderBy: i.defaultOrderBy,
|
|
48
|
+
initialPage: i.initialPage ?? 0,
|
|
49
|
+
initialSearch: i.initialSearch ?? "",
|
|
50
|
+
initialSort: i.initialSort
|
|
86
51
|
}), p = {
|
|
87
52
|
data: [],
|
|
88
53
|
totalCount: 0,
|
|
89
54
|
loading: !0,
|
|
90
55
|
error: null,
|
|
91
|
-
page:
|
|
92
|
-
sortCriteria:
|
|
93
|
-
search:
|
|
56
|
+
page: f.page,
|
|
57
|
+
sortCriteria: f.sortCriteria,
|
|
58
|
+
search: f.search
|
|
94
59
|
}, m = x(), h = /* @__PURE__ */ new Set(), g = null, _ = !1, v = null;
|
|
95
60
|
function y() {
|
|
96
|
-
return
|
|
61
|
+
return u.enabled ?? !0;
|
|
97
62
|
}
|
|
98
63
|
function b() {
|
|
99
|
-
return
|
|
64
|
+
return u.pageSize ?? 20;
|
|
100
65
|
}
|
|
101
66
|
function x() {
|
|
102
67
|
return {
|
|
@@ -119,11 +84,11 @@ function d(e, t = {}) {
|
|
|
119
84
|
}
|
|
120
85
|
function w() {
|
|
121
86
|
return JSON.stringify([
|
|
122
|
-
|
|
123
|
-
|
|
87
|
+
c,
|
|
88
|
+
u.baseUrl,
|
|
124
89
|
b(),
|
|
125
|
-
|
|
126
|
-
|
|
90
|
+
u.defaultOrderBy,
|
|
91
|
+
d,
|
|
127
92
|
p.page,
|
|
128
93
|
p.sortCriteria,
|
|
129
94
|
p.search
|
|
@@ -147,26 +112,26 @@ function d(e, t = {}) {
|
|
|
147
112
|
g?.abort();
|
|
148
113
|
let e = new AbortController();
|
|
149
114
|
g = e;
|
|
150
|
-
let { page: t, sortCriteria:
|
|
115
|
+
let { page: t, sortCriteria: i, search: l } = p, d = b(), f = u.fetcher ?? fetch, m = u.onUnauthorized;
|
|
151
116
|
S({
|
|
152
117
|
loading: !0,
|
|
153
118
|
error: null
|
|
154
119
|
});
|
|
155
|
-
let h =
|
|
120
|
+
let h = n({
|
|
156
121
|
page: t,
|
|
157
122
|
pageSize: d,
|
|
158
|
-
sortCriteria:
|
|
159
|
-
defaultOrderBy:
|
|
160
|
-
search:
|
|
161
|
-
fixedFilter:
|
|
162
|
-
}), _ = `${
|
|
123
|
+
sortCriteria: i,
|
|
124
|
+
defaultOrderBy: u.defaultOrderBy,
|
|
125
|
+
search: l,
|
|
126
|
+
fixedFilter: u.fixedFilter
|
|
127
|
+
}), _ = `${u.baseUrl ?? window.location.origin}${c}${h}`, v = async (t) => {
|
|
163
128
|
let n;
|
|
164
129
|
try {
|
|
165
130
|
n = await f(t, { signal: e.signal });
|
|
166
131
|
} catch (e) {
|
|
167
|
-
throw e?.name === "AbortError" ? e :
|
|
132
|
+
throw e?.name === "AbortError" ? e : o(e);
|
|
168
133
|
}
|
|
169
|
-
if (!n.ok) throw n.status === 401 && m && m(n), new
|
|
134
|
+
if (!n.ok) throw n.status === 401 && m && m(n), new a(await r(n));
|
|
170
135
|
return n.json();
|
|
171
136
|
};
|
|
172
137
|
(async () => {
|
|
@@ -198,7 +163,7 @@ function d(e, t = {}) {
|
|
|
198
163
|
});
|
|
199
164
|
}).catch((t) => {
|
|
200
165
|
e.signal.aborted || t?.name !== "AbortError" && S({
|
|
201
|
-
error:
|
|
166
|
+
error: s(t),
|
|
202
167
|
data: [],
|
|
203
168
|
totalCount: 0
|
|
204
169
|
});
|
|
@@ -232,16 +197,16 @@ function d(e, t = {}) {
|
|
|
232
197
|
y() && (v = null, T());
|
|
233
198
|
},
|
|
234
199
|
update(e, t = {}) {
|
|
235
|
-
let
|
|
236
|
-
|
|
200
|
+
let n = l(t.fixedFilter), r = n !== d;
|
|
201
|
+
c = e, u = t, d = n, r && (p = {
|
|
237
202
|
...p,
|
|
238
203
|
page: 0
|
|
239
204
|
}), C(), T();
|
|
240
205
|
}
|
|
241
206
|
};
|
|
242
207
|
}
|
|
243
|
-
function
|
|
208
|
+
function l(e) {
|
|
244
209
|
return e ? JSON.stringify(e) : "";
|
|
245
210
|
}
|
|
246
211
|
//#endregion
|
|
247
|
-
export {
|
|
212
|
+
export { c as t };
|
package/package.json
CHANGED
|
@@ -28,7 +28,7 @@ npm install @lit/react react
|
|
|
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
30
|
| `@iyulab/flex-table/odata` | No React: `createODataSource` (the source `useODataSource` adapts), `ODataSourceController` (Lit), `buildODataQuery`, `buildSearchExpression`, `parseOrderBy` |
|
|
31
|
-
| `@iyulab/flex-table/array` |
|
|
31
|
+
| `@iyulab/flex-table/array` | No React: `createArraySource` (same members as `createODataSource`, plus `update(rows)`), `ArraySourceController` (Lit), `computeArrayView` |
|
|
32
32
|
|
|
33
33
|
## Quick start
|
|
34
34
|
|
|
@@ -126,7 +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
|
-
- Always
|
|
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.
|
|
130
130
|
|
|
131
131
|
## References
|
|
132
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 |
|
|
@@ -71,12 +71,12 @@ function Orders() {
|
|
|
71
71
|
return (
|
|
72
72
|
<>
|
|
73
73
|
<input value={source.search} onChange={(e) => source.setSearch(e.target.value)} />
|
|
74
|
-
{source.error && <p role="alert">{source.error.message}</p>}
|
|
75
74
|
<FlexTableReact<Order>
|
|
76
75
|
dataMode="server"
|
|
77
76
|
columns={columns}
|
|
78
77
|
data={source.data}
|
|
79
78
|
loading={source.loading}
|
|
79
|
+
error={source.error}
|
|
80
80
|
onSortChange={source.onSortChange}
|
|
81
81
|
clearSelectionOnDataChange
|
|
82
82
|
/>
|
|
@@ -109,7 +109,7 @@ function Orders() {
|
|
|
109
109
|
|---|---|
|
|
110
110
|
| `data`, `totalCount` | Current page and `@odata.count`; `@odata.nextLink` is followed to fill a page |
|
|
111
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" |
|
|
112
|
-
| `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`) |
|
|
113
113
|
| `page`, `setPage` | Zero-based |
|
|
114
114
|
| `sortCriteria`, `onSortChange` | Pass `onSortChange` to the table's `sort-change` (resets to page 0) |
|
|
115
115
|
| `search`, `setSearch` | Literal text (resets to page 0) |
|
|
@@ -170,8 +170,7 @@ class OrdersPage extends LitElement {
|
|
|
170
170
|
private orders = new ODataSourceController<Order>(this, '/api/orders', { pageSize: 20 });
|
|
171
171
|
render() {
|
|
172
172
|
const { data, loading, error } = this.orders.state;
|
|
173
|
-
return html
|
|
174
|
-
<flex-table data-mode="server" .data=${data} .loading=${loading}
|
|
173
|
+
return html`<flex-table data-mode="server" .data=${data} .loading=${loading} .error=${error}
|
|
175
174
|
@sort-change=${(e: CustomEvent) => this.orders.source.setSort(e.detail.criteria)}></flex-table>`;
|
|
176
175
|
}
|
|
177
176
|
}
|
|
@@ -182,6 +181,7 @@ Pure helpers:
|
|
|
182
181
|
```ts
|
|
183
182
|
import { buildODataQuery, buildSearchExpression, parseOrderBy } from '@iyulab/flex-table/odata';
|
|
184
183
|
import { computeArrayView } from '@iyulab/flex-table/array';
|
|
184
|
+
// createArraySource / ArraySourceController: the in-memory source with createODataSource's members + update(rows)
|
|
185
185
|
|
|
186
186
|
buildSearchExpression('red shirt'); // '"red" AND "shirt"' ('' → undefined)
|
|
187
187
|
parseOrderBy('name desc'); // [{ key: 'name', direction: 'desc' }]
|