ezfw-core 1.0.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/components/EzBaseComponent.ts +648 -0
- package/components/EzComponent.ts +89 -0
- package/components/EzInput.module.scss +183 -0
- package/components/EzInput.ts +104 -0
- package/components/EzLabel.ts +22 -0
- package/components/EzOutlet.ts +181 -0
- package/components/HtmlWrapper.ts +305 -0
- package/components/avatar/EzAvatar.module.scss +200 -0
- package/components/avatar/EzAvatar.ts +130 -0
- package/components/badge/EzBadge.module.scss +202 -0
- package/components/badge/EzBadge.ts +77 -0
- package/components/button/EzButton.module.scss +402 -0
- package/components/button/EzButton.ts +175 -0
- package/components/button/EzButtonGroup.ts +48 -0
- package/components/card/EzCard.module.scss +71 -0
- package/components/card/EzCard.ts +120 -0
- package/components/chart/EzBarChart.ts +47 -0
- package/components/chart/EzChart.module.scss +14 -0
- package/components/chart/EzChart.ts +279 -0
- package/components/chart/EzDoughnutChart.ts +47 -0
- package/components/chart/EzLineChart.ts +53 -0
- package/components/checkbox/EzCheckbox.module.scss +145 -0
- package/components/checkbox/EzCheckbox.ts +115 -0
- package/components/dataview/EzDataView.module.scss +115 -0
- package/components/dataview/EzDataView.ts +355 -0
- package/components/dataview/modes/EzDataViewCards.ts +322 -0
- package/components/dataview/modes/EzDataViewGrid.ts +76 -0
- package/components/datepicker/EzDatePicker.module.scss +348 -0
- package/components/datepicker/EzDatePicker.ts +519 -0
- package/components/dialog/EzDialog.module.scss +180 -0
- package/components/dropdown/EzDropdown.module.scss +107 -0
- package/components/dropdown/EzDropdown.ts +235 -0
- package/components/feed/EzActivityFeed.module.scss +90 -0
- package/components/feed/EzActivityFeed.ts +78 -0
- package/components/form/EzForm.ts +364 -0
- package/components/form/EzValidators.test.js +421 -0
- package/components/form/EzValidators.ts +202 -0
- package/components/grid/EzGrid.scss +88 -0
- package/components/grid/EzGrid.ts +1085 -0
- package/components/grid/EzGridContainer.ts +104 -0
- package/components/grid/body/EzGridBody.scss +283 -0
- package/components/grid/body/EzGridBody.ts +549 -0
- package/components/grid/body/EzGridCell.ts +211 -0
- package/components/grid/body/EzGridRow.ts +196 -0
- package/components/grid/filter/EzGridFilters.scss +78 -0
- package/components/grid/filter/EzGridFilters.ts +285 -0
- package/components/grid/footer/EzGridFooter.scss +136 -0
- package/components/grid/footer/EzGridFooter.ts +448 -0
- package/components/grid/header/EzGridHeader.scss +199 -0
- package/components/grid/header/EzGridHeader.ts +430 -0
- package/components/grid/query/EzGridQuery.ts +81 -0
- package/components/grid/state/EzGridColumns.ts +155 -0
- package/components/grid/state/EzGridController.ts +470 -0
- package/components/grid/state/EzGridLifecycle.ts +136 -0
- package/components/grid/state/EzGridNormalizers.test.js +273 -0
- package/components/grid/state/EzGridNormalizers.ts +162 -0
- package/components/grid/state/EzGridParts.ts +233 -0
- package/components/grid/state/EzGridPersistence.ts +140 -0
- package/components/grid/state/EzGridRemote.test.js +573 -0
- package/components/grid/state/EzGridRemote.ts +335 -0
- package/components/grid/state/EzGridSelection.ts +231 -0
- package/components/grid/state/EzGridSort.ts +286 -0
- package/components/grid/title/EzGridActionBar.ts +98 -0
- package/components/grid/title/EzGridTitle.ts +114 -0
- package/components/grid/title/EzGridTitleBar.scss +65 -0
- package/components/grid/title/EzGridTitleBar.ts +87 -0
- package/components/grid/types.ts +607 -0
- package/components/panel/EzPanel.module.scss +133 -0
- package/components/panel/EzPanel.ts +147 -0
- package/components/radio/EzRadio.module.scss +190 -0
- package/components/radio/EzRadio.ts +149 -0
- package/components/select/EzSelect.module.scss +153 -0
- package/components/select/EzSelect.ts +238 -0
- package/components/skeleton/EzSkeleton.module.scss +95 -0
- package/components/skeleton/EzSkeleton.ts +70 -0
- package/components/store/EzStore.ts +344 -0
- package/components/switch/EzSwitch.module.scss +164 -0
- package/components/switch/EzSwitch.ts +117 -0
- package/components/tabs/EzTabPanel.module.scss +181 -0
- package/components/tabs/EzTabPanel.ts +402 -0
- package/components/textarea/EzTextarea.module.scss +131 -0
- package/components/textarea/EzTextarea.ts +161 -0
- package/components/timepicker/EzTimePicker.module.scss +282 -0
- package/components/timepicker/EzTimePicker.ts +540 -0
- package/components/toast/EzToast.module.scss +291 -0
- package/components/tooltip/EzTooltip.module.scss +124 -0
- package/components/tooltip/EzTooltip.ts +153 -0
- package/core/EzComponentTypes.ts +693 -0
- package/core/EzError.ts +63 -0
- package/core/EzModel.ts +268 -0
- package/core/EzTypes.ts +328 -0
- package/core/eventBus.ts +284 -0
- package/core/ez.ts +617 -0
- package/core/loader.ts +725 -0
- package/core/renderer.ts +1010 -0
- package/core/router.ts +490 -0
- package/core/services.ts +124 -0
- package/core/state.ts +142 -0
- package/core/utils.ts +81 -0
- package/package.json +51 -0
- package/services/RouteUI.js +17 -0
- package/services/crypto.js +64 -0
- package/services/dialog.js +222 -0
- package/services/fetchApi.js +63 -0
- package/services/firebase.js +30 -0
- package/services/toast.js +214 -0
- package/template/doc/EzDocs.js +15 -0
- package/template/doc/EzDocs.module.scss +627 -0
- package/template/doc/EzDocsController.js +164 -0
- package/template/doc/data/activityfeed/EzActivityFeedDoc.js +42 -0
- package/template/doc/data/avatar/EzAvatarDoc.js +71 -0
- package/template/doc/data/badge/EzBadgeDoc.js +92 -0
- package/template/doc/data/button/EzButtonDoc.js +77 -0
- package/template/doc/data/buttongroup/EzButtonGroupDoc.js +102 -0
- package/template/doc/data/card/EzCardDoc.js +39 -0
- package/template/doc/data/chart/EzChartDoc.js +60 -0
- package/template/doc/data/checkbox/EzCheckboxDoc.js +67 -0
- package/template/doc/data/component/EzComponentDoc.js +34 -0
- package/template/doc/data/cssmodules/CSSModulesDoc.js +70 -0
- package/template/doc/data/datepicker/EzDatePickerDoc.js +126 -0
- package/template/doc/data/dialog/EzDialogDoc.js +217 -0
- package/template/doc/data/dropdown/EzDropdownDoc.js +178 -0
- package/template/doc/data/form/EzFormDoc.js +90 -0
- package/template/doc/data/grid/EzGridDoc.js +99 -0
- package/template/doc/data/input/EzInputDoc.js +92 -0
- package/template/doc/data/label/EzLabelDoc.js +40 -0
- package/template/doc/data/model/EzModelDoc.js +53 -0
- package/template/doc/data/outlet/EzOutletDoc.js +63 -0
- package/template/doc/data/panel/EzPanelDoc.js +214 -0
- package/template/doc/data/radio/EzRadioDoc.js +174 -0
- package/template/doc/data/router/EzRouterDoc.js +75 -0
- package/template/doc/data/select/EzSelectDoc.js +37 -0
- package/template/doc/data/skeleton/EzSkeletonDoc.js +149 -0
- package/template/doc/data/switch/EzSwitchDoc.js +82 -0
- package/template/doc/data/tabpanel/EzTabPanelDoc.js +44 -0
- package/template/doc/data/textarea/EzTextareaDoc.js +131 -0
- package/template/doc/data/timepicker/EzTimePickerDoc.js +107 -0
- package/template/doc/data/tooltip/EzTooltipDoc.js +193 -0
- package/template/doc/data/validators/EzValidatorsDoc.js +37 -0
- package/template/doc/sidebar/EzDocsSidebar.js +32 -0
- package/template/doc/sidebar/category/EzDocsCategory.js +33 -0
- package/template/doc/sidebar/item/EzDocsComponentItem.js +24 -0
- package/template/doc/viewer/EzDocsViewer.js +18 -0
- package/template/doc/viewer/codepanel/EzDocsCodePanel.js +51 -0
- package/template/doc/viewer/content/EzDocsContent.js +315 -0
- package/template/doc/viewer/header/EzDocsViewerHeader.js +46 -0
- package/template/doc/viewer/showcase/EzDocsShowcase.js +59 -0
- package/template/doc/viewer/showcase/EzDocsShowcaseSection.js +25 -0
- package/template/doc/viewer/showcase/EzDocsVariantItem.js +29 -0
- package/template/doc/welcome/EzDocsWelcome.js +48 -0
- package/themes/ez-theme.scss +179 -0
- package/themes/nature-fresh.scss +169 -0
- package/types/global.d.ts +21 -0
- package/utils/cssModules.js +81 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
// EzGrid/state/EzGridController.ts
|
|
2
|
+
|
|
3
|
+
// WARNING: EzGridController is a state owner, NOT a lifecycle or rendering component.
|
|
4
|
+
// It must NEVER:
|
|
5
|
+
// - trigger grid refreshes
|
|
6
|
+
// - access EzGridLifecycle
|
|
7
|
+
// - mutate selection, columns, or sort
|
|
8
|
+
// - perform rendering or persistence
|
|
9
|
+
//
|
|
10
|
+
// Allowed responsibilities:
|
|
11
|
+
// - own controller.state
|
|
12
|
+
// - emit events (datachange)
|
|
13
|
+
// - notify loading / error state via grid callbacks
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
DataMode,
|
|
17
|
+
RowData,
|
|
18
|
+
SortSnapshot,
|
|
19
|
+
FilterSnapshot,
|
|
20
|
+
TransportParams,
|
|
21
|
+
TransportResult,
|
|
22
|
+
TransportFn,
|
|
23
|
+
RemoteConfig,
|
|
24
|
+
EzGridModel,
|
|
25
|
+
EzGridController,
|
|
26
|
+
EzGridControllerState,
|
|
27
|
+
EventCallback
|
|
28
|
+
} from '../types.js';
|
|
29
|
+
|
|
30
|
+
// NOTE: Remote transport contract:
|
|
31
|
+
// transport(params) => Promise<{ data: RowData[], total?: number }>
|
|
32
|
+
//
|
|
33
|
+
// - params is a plain object (paging, filters, sort, etc.)
|
|
34
|
+
// - controller does NOT know how data is fetched
|
|
35
|
+
// - transport must return an object with "data"
|
|
36
|
+
// - EzGridRemote provides the transport implementation
|
|
37
|
+
|
|
38
|
+
export interface EzGridControllerRef {
|
|
39
|
+
_lifecycle?: {
|
|
40
|
+
onControllerDataChange?: () => void;
|
|
41
|
+
};
|
|
42
|
+
_onLoadingChanged?: (loading: boolean) => void;
|
|
43
|
+
_onError?: (error: Error | null) => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type EventListeners = Record<string, EventCallback[]>;
|
|
47
|
+
|
|
48
|
+
export function createEzGridController(grid: EzGridControllerRef): EzGridController {
|
|
49
|
+
const listeners: EventListeners = Object.create(null);
|
|
50
|
+
|
|
51
|
+
function devWarn(message: string): void {
|
|
52
|
+
// NOTE: Dev-only warnings. Never executed in production builds.
|
|
53
|
+
if (import.meta?.env?.DEV) {
|
|
54
|
+
console.warn(`[EzGridController] ${message}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function on(event: string, fn: EventCallback): void {
|
|
59
|
+
(listeners[event] ??= []).push(fn);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function off(event: string, fn: EventCallback): void {
|
|
63
|
+
if (!listeners[event]) return;
|
|
64
|
+
listeners[event] = listeners[event].filter(l => l !== fn);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function emit(event: string, payload?: unknown): void {
|
|
68
|
+
// WARNING: 'datachange' is the ONLY event that triggers grid refresh.
|
|
69
|
+
// Do NOT emit custom events that affect rendering.
|
|
70
|
+
if (event !== 'datachange') {
|
|
71
|
+
devWarn(
|
|
72
|
+
`Emitting non-contract event "${event}". ` +
|
|
73
|
+
`Only "datachange" is allowed to affect grid rendering.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!listeners[event]) return;
|
|
78
|
+
for (const fn of listeners[event]) {
|
|
79
|
+
fn(payload);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ======================================================
|
|
84
|
+
// PUBLIC CONTROLLER API
|
|
85
|
+
// ------------------------------------------------------
|
|
86
|
+
// The following methods are intended to be used by:
|
|
87
|
+
// - EzGrid
|
|
88
|
+
// - Grid behaviors
|
|
89
|
+
// - Application code (advanced use)
|
|
90
|
+
//
|
|
91
|
+
// Public methods:
|
|
92
|
+
// - load()
|
|
93
|
+
// - reload()
|
|
94
|
+
// - requestReload(reason)
|
|
95
|
+
// - setPage(page)
|
|
96
|
+
// - setPageSize(size)
|
|
97
|
+
// - setSortSnapshot(snapshot)
|
|
98
|
+
// - setFilterSnapshot(snapshot)
|
|
99
|
+
//
|
|
100
|
+
// WARNING:
|
|
101
|
+
// - Methods not listed above are considered INTERNAL.
|
|
102
|
+
// - Internal methods may change without notice.
|
|
103
|
+
// - Do NOT call transport directly from outside.
|
|
104
|
+
// ======================================================
|
|
105
|
+
|
|
106
|
+
const controller: EzGridController = {
|
|
107
|
+
grid: grid as any,
|
|
108
|
+
|
|
109
|
+
on,
|
|
110
|
+
off,
|
|
111
|
+
emit,
|
|
112
|
+
|
|
113
|
+
// NOTE: Prevents concurrent remote loads.
|
|
114
|
+
_isLoadingRemote: false,
|
|
115
|
+
|
|
116
|
+
// NOTE: Ensures page/pageSize changes during an active load are not lost.
|
|
117
|
+
_pendingRemoteReload: false,
|
|
118
|
+
|
|
119
|
+
// NOTE: Latest sort snapshot provided by the grid.
|
|
120
|
+
_lastSortSnapshot: null,
|
|
121
|
+
|
|
122
|
+
// NOTE: Latest filters snapshot provided by the grid.
|
|
123
|
+
_lastFilterSnapshot: null,
|
|
124
|
+
|
|
125
|
+
// NOTE: Controller state is the single source of truth for grid data.
|
|
126
|
+
// Grid rendering reacts to changes emitted via events.
|
|
127
|
+
state: {
|
|
128
|
+
data: [],
|
|
129
|
+
loading: false,
|
|
130
|
+
error: null,
|
|
131
|
+
|
|
132
|
+
page: 1,
|
|
133
|
+
pageSize: 25,
|
|
134
|
+
total: 0,
|
|
135
|
+
|
|
136
|
+
// NOTE: Data mode.
|
|
137
|
+
// 'local' is the default and only active mode for now.
|
|
138
|
+
// 'remote' is a contract placeholder.
|
|
139
|
+
mode: 'local' as DataMode
|
|
140
|
+
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// ---- lifecycle ----
|
|
144
|
+
async load(params: Partial<TransportParams> = {}) {
|
|
145
|
+
// NOTE: Local mode does nothing.
|
|
146
|
+
if (this.state.mode !== 'remote') {
|
|
147
|
+
if (import.meta?.env?.DEV) {
|
|
148
|
+
console.warn(
|
|
149
|
+
'[EzGridController] load() called in local mode. No action taken.'
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const transport = this._remoteConfig?.transport;
|
|
156
|
+
|
|
157
|
+
if (typeof transport !== 'function') {
|
|
158
|
+
if (import.meta?.env?.DEV) {
|
|
159
|
+
console.warn(
|
|
160
|
+
'[EzGridController] Remote load called without a valid transport.'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (this._isLoadingRemote === true) {
|
|
167
|
+
// NOTE: Do not drop reload requests triggered during an active load.
|
|
168
|
+
this._pendingRemoteReload = true;
|
|
169
|
+
|
|
170
|
+
if (import.meta?.env?.DEV) {
|
|
171
|
+
console.warn(
|
|
172
|
+
'[EzGridController] Remote load deferred: already loading.'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.setLoading(true);
|
|
179
|
+
this.setError(null);
|
|
180
|
+
|
|
181
|
+
// Clear current data before loading new data
|
|
182
|
+
this.state.data = [];
|
|
183
|
+
emit('datachange', this.state.data);
|
|
184
|
+
|
|
185
|
+
this._isLoadingRemote = true;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
// WARNING: transport execution is centralized here.
|
|
189
|
+
// Controller is the only layer allowed to call it.
|
|
190
|
+
const finalParams: TransportParams = {
|
|
191
|
+
page: this.state.page,
|
|
192
|
+
pageSize: this.state.pageSize,
|
|
193
|
+
sort: this._lastSortSnapshot,
|
|
194
|
+
filters: this._lastFilterSnapshot,
|
|
195
|
+
...params
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const result = await transport(finalParams);
|
|
199
|
+
|
|
200
|
+
const data: RowData[] = Array.isArray(result?.data)
|
|
201
|
+
? result.data
|
|
202
|
+
: [];
|
|
203
|
+
|
|
204
|
+
const total =
|
|
205
|
+
typeof result?.total === 'number'
|
|
206
|
+
? result.total
|
|
207
|
+
: data.length;
|
|
208
|
+
|
|
209
|
+
this.setData(data, { total });
|
|
210
|
+
} catch (err) {
|
|
211
|
+
this.setError(err as Error);
|
|
212
|
+
|
|
213
|
+
if (import.meta?.env?.DEV) {
|
|
214
|
+
console.error(
|
|
215
|
+
'[EzGridController] Remote load failed:',
|
|
216
|
+
err
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
this._isLoadingRemote = false;
|
|
221
|
+
this.setLoading(false);
|
|
222
|
+
|
|
223
|
+
if (this._pendingRemoteReload === true) {
|
|
224
|
+
this._pendingRemoteReload = false;
|
|
225
|
+
// NOTE: Replay the most recent requested reload.
|
|
226
|
+
this.load();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// ======================================================
|
|
232
|
+
// STATE SETTERS
|
|
233
|
+
// ------------------------------------------------------
|
|
234
|
+
// WARNING:
|
|
235
|
+
// - These methods mutate controller state.
|
|
236
|
+
// - Some of them MAY request a reload in remote mode.
|
|
237
|
+
// - They must NEVER perform transport logic directly.
|
|
238
|
+
// ======================================================
|
|
239
|
+
|
|
240
|
+
// ---- paging ----
|
|
241
|
+
setPage(page: number) {
|
|
242
|
+
const next = Number(page);
|
|
243
|
+
if (!Number.isInteger(next) || next < 1) return;
|
|
244
|
+
|
|
245
|
+
if (this.state.page === next) return;
|
|
246
|
+
|
|
247
|
+
this.state.page = next;
|
|
248
|
+
|
|
249
|
+
if (this.state.mode === 'remote') {
|
|
250
|
+
this.requestReload('page-change');
|
|
251
|
+
} else {
|
|
252
|
+
// Local mode: trigger body refresh
|
|
253
|
+
this.grid?._lifecycle?.onControllerDataChange?.();
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
setPageSize(size: number) {
|
|
258
|
+
const next = Number(size);
|
|
259
|
+
if (!Number.isInteger(next) || next < 1) return;
|
|
260
|
+
|
|
261
|
+
if (this.state.pageSize === next) return;
|
|
262
|
+
|
|
263
|
+
this.state.pageSize = next;
|
|
264
|
+
this.state.page = 1;
|
|
265
|
+
|
|
266
|
+
if (this.state.mode === 'remote') {
|
|
267
|
+
this.requestReload('pageSize-change');
|
|
268
|
+
} else {
|
|
269
|
+
// Local mode: trigger body refresh
|
|
270
|
+
this.grid?._lifecycle?.onControllerDataChange?.();
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
// ---- sort bridge (remote) ----
|
|
275
|
+
setSortSnapshot(snapshot: SortSnapshot | null) {
|
|
276
|
+
// NOTE: Snapshot is owned by EzGridSort.
|
|
277
|
+
// Controller only stores and forwards it to transport.
|
|
278
|
+
this._lastSortSnapshot = snapshot;
|
|
279
|
+
|
|
280
|
+
if (this.state.mode === 'remote') {
|
|
281
|
+
this.requestReload('sort-change');
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
// ---- filters bridge (remote contract) ----
|
|
286
|
+
setFilterSnapshot(snapshot: FilterSnapshot[] | null) {
|
|
287
|
+
// NOTE: Snapshot is owned by the grid/filter system.
|
|
288
|
+
// Controller only stores and forwards it to transport.
|
|
289
|
+
this._lastFilterSnapshot = snapshot;
|
|
290
|
+
|
|
291
|
+
if (this.state.mode === 'remote') {
|
|
292
|
+
this.requestReload('filter-change');
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
setData(data: RowData[], meta: { total?: number } = {}) {
|
|
297
|
+
let processedData = Array.isArray(data) ? data : [];
|
|
298
|
+
|
|
299
|
+
// Process through model if defined
|
|
300
|
+
if (this._model && processedData.length > 0) {
|
|
301
|
+
processedData = this._model.processAll(processedData);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.state.data = processedData;
|
|
305
|
+
this.state.total = meta.total ?? this.state.data.length;
|
|
306
|
+
|
|
307
|
+
emit('datachange', this.state.data);
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
setLoading(value: boolean) {
|
|
311
|
+
// NOTE: Loading state notification is delegated to the grid lifecycle.
|
|
312
|
+
// Controller must not assume how loading is rendered.
|
|
313
|
+
this.state.loading = !!value;
|
|
314
|
+
grid._onLoadingChanged?.(this.state.loading);
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
setError(error: Error | null) {
|
|
318
|
+
// NOTE: Error propagation is delegated to the grid lifecycle.
|
|
319
|
+
// Controller must not perform UI-side error handling.
|
|
320
|
+
this.state.error = error;
|
|
321
|
+
grid._onError?.(error);
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
// ======================================================
|
|
325
|
+
// ACTIONS (DATA LOADING)
|
|
326
|
+
// ------------------------------------------------------
|
|
327
|
+
// WARNING:
|
|
328
|
+
// - These methods may trigger remote IO.
|
|
329
|
+
// - load() executes the transport.
|
|
330
|
+
// - reload() is an alias for load().
|
|
331
|
+
// - requestReload() is the preferred entry point.
|
|
332
|
+
// ======================================================
|
|
333
|
+
|
|
334
|
+
reload() {
|
|
335
|
+
return this.load();
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
// ---- reload orchestration (DX) ----
|
|
339
|
+
requestReload(reason: string = 'unspecified') {
|
|
340
|
+
// NOTE: Centralized reload request entry point.
|
|
341
|
+
// This method does NOT change behavior.
|
|
342
|
+
// It exists to make reload intent explicit and traceable.
|
|
343
|
+
|
|
344
|
+
if (import.meta?.env?.DEV) {
|
|
345
|
+
console.debug(
|
|
346
|
+
`[EzGridController] Reload requested (${reason})`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return this.reload();
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
setRemoteConfig(config: Partial<RemoteConfig & { transport?: TransportFn }> = {}) {
|
|
354
|
+
this.state.mode = 'remote';
|
|
355
|
+
|
|
356
|
+
this._remoteConfig = {
|
|
357
|
+
transport: typeof config.transport === 'function'
|
|
358
|
+
? config.transport
|
|
359
|
+
: null,
|
|
360
|
+
|
|
361
|
+
autoLoad: config.autoLoad === true,
|
|
362
|
+
pageSize:
|
|
363
|
+
typeof config.pageSize === 'number'
|
|
364
|
+
? config.pageSize
|
|
365
|
+
: this.state.pageSize
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Apply pageSize from config
|
|
369
|
+
if (typeof config.pageSize === 'number') {
|
|
370
|
+
this.state.pageSize = config.pageSize;
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
appendData(data: RowData[]) {
|
|
375
|
+
if (Array.isArray(data)) {
|
|
376
|
+
// Process through model if defined
|
|
377
|
+
const processed = this._model
|
|
378
|
+
? this._model.processAll(data)
|
|
379
|
+
: data;
|
|
380
|
+
|
|
381
|
+
this.state.data = this.state.data.concat(processed);
|
|
382
|
+
emit('datachange', this.state.data);
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
clear() {
|
|
387
|
+
this.state.data = [];
|
|
388
|
+
this.state.total = 0;
|
|
389
|
+
emit('datachange', this.state.data);
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
// ======================================================
|
|
393
|
+
// MODEL-AWARE RECORD OPERATIONS
|
|
394
|
+
// ------------------------------------------------------
|
|
395
|
+
// These methods leverage the model (if defined) to process
|
|
396
|
+
// records before adding/updating them in the grid.
|
|
397
|
+
// ======================================================
|
|
398
|
+
|
|
399
|
+
addRecord(record: RowData) {
|
|
400
|
+
const processed = this._model
|
|
401
|
+
? this._model.process(record)
|
|
402
|
+
: record;
|
|
403
|
+
|
|
404
|
+
this.state.data = [...this.state.data, processed];
|
|
405
|
+
this.state.total = this.state.data.length;
|
|
406
|
+
emit('datachange', this.state.data);
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
updateRecord(key: unknown, updates: Partial<RowData>): boolean {
|
|
410
|
+
const primaryKey = this._model?.primaryKey || 'id';
|
|
411
|
+
const idx = this.state.data.findIndex(r => r[primaryKey] === key);
|
|
412
|
+
|
|
413
|
+
if (idx < 0) {
|
|
414
|
+
if (import.meta?.env?.DEV) {
|
|
415
|
+
console.warn(
|
|
416
|
+
`[EzGridController] updateRecord: No record found with ${primaryKey}="${key}"`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const merged = { ...this.state.data[idx], ...updates };
|
|
423
|
+
const processed = this._model
|
|
424
|
+
? this._model.process(merged)
|
|
425
|
+
: merged;
|
|
426
|
+
|
|
427
|
+
this.state.data[idx] = processed;
|
|
428
|
+
this.state.data = [...this.state.data]; // Trigger reactivity
|
|
429
|
+
emit('datachange', this.state.data);
|
|
430
|
+
return true;
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
removeRecord(key: unknown): boolean {
|
|
434
|
+
const primaryKey = this._model?.primaryKey || 'id';
|
|
435
|
+
const idx = this.state.data.findIndex(r => r[primaryKey] === key);
|
|
436
|
+
|
|
437
|
+
if (idx < 0) {
|
|
438
|
+
if (import.meta?.env?.DEV) {
|
|
439
|
+
console.warn(
|
|
440
|
+
`[EzGridController] removeRecord: No record found with ${primaryKey}="${key}"`
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
this.state.data.splice(idx, 1);
|
|
447
|
+
this.state.data = [...this.state.data]; // Trigger reactivity
|
|
448
|
+
this.state.total = this.state.data.length;
|
|
449
|
+
emit('datachange', this.state.data);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
return controller;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Re-export types for backwards compatibility
|
|
459
|
+
export type {
|
|
460
|
+
DataMode,
|
|
461
|
+
SortSnapshot,
|
|
462
|
+
FilterSnapshot,
|
|
463
|
+
TransportParams,
|
|
464
|
+
TransportResult,
|
|
465
|
+
TransportFn,
|
|
466
|
+
RemoteConfig,
|
|
467
|
+
EzGridModel,
|
|
468
|
+
EzGridController,
|
|
469
|
+
EzGridControllerState
|
|
470
|
+
} from '../types.js';
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// EzGrid/state/EzGridLifecycle.ts
|
|
2
|
+
|
|
3
|
+
import type { EzGridBodyRef } from '../types.js';
|
|
4
|
+
|
|
5
|
+
export interface EzGridLifecycleRef {
|
|
6
|
+
el?: HTMLElement | null;
|
|
7
|
+
_suppressSelectionPersistence?: boolean;
|
|
8
|
+
_bodyInstance?: EzGridBodyRef;
|
|
9
|
+
_refreshBodyInternal: () => Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class EzGridLifecycle {
|
|
13
|
+
grid: EzGridLifecycleRef;
|
|
14
|
+
private _isRefreshing: boolean;
|
|
15
|
+
private _pendingRefresh: boolean;
|
|
16
|
+
private _refreshSuspended: boolean;
|
|
17
|
+
private _hydrated: boolean;
|
|
18
|
+
private _loading: boolean;
|
|
19
|
+
private _error: Error | null;
|
|
20
|
+
|
|
21
|
+
constructor(grid: EzGridLifecycleRef) {
|
|
22
|
+
this.grid = grid;
|
|
23
|
+
|
|
24
|
+
// WARNING: Lifecycle owns refresh orchestration ONLY.
|
|
25
|
+
// Rendering and side effects remain in EzGrid executor methods.
|
|
26
|
+
this._isRefreshing = false;
|
|
27
|
+
this._pendingRefresh = false;
|
|
28
|
+
this._refreshSuspended = false;
|
|
29
|
+
|
|
30
|
+
// NOTE: Tracks whether the grid has completed its first successful hydration.
|
|
31
|
+
// This controls when selection persistence is allowed.
|
|
32
|
+
this._hydrated = false;
|
|
33
|
+
|
|
34
|
+
this._loading = false;
|
|
35
|
+
this._error = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
onControllerDataChange(): void {
|
|
39
|
+
// WARNING: If the grid is not rendered yet, we must NOT attempt a refresh.
|
|
40
|
+
// The request must be deferred, otherwise the first refresh can be lost.
|
|
41
|
+
if (!this.grid?.el) {
|
|
42
|
+
this._pendingRefresh = true;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this._refreshSuspended) {
|
|
47
|
+
this._pendingRefresh = true;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (this._isRefreshing) {
|
|
52
|
+
this._pendingRefresh = true;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
void this._runRefresh();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
markHydrated(): void {
|
|
60
|
+
// WARNING: This must be called exactly once,
|
|
61
|
+
// after the first successful body refresh.
|
|
62
|
+
if (this._hydrated === true) return;
|
|
63
|
+
|
|
64
|
+
this._hydrated = true;
|
|
65
|
+
|
|
66
|
+
// NOTE: Selection persistence is enabled only after hydration.
|
|
67
|
+
this.grid._suppressSelectionPersistence = false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async _runRefresh(): Promise<void> {
|
|
72
|
+
this._isRefreshing = true;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await this.grid._refreshBodyInternal();
|
|
76
|
+
} finally {
|
|
77
|
+
this._isRefreshing = false;
|
|
78
|
+
|
|
79
|
+
if (this._pendingRefresh) {
|
|
80
|
+
// WARNING: Must clear flag BEFORE rerun to avoid endless loops.
|
|
81
|
+
this._pendingRefresh = false;
|
|
82
|
+
await this._runRefresh();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async suspend<T>(fn: () => Promise<T>): Promise<void> {
|
|
88
|
+
this._refreshSuspended = true;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await fn();
|
|
92
|
+
} finally {
|
|
93
|
+
this._refreshSuspended = false;
|
|
94
|
+
|
|
95
|
+
if (this._pendingRefresh) {
|
|
96
|
+
this._pendingRefresh = false;
|
|
97
|
+
await this._runRefresh();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
destroy(): void {
|
|
103
|
+
// WARNING: Cleanup must be idempotent.
|
|
104
|
+
this._pendingRefresh = false;
|
|
105
|
+
this._refreshSuspended = false;
|
|
106
|
+
this._isRefreshing = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onLoadingChanged(loading: boolean): void {
|
|
110
|
+
// NOTE: Lifecycle receives loading state changes.
|
|
111
|
+
// It must NOT re-dispatch them back to the grid,
|
|
112
|
+
// otherwise a recursion loop will occur.
|
|
113
|
+
|
|
114
|
+
this._loading = loading;
|
|
115
|
+
|
|
116
|
+
// Notify body instance of loading state
|
|
117
|
+
const body = this.grid._bodyInstance;
|
|
118
|
+
if (body?.setLoading) {
|
|
119
|
+
body.setLoading(loading);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onError(error: Error | null): void {
|
|
124
|
+
// NOTE: Lifecycle receives error state.
|
|
125
|
+
// It must NOT re-dispatch back to the grid.
|
|
126
|
+
|
|
127
|
+
this._error = error;
|
|
128
|
+
|
|
129
|
+
// Notify body instance of error state
|
|
130
|
+
const body = this.grid._bodyInstance;
|
|
131
|
+
if (body?.setError) {
|
|
132
|
+
body.setError(error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
}
|