@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
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,2 +1,3 @@
|
|
|
1
|
-
import { a as e,
|
|
2
|
-
|
|
1
|
+
import { a as e, i as t, n, o as r, r as i, t as a } from "./flex-table-DlvIBu4z.js";
|
|
2
|
+
import { t as o } from "./locale-B1SiVDS1.js";
|
|
3
|
+
export { a as FlexTable, e as RowSelectionState, t as UndoStack, n as effectiveAlign, i as exportData, o as flexTableLocale, r as renderCell };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Locale as e } from "@iyulab/components/dist/utilities/Locale.js";
|
|
2
|
+
//#region src/locale.ts
|
|
3
|
+
var t = e.namespace("flex-table");
|
|
4
|
+
t.register("en", {
|
|
5
|
+
showHiddenColumns: "Show hidden column(s)",
|
|
6
|
+
columnMenu: "Column menu",
|
|
7
|
+
cellActions: "Cell actions",
|
|
8
|
+
noColumnsDefined: "No columns defined",
|
|
9
|
+
addCommentPlaceholder: "Add a comment… (Ctrl+Enter to save)",
|
|
10
|
+
cancel: "Cancel",
|
|
11
|
+
save: "Save",
|
|
12
|
+
clear: "Clear",
|
|
13
|
+
blankCells: "Blank cells",
|
|
14
|
+
emptyOnly: "Empty only",
|
|
15
|
+
nonEmptyOnly: "Non-empty only",
|
|
16
|
+
blankAll: "— All —",
|
|
17
|
+
all: "All",
|
|
18
|
+
contains: "Contains",
|
|
19
|
+
startsWith: "Starts with",
|
|
20
|
+
endsWith: "Ends with",
|
|
21
|
+
wildcard: "Wildcard",
|
|
22
|
+
searchPlaceholder: "Search...",
|
|
23
|
+
valuePlaceholder: "Value",
|
|
24
|
+
fromPlaceholder: "From",
|
|
25
|
+
toPlaceholder: "To",
|
|
26
|
+
findPlaceholder: "Find...",
|
|
27
|
+
findPrevious: "Previous match",
|
|
28
|
+
findNext: "Next match",
|
|
29
|
+
matchCase: "Match case",
|
|
30
|
+
wholeCell: "Whole cell",
|
|
31
|
+
closeFind: "Close find",
|
|
32
|
+
findNoResults: "No results",
|
|
33
|
+
replaceWithPlaceholder: "Replace with...",
|
|
34
|
+
replace: "Replace",
|
|
35
|
+
replaceAll: "Replace all",
|
|
36
|
+
findInput: "Find",
|
|
37
|
+
replaceInput: "Replace with",
|
|
38
|
+
filterFor: "Filter {header}",
|
|
39
|
+
filterMatchMode: "Match",
|
|
40
|
+
filterText: "Filter text",
|
|
41
|
+
conditionOperator: "Condition {n}",
|
|
42
|
+
conditionValue: "Condition {n} value",
|
|
43
|
+
conditionJoin: "Combine conditions",
|
|
44
|
+
sortAscending: "Sort ascending",
|
|
45
|
+
sortDescending: "Sort descending",
|
|
46
|
+
filter: "Filter…",
|
|
47
|
+
clearFilter: "Clear filter",
|
|
48
|
+
hideColumn: "Hide column",
|
|
49
|
+
showColumn: "Show: {header}",
|
|
50
|
+
autoFitWidth: "Auto-fit width",
|
|
51
|
+
wider: "Wider",
|
|
52
|
+
narrower: "Narrower",
|
|
53
|
+
columnMenuFor: "Column menu: {header}",
|
|
54
|
+
columnMenuRegion: "{header} column",
|
|
55
|
+
copy: "Copy",
|
|
56
|
+
insertRowAbove: "Insert row above",
|
|
57
|
+
insertRowBelow: "Insert row below",
|
|
58
|
+
deleteRow: "Delete row",
|
|
59
|
+
selectRow: "Select row",
|
|
60
|
+
selectAllRows: "Select all rows",
|
|
61
|
+
filterByThisValue: "Filter by this value",
|
|
62
|
+
addComment: "Add Comment",
|
|
63
|
+
editComment: "Edit Comment",
|
|
64
|
+
deleteComment: "Delete Comment",
|
|
65
|
+
booleanTrue: "True",
|
|
66
|
+
booleanFalse: "False",
|
|
67
|
+
dropFileToImport: "Drop file to import (.xlsx / .csv)",
|
|
68
|
+
notANumber: "Enter a number",
|
|
69
|
+
notADate: "Enter a date as YYYY-MM-DD",
|
|
70
|
+
notADateTime: "Enter a date and time as YYYY-MM-DD HH:mm",
|
|
71
|
+
notInList: "Value must be from the existing list",
|
|
72
|
+
requestFailed: "Request failed ({status})",
|
|
73
|
+
networkFailed: "Could not reach the server."
|
|
74
|
+
}), t.register("ko", {
|
|
75
|
+
showHiddenColumns: "숨긴 열 보기",
|
|
76
|
+
columnMenu: "열 메뉴",
|
|
77
|
+
cellActions: "셀 작업",
|
|
78
|
+
noColumnsDefined: "정의된 열이 없습니다",
|
|
79
|
+
addCommentPlaceholder: "메모 입력… (Ctrl+Enter 로 저장)",
|
|
80
|
+
cancel: "취소",
|
|
81
|
+
save: "저장",
|
|
82
|
+
clear: "지우기",
|
|
83
|
+
blankCells: "빈 셀",
|
|
84
|
+
emptyOnly: "빈 값만",
|
|
85
|
+
nonEmptyOnly: "빈 값 제외",
|
|
86
|
+
blankAll: "— 전체 —",
|
|
87
|
+
all: "전체",
|
|
88
|
+
contains: "포함",
|
|
89
|
+
startsWith: "시작 문자",
|
|
90
|
+
endsWith: "끝 문자",
|
|
91
|
+
wildcard: "와일드카드",
|
|
92
|
+
searchPlaceholder: "검색...",
|
|
93
|
+
valuePlaceholder: "값",
|
|
94
|
+
fromPlaceholder: "시작",
|
|
95
|
+
toPlaceholder: "끝",
|
|
96
|
+
findPlaceholder: "찾기...",
|
|
97
|
+
findPrevious: "이전 일치",
|
|
98
|
+
findNext: "다음 일치",
|
|
99
|
+
matchCase: "대/소문자 구분",
|
|
100
|
+
wholeCell: "셀 전체 일치",
|
|
101
|
+
closeFind: "찾기 닫기",
|
|
102
|
+
findNoResults: "결과 없음",
|
|
103
|
+
replaceWithPlaceholder: "바꿀 내용...",
|
|
104
|
+
replace: "바꾸기",
|
|
105
|
+
replaceAll: "모두 바꾸기",
|
|
106
|
+
findInput: "찾을 내용",
|
|
107
|
+
replaceInput: "바꿀 내용",
|
|
108
|
+
filterFor: "{header} 필터",
|
|
109
|
+
filterMatchMode: "일치 방식",
|
|
110
|
+
filterText: "필터 텍스트",
|
|
111
|
+
conditionOperator: "조건 {n}",
|
|
112
|
+
conditionValue: "조건 {n} 값",
|
|
113
|
+
conditionJoin: "조건 결합",
|
|
114
|
+
sortAscending: "오름차순 정렬",
|
|
115
|
+
sortDescending: "내림차순 정렬",
|
|
116
|
+
filter: "필터…",
|
|
117
|
+
clearFilter: "필터 지우기",
|
|
118
|
+
hideColumn: "열 숨기기",
|
|
119
|
+
showColumn: "{header} 보기",
|
|
120
|
+
autoFitWidth: "너비 자동 맞춤",
|
|
121
|
+
wider: "넓게",
|
|
122
|
+
narrower: "좁게",
|
|
123
|
+
columnMenuFor: "{header} 열 메뉴",
|
|
124
|
+
columnMenuRegion: "{header} 열",
|
|
125
|
+
copy: "복사",
|
|
126
|
+
insertRowAbove: "위에 행 삽입",
|
|
127
|
+
insertRowBelow: "아래에 행 삽입",
|
|
128
|
+
deleteRow: "행 삭제",
|
|
129
|
+
selectRow: "행 선택",
|
|
130
|
+
selectAllRows: "모든 행 선택",
|
|
131
|
+
filterByThisValue: "이 값으로 필터",
|
|
132
|
+
addComment: "메모 추가",
|
|
133
|
+
editComment: "메모 편집",
|
|
134
|
+
deleteComment: "메모 삭제",
|
|
135
|
+
booleanTrue: "참",
|
|
136
|
+
booleanFalse: "거짓",
|
|
137
|
+
dropFileToImport: "가져올 파일을 놓으세요 (.xlsx / .csv)",
|
|
138
|
+
notANumber: "숫자를 입력하세요",
|
|
139
|
+
notADate: "날짜를 YYYY-MM-DD 로 입력하세요",
|
|
140
|
+
notADateTime: "날짜와 시간을 YYYY-MM-DD HH:mm 으로 입력하세요",
|
|
141
|
+
notInList: "목록에 있는 값이어야 합니다",
|
|
142
|
+
requestFailed: "요청이 실패했습니다 ({status})",
|
|
143
|
+
networkFailed: "서버에 연결하지 못했습니다."
|
|
144
|
+
});
|
|
145
|
+
var n = (e, n) => t.text(e, n);
|
|
146
|
+
//#endregion
|
|
147
|
+
export { n, t };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
2
|
+
import { type ODataSource, type ODataSourceOptions, type ODataSourceState } from './source.js';
|
|
3
|
+
/**
|
|
4
|
+
* Lit 어댑터 — 프레임워크 중립 OData 소스를 호스트의 연결 수명에 묶는다. React 의 `useODataSource` 와 같은 소스다.
|
|
5
|
+
*
|
|
6
|
+
* 연결되면 구독하고(첫 조회가 나간다) 끊기면 해지한다(진행 중 요청을 거둔다). 상태가 바뀌면 호스트를 다시 그린다.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* class OrdersPage extends LitElement {
|
|
10
|
+
* private orders = new ODataSourceController<Order>(this, '/odata/Orders', { pageSize: 25 });
|
|
11
|
+
* render() {
|
|
12
|
+
* const { data, loading, error } = this.orders.state;
|
|
13
|
+
* return html`<flex-table .data=${data} .loading=${loading} data-mode="server"
|
|
14
|
+
* @sort-change=${(e: CustomEvent) => this.orders.source.setSort(e.detail.criteria)}></flex-table>`;
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare class ODataSourceController<T = Record<string, unknown>> implements ReactiveController {
|
|
20
|
+
private readonly host;
|
|
21
|
+
/** 소스 — 조작(`setPage`·`setSort`·`setSearch`·`refresh`·`update`)은 이것으로 한다. */
|
|
22
|
+
readonly source: ODataSource<T>;
|
|
23
|
+
private unsubscribe?;
|
|
24
|
+
constructor(host: ReactiveControllerHost, url: string, options?: ODataSourceOptions);
|
|
25
|
+
/** 현재 상태. */
|
|
26
|
+
get state(): ODataSourceState<T>;
|
|
27
|
+
hostConnected(): void;
|
|
28
|
+
hostDisconnected(): void;
|
|
29
|
+
}
|
package/dist/odata/index.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export { buildSearchExpression, parseOrderBy, buildODataQuery } from './query.js';
|
|
2
2
|
export type { ODataQueryState } from './query.js';
|
|
3
|
+
export { createODataSource } from './source.js';
|
|
4
|
+
export type { ODataSource, ODataSourceOptions, ODataSourceState } from './source.js';
|
|
5
|
+
export { ODataSourceController } from './controller.js';
|
package/dist/odata/index.js
CHANGED
|
@@ -1,2 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { i as e, n as t, r as n, t as r } from "../source-CwbD-6ea.js";
|
|
2
|
+
//#region src/odata/controller.ts
|
|
3
|
+
var i = class {
|
|
4
|
+
constructor(e, t, n = {}) {
|
|
5
|
+
this.host = e, this.source = r(t, n), e.addController(this);
|
|
6
|
+
}
|
|
7
|
+
get state() {
|
|
8
|
+
return this.source.getState();
|
|
9
|
+
}
|
|
10
|
+
hostConnected() {
|
|
11
|
+
this.unsubscribe ??= this.source.subscribe(() => this.host.requestUpdate());
|
|
12
|
+
}
|
|
13
|
+
hostDisconnected() {
|
|
14
|
+
this.unsubscribe?.(), this.unsubscribe = void 0;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
//#endregion
|
|
18
|
+
export { i as ODataSourceController, t as buildODataQuery, n as buildSearchExpression, r as createODataSource, e as parseOrderBy };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { SortCriteria } from '../core/sorting.js';
|
|
2
|
+
import { type SourceError } from '../core/source-error.js';
|
|
3
|
+
/** OData 소스의 설정 — `useODataSource` 의 옵션과 같다(`initial*` 은 만들 때만 읽는다). */
|
|
4
|
+
export interface ODataSourceOptions {
|
|
5
|
+
pageSize?: number;
|
|
6
|
+
defaultOrderBy?: string;
|
|
7
|
+
/**
|
|
8
|
+
* 초기 페이지. **0-based** — `page`/`setPage` 와 같은 축이고, 요청의 `$skip` 은 `page * pageSize` 다. 기본값 `0`.
|
|
9
|
+
* 만들 때만 읽는다 — 이후에 옮기려면 `setPage`.
|
|
10
|
+
*/
|
|
11
|
+
initialPage?: number;
|
|
12
|
+
/** 초기 검색어. 기본값 `''`. 만들 때만 읽는다. */
|
|
13
|
+
initialSearch?: string;
|
|
14
|
+
/**
|
|
15
|
+
* 초기 정렬. 주면 `defaultOrderBy` 파싱보다 **우선**한다 — 이쪽은 `sortCriteria`/`setSort` 가 주고받는 모양 그대로라
|
|
16
|
+
* 저장해 둔 정렬 상태를 파싱 없이 되돌릴 수 있다. 만들 때만 읽는다.
|
|
17
|
+
*/
|
|
18
|
+
initialSort?: SortCriteria[];
|
|
19
|
+
/** 고정 필터. 값(직렬화)이 바뀌면 페이지가 첫 장으로 돌아간다 — 참조만 다른 같은 값은 아무 일도 없다. */
|
|
20
|
+
fixedFilter?: Record<string, unknown>;
|
|
21
|
+
/** 기본값: `window.location.origin`. 프록시/BFF 등 다른 origin 으로 요청해야 할 때 지정. */
|
|
22
|
+
baseUrl?: string;
|
|
23
|
+
/** 커스텀 fetch transport(예: 인증 헤더를 주입하는 `HttpClient` 래퍼). 기본값: 전역 `fetch`. */
|
|
24
|
+
fetcher?: (input: string, init: RequestInit) => Promise<Response>;
|
|
25
|
+
/**
|
|
26
|
+
* 응답이 401 일 때 호출(세션 만료 리다이렉트 등). 호출 후에도 기존 에러 처리는 계속 진행된다.
|
|
27
|
+
* 403(인증됐지만 권한 없음)에는 부르지 않는다 — 그 실패는 `error` 로 드러난다.
|
|
28
|
+
*/
|
|
29
|
+
onUnauthorized?: (response: Response) => void;
|
|
30
|
+
/**
|
|
31
|
+
* `false` 인 동안 요청하지 않는다. 기본값 `true`. `false → true` 가 되는 순간 그때의 조건으로 첫 조회가 나가고,
|
|
32
|
+
* 그 사이 `loading` 은 `true` 다. 조회 중에 `false` 가 되면 진행 중 요청을 취소한다. `false` 인 동안의 `refresh()` 는
|
|
33
|
+
* 아무 일도 하지 않는다 — 켜지는 순간의 조회가 이미 최신 조건이다.
|
|
34
|
+
*/
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** 소스의 현재 상태 — 바뀔 때마다 새 객체다(같은 객체면 바뀌지 않았다). */
|
|
38
|
+
export interface ODataSourceState<T> {
|
|
39
|
+
data: T[];
|
|
40
|
+
totalCount: number;
|
|
41
|
+
/**
|
|
42
|
+
* 조회 중이거나 아직 한 번도 답을 받지 않았으면 `true` — 첫 응답(성공·실패)이 정착할 때까지, 그리고 `enabled: false`
|
|
43
|
+
* 인 동안. 그래서 `!loading && totalCount === 0` 이 곧 «결과 없음» 이다.
|
|
44
|
+
*/
|
|
45
|
+
loading: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* 마지막 요청의 실패, 없으면 `null`. 화면에는 `error.message` 를 그린다. «어떤 실패인가» 는 `status`(HTTP) ·
|
|
48
|
+
* `code`(서버의 거절 코드) · `details` 로 가른다 — 401 은 `onUnauthorized` 도 부른다.
|
|
49
|
+
*/
|
|
50
|
+
error: SourceError | null;
|
|
51
|
+
/** 0-based. */
|
|
52
|
+
page: number;
|
|
53
|
+
sortCriteria: SortCriteria[];
|
|
54
|
+
search: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 프레임워크 중립 OData 소스 — 상태 · 구독 · 조작. React 는 `useODataSource`, Lit 은 `ODataSourceController` 가 이것을 감싼다.
|
|
58
|
+
*
|
|
59
|
+
* 요청은 **구독자가 있을 때만** 나간다(첫 `subscribe` 가 시작, 마지막 해지가 진행 중 요청을 거둔다). 같은 틱의 조작은
|
|
60
|
+
* 한 요청으로 모인다 — `setSearch` 의 «페이지를 첫 장으로» 나 `update` 의 필터 변경이 낡은 페이지로 요청을 내지 않는다.
|
|
61
|
+
*/
|
|
62
|
+
export interface ODataSource<T> {
|
|
63
|
+
/** 현재 상태. 바뀌지 않았으면 같은 객체를 돌려준다(`useSyncExternalStore` 의 스냅숏 계약). */
|
|
64
|
+
getState(): ODataSourceState<T>;
|
|
65
|
+
/** 상태가 바뀔 때마다 부른다. 해지 함수를 돌려준다. */
|
|
66
|
+
subscribe(listener: () => void): () => void;
|
|
67
|
+
/** 0-based 페이지로 옮긴다. */
|
|
68
|
+
setPage(page: number): void;
|
|
69
|
+
/** 정렬을 바꾸고 첫 장으로 간다. */
|
|
70
|
+
setSort(criteria: SortCriteria[]): void;
|
|
71
|
+
/** 검색어를 바꾸고 첫 장으로 간다. */
|
|
72
|
+
setSearch(term: string): void;
|
|
73
|
+
/** 같은 조건으로 다시 읽는다. */
|
|
74
|
+
refresh(): void;
|
|
75
|
+
/** 설정을 바꾼다 — 요청 조건이 바뀌면 다시 읽고(`fixedFilter` 값이 바뀌면 첫 장으로), `fetcher`·`onUnauthorized` 는 다음 요청부터. */
|
|
76
|
+
update(url: string, options?: ODataSourceOptions): void;
|
|
77
|
+
}
|
|
78
|
+
/** OData v4 서버 사이드 데이터 소스를 만든다. flex-table 의 `dataMode="server"` 와 함께 쓴다. */
|
|
79
|
+
export declare function createODataSource<T = Record<string, unknown>>(url: string, options?: ODataSourceOptions): ODataSource<T>;
|
package/dist/odata/types.d.ts
CHANGED
|
@@ -1,63 +1,13 @@
|
|
|
1
1
|
import type { SortCriteria } from '../core/sorting.js';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 초기 페이지. **0-based** — 반환되는 `page`/`setPage`와 같은 축이고, 요청의
|
|
8
|
-
* `$skip`은 `page * pageSize`다. 기본값 `0`.
|
|
9
|
-
*
|
|
10
|
-
* ⚠`useState`의 초기값이므로 **첫 렌더에서만** 읽힌다(`defaultOrderBy`와 같은 계약).
|
|
11
|
-
* 마운트 이후에 페이지를 옮기려면 `setPage`를 쓴다.
|
|
12
|
-
*/
|
|
13
|
-
initialPage?: number;
|
|
14
|
-
/** 초기 검색어. 기본값 `''`. `initialPage`와 같은 「첫 렌더에서만」 계약이다. */
|
|
15
|
-
initialSearch?: string;
|
|
16
|
-
/**
|
|
17
|
-
* 초기 정렬. 주면 `defaultOrderBy` 파싱보다 **우선**한다 — 두 옵션은 같은 것을 서로
|
|
18
|
-
* 다른 표기로 말하고, 이쪽은 `sortCriteria`/`onSortChange`가 주고받는 모양 그대로라
|
|
19
|
-
* 저장해 둔 정렬 상태를 파싱 없이 되돌릴 수 있다.
|
|
20
|
-
*/
|
|
21
|
-
initialSort?: SortCriteria[];
|
|
22
|
-
fixedFilter?: Record<string, unknown>;
|
|
23
|
-
/** 기본값: `window.location.origin`. 프록시/BFF 등 다른 origin으로 요청해야 할 때 지정. */
|
|
24
|
-
baseUrl?: string;
|
|
25
|
-
/** 커스텀 fetch transport(예: 인증 헤더를 주입하는 `HttpClient` 래퍼). 기본값: 전역 `fetch`. */
|
|
26
|
-
fetcher?: (input: string, init: RequestInit) => Promise<Response>;
|
|
27
|
-
/**
|
|
28
|
-
* 응답이 401 일 때 호출(세션 만료 리다이렉트 등). 호출 후에도 기존 에러 처리는 계속 진행된다.
|
|
29
|
-
* 403(인증됐지만 권한 없음)에는 부르지 않는다 — 그 실패는 `error` 로 드러난다.
|
|
30
|
-
*/
|
|
31
|
-
onUnauthorized?: (response: Response) => void;
|
|
32
|
-
/**
|
|
33
|
-
* `false` 인 동안 요청하지 않는다. 기본값 `true`.
|
|
34
|
-
*
|
|
35
|
-
* 조회 조건이 다른 비동기 값(현재 사용자·기본 필터·선택된 부모 레코드)에 달린 목록에서
|
|
36
|
-
* 그 값이 오기 전에 «틀린 조건의 첫 조회» 가 나가지 않게 한다. `false → true` 가 되는 순간
|
|
37
|
-
* 첫 조회가 나가고, 그 사이 `loading` 은 `true` 다(표가 «데이터 없음» 이 아니라 «불러오는 중»
|
|
38
|
-
* 을 보인다). 조회 중에 `false` 가 되면 진행 중 요청을 취소한다. `false` 인 동안의 `refresh()` 는
|
|
39
|
-
* 아무 일도 하지 않는다 — 켜지는 순간의 조회가 이미 최신 조건이다.
|
|
40
|
-
*/
|
|
41
|
-
enabled?: boolean;
|
|
42
|
-
}
|
|
43
|
-
export interface UseODataSourceResult<T> {
|
|
44
|
-
data: T[];
|
|
45
|
-
totalCount: number;
|
|
46
|
-
/**
|
|
47
|
-
* 조회 중이거나 아직 한 번도 답을 받지 않았으면 `true` — 첫 렌더부터 첫 응답(성공·실패)이 정착할 때까지,
|
|
48
|
-
* 그리고 `enabled: false` 인 동안. 그래서 `!loading && totalCount === 0` 이 곧 «결과 없음» 이다.
|
|
49
|
-
*/
|
|
50
|
-
loading: boolean;
|
|
51
|
-
/**
|
|
52
|
-
* 마지막 요청의 실패, 없으면 `null`. 화면에는 `error.message` 를 그린다. «어떤 실패인가» 는
|
|
53
|
-
* `status`(HTTP) · `code`(서버의 거절 코드) · `details` 로 가른다 — 401 은 `onUnauthorized` 도 부른다.
|
|
54
|
-
*/
|
|
55
|
-
error: SourceError | null;
|
|
56
|
-
page: number;
|
|
2
|
+
import type { ODataSourceOptions, ODataSourceState } from './source.js';
|
|
3
|
+
/** `useODataSource` 의 옵션 — 프레임워크 중립 소스(`createODataSource`)의 설정과 같다. `initial*` 은 첫 렌더에서만 읽힌다. */
|
|
4
|
+
export type UseODataSourceOptions = ODataSourceOptions;
|
|
5
|
+
export interface UseODataSourceResult<T> extends ODataSourceState<T> {
|
|
57
6
|
setPage: (page: number) => void;
|
|
58
|
-
|
|
7
|
+
/** flex-table 의 `sort-change` 를 받는다(`detail.criteria`) — 정렬을 바꾸고 첫 장으로 간다. */
|
|
59
8
|
onSortChange: (e: CustomEvent) => void;
|
|
9
|
+
/** 검색어를 바꾸고 첫 장으로 간다. */
|
|
60
10
|
setSearch: (term: string) => void;
|
|
61
|
-
search: string;
|
|
62
11
|
refresh: () => void;
|
|
63
12
|
}
|
|
13
|
+
export type { SortCriteria };
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { UseODataSourceOptions, UseODataSourceResult } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* OData v4 서버 사이드 데이터소스 React
|
|
3
|
+
* OData v4 서버 사이드 데이터소스 React 훅 — 프레임워크 중립 소스(`createODataSource`)의 React 어댑터다.
|
|
4
4
|
* flex-table의 dataMode="server"와 함께 사용한다.
|
|
5
|
+
*
|
|
6
|
+
* 요청·페이지 보정·`nextLink`·취소는 전부 소스가 한다. 이 훅은 소스를 한 번 만들고(`initial*` 은 그때만 읽힌다),
|
|
7
|
+
* 렌더마다 바뀐 옵션을 넘기고, 상태를 구독한다. 같은 틱의 조작은 소스가 한 요청으로 모으므로 필터를 바꾼 렌더도
|
|
8
|
+
* 낡은 페이지로 요청하지 않는다.
|
|
5
9
|
*/
|
|
6
10
|
export declare function useODataSource<T = Record<string, unknown>>(url: string, options?: UseODataSourceOptions): UseODataSourceResult<T>;
|
package/dist/react.js
CHANGED
|
@@ -1,155 +1,44 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { t as
|
|
4
|
-
import
|
|
5
|
-
import { createComponent as
|
|
6
|
-
//#region src/core/source-error.ts
|
|
7
|
-
async function f(t) {
|
|
8
|
-
let n = await t.text().catch(() => ""), r = {
|
|
9
|
-
message: e("requestFailed", { status: t.status }),
|
|
10
|
-
status: t.status
|
|
11
|
-
};
|
|
12
|
-
if (!n) return r;
|
|
13
|
-
let i = n;
|
|
14
|
-
try {
|
|
15
|
-
i = JSON.parse(n);
|
|
16
|
-
} catch {}
|
|
17
|
-
if (r.body = i, !i || typeof i != "object") return r;
|
|
18
|
-
let a = i, o = a.error && typeof a.error == "object" ? a.error : a;
|
|
19
|
-
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);
|
|
20
|
-
let s = p(o.details);
|
|
21
|
-
return s && (r.details = s), r;
|
|
22
|
-
}
|
|
23
|
-
function p(e) {
|
|
24
|
-
if (!Array.isArray(e)) return;
|
|
25
|
-
let t = e.filter((e) => {
|
|
26
|
-
if (!e || typeof e != "object") return !1;
|
|
27
|
-
let t = e;
|
|
28
|
-
return typeof t.code == "string" && typeof t.message == "string";
|
|
29
|
-
});
|
|
30
|
-
return t.length > 0 ? t : void 0;
|
|
31
|
-
}
|
|
32
|
-
var m = class extends Error {
|
|
33
|
-
constructor(e) {
|
|
34
|
-
super(e.message), this.name = "SourceRequestError", this.failure = e;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
function h(t) {
|
|
38
|
-
return new m({
|
|
39
|
-
message: e("networkFailed"),
|
|
40
|
-
cause: t
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
function g(e) {
|
|
44
|
-
return e instanceof m ? e.failure : e instanceof Error ? { message: e.message } : { message: String(e) };
|
|
45
|
-
}
|
|
46
|
-
//#endregion
|
|
1
|
+
import { t as e } from "./flex-table-DlvIBu4z.js";
|
|
2
|
+
import { a as t, t as n } from "./source-CwbD-6ea.js";
|
|
3
|
+
import { t as r } from "./view-DiLr-TdY.js";
|
|
4
|
+
import i, { useCallback as a, useEffect as o, useMemo as s, useState as c, useSyncExternalStore as l } from "react";
|
|
5
|
+
import { createComponent as u } from "@lit/react";
|
|
47
6
|
//#region src/odata/use-odata-source.ts
|
|
48
|
-
function
|
|
49
|
-
let
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}), [M, N] = u(j.page), [P, F] = u(j.sortCriteria), [I, L] = u(j.search), [R, z] = u(0), [B, V] = u(S);
|
|
55
|
-
S !== B && (V(S), N(0));
|
|
56
|
-
let H = l(null), U = o(() => {
|
|
57
|
-
z((e) => e + 1);
|
|
58
|
-
}, []), W = o((e) => {
|
|
59
|
-
L(e), N(0);
|
|
60
|
-
}, []), G = o((e) => {
|
|
7
|
+
function d(e, t = {}) {
|
|
8
|
+
let [r] = c(() => n(e, t));
|
|
9
|
+
o(() => {
|
|
10
|
+
r.update(e, t);
|
|
11
|
+
});
|
|
12
|
+
let i = l(r.subscribe, r.getState, r.getState), s = a((e) => {
|
|
61
13
|
let t = e.detail?.criteria;
|
|
62
|
-
t && (
|
|
63
|
-
}, []);
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
pageSize: i,
|
|
71
|
-
sortCriteria: P,
|
|
72
|
-
defaultOrderBy: a,
|
|
73
|
-
search: I,
|
|
74
|
-
fixedFilter: _
|
|
75
|
-
}), o = `${v ?? window.location.origin}${e}${n}`, s = async (e) => {
|
|
76
|
-
let n;
|
|
77
|
-
try {
|
|
78
|
-
n = await y(e, { signal: t.signal });
|
|
79
|
-
} catch (e) {
|
|
80
|
-
throw e?.name === "AbortError" ? e : h(e);
|
|
81
|
-
}
|
|
82
|
-
if (!n.ok) throw n.status === 401 && b && b(n), new m(await f(n));
|
|
83
|
-
return n.json();
|
|
84
|
-
};
|
|
85
|
-
return (async () => {
|
|
86
|
-
let e = await s(o), t = [...e.value ?? e], n = new URL(o).origin, r = /* @__PURE__ */ new Set([o]), a = o, c = e["@odata.nextLink"];
|
|
87
|
-
for (; typeof c == "string" && c && t.length < i;) {
|
|
88
|
-
let e = new URL(c, a);
|
|
89
|
-
if (e.origin !== n) throw Error(`OData nextLink points outside the source origin (${e.origin})`);
|
|
90
|
-
if (a = e.toString(), r.has(a)) throw Error(`OData nextLink repeats an already-read page (${a})`);
|
|
91
|
-
r.add(a);
|
|
92
|
-
let i = await s(a);
|
|
93
|
-
t.push(...i.value ?? []), c = i["@odata.nextLink"];
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
count: e["@odata.count"],
|
|
97
|
-
rows: t
|
|
98
|
-
};
|
|
99
|
-
})().then(({ count: e, rows: n }) => {
|
|
100
|
-
if (t.signal.aborted) return;
|
|
101
|
-
let r = e ?? 0;
|
|
102
|
-
w(n), E(r), A(null);
|
|
103
|
-
let a = r === 0 ? 0 : Math.ceil(r / i) - 1;
|
|
104
|
-
M > a && N(a);
|
|
105
|
-
}).catch((e) => {
|
|
106
|
-
t.signal.aborted || e.name !== "AbortError" && (A(g(e)), w([]), E(0));
|
|
107
|
-
}).finally(() => {
|
|
108
|
-
t.signal.aborted || O(!1);
|
|
109
|
-
}), () => t.abort();
|
|
110
|
-
}, [
|
|
111
|
-
x,
|
|
112
|
-
e,
|
|
113
|
-
M,
|
|
114
|
-
i,
|
|
115
|
-
P,
|
|
116
|
-
I,
|
|
117
|
-
S,
|
|
118
|
-
a,
|
|
119
|
-
R,
|
|
120
|
-
v
|
|
121
|
-
]), {
|
|
122
|
-
data: C,
|
|
123
|
-
totalCount: T,
|
|
124
|
-
loading: D || !x,
|
|
125
|
-
error: k,
|
|
126
|
-
page: M,
|
|
127
|
-
setPage: N,
|
|
128
|
-
sortCriteria: P,
|
|
129
|
-
onSortChange: G,
|
|
130
|
-
search: I,
|
|
131
|
-
setSearch: W,
|
|
132
|
-
refresh: U
|
|
14
|
+
t && r.setSort(t);
|
|
15
|
+
}, [r]);
|
|
16
|
+
return {
|
|
17
|
+
...i,
|
|
18
|
+
setPage: r.setPage,
|
|
19
|
+
onSortChange: s,
|
|
20
|
+
setSearch: r.setSearch,
|
|
21
|
+
refresh: r.refresh
|
|
133
22
|
};
|
|
134
23
|
}
|
|
135
24
|
//#endregion
|
|
136
25
|
//#region src/array/use-array-source.ts
|
|
137
|
-
function
|
|
138
|
-
let { pageSize:
|
|
139
|
-
defaultOrderBy:
|
|
140
|
-
initialPage:
|
|
141
|
-
initialSearch:
|
|
26
|
+
function f(e, n = {}) {
|
|
27
|
+
let { pageSize: i = 20, defaultOrderBy: o, initialPage: l = 0, initialSearch: u = "", initialSort: d, columns: f, searchFields: p } = n, m = t({
|
|
28
|
+
defaultOrderBy: o,
|
|
29
|
+
initialPage: l,
|
|
30
|
+
initialSearch: u,
|
|
142
31
|
initialSort: d
|
|
143
|
-
}), [h, g] =
|
|
32
|
+
}), [h, g] = c(m.page), [_, v] = c(m.sortCriteria), [y, b] = c(m.search), x = a((e) => {
|
|
144
33
|
b(e), g(0);
|
|
145
|
-
}, []), S =
|
|
34
|
+
}, []), S = a((e) => {
|
|
146
35
|
let t = e.detail?.criteria;
|
|
147
36
|
t && (v(t), g(0));
|
|
148
|
-
}, []), C =
|
|
37
|
+
}, []), C = a(() => {}, []), { data: w, totalCount: T } = s(() => r(e, {
|
|
149
38
|
search: y,
|
|
150
39
|
sortCriteria: _,
|
|
151
40
|
page: h,
|
|
152
|
-
pageSize:
|
|
41
|
+
pageSize: i,
|
|
153
42
|
columns: f,
|
|
154
43
|
searchFields: p
|
|
155
44
|
}), [
|
|
@@ -157,10 +46,10 @@ function v(e, t = {}) {
|
|
|
157
46
|
y,
|
|
158
47
|
_,
|
|
159
48
|
h,
|
|
160
|
-
|
|
49
|
+
i,
|
|
161
50
|
f,
|
|
162
51
|
p
|
|
163
|
-
]), E = T === 0 ? 0 : Math.ceil(T /
|
|
52
|
+
]), E = T === 0 ? 0 : Math.ceil(T / i) - 1;
|
|
164
53
|
return h > E && g(E), {
|
|
165
54
|
data: w,
|
|
166
55
|
totalCount: T,
|
|
@@ -175,10 +64,10 @@ function v(e, t = {}) {
|
|
|
175
64
|
refresh: C
|
|
176
65
|
};
|
|
177
66
|
}
|
|
178
|
-
var
|
|
67
|
+
var p = u({
|
|
179
68
|
tagName: "flex-table",
|
|
180
|
-
elementClass:
|
|
181
|
-
react:
|
|
69
|
+
elementClass: e,
|
|
70
|
+
react: i,
|
|
182
71
|
events: {
|
|
183
72
|
onCellSelect: "cell-select",
|
|
184
73
|
onCellEditCommit: "cell-edit-commit",
|
|
@@ -214,4 +103,4 @@ var y = d({
|
|
|
214
103
|
}
|
|
215
104
|
});
|
|
216
105
|
//#endregion
|
|
217
|
-
export {
|
|
106
|
+
export { p as FlexTableReact, f as useArraySource, d as useODataSource };
|