@iyulab/flex-table 0.59.0 → 0.61.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 CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.61.0] - 2026-10-07
4
+
5
+ ### Added
6
+
7
+ - **`sortCriteria` is settable.** It was a getter only, so the sort a data source holds — the initial sort, or one
8
+ restored from a saved view — could not be shown on a server-paged table (`u-rich-table` takes it as a property).
9
+ Setting it shows that sort on the headers and re-sorts in client mode; it does not fire `sort-change`, which stays
10
+ the user's act. A binding that writes a source's state to the table (`bindSource` of `@iyulab/enterprise`) needs it.
11
+
12
+ ## [0.60.0] - 2026-10-07
13
+
14
+ ### Added
15
+
16
+ - **`fetchAll(options?)` on both data sources and both hooks** — the whole result of the current sort, search and
17
+ fixed filter, for exporting what a list shows. A server-paged table holds one page, so its export was one page: a
18
+ 1,240-row register came out as 20 rows with nothing saying so. The OData source asks without `$top`/`$skip` and
19
+ follows `@odata.nextLink` to the end (the same origin and repeat checks as a page read). `maxRows` (default
20
+ `100000`, `DEFAULT_MAX_ROWS`) rejects with `RowLimitError` instead of cutting — on the first response when the server
21
+ counts (`@odata.count`). `signal` and `onProgress(loaded, total)` too. It leaves the source state alone and needs no
22
+ subscriber. The array source returns the searched and sorted rows under the same contract.
23
+ - **The table exports rows it is given**: `exportToString` / `exportToBlob` / `exportToFile` take `{ rows }` — those
24
+ rows with the table's visible columns and formats (`exportToFile` gained an `options` argument). With the above:
25
+ `table.exportToFile('xlsx', 'x.xlsx', { rows: await source.fetchAll() })`.
26
+ - `exportDataBlob` (compressed XLSX) and `downloadBlob` are exported from the root entry, for an export without a table.
27
+ - `SourceRequestError` (its `failure` is the `SourceError`) and `RowLimitError` are exported from `./odata` and
28
+ `./react` (`RowLimitError` also from `./array`), and `SourceError` / `SourceErrorDetail` types from `./odata`.
29
+ - `buildODataQuery` builds the whole-result query when `pageSize` is left out (`page` and `pageSize` are optional).
30
+
3
31
  ## [0.59.0] - 2026-10-07
4
32
 
5
33
  ### Changed (breaking)
package/README.md CHANGED
@@ -127,7 +127,7 @@ guarantee about a *constrained* host. `height-model.browser.test.ts` pins both s
127
127
  | `canRedo` | `boolean` | Whether redo is available |
128
128
  | `activeCell` | `CellPosition \| null` | Currently focused cell `{ row, col }` |
129
129
  | `editingCell` | `CellPosition \| null` | Currently editing cell `{ row, col }` |
130
- | `sortCriteria` | `SortCriteria[]` | Active sort criteria `[{ key, direction }]` |
130
+ | `sortCriteria` | `SortCriteria[]` | Active sort criteria `[{ key, direction }]`. Also settable: shows that sort on the headers and re-sorts in client mode, without firing `sort-change` — give it a data source's `sortCriteria` (the initial or restored sort of a server-paged list) |
131
131
  | `filterKeys` | `string[]` | Column keys with active filters |
132
132
 
133
133
  ## Column Definition
@@ -288,9 +288,29 @@ Default is `false`, matching `clear-undo-on-data-change`.
288
288
 
289
289
  | Method | Returns | Description |
290
290
  |--------|---------|-------------|
291
- | `exportToString(format, options?)` | `string \| Uint8Array` | Export to `'csv'` / `'tsv'` / `'json'` (a string) or `'xlsx'` (bytes, uncompressed). Pass `{ selectionOnly: true }` for selection range |
291
+ | `exportToString(format, options?)` | `string \| Uint8Array` | Export to `'csv'` / `'tsv'` / `'json'` (a string) or `'xlsx'` (bytes, uncompressed). Pass `{ selectionOnly: true }` for selection range, or `{ rows }` to export rows the table does not hold (see below) |
292
292
  | `exportToBlob(format, options?)` | `Promise<Blob>` | The same export as a `Blob` of the format's MIME type — `'xlsx'` is DEFLATE-compressed |
293
- | `exportToFile(format, filename?)` | `Promise<void>` | Export and trigger browser file download (`'xlsx'` compressed) |
293
+ | `exportToFile(format, filename?, options?)` | `Promise<void>` | Export and trigger browser file download (`'xlsx'` compressed) |
294
+
295
+ The table exports the rows it holds — the filtered rows in sort order, or the selection. **A server-paged table
296
+ (`data-mode="server"`) holds one page**, so its export is one page. To export what the list shows — the whole result
297
+ of the current sort, search and filter — read it from the source and hand it to the table, which keeps its own
298
+ columns and formats:
299
+
300
+ ```ts
301
+ import { RowLimitError } from '@iyulab/flex-table/odata';
302
+
303
+ try {
304
+ const rows = await orders.fetchAll({ maxRows: 50_000 }); // follows @odata.nextLink to the end
305
+ await table.exportToFile('xlsx', 'orders.xlsx', { rows });
306
+ } catch (e) {
307
+ if (e instanceof RowLimitError) showMessage(e.failure.message); // more rows than maxRows — nothing is cut silently
308
+ else throw e;
309
+ }
310
+ ```
311
+
312
+ Without a table, `exportDataBlob(rows, columns, format)` and `downloadBlob(blob, filename)` (root entry) do the same
313
+ — `exportData` stays synchronous and writes XLSX uncompressed.
294
314
 
295
315
  ## Events
296
316
 
@@ -834,6 +854,7 @@ The hook returns:
834
854
  | `sortCriteria` / `onSortChange` | Bind `onSortChange` to the table's `sort-change` event |
835
855
  | `search` / `setSearch` | Current search term and its setter (resets to page 0) |
836
856
  | `refresh` | Re-run the current request |
857
+ | `fetchAll` | The whole result of the current conditions — the source's `fetchAll` (below), for exporting what the list shows |
837
858
 
838
859
  #### Search semantics
839
860
 
@@ -888,6 +909,7 @@ off(); // the last unsu
888
909
  | `setSort(criteria)` / `setSearch(term)` | Change the sort or search and go back to page 0 |
889
910
  | `refresh()` | Re-read with the same conditions (nothing while `enabled: false`) |
890
911
  | `update(url, options)` | New options. Only a changed request re-reads; a changed `fixedFilter` value goes back to page 0; a changed `pageSize` value takes effect (page 0), the same value again keeps the size `setPageSize` set; `initial*` are read at creation only |
912
+ | `fetchAll(options?)` | `Promise<T[]>` — **the whole result of the current sort, search and fixed filter**: no `$top`/`$skip`, following `@odata.nextLink` to the end. For an export of what the list shows. Leaves the state alone and needs no subscriber. Options: `maxRows` (default `100000` — past it, it rejects with `RowLimitError` instead of cutting; when the server counts, on the first response), `signal`, `onProgress(loaded, total)`. A failed request rejects with `SourceRequestError` (`failure` is the `SourceError`) |
891
913
 
892
914
  Changes made in the same tick become one request with the final conditions, so `setSearch` followed by `setPage` does not send the intermediate one.
893
915
 
@@ -954,8 +976,8 @@ count after search (not a server-reported total), and `loading`/`error` are alwa
954
976
  "refresh" button wired unconditionally against either hook doesn't need a branch.
955
977
 
956
978
  Without React, `createArraySource(rows, options)` (`@iyulab/flex-table/array`) is the same source with the same
957
- members as `createODataSource` — `getState()`, `subscribe()`, `setPage`, `setPageSize`, `setSort`, `setSearch`, `refresh` (no-op) —
958
- plus `update(rows, options)` when the rows change. `ArraySourceController` binds it to a Lit element (from rows, or an existing source):
979
+ members as `createODataSource` — `getState()`, `subscribe()`, `setPage`, `setPageSize`, `setSort`, `setSearch`, `refresh` (no-op),
980
+ `fetchAll` (the whole searched and sorted result, same options and `RowLimitError`) — plus `update(rows, options)` when the rows change. `ArraySourceController` binds it to a Lit element (from rows, or an existing source):
959
981
 
960
982
  ```ts
961
983
  import { LitElement } from 'lit';
@@ -2,3 +2,6 @@ export { computeArrayView } from './view.js';
2
2
  export type { ComputeArrayViewOptions } from './view.js';
3
3
  export { createArraySource, ArraySourceController } from './source.js';
4
4
  export type { ArraySource, ArraySourceOptions } from './source.js';
5
+ export { DEFAULT_MAX_ROWS } from '../core/fetch-all.js';
6
+ export type { FetchAllOptions } from '../core/fetch-all.js';
7
+ export { RowLimitError } from '../core/source-error.js';
@@ -1,65 +1,73 @@
1
- import { i as e } from "../query-Dw4iO0ir.js";
2
- import { t } from "../view-DiLr-TdY.js";
1
+ import { n as e, t, u as n } from "../fetch-all-J37bOtgc.js";
2
+ import { n as r, t as i } from "../view-BtoXdUGF.js";
3
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 = r.pageSize ?? 20, d = p(), f = /* @__PURE__ */ new Set();
11
- function p() {
12
- let e = t(i, {
13
- search: l,
14
- sortCriteria: c,
15
- page: s,
16
- pageSize: u,
17
- columns: a.columns,
18
- searchFields: a.searchFields
19
- }), n = e.totalCount === 0 ? 0 : Math.ceil(e.totalCount / u) - 1;
20
- return s > n && (s = n), {
4
+ function a(e, t = {}) {
5
+ let a = e, o = t, s = n({
6
+ defaultOrderBy: t.defaultOrderBy,
7
+ initialPage: t.initialPage ?? 0,
8
+ initialSearch: t.initialSearch ?? "",
9
+ initialSort: t.initialSort
10
+ }), c = s.page, l = s.sortCriteria, u = s.search, d = t.pageSize ?? 20, f = m(), p = /* @__PURE__ */ new Set();
11
+ function m() {
12
+ let e = i(a, {
13
+ search: u,
14
+ sortCriteria: l,
15
+ page: c,
16
+ pageSize: d,
17
+ columns: o.columns,
18
+ searchFields: o.searchFields
19
+ }), t = e.totalCount === 0 ? 0 : Math.ceil(e.totalCount / d) - 1;
20
+ return c > t && (c = t), {
21
21
  data: e.data,
22
22
  totalCount: e.totalCount,
23
23
  loading: !1,
24
24
  error: null,
25
- page: s,
26
- pageSize: u,
27
- sortCriteria: c,
28
- search: l
25
+ page: c,
26
+ pageSize: d,
27
+ sortCriteria: l,
28
+ search: u
29
29
  };
30
30
  }
31
- function m() {
32
- d = p();
33
- for (let e of [...f]) e();
31
+ function h() {
32
+ f = m();
33
+ for (let e of [...p]) e();
34
34
  }
35
35
  return {
36
- getState: () => d,
36
+ getState: () => f,
37
37
  subscribe(e) {
38
- return f.add(e), () => {
39
- f.delete(e);
38
+ return p.add(e), () => {
39
+ p.delete(e);
40
40
  };
41
41
  },
42
42
  setPage(e) {
43
- s = e, m();
43
+ c = e, h();
44
44
  },
45
45
  setPageSize(e) {
46
- e !== u && (u = e, s = 0, m());
46
+ e !== d && (d = e, c = 0, h());
47
47
  },
48
48
  setSort(e) {
49
- c = e, s = 0, m();
49
+ l = e, c = 0, h();
50
50
  },
51
51
  setSearch(e) {
52
- l = e, s = 0, m();
52
+ u = e, c = 0, h();
53
53
  },
54
54
  refresh() {},
55
+ fetchAll(e) {
56
+ return r(a, {
57
+ search: u,
58
+ sortCriteria: l,
59
+ columns: o.columns,
60
+ searchFields: o.searchFields
61
+ }, e);
62
+ },
55
63
  update(e, t = {}) {
56
- t.pageSize !== void 0 && t.pageSize !== a.pageSize && t.pageSize !== u && (u = t.pageSize, s = 0), i = e, a = t, m();
64
+ t.pageSize !== void 0 && t.pageSize !== o.pageSize && t.pageSize !== d && (d = t.pageSize, c = 0), a = e, o = t, h();
57
65
  }
58
66
  };
59
67
  }
60
- var r = class {
61
- constructor(e, t, r = {}) {
62
- this.host = e, this.source = Array.isArray(t) ? n(t, r) : t, e.addController(this);
68
+ var o = class {
69
+ constructor(e, t, n = {}) {
70
+ this.host = e, this.source = Array.isArray(t) ? a(t, n) : t, e.addController(this);
63
71
  }
64
72
  get state() {
65
73
  return this.source.getState();
@@ -72,4 +80,4 @@ var r = class {
72
80
  }
73
81
  };
74
82
  //#endregion
75
- export { r as ArraySourceController, t as computeArrayView, n as createArraySource };
83
+ export { o as ArraySourceController, t as DEFAULT_MAX_ROWS, e as RowLimitError, i as computeArrayView, a as createArraySource };
@@ -3,6 +3,7 @@ import type { SortCriteria } from '../core/sorting.js';
3
3
  import type { DataRow } from '../models/types.js';
4
4
  import type { ODataSourceState } from '../odata/source.js';
5
5
  import type { UseArraySourceOptions } from './types.js';
6
+ import type { FetchAllOptions } from '../core/fetch-all.js';
6
7
  /** 메모리 소스의 설정 — `useArraySource` 의 옵션과 같다(`initial*` 은 만들 때만 읽는다). */
7
8
  export type ArraySourceOptions<T> = UseArraySourceOptions<T>;
8
9
  /** 메모리 소스 — `createODataSource` 와 같은 상태·조작 모양이라 같은 표 배선이 두 소스에서 돈다. */
@@ -23,6 +24,8 @@ export interface ArraySource<T> {
23
24
  refresh(): void;
24
25
  /** 행이나 설정이 바뀌었다 — 다시 계산한다. 결과가 줄어 지금 페이지가 없어지면 마지막 페이지로 내려온다. */
25
26
  update(data: T[], options?: ArraySourceOptions<T>): void;
27
+ /** 지금 검색·정렬의 결과 전체(페이지 없이) — `createODataSource` 의 `fetchAll` 과 같은 계약(상한 · 취소 · 진행). */
28
+ fetchAll(options?: FetchAllOptions): Promise<T[]>;
26
29
  }
27
30
  /**
28
31
  * 이미 메모리에 있는 행의 데이터 소스 — 검색·정렬·페이지 나누기를 로컬에서 한다(`computeArrayView`). React 없이 쓴다;
@@ -1,5 +1,6 @@
1
1
  import type { SortCriteria } from '../core/sorting.js';
2
2
  import type { SourceError } from '../core/source-error.js';
3
+ import type { FetchAllOptions } from '../core/fetch-all.js';
3
4
  export interface UseArraySourceOptions<T> {
4
5
  /** 페이지당 행 수. `useODataSource`와 동일 기본값. */
5
6
  pageSize?: number;
@@ -51,4 +52,6 @@ export interface UseArraySourceResult<T> {
51
52
  /** no-op — 로컬 배열에는 다시 불러올 원격 상태가 없다. 소비자가 `useODataSource`와
52
53
  * 같은 자리에 무조건 배선한 refresh 버튼이 있어도 안전하게 아무 일도 하지 않는다. */
53
54
  refresh: () => void;
55
+ /** 지금 검색·정렬의 결과 전체(페이지 없이) — `useODataSource` 의 `fetchAll` 과 같은 계약. 내보내기에 쓴다. */
56
+ fetchAll: (options?: FetchAllOptions) => Promise<T[]>;
54
57
  }
@@ -1,5 +1,6 @@
1
1
  import type { SortCriteria } from '../core/sorting.js';
2
2
  import type { ColumnDefinition, DataRow } from '../models/types.js';
3
+ import { type FetchAllOptions } from '../core/fetch-all.js';
3
4
  export interface ComputeArrayViewOptions<T> {
4
5
  search: string;
5
6
  sortCriteria: SortCriteria[];
@@ -22,3 +23,8 @@ export declare function computeArrayView<T extends DataRow>(data: T[], { search,
22
23
  data: T[];
23
24
  totalCount: number;
24
25
  };
26
+ /**
27
+ * 페이지 없이 — 검색·정렬만 적용한 결과 전체. 메모리 소스의 `fetchAll`(소스·훅 둘 다)이 이것을 부른다. OData 소스와
28
+ * 같은 계약이다: 상한을 넘으면 `RowLimitError`, 거둔 신호면 `AbortError`, 진행 콜백은 한 번(전부 한 «페이지»).
29
+ */
30
+ export declare function readAllArrayRows<T extends DataRow>(data: T[], view: Omit<ComputeArrayViewOptions<T>, 'page' | 'pageSize'>, { maxRows, signal, onProgress }?: FetchAllOptions): Promise<T[]>;
@@ -0,0 +1,14 @@
1
+ /** `fetchAll` 의 옵션 — 두 소스가 같은 모양이다. */
2
+ export interface FetchAllOptions {
3
+ /**
4
+ * 읽을 수 있는 행의 상한. 기본값 `100000`(`DEFAULT_MAX_ROWS`). 결과가 이보다 크면 **잘라서 주지 않고**
5
+ * `RowLimitError` 로 거절한다 — OData 소스는 서버가 센 개수(`@odata.count`)로 첫 응답에서 알면 더 읽지 않는다.
6
+ */
7
+ maxRows?: number;
8
+ /** 취소. 거두면 `AbortError` 로 거절한다. */
9
+ signal?: AbortSignal;
10
+ /** 페이지를 하나 받을 때마다 — 받은 행 수와 전체(서버가 세지 않았으면 `undefined`). */
11
+ onProgress?: (loaded: number, total: number | undefined) => void;
12
+ }
13
+ /** «전부 읽기» 의 기본 상한 — 브라우저 메모리와 서버 부하를 지키는 선. 넘으면 조용히 자르지 않고 거절한다. */
14
+ export declare const DEFAULT_MAX_ROWS = 100000;
@@ -31,11 +31,23 @@ export interface SourceError {
31
31
  }
32
32
  /** 실패 응답을 `SourceError` 로 읽는다. 본문을 읽을 수 없어도 상태와 기본 문장은 남긴다. */
33
33
  export declare function readFailedResponse(res: Response): Promise<SourceError>;
34
- /** 실패 응답을 실어 나르는 내부 예외 — `catch` 가 상태를 잃지 않고 `SourceError` 를 꺼낸다. */
34
+ /**
35
+ * 소스 요청의 실패 — `failure` 가 `SourceError`(상태 · 거절 코드 · 상세)다. `fetchAll` 은 이것으로 거절한다(상한을 넘으면
36
+ * 하위 `RowLimitError`). 화면에는 `failure.message`. 상태로 드러나는 실패(`error`)는 이 `failure` 그대로다.
37
+ */
35
38
  export declare class SourceRequestError extends Error {
36
39
  readonly failure: SourceError;
37
40
  constructor(failure: SourceError);
38
41
  }
42
+ /**
43
+ * 조회 결과가 «전부 읽기» 의 상한(`maxRows`)을 넘었다 — 잘라서 주지 않고 거절한 것이다. 화면에는 `failure.message` 를
44
+ * 그린다(로케일 `tooManyRows`). `total` 은 서버가 센 행 수(`@odata.count`)이고, 서버가 세지 않았으면 없다.
45
+ */
46
+ export declare class RowLimitError extends SourceRequestError {
47
+ readonly total: number | undefined;
48
+ readonly maxRows: number;
49
+ constructor(maxRows: number, total?: number);
50
+ }
39
51
  /** 전송이 응답 없이 던진 실패(네트워크 · 오프라인)를 `SourceError` 로 — 브라우저마다 다른 예외 문구 대신 로케일 문장. */
40
52
  export declare function networkFailure(cause: unknown): SourceRequestError;
41
53
  /** 잡힌 예외를 `SourceError` 로 — 응답이 있던 실패는 그 구조 그대로, 나머지는 메시지만. */
@@ -0,0 +1,86 @@
1
+ import { n as e } from "./locale-D9g0ksXq.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 = 0, 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 = i === void 0 ? { count: !0 } : {
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
+ }, l = class extends c {
69
+ constructor(t, n) {
70
+ super({ message: e("tooManyRows", { maxRows: t }) }), this.name = "RowLimitError", this.total = n, this.maxRows = t;
71
+ }
72
+ };
73
+ function u(t) {
74
+ return new c({
75
+ message: e("networkFailed"),
76
+ cause: t
77
+ });
78
+ }
79
+ function d(e) {
80
+ return e instanceof c ? e.failure : e instanceof Error ? { message: e.message } : { message: String(e) };
81
+ }
82
+ //#endregion
83
+ //#region src/core/fetch-all.ts
84
+ var f = 1e5;
85
+ //#endregion
86
+ export { o as a, n as c, u as i, i as l, l as n, d as o, c as r, a as s, f as t, r as u };