@warkypublic/svelix 0.1.46 → 0.1.47
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/README.md +93 -33
- package/dist/components/BetterMenu/BetterMenu.svelte +21 -37
- package/dist/components/BetterMenu/BetterMenuAsyncButton.svelte +21 -31
- package/dist/components/BetterMenu/BetterMenuPreview.svelte +27 -29
- package/dist/components/BetterMenu/MenuRenderer.svelte +40 -51
- package/dist/components/Boxer/Boxer.svelte +488 -602
- package/dist/components/Boxer/BoxerTarget.svelte +6 -46
- package/dist/components/Boxer/BoxerTarget.svelte.d.ts +1 -0
- package/dist/components/Boxer/types.d.ts +0 -1
- package/dist/components/Button.svelte +19 -43
- package/dist/components/ButtonPreview.svelte +2 -11
- package/dist/components/CardGrid/CardGrid.svelte +443 -614
- package/dist/components/CardGrid/CardGridFilterPanel.svelte +111 -90
- package/dist/components/CardGrid/DefaultCard.svelte +80 -96
- package/dist/components/CardGrid/ImageCardStory.svelte +87 -36
- package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
- package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
- package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
- package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
- package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
- package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
- package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
- package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
- package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
- package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
- package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
- package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
- package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
- package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
- package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
- package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
- package/dist/components/Former/Former.svelte +306 -424
- package/dist/components/Former/FormerButtonArea.svelte +17 -68
- package/dist/components/Former/FormerDrawer.svelte +26 -78
- package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
- package/dist/components/Former/FormerModal.svelte +24 -70
- package/dist/components/Former/FormerModalPreview.svelte +99 -89
- package/dist/components/Former/FormerPreview.svelte +76 -78
- package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
- package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -44
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +280 -445
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
- package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -42
- package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
- package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
- package/dist/components/FormerControllers/NativeSelectCtrl.svelte +3 -29
- package/dist/components/FormerControllers/NumberInputCtrl.svelte +9 -35
- package/dist/components/FormerControllers/PasswordInputCtrl.svelte +4 -23
- package/dist/components/FormerControllers/SwitchCtrl.svelte +3 -20
- package/dist/components/FormerControllers/TextAreaCtrl.svelte +3 -24
- package/dist/components/FormerControllers/TextInputCtrl.svelte +3 -24
- package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
- package/dist/components/Gridler/components/Gridler.svelte +338 -442
- package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
- package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
- package/dist/components/Gridler/components/GridlerFull.svelte +520 -841
- package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
- package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
- package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
- package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
- package/dist/components/Gridler/types.d.ts +1 -1
- package/dist/components/Icons/IconAdd.svelte +1 -3
- package/dist/components/Icons/IconAlertCircle.svelte +1 -3
- package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
- package/dist/components/Icons/IconCamera.svelte +1 -3
- package/dist/components/Icons/IconClose.svelte +1 -3
- package/dist/components/Icons/IconEdit.svelte +1 -3
- package/dist/components/Icons/IconFilter.svelte +1 -3
- package/dist/components/Icons/IconSearch.svelte +1 -3
- package/dist/components/Icons/IconSort.svelte +1 -3
- package/dist/components/Icons/IconTrash.svelte +1 -3
- package/dist/components/Portal/Portal.svelte +14 -38
- package/dist/components/Screenshot/Screenshot.svelte +7 -13
- package/dist/components/Svark/Svark.svelte +680 -852
- package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
- package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
- package/dist/components/Svark/SvarkPager.svelte +2 -28
- package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
- package/dist/components/Svark/SvarkTopBar.svelte +9 -74
- package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
- package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
- package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
- package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
- package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
- package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
- package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
- package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
- package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
- package/dist/components/VTree/VTree.svelte +349 -431
- package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
- package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
- package/dist/components/VTree/VTreeRow.svelte +13 -65
- package/dist/components/VTree/VTreeSearch.svelte +6 -13
- package/dist/components/VTree/VTreeVirtualViewport.svelte +37 -106
- package/package.json +30 -29
|
@@ -1,1177 +1,931 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
selectedItemsToIds,
|
|
31
|
-
sortOrderToSvarkSortState,
|
|
32
|
-
svarkSortStateToSortMarks,
|
|
33
|
-
svarkSortStateToSortOrder,
|
|
34
|
-
toCssSize
|
|
35
|
-
} from './utils/svarGridMapping.js';
|
|
36
|
-
import SvarkHeaderFilterCell from './components/SvarkHeaderFilterCell.svelte';
|
|
37
|
-
import { SVARKGRID_DEFAULT_ITEMS_MARKER_ID } from './utils/contextMenu.js';
|
|
38
|
-
|
|
39
|
-
export type SvarProps = Partial<
|
|
40
|
-
Omit<
|
|
41
|
-
SvarkProps,
|
|
42
|
-
// Generic-first equivalents / resolved from GridCommonProps:
|
|
43
|
-
| 'columns'
|
|
44
|
-
| 'height'
|
|
45
|
-
// Server config is generic-first but allowed as fallback:
|
|
46
|
-
| 'url'
|
|
47
|
-
| 'baseUrl'
|
|
48
|
-
| 'token'
|
|
49
|
-
| 'schema'
|
|
50
|
-
| 'entity'
|
|
51
|
-
>
|
|
52
|
-
> & {
|
|
53
|
-
columns?: IColumnConfig[];
|
|
54
|
-
url?: string;
|
|
55
|
-
baseUrl?: string;
|
|
56
|
-
token?: string;
|
|
57
|
-
schema?: string;
|
|
58
|
-
entity?: string;
|
|
59
|
-
height?: string | number;
|
|
60
|
-
config?: Omit<IConfig, 'data'>;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
export interface Props extends GridCommonProps<Record<string, unknown>, unknown> {
|
|
64
|
-
columns: Array<GridColumn<Record<string, unknown>, unknown>>;
|
|
65
|
-
/** Svark/SVAR-grid escape hatch props. Generic props take precedence when overlapping. */
|
|
66
|
-
SvarProps?: SvarProps;
|
|
67
|
-
/** Used only when `searchValue` is undefined (uncontrolled). */
|
|
68
|
-
defaultSearchValue?: string;
|
|
69
|
-
/** Rows per fetch/page. If omitted, falls back to `SvarProps.pageSize` then 100. */
|
|
70
|
-
pageSize?: number;
|
|
71
|
-
/** Enables SVAR virtual scrolling. If omitted, falls back to `SvarProps.virtualScroll` then false. */
|
|
72
|
-
virtualScroll?: boolean;
|
|
73
|
-
/** Enables SVAR infinite scrolling. If omitted, falls back to `SvarProps.infiniteScroll` then false. */
|
|
74
|
-
infiniteScroll?: boolean;
|
|
75
|
-
/** Enables SVAR row selection. If omitted, falls back to `SvarProps.select` then true. */
|
|
76
|
-
select?: boolean;
|
|
77
|
-
/** Enables SVAR multi-row selection. If omitted, falls back to `SvarProps.multiselect` then false. */
|
|
78
|
-
multiselect?: boolean;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
let {
|
|
82
|
-
columns,
|
|
83
|
-
data: dataProp,
|
|
84
|
-
width,
|
|
85
|
-
height,
|
|
86
|
-
rowHeight,
|
|
87
|
-
headerHeight,
|
|
88
|
-
onColumnMoved: _onColumnMoved,
|
|
89
|
-
sortOrder,
|
|
90
|
-
onSortOrderChange,
|
|
91
|
-
filters,
|
|
92
|
-
onFilterChange,
|
|
93
|
-
searchValue,
|
|
94
|
-
onSearchValueChange,
|
|
95
|
-
selectedItems,
|
|
96
|
-
onSelectedItemsChange,
|
|
97
|
-
dataSource,
|
|
98
|
-
dataSourceOptions,
|
|
99
|
-
onDataChange,
|
|
100
|
-
settings,
|
|
101
|
-
onCellEvent,
|
|
102
|
-
onGridEvent,
|
|
103
|
-
onMenuClick,
|
|
104
|
-
SvarProps: svarProps = {},
|
|
105
|
-
defaultSearchValue = '',
|
|
106
|
-
pageSize: pageSizeProp,
|
|
107
|
-
virtualScroll: virtualScrollProp,
|
|
108
|
-
infiniteScroll: infiniteScrollProp,
|
|
109
|
-
select: selectProp,
|
|
110
|
-
multiselect: multiselectProp
|
|
111
|
-
}: Props = $props();
|
|
112
|
-
|
|
113
|
-
function normalizeAdapterType(t: SvarkAdapterType): 'ResolveSpec' | 'RestHeaderSpec' {
|
|
114
|
-
return t === 'header' || t === 'RestHeaderSpec' ? 'RestHeaderSpec' : 'ResolveSpec';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const resolvedUrl = $derived(dataSourceOptions?.url ?? svarProps.url ?? svarProps.baseUrl ?? '');
|
|
118
|
-
const resolvedToken = $derived(dataSourceOptions?.authToken ?? svarProps.token);
|
|
119
|
-
const resolvedSchema = $derived(dataSourceOptions?.schema ?? svarProps.schema ?? '');
|
|
120
|
-
const resolvedEntity = $derived(dataSourceOptions?.entity ?? svarProps.entity ?? '');
|
|
121
|
-
const resolvedUniqueID = $derived(dataSourceOptions?.uniqueID ?? 'id');
|
|
122
|
-
|
|
123
|
-
const resolvedAdapterType = $derived(
|
|
124
|
-
normalizeAdapterType(
|
|
125
|
-
svarProps.adapterType ??
|
|
126
|
-
svarProps.clientType ??
|
|
127
|
-
adapterKindFromDataSource(dataSource)
|
|
128
|
-
)
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
const resolvedServerSide = $derived(
|
|
132
|
-
svarProps.serverSide ??
|
|
133
|
-
(!!dataSource && dataSource !== 'local' && dataSource !== 'custom')
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
const resolvedResizableColumns = $derived(settings?.resizableColumns ?? true);
|
|
137
|
-
const resolvedHideHeader = $derived(settings?.hideHeader ?? false);
|
|
138
|
-
const resolvedReadonly = $derived(settings?.readonly ?? false);
|
|
139
|
-
const enableColumnFilters = $derived(resolvedServerSide || dataSource === 'local');
|
|
140
|
-
|
|
141
|
-
const resolvedHeight = $derived(height ?? svarProps.height ?? '400px');
|
|
142
|
-
const resolvedWidth = $derived(width ?? '100%');
|
|
143
|
-
|
|
144
|
-
const baseColumns = $derived.by((): IColumnConfig[] => {
|
|
145
|
-
if (svarProps.columns?.length) return svarProps.columns;
|
|
146
|
-
return mapGenericColumnsToSvarColumns(columns, {
|
|
147
|
-
serverSide: resolvedServerSide,
|
|
148
|
-
resizableColumns: resolvedResizableColumns,
|
|
149
|
-
hideHeader: resolvedHideHeader
|
|
150
|
-
});
|
|
1
|
+
<script lang="ts">import SvarkGridView from "./internal/SvarkGridView.svelte";
|
|
2
|
+
import { SvarkResolveSpecAdapter } from "./adapters/SvarkResolveSpecAdapter.js";
|
|
3
|
+
import { SvarkRestHeaderSpecAdapter } from "./adapters/SvarkRestHeaderSpecAdapter.js";
|
|
4
|
+
import { adapterKindFromDataSource, gridColumnFiltersToSvarkFilterState, idsToSelectedItems, mapGenericColumnsToSvarColumns, selectedItemsToIds, sortOrderToSvarkSortState, svarkSortStateToSortMarks, svarkSortStateToSortOrder, toCssSize } from "./utils/svarGridMapping.js";
|
|
5
|
+
import SvarkHeaderFilterCell from "./components/SvarkHeaderFilterCell.svelte";
|
|
6
|
+
import { SVARKGRID_DEFAULT_ITEMS_MARKER_ID } from "./utils/contextMenu.js";
|
|
7
|
+
let { columns, data: dataProp, width, height, rowHeight, headerHeight, onColumnMoved: _onColumnMoved, sortOrder, onSortOrderChange, filters, onFilterChange, searchValue, onSearchValueChange, selectedItems, onSelectedItemsChange, dataSource, dataSourceOptions, onDataChange, settings, onCellEvent, onGridEvent, onMenuClick, SvarProps: svarProps = {}, defaultSearchValue = "", pageSize: pageSizeProp, virtualScroll: virtualScrollProp, infiniteScroll: infiniteScrollProp, select: selectProp, multiselect: multiselectProp } = $props();
|
|
8
|
+
function normalizeAdapterType(t) {
|
|
9
|
+
return t === "header" || t === "RestHeaderSpec" ? "RestHeaderSpec" : "ResolveSpec";
|
|
10
|
+
}
|
|
11
|
+
const resolvedUrl = $derived(dataSourceOptions?.url ?? svarProps.url ?? svarProps.baseUrl ?? "");
|
|
12
|
+
const resolvedToken = $derived(dataSourceOptions?.authToken ?? svarProps.token);
|
|
13
|
+
const resolvedSchema = $derived(dataSourceOptions?.schema ?? svarProps.schema ?? "");
|
|
14
|
+
const resolvedEntity = $derived(dataSourceOptions?.entity ?? svarProps.entity ?? "");
|
|
15
|
+
const resolvedUniqueID = $derived(dataSourceOptions?.uniqueID ?? "id");
|
|
16
|
+
const resolvedAdapterType = $derived(normalizeAdapterType(svarProps.adapterType ?? svarProps.clientType ?? adapterKindFromDataSource(dataSource)));
|
|
17
|
+
const resolvedServerSide = $derived(svarProps.serverSide ?? (!!dataSource && dataSource !== "local" && dataSource !== "custom"));
|
|
18
|
+
const resolvedResizableColumns = $derived(settings?.resizableColumns ?? true);
|
|
19
|
+
const resolvedHideHeader = $derived(settings?.hideHeader ?? false);
|
|
20
|
+
const resolvedReadonly = $derived(settings?.readonly ?? false);
|
|
21
|
+
const enableColumnFilters = $derived(resolvedServerSide || dataSource === "local");
|
|
22
|
+
const resolvedHeight = $derived(height ?? svarProps.height ?? "400px");
|
|
23
|
+
const resolvedWidth = $derived(width ?? "100%");
|
|
24
|
+
const baseColumns = $derived.by(() => {
|
|
25
|
+
if (svarProps.columns?.length) return svarProps.columns;
|
|
26
|
+
return mapGenericColumnsToSvarColumns(columns, {
|
|
27
|
+
serverSide: resolvedServerSide,
|
|
28
|
+
resizableColumns: resolvedResizableColumns,
|
|
29
|
+
hideHeader: resolvedHideHeader
|
|
151
30
|
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const genericColumnsById = $derived.by(() => {
|
|
250
|
-
const map: Record<string, GridColumn<Record<string, unknown>, unknown>> = {};
|
|
251
|
-
for (const col of columns ?? []) map[String(col.id)] = col;
|
|
252
|
-
return map;
|
|
31
|
+
});
|
|
32
|
+
const heightStyle = $derived(toCssSize(resolvedHeight, "400px"));
|
|
33
|
+
const widthStyle = $derived(toCssSize(resolvedWidth, "100%"));
|
|
34
|
+
const pageSize = $derived(pageSizeProp ?? svarProps.pageSize ?? 100);
|
|
35
|
+
const virtualScroll = $derived(virtualScrollProp ?? svarProps.virtualScroll ?? false);
|
|
36
|
+
const infiniteScroll = $derived(infiniteScrollProp ?? svarProps.infiniteScroll ?? false);
|
|
37
|
+
const select = $derived(selectProp ?? svarProps.select ?? true);
|
|
38
|
+
const multiselect = $derived(multiselectProp ?? svarProps.multiselect ?? false);
|
|
39
|
+
const query = $derived(svarProps.query ?? {});
|
|
40
|
+
const config = $derived(svarProps.config ?? {});
|
|
41
|
+
const topBarLeft = $derived(svarProps.topBarLeft);
|
|
42
|
+
const topBarRight = $derived(svarProps.topBarRight);
|
|
43
|
+
const onchange = $derived(svarProps.onchange);
|
|
44
|
+
const onerror = $derived(svarProps.onerror);
|
|
45
|
+
const onselect = $derived(svarProps.onselect);
|
|
46
|
+
const onrowclick = $derived(svarProps.onrowclick);
|
|
47
|
+
const onrowdblclick = $derived(svarProps.onrowdblclick);
|
|
48
|
+
const onrowcontextmenu = $derived(svarProps.onrowcontextmenu);
|
|
49
|
+
const userContextMenuProps = $derived(svarProps.contextMenuProps);
|
|
50
|
+
let data = $state([]);
|
|
51
|
+
let localRawData = $state([]);
|
|
52
|
+
let loading = $state(false);
|
|
53
|
+
let infiniteLoading = $state(false);
|
|
54
|
+
let error = $state(null);
|
|
55
|
+
let total = $state(0);
|
|
56
|
+
let sortState = $state([]);
|
|
57
|
+
let gridFilterState = $state({});
|
|
58
|
+
let searchFilterState = $state({});
|
|
59
|
+
let page = $state(0);
|
|
60
|
+
let reload = $state(0);
|
|
61
|
+
let lastServerRequestKey = $state("");
|
|
62
|
+
let activeServerRequestId = 0;
|
|
63
|
+
let activeServerRequestKey = "";
|
|
64
|
+
let gridApi = $state(null);
|
|
65
|
+
let searchOpen = $state(false);
|
|
66
|
+
// eslint-disable-next-line svelte/no-unused-svelte-ignore
|
|
67
|
+
// svelte-ignore state_referenced_locally -- intentional initial capture; controlled changes handled by $effect
|
|
68
|
+
let searchValueState = $state(defaultSearchValue);
|
|
69
|
+
const effectiveFilterState = $derived({
|
|
70
|
+
...gridFilterState,
|
|
71
|
+
...searchFilterState
|
|
72
|
+
});
|
|
73
|
+
// Paging is active when serverSide with no scroll variant
|
|
74
|
+
const showPager = $derived(resolvedServerSide && !infiniteScroll && !virtualScroll);
|
|
75
|
+
const totalPages = $derived(pageSize > 0 ? Math.ceil(total / pageSize) : 1);
|
|
76
|
+
const pageStart = $derived(total === 0 ? 0 : page * pageSize + 1);
|
|
77
|
+
const pageEnd = $derived(Math.min((page + 1) * pageSize, total));
|
|
78
|
+
// ── Generic controlled/uncontrolled bridges ───────────────────────────────
|
|
79
|
+
let internalSortOrder = $state({});
|
|
80
|
+
$effect(() => {
|
|
81
|
+
if (sortOrder === undefined) return;
|
|
82
|
+
internalSortOrder = sortOrder;
|
|
83
|
+
});
|
|
84
|
+
const resolvedSortOrder = $derived(sortOrder ?? internalSortOrder);
|
|
85
|
+
let internalFilters = $state({});
|
|
86
|
+
$effect(() => {
|
|
87
|
+
if (filters === undefined) return;
|
|
88
|
+
internalFilters = filters;
|
|
89
|
+
});
|
|
90
|
+
const resolvedFilters = $derived(filters ?? internalFilters);
|
|
91
|
+
const instanceId = $state(`svarkgrid-${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`);
|
|
92
|
+
let internalSelectedItems = $state([]);
|
|
93
|
+
$effect(() => {
|
|
94
|
+
if (selectedItems === undefined) return;
|
|
95
|
+
internalSelectedItems = selectedItems;
|
|
96
|
+
});
|
|
97
|
+
const resolvedSelectedItems = $derived(selectedItems ?? internalSelectedItems);
|
|
98
|
+
const resolvedSelectedIds = $derived(selectedItemsToIds(resolvedSelectedItems, resolvedUniqueID));
|
|
99
|
+
function emitSortOrder(next) {
|
|
100
|
+
if (sortOrder === undefined) internalSortOrder = next;
|
|
101
|
+
onSortOrderChange?.(next);
|
|
102
|
+
}
|
|
103
|
+
function emitFilters(next) {
|
|
104
|
+
if (filters === undefined) internalFilters = next;
|
|
105
|
+
onFilterChange?.(next);
|
|
106
|
+
}
|
|
107
|
+
function emitGridEvent(type, item, column, coords, detail) {
|
|
108
|
+
onGridEvent?.(type, item, column, coords, detail);
|
|
109
|
+
}
|
|
110
|
+
const genericColumnsById = $derived.by(() => {
|
|
111
|
+
const map = {};
|
|
112
|
+
for (const col of columns ?? []) map[String(col.id)] = col;
|
|
113
|
+
return map;
|
|
114
|
+
});
|
|
115
|
+
function setColumnFilter(columnId, next) {
|
|
116
|
+
const col = genericColumnsById[columnId];
|
|
117
|
+
const dataKey = col?.dataKey;
|
|
118
|
+
const merged = {
|
|
119
|
+
...resolvedFilters[columnId] ?? {},
|
|
120
|
+
...next,
|
|
121
|
+
...dataKey ? { dataKey } : {}
|
|
122
|
+
};
|
|
123
|
+
emitFilters({
|
|
124
|
+
...resolvedFilters,
|
|
125
|
+
[columnId]: merged
|
|
253
126
|
});
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
function setColumnSort(columnId: string, next: 'none' | 'asc' | 'desc') {
|
|
272
|
-
// Prefer using the grid API so local mode actually sorts rows and
|
|
273
|
-
// server-side mode triggers the existing intercept logic.
|
|
274
|
-
if (gridApi) {
|
|
275
|
-
if (next === 'none') {
|
|
276
|
-
gridApi.exec('sort-rows', {} as any);
|
|
277
|
-
} else {
|
|
278
|
-
gridApi.exec('sort-rows', { key: columnId, add: false, order: next });
|
|
279
|
-
}
|
|
280
|
-
return;
|
|
127
|
+
}
|
|
128
|
+
function clearColumnFilter(columnId) {
|
|
129
|
+
const { [columnId]: _ignored, ...rest } = resolvedFilters;
|
|
130
|
+
emitFilters(rest);
|
|
131
|
+
}
|
|
132
|
+
function setColumnSort(columnId, next) {
|
|
133
|
+
// Prefer using the grid API so local mode actually sorts rows and
|
|
134
|
+
// server-side mode triggers the existing intercept logic.
|
|
135
|
+
if (gridApi) {
|
|
136
|
+
if (next === "none") {
|
|
137
|
+
gridApi.exec("sort-rows", {});
|
|
138
|
+
} else {
|
|
139
|
+
gridApi.exec("sort-rows", {
|
|
140
|
+
key: columnId,
|
|
141
|
+
add: false,
|
|
142
|
+
order: next
|
|
143
|
+
});
|
|
281
144
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const merged = { ...resolvedSortOrder ?? {} };
|
|
148
|
+
merged[columnId] = next;
|
|
149
|
+
emitSortOrder(merged);
|
|
150
|
+
}
|
|
151
|
+
function isGenericColumnFilterDisabled(columnId) {
|
|
152
|
+
const col = genericColumnsById[columnId];
|
|
153
|
+
return col?.disableFilter === true;
|
|
154
|
+
}
|
|
155
|
+
function enhanceColumnHeader(col) {
|
|
156
|
+
const id = col?.id;
|
|
157
|
+
const columnId = id == null ? "" : String(id);
|
|
158
|
+
const queryText = searchValueState.trim();
|
|
159
|
+
// Enable column sorting by default (SVAR Grid expects `sort` opt-out).
|
|
160
|
+
const sort = col.sort ?? true;
|
|
161
|
+
if (resolvedHideHeader) return {
|
|
162
|
+
...col,
|
|
163
|
+
sort,
|
|
164
|
+
header: undefined
|
|
165
|
+
};
|
|
166
|
+
const rawHeader = col.header;
|
|
167
|
+
// Avoid messing with complex multi-row headers.
|
|
168
|
+
if (Array.isArray(rawHeader)) return {
|
|
169
|
+
...col,
|
|
170
|
+
sort
|
|
171
|
+
};
|
|
172
|
+
const headerCfg = typeof rawHeader === "string" ? { text: rawHeader } : rawHeader && typeof rawHeader === "object" ? { ...rawHeader } : { text: columnId };
|
|
173
|
+
const isSearchTarget = !!queryText && !!columnId && !col.hidden && (resolvedServerSide ? col.searchable === true : col.searchable !== false);
|
|
174
|
+
if (!headerCfg.cell) {
|
|
175
|
+
const filterable = enableColumnFilters && !!columnId && !col.hidden && !isGenericColumnFilterDisabled(columnId);
|
|
176
|
+
headerCfg.cell = SvarkHeaderFilterCell;
|
|
177
|
+
headerCfg.__svarkMenuId = `${instanceId}:filter:${columnId}`;
|
|
178
|
+
headerCfg.__svarkFilterable = filterable;
|
|
179
|
+
headerCfg.__svarkColumnId = columnId;
|
|
180
|
+
headerCfg.__svarkColumnLabel = String(headerCfg.text ?? columnId);
|
|
181
|
+
headerCfg.__svarkSearchHighlight = isSearchTarget;
|
|
182
|
+
headerCfg.__svarkCurrentSort = resolvedSortOrder?.[columnId] ?? "none";
|
|
183
|
+
headerCfg.__svarkSetSort = (next) => setColumnSort(columnId, next);
|
|
184
|
+
headerCfg.__svarkCurrentFilter = resolvedFilters[columnId];
|
|
185
|
+
headerCfg.__svarkSetFilter = (next) => setColumnFilter(columnId, next);
|
|
186
|
+
headerCfg.__svarkClearFilter = () => clearColumnFilter(columnId);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
...col,
|
|
190
|
+
sort,
|
|
191
|
+
header: headerCfg
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const gridColumns = $derived.by(() => (baseColumns ?? []).map((col) => enhanceColumnHeader(col)));
|
|
195
|
+
function emitSearchValue(next) {
|
|
196
|
+
if (searchValue === undefined) searchValueState = next;
|
|
197
|
+
onSearchValueChange?.(next);
|
|
198
|
+
}
|
|
199
|
+
function emitSelectedItems(next) {
|
|
200
|
+
if (selectedItems === undefined) internalSelectedItems = next;
|
|
201
|
+
onSelectedItemsChange?.(next);
|
|
202
|
+
}
|
|
203
|
+
$effect(() => {
|
|
204
|
+
sortState = sortOrderToSvarkSortState(resolvedSortOrder);
|
|
205
|
+
});
|
|
206
|
+
$effect(() => {
|
|
207
|
+
gridFilterState = gridColumnFiltersToSvarkFilterState(resolvedFilters, { prefix: "" });
|
|
208
|
+
});
|
|
209
|
+
$effect(() => {
|
|
210
|
+
if (searchValue === undefined) return;
|
|
211
|
+
searchValueState = searchValue;
|
|
212
|
+
});
|
|
213
|
+
// ── Local data mode (dataSource: 'local' | 'custom') ──────────────────────
|
|
214
|
+
$effect(() => {
|
|
215
|
+
if (resolvedServerSide) return;
|
|
216
|
+
void reload;
|
|
217
|
+
error = null;
|
|
218
|
+
loading = false;
|
|
219
|
+
if (!dataProp) {
|
|
220
|
+
localRawData = [];
|
|
221
|
+
data = [];
|
|
222
|
+
total = 0;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
let cancelled = false;
|
|
226
|
+
if (typeof dataProp === "function") {
|
|
227
|
+
loading = true;
|
|
228
|
+
dataProp().then((result) => {
|
|
229
|
+
if (cancelled) return;
|
|
230
|
+
localRawData = result ?? [];
|
|
231
|
+
}).catch((e) => {
|
|
232
|
+
if (cancelled) return;
|
|
233
|
+
const msg = e instanceof Error ? e.message : "Failed to load data";
|
|
234
|
+
error = msg;
|
|
235
|
+
onerror?.(msg);
|
|
236
|
+
}).finally(() => {
|
|
237
|
+
if (!cancelled) loading = false;
|
|
238
|
+
});
|
|
239
|
+
} else {
|
|
240
|
+
localRawData = dataProp ?? [];
|
|
291
241
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
242
|
+
return () => {
|
|
243
|
+
cancelled = true;
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
function valueMatchesLocal(cellValue, filter) {
|
|
247
|
+
const operator = String(filter.operator ?? "eq").toLowerCase();
|
|
248
|
+
const rawValue = filter.value;
|
|
249
|
+
const actual = cellValue ?? "";
|
|
250
|
+
if (operator === "eq") return String(actual) === String(rawValue ?? "");
|
|
251
|
+
if (operator === "neq") return String(actual) !== String(rawValue ?? "");
|
|
252
|
+
if (operator === "gt" || operator === "gte" || operator === "lt" || operator === "lte") {
|
|
253
|
+
const left = Number(actual);
|
|
254
|
+
const right = Number(rawValue);
|
|
255
|
+
if (Number.isNaN(left) || Number.isNaN(right)) return false;
|
|
256
|
+
if (operator === "gt") return left > right;
|
|
257
|
+
if (operator === "gte") return left >= right;
|
|
258
|
+
if (operator === "lt") return left < right;
|
|
259
|
+
return left <= right;
|
|
260
|
+
}
|
|
261
|
+
const haystack = String(actual).toLowerCase();
|
|
262
|
+
let needle = String(rawValue ?? "").toLowerCase();
|
|
263
|
+
if (operator === "like" || operator === "ilike" || operator === "contains") {
|
|
264
|
+
const startsWith = needle.startsWith("%");
|
|
265
|
+
const endsWith = needle.endsWith("%");
|
|
266
|
+
const core = needle.replace(/^%|%$/g, "");
|
|
267
|
+
// If the UI provided a bare string, treat it like "%value%".
|
|
268
|
+
if (!startsWith && !endsWith) return haystack.includes(needle);
|
|
269
|
+
if (startsWith && endsWith) return haystack.includes(core);
|
|
270
|
+
if (startsWith) return haystack.endsWith(core);
|
|
271
|
+
if (endsWith) return haystack.startsWith(core);
|
|
272
|
+
return haystack.includes(core);
|
|
273
|
+
}
|
|
274
|
+
if (operator === "startswith") return haystack.startsWith(needle);
|
|
275
|
+
if (operator === "endswith") return haystack.endsWith(needle);
|
|
276
|
+
return haystack.includes(needle);
|
|
277
|
+
}
|
|
278
|
+
function applyLocalFiltersAndSearch(rows) {
|
|
279
|
+
let result = rows.slice();
|
|
280
|
+
const entries = Object.values(effectiveFilterState);
|
|
281
|
+
const andFilters = entries.filter((f) => String(f.logic_operator ?? "AND").toUpperCase() !== "OR");
|
|
282
|
+
const orFilters = entries.filter((f) => String(f.logic_operator ?? "AND").toUpperCase() === "OR");
|
|
283
|
+
for (const f of andFilters) {
|
|
284
|
+
const col = String(f.column ?? "");
|
|
285
|
+
if (!col) continue;
|
|
286
|
+
result = result.filter((row) => valueMatchesLocal(row[col], f));
|
|
287
|
+
}
|
|
288
|
+
if (orFilters.length > 0) {
|
|
289
|
+
result = result.filter((row) => orFilters.some((f) => {
|
|
290
|
+
const col = String(f.column ?? "");
|
|
291
|
+
if (!col) return false;
|
|
292
|
+
return valueMatchesLocal(row[col], f);
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
const q = searchValueState.trim().toLowerCase();
|
|
296
|
+
if (q) {
|
|
297
|
+
const searchableCols = gridColumns.filter((c) => c?.id != null && !c.hidden && c.searchable !== false).map((c) => String(c.id));
|
|
298
|
+
if (searchableCols.length > 0) {
|
|
299
|
+
result = result.filter((row) => searchableCols.some((id) => String(row[id] ?? "").toLowerCase().includes(q)));
|
|
338
300
|
}
|
|
339
|
-
|
|
340
|
-
return { ...col, sort, header: headerCfg };
|
|
341
301
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
$effect(() => {
|
|
305
|
+
if (resolvedServerSide) return;
|
|
306
|
+
const next = applyLocalFiltersAndSearch(localRawData);
|
|
307
|
+
data = next;
|
|
308
|
+
total = next.length;
|
|
309
|
+
});
|
|
310
|
+
const sortMarks = $derived.by(() => svarkSortStateToSortMarks(sortState));
|
|
311
|
+
const gridConfig = $derived.by(() => {
|
|
312
|
+
const base = config ?? {};
|
|
313
|
+
const sizes = {
|
|
314
|
+
...base.sizes,
|
|
315
|
+
...rowHeight != null ? { rowHeight } : {},
|
|
316
|
+
...headerHeight != null ? { headerHeight } : {}
|
|
317
|
+
};
|
|
318
|
+
const next = {
|
|
319
|
+
...base,
|
|
320
|
+
sizes,
|
|
321
|
+
// Keep SVAR's sort indicators in sync with generic sortOrder.
|
|
322
|
+
...resolvedServerSide ? { sortMarks } : {},
|
|
323
|
+
...selectedItems !== undefined ? { selectedRows: resolvedSelectedIds } : {}
|
|
324
|
+
};
|
|
325
|
+
return next;
|
|
326
|
+
});
|
|
327
|
+
// Debounced mirrors of sortState/filterState used by the main fetch effect.
|
|
328
|
+
// When serverSide is true, changes are held for 300 ms before committing so
|
|
329
|
+
// rapid keystrokes / click-sort chains don't fire a request per event.
|
|
330
|
+
// Page resets to 0 atomically with the debounced update so only one fetch fires.
|
|
331
|
+
let debouncedSort = $state([]);
|
|
332
|
+
let debouncedFilter = $state({});
|
|
333
|
+
let _debounceTimer = null;
|
|
334
|
+
function isSameSortState(a, b) {
|
|
335
|
+
if (a.length !== b.length) return false;
|
|
336
|
+
return a.every((item, index) => item.key === b[index]?.key && item.order === b[index]?.order);
|
|
337
|
+
}
|
|
338
|
+
function isSameFilterState(a, b) {
|
|
339
|
+
const aKeys = Object.keys(a);
|
|
340
|
+
const bKeys = Object.keys(b);
|
|
341
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
342
|
+
for (const key of aKeys) {
|
|
343
|
+
const left = a[key];
|
|
344
|
+
const right = b[key];
|
|
345
|
+
if (!right || left?.column !== right.column || left?.operator !== right.operator || left?.logic_operator !== right.logic_operator || left?.value !== right.value) {
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
353
348
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
// ── Local data mode (dataSource: 'local' | 'custom') ──────────────────────
|
|
369
|
-
|
|
370
|
-
$effect(() => {
|
|
371
|
-
if (resolvedServerSide) return;
|
|
372
|
-
|
|
373
|
-
void reload; // allow Refresh to re-run local loaders
|
|
374
|
-
|
|
375
|
-
error = null;
|
|
376
|
-
loading = false;
|
|
377
|
-
|
|
378
|
-
if (!dataProp) {
|
|
379
|
-
localRawData = [];
|
|
380
|
-
data = [];
|
|
381
|
-
total = 0;
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
$effect(() => {
|
|
352
|
+
const s = sortState;
|
|
353
|
+
const f = effectiveFilterState;
|
|
354
|
+
if (!resolvedServerSide) {
|
|
355
|
+
debouncedSort = s;
|
|
356
|
+
debouncedFilter = f;
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (_debounceTimer !== null) clearTimeout(_debounceTimer);
|
|
360
|
+
_debounceTimer = setTimeout(() => {
|
|
361
|
+
_debounceTimer = null;
|
|
362
|
+
if (page === 0 && isSameSortState(debouncedSort, s) && isSameFilterState(debouncedFilter, f)) {
|
|
382
363
|
return;
|
|
383
364
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
localRawData = (result ?? []) as any;
|
|
393
|
-
})
|
|
394
|
-
.catch((e) => {
|
|
395
|
-
if (cancelled) return;
|
|
396
|
-
const msg = e instanceof Error ? e.message : 'Failed to load data';
|
|
397
|
-
error = msg;
|
|
398
|
-
onerror?.(msg);
|
|
399
|
-
})
|
|
400
|
-
.finally(() => {
|
|
401
|
-
if (!cancelled) loading = false;
|
|
402
|
-
});
|
|
403
|
-
} else {
|
|
404
|
-
localRawData = (dataProp ?? []) as any;
|
|
365
|
+
page = 0;
|
|
366
|
+
debouncedSort = s;
|
|
367
|
+
debouncedFilter = f;
|
|
368
|
+
}, 300);
|
|
369
|
+
return () => {
|
|
370
|
+
if (_debounceTimer !== null) {
|
|
371
|
+
clearTimeout(_debounceTimer);
|
|
372
|
+
_debounceTimer = null;
|
|
405
373
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
function getLocalSearchColumns() {
|
|
377
|
+
const columnsMap = {};
|
|
378
|
+
for (const col of gridColumns) {
|
|
379
|
+
const id = col.id;
|
|
380
|
+
if (id == null) continue;
|
|
381
|
+
if (col.hidden) continue;
|
|
382
|
+
if (col.searchable === false) continue;
|
|
383
|
+
columnsMap[String(id)] = true;
|
|
384
|
+
}
|
|
385
|
+
return Object.keys(columnsMap).length > 0 ? columnsMap : undefined;
|
|
386
|
+
}
|
|
387
|
+
function applyServerSearchFilters(value) {
|
|
388
|
+
if (!resolvedServerSide) return;
|
|
389
|
+
if (!value) {
|
|
390
|
+
// Avoid triggering a refetch loop by reassigning `{}` repeatedly.
|
|
391
|
+
if (Object.keys(searchFilterState).length === 0) return;
|
|
392
|
+
searchFilterState = {};
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const searchableIds = [];
|
|
396
|
+
for (const col of gridColumns) {
|
|
397
|
+
const id = col.id;
|
|
398
|
+
if (id == null) continue;
|
|
399
|
+
if (col.hidden) continue;
|
|
400
|
+
if (col.searchable === true) searchableIds.push(String(id));
|
|
401
|
+
}
|
|
402
|
+
if (searchableIds.length === 0) {
|
|
403
|
+
if (Object.keys(searchFilterState).length === 0) return;
|
|
404
|
+
searchFilterState = {};
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const next = {};
|
|
408
|
+
for (const id of searchableIds) {
|
|
409
|
+
// Avoid clobbering an existing column filter the user set elsewhere.
|
|
410
|
+
const hasExistingFilter = Object.entries({ ...gridFilterState }).some(([key, filter]) => {
|
|
411
|
+
const target = filter.column ?? key;
|
|
412
|
+
return target === id && filter.value != null && filter.value !== "";
|
|
413
|
+
});
|
|
414
|
+
if (hasExistingFilter) continue;
|
|
415
|
+
next[`search:${id}`] = {
|
|
416
|
+
column: id,
|
|
417
|
+
value,
|
|
418
|
+
operator: "ilike",
|
|
419
|
+
logic_operator: "OR"
|
|
409
420
|
};
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
function valueMatchesLocal(cellValue: unknown, filter: { operator?: string; value?: unknown }) {
|
|
413
|
-
const operator = String(filter.operator ?? 'eq').toLowerCase();
|
|
414
|
-
const rawValue = filter.value;
|
|
415
|
-
const actual = cellValue ?? '';
|
|
416
|
-
|
|
417
|
-
if (operator === 'eq') return String(actual) === String(rawValue ?? '');
|
|
418
|
-
if (operator === 'neq') return String(actual) !== String(rawValue ?? '');
|
|
419
|
-
|
|
420
|
-
if (operator === 'gt' || operator === 'gte' || operator === 'lt' || operator === 'lte') {
|
|
421
|
-
const left = Number(actual);
|
|
422
|
-
const right = Number(rawValue);
|
|
423
|
-
if (Number.isNaN(left) || Number.isNaN(right)) return false;
|
|
424
|
-
if (operator === 'gt') return left > right;
|
|
425
|
-
if (operator === 'gte') return left >= right;
|
|
426
|
-
if (operator === 'lt') return left < right;
|
|
427
|
-
return left <= right;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const haystack = String(actual).toLowerCase();
|
|
431
|
-
let needle = String(rawValue ?? '').toLowerCase();
|
|
432
|
-
|
|
433
|
-
if (operator === 'like' || operator === 'ilike' || operator === 'contains') {
|
|
434
|
-
const startsWith = needle.startsWith('%');
|
|
435
|
-
const endsWith = needle.endsWith('%');
|
|
436
|
-
const core = needle.replace(/^%|%$/g, '');
|
|
437
|
-
// If the UI provided a bare string, treat it like "%value%".
|
|
438
|
-
if (!startsWith && !endsWith) return haystack.includes(needle);
|
|
439
|
-
if (startsWith && endsWith) return haystack.includes(core);
|
|
440
|
-
if (startsWith) return haystack.endsWith(core);
|
|
441
|
-
if (endsWith) return haystack.startsWith(core);
|
|
442
|
-
return haystack.includes(core);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (operator === 'startswith') return haystack.startsWith(needle);
|
|
446
|
-
if (operator === 'endswith') return haystack.endsWith(needle);
|
|
447
|
-
|
|
448
|
-
return haystack.includes(needle);
|
|
449
421
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
orFilters.some((f) => {
|
|
467
|
-
const col = String(f.column ?? '');
|
|
468
|
-
if (!col) return false;
|
|
469
|
-
return valueMatchesLocal((row as any)[col], f);
|
|
470
|
-
})
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const q = searchValueState.trim().toLowerCase();
|
|
475
|
-
if (q) {
|
|
476
|
-
const searchableCols = gridColumns
|
|
477
|
-
.filter((c: any) => c?.id != null && !c.hidden && c.searchable !== false)
|
|
478
|
-
.map((c: any) => String(c.id));
|
|
479
|
-
|
|
480
|
-
if (searchableCols.length > 0) {
|
|
481
|
-
result = result.filter((row) =>
|
|
482
|
-
searchableCols.some((id) => String((row as any)[id] ?? '').toLowerCase().includes(q))
|
|
483
|
-
);
|
|
422
|
+
// Idempotency guard: avoid reassigning equivalent filter state.
|
|
423
|
+
const prev = searchFilterState;
|
|
424
|
+
const prevKeys = Object.keys(prev);
|
|
425
|
+
const nextKeys = Object.keys(next);
|
|
426
|
+
if (prevKeys.length === nextKeys.length) {
|
|
427
|
+
let same = true;
|
|
428
|
+
for (const k of nextKeys) {
|
|
429
|
+
const a = prev[k];
|
|
430
|
+
const b = next[k];
|
|
431
|
+
if (!a || !b) {
|
|
432
|
+
same = false;
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
if (String(a.column ?? "") !== String(b.column ?? "") || String(a.operator ?? "") !== String(b.operator ?? "") || String(a.logic_operator ?? "") !== String(b.logic_operator ?? "") || String(a.value ?? "") !== String(b.value ?? "")) {
|
|
436
|
+
same = false;
|
|
437
|
+
break;
|
|
484
438
|
}
|
|
485
439
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
440
|
+
if (same) return;
|
|
441
|
+
}
|
|
442
|
+
searchFilterState = next;
|
|
443
|
+
}
|
|
444
|
+
function setSearch(next) {
|
|
445
|
+
emitSearchValue(next);
|
|
446
|
+
gridApi?.exec("search-rows", {
|
|
447
|
+
search: next,
|
|
448
|
+
columns: getLocalSearchColumns()
|
|
495
449
|
});
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
450
|
+
applyServerSearchFilters(next.trim());
|
|
451
|
+
}
|
|
452
|
+
function refreshGrid() {
|
|
453
|
+
reload++;
|
|
454
|
+
}
|
|
455
|
+
function clearAllSort() {
|
|
456
|
+
// Clear in the UI/store first (local mode), then clear generic sort state.
|
|
457
|
+
gridApi?.exec("sort-rows", {});
|
|
458
|
+
emitSortOrder({});
|
|
459
|
+
}
|
|
460
|
+
function clearAllFilters() {
|
|
461
|
+
emitFilters({});
|
|
462
|
+
}
|
|
463
|
+
async function applyLocalDataChange(next) {
|
|
464
|
+
localRawData = next;
|
|
465
|
+
await onDataChange?.(next);
|
|
466
|
+
}
|
|
467
|
+
function resolveGenericColumn(columnId) {
|
|
468
|
+
if (!columnId) return undefined;
|
|
469
|
+
return genericColumnsById[columnId];
|
|
470
|
+
}
|
|
471
|
+
function extractRowsFromResponse(response) {
|
|
472
|
+
if (Array.isArray(response)) {
|
|
473
|
+
return response;
|
|
474
|
+
}
|
|
475
|
+
if (response && typeof response === "object" && Array.isArray(response.data)) {
|
|
476
|
+
return response.data;
|
|
477
|
+
}
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
function normalizeGridRows(rows, uniqueID) {
|
|
481
|
+
return rows.map((row) => {
|
|
482
|
+
const rowId = row?.id;
|
|
483
|
+
const uniqueValue = row?.[uniqueID];
|
|
484
|
+
if (rowId != null || uniqueValue == null) return row;
|
|
485
|
+
return {
|
|
486
|
+
...row,
|
|
487
|
+
id: uniqueValue
|
|
515
488
|
};
|
|
516
|
-
|
|
517
|
-
return next;
|
|
518
489
|
});
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
490
|
+
}
|
|
491
|
+
function extractTotalFromResponse(response, rows) {
|
|
492
|
+
if (response && typeof response === "object" && typeof response.metadata?.total === "number") {
|
|
493
|
+
return response.metadata.total;
|
|
494
|
+
}
|
|
495
|
+
return rows.length;
|
|
496
|
+
}
|
|
497
|
+
$effect(() => {
|
|
498
|
+
// Keep SVAR's local highlight + server-side search filters in sync with controlled searchValue.
|
|
499
|
+
const next = searchValueState;
|
|
500
|
+
gridApi?.exec("search-rows", {
|
|
501
|
+
search: next,
|
|
502
|
+
columns: getLocalSearchColumns()
|
|
503
|
+
});
|
|
504
|
+
applyServerSearchFilters(next.trim());
|
|
505
|
+
});
|
|
506
|
+
function openSearch() {
|
|
507
|
+
searchOpen = true;
|
|
508
|
+
}
|
|
509
|
+
function closeSearch() {
|
|
510
|
+
searchOpen = false;
|
|
511
|
+
}
|
|
512
|
+
let selectedIdsInternal = $state([]);
|
|
513
|
+
$effect(() => {
|
|
514
|
+
if (selectedItems === undefined) return;
|
|
515
|
+
selectedIdsInternal = resolvedSelectedIds;
|
|
516
|
+
});
|
|
517
|
+
function handleSelectedIdsChange(ids) {
|
|
518
|
+
selectedIdsInternal = ids;
|
|
519
|
+
emitSelectedItems(idsToSelectedItems(ids, data, resolvedUniqueID));
|
|
520
|
+
}
|
|
521
|
+
const contextMenuProps = $derived.by(() => {
|
|
522
|
+
const hasSort = Object.values(resolvedSortOrder ?? {}).some((dir) => dir === "asc" || dir === "desc");
|
|
523
|
+
const hasFilters = Object.keys(resolvedFilters ?? {}).length > 0;
|
|
524
|
+
const hasSearch = searchValueState.trim().length > 0;
|
|
525
|
+
return {
|
|
526
|
+
items: (ctx) => {
|
|
527
|
+
const defaults = [
|
|
528
|
+
{
|
|
529
|
+
id: "refresh",
|
|
530
|
+
label: "Refresh",
|
|
531
|
+
onselect: () => refreshGrid()
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
id: "clear-sort",
|
|
535
|
+
label: "Clear Sort",
|
|
536
|
+
disabled: !hasSort,
|
|
537
|
+
onselect: () => clearAllSort()
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
id: "clear-filters",
|
|
541
|
+
label: "Clear Filters",
|
|
542
|
+
disabled: !hasFilters,
|
|
543
|
+
onselect: () => clearAllFilters()
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
id: "clear-search",
|
|
547
|
+
label: "Clear Search",
|
|
548
|
+
disabled: !hasSearch,
|
|
549
|
+
onselect: () => setSearch("")
|
|
550
|
+
}
|
|
551
|
+
];
|
|
552
|
+
const userItemsRaw = typeof userContextMenuProps?.items === "function" ? userContextMenuProps.items(ctx) : userContextMenuProps?.items;
|
|
553
|
+
const userItems = Array.isArray(userItemsRaw) ? userItemsRaw : [];
|
|
554
|
+
if (userItems.length === 0) return defaults;
|
|
555
|
+
const hasMarker = userItems.some((it) => it?.id === SVARKGRID_DEFAULT_ITEMS_MARKER_ID);
|
|
556
|
+
if (hasMarker) {
|
|
557
|
+
const merged = [];
|
|
558
|
+
for (const it of userItems) {
|
|
559
|
+
if (it?.id === SVARKGRID_DEFAULT_ITEMS_MARKER_ID) merged.push(...defaults);
|
|
560
|
+
else merged.push(it);
|
|
561
|
+
}
|
|
562
|
+
return merged;
|
|
549
563
|
}
|
|
564
|
+
return [
|
|
565
|
+
...defaults,
|
|
566
|
+
{
|
|
567
|
+
id: "__sep__",
|
|
568
|
+
kind: "separator",
|
|
569
|
+
label: ""
|
|
570
|
+
},
|
|
571
|
+
...userItems
|
|
572
|
+
];
|
|
573
|
+
},
|
|
574
|
+
onselectitem: userContextMenuProps?.onselectitem
|
|
575
|
+
};
|
|
576
|
+
});
|
|
577
|
+
const isServerConfigured = $derived(!!(resolvedUrl && resolvedSchema && resolvedEntity));
|
|
578
|
+
const adapter = $derived.by(() => {
|
|
579
|
+
if (!isServerConfigured) return null;
|
|
580
|
+
return resolvedAdapterType === "RestHeaderSpec" ? new SvarkRestHeaderSpecAdapter({
|
|
581
|
+
url: resolvedUrl,
|
|
582
|
+
token: resolvedToken,
|
|
583
|
+
schema: resolvedSchema,
|
|
584
|
+
entity: resolvedEntity
|
|
585
|
+
}) : new SvarkResolveSpecAdapter({
|
|
586
|
+
url: resolvedUrl,
|
|
587
|
+
token: resolvedToken,
|
|
588
|
+
schema: resolvedSchema,
|
|
589
|
+
entity: resolvedEntity
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
// For virtual scroll: set dynamic rowCount once total is known
|
|
593
|
+
const gridDynamic = $derived(virtualScroll && total > 0 ? { rowCount: total } : undefined);
|
|
594
|
+
// ── Main data effect (initial load + sort/filter/page/reload triggers) ──────
|
|
595
|
+
$effect(() => {
|
|
596
|
+
const _adapter = adapter;
|
|
597
|
+
const _limit = virtualScroll ? 1 : pageSize;
|
|
598
|
+
const _sort = resolvedServerSide ? debouncedSort : [];
|
|
599
|
+
const _filters = resolvedServerSide ? debouncedFilter : {};
|
|
600
|
+
// Paging offset: only applied when server-side paging is active
|
|
601
|
+
const _offset = showPager ? page * pageSize : 0;
|
|
602
|
+
void reload;
|
|
603
|
+
if (!_adapter) {
|
|
604
|
+
activeServerRequestId += 1;
|
|
605
|
+
activeServerRequestKey = "";
|
|
606
|
+
loading = false;
|
|
607
|
+
error = resolvedServerSide ? "Missing server configuration (dataSourceOptions.url/schema/entity)" : null;
|
|
608
|
+
if (error) onerror?.(error);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
// Guard: if something upstream keeps re-triggering the effect without changing the
|
|
612
|
+
// actual server query parameters, avoid a tight refetch loop.
|
|
613
|
+
let requestId = 0;
|
|
614
|
+
let reqKey = "";
|
|
615
|
+
if (resolvedServerSide) {
|
|
616
|
+
const sortKey = JSON.stringify(_sort ?? []);
|
|
617
|
+
const filterKey = JSON.stringify(Object.keys(_filters ?? {}).sort().map((k) => [
|
|
618
|
+
k,
|
|
619
|
+
_filters[k]?.column,
|
|
620
|
+
_filters[k]?.operator,
|
|
621
|
+
_filters[k]?.value
|
|
622
|
+
]));
|
|
623
|
+
const queryKey = JSON.stringify(query ?? {});
|
|
624
|
+
reqKey = JSON.stringify({
|
|
625
|
+
url: resolvedUrl,
|
|
626
|
+
schema: resolvedSchema,
|
|
627
|
+
entity: resolvedEntity,
|
|
628
|
+
adapter: resolvedAdapterType,
|
|
629
|
+
limit: _limit,
|
|
630
|
+
offset: _offset,
|
|
631
|
+
sortKey,
|
|
632
|
+
filterKey,
|
|
633
|
+
queryKey,
|
|
634
|
+
reload
|
|
635
|
+
});
|
|
636
|
+
if (reqKey === lastServerRequestKey) {
|
|
637
|
+
return;
|
|
550
638
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (
|
|
560
|
-
debouncedSort = s;
|
|
561
|
-
debouncedFilter = f;
|
|
639
|
+
lastServerRequestKey = reqKey;
|
|
640
|
+
activeServerRequestKey = reqKey;
|
|
641
|
+
activeServerRequestId += 1;
|
|
642
|
+
requestId = activeServerRequestId;
|
|
643
|
+
}
|
|
644
|
+
loading = true;
|
|
645
|
+
error = null;
|
|
646
|
+
_adapter.read(_sort, _filters, _limit, _offset, query).then((response) => {
|
|
647
|
+
if (resolvedServerSide && (requestId !== activeServerRequestId || reqKey !== activeServerRequestKey)) {
|
|
562
648
|
return;
|
|
563
649
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
650
|
+
if (response?.success !== false) {
|
|
651
|
+
const incoming = normalizeGridRows(extractRowsFromResponse(response), resolvedUniqueID);
|
|
652
|
+
total = extractTotalFromResponse(response, incoming);
|
|
653
|
+
if (virtualScroll) {
|
|
654
|
+
data = [];
|
|
655
|
+
} else {
|
|
656
|
+
data = incoming;
|
|
570
657
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
_debounceTimer = null;
|
|
658
|
+
emitGridEvent("load", incoming[0], undefined, {
|
|
659
|
+
x: 0,
|
|
660
|
+
y: 0
|
|
661
|
+
}, { total });
|
|
662
|
+
// If selection is currently id-based, refresh selectedItems mapping.
|
|
663
|
+
if (selectedIdsInternal.length > 0 && onSelectedItemsChange) {
|
|
664
|
+
const items = idsToSelectedItems(selectedIdsInternal, incoming, resolvedUniqueID);
|
|
665
|
+
emitSelectedItems(items);
|
|
580
666
|
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const columnsMap: Record<string, boolean> = {};
|
|
586
|
-
for (const col of gridColumns) {
|
|
587
|
-
const id = (col as any).id;
|
|
588
|
-
if (id == null) continue;
|
|
589
|
-
if ((col as any).hidden) continue;
|
|
590
|
-
if ((col as any).searchable === false) continue;
|
|
591
|
-
columnsMap[String(id)] = true;
|
|
592
|
-
}
|
|
593
|
-
return Object.keys(columnsMap).length > 0 ? columnsMap : undefined;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
function applyServerSearchFilters(value: string) {
|
|
597
|
-
if (!resolvedServerSide) return;
|
|
598
|
-
|
|
599
|
-
if (!value) {
|
|
600
|
-
// Avoid triggering a refetch loop by reassigning `{}` repeatedly.
|
|
601
|
-
if (Object.keys(searchFilterState).length === 0) return;
|
|
602
|
-
searchFilterState = {};
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const searchableIds: string[] = [];
|
|
607
|
-
for (const col of gridColumns) {
|
|
608
|
-
const id = (col as any).id;
|
|
609
|
-
if (id == null) continue;
|
|
610
|
-
if ((col as any).hidden) continue;
|
|
611
|
-
if ((col as any).searchable === true) searchableIds.push(String(id));
|
|
667
|
+
} else {
|
|
668
|
+
const msg = response.error?.message ?? "Failed to load data";
|
|
669
|
+
error = msg;
|
|
670
|
+
onerror?.(msg);
|
|
612
671
|
}
|
|
613
|
-
|
|
614
|
-
if (
|
|
615
|
-
if (Object.keys(searchFilterState).length === 0) return;
|
|
616
|
-
searchFilterState = {};
|
|
672
|
+
}).catch((e) => {
|
|
673
|
+
if (resolvedServerSide && (requestId !== activeServerRequestId || reqKey !== activeServerRequestKey)) {
|
|
617
674
|
return;
|
|
618
675
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
return target === id && filter.value != null && filter.value !== '';
|
|
626
|
-
});
|
|
627
|
-
if (hasExistingFilter) continue;
|
|
628
|
-
next[`search:${id}`] = { column: id, value, operator: 'ilike', logic_operator: 'OR' };
|
|
676
|
+
const msg = e instanceof Error ? e.message : "Unknown error";
|
|
677
|
+
error = msg;
|
|
678
|
+
onerror?.(msg);
|
|
679
|
+
}).finally(() => {
|
|
680
|
+
if (!resolvedServerSide || requestId === activeServerRequestId && reqKey === activeServerRequestKey) {
|
|
681
|
+
loading = false;
|
|
629
682
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
) {
|
|
650
|
-
same = false;
|
|
651
|
-
break;
|
|
652
|
-
}
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
async function loadNextPage() {
|
|
686
|
+
const _adapter = adapter;
|
|
687
|
+
if (!_adapter) return;
|
|
688
|
+
// Block if main fetch is running (sort/filter change in progress)
|
|
689
|
+
if (loading || infiniteLoading || data.length >= total) return;
|
|
690
|
+
// Snapshot debounced sort+filter so we can discard the response if they
|
|
691
|
+
// change while the request is in-flight (race condition guard).
|
|
692
|
+
const snapshotSort = resolvedServerSide ? JSON.stringify(debouncedSort) : "";
|
|
693
|
+
const snapshotFilter = resolvedServerSide ? JSON.stringify(debouncedFilter) : "";
|
|
694
|
+
const snapshotOffset = data.length;
|
|
695
|
+
infiniteLoading = true;
|
|
696
|
+
try {
|
|
697
|
+
const response = await _adapter.read(resolvedServerSide ? debouncedSort : [], resolvedServerSide ? debouncedFilter : {}, pageSize, snapshotOffset, query);
|
|
698
|
+
// Discard if sort/filter changed while we were fetching
|
|
699
|
+
if (resolvedServerSide) {
|
|
700
|
+
if (JSON.stringify(debouncedSort) !== snapshotSort || JSON.stringify(debouncedFilter) !== snapshotFilter) {
|
|
701
|
+
return;
|
|
653
702
|
}
|
|
654
|
-
if (same) return;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
searchFilterState = next;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
function setSearch(next: string) {
|
|
661
|
-
emitSearchValue(next);
|
|
662
|
-
gridApi?.exec('search-rows', { search: next, columns: getLocalSearchColumns() });
|
|
663
|
-
applyServerSearchFilters(next.trim());
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function refreshGrid() {
|
|
667
|
-
reload++;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function clearAllSort() {
|
|
671
|
-
// Clear in the UI/store first (local mode), then clear generic sort state.
|
|
672
|
-
gridApi?.exec('sort-rows', {} as any);
|
|
673
|
-
emitSortOrder({});
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function clearAllFilters() {
|
|
677
|
-
emitFilters({});
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
async function applyLocalDataChange(next: Record<string, unknown>[]) {
|
|
681
|
-
localRawData = next;
|
|
682
|
-
await onDataChange?.(next);
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function resolveGenericColumn(columnId: string | undefined) {
|
|
686
|
-
if (!columnId) return undefined;
|
|
687
|
-
return genericColumnsById[columnId];
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function extractRowsFromResponse(response: unknown): Record<string, unknown>[] {
|
|
691
|
-
if (Array.isArray(response)) {
|
|
692
|
-
return response as Record<string, unknown>[];
|
|
693
703
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
typeof response === 'object' &&
|
|
698
|
-
Array.isArray((response as { data?: unknown }).data)
|
|
699
|
-
) {
|
|
700
|
-
return (response as { data: Record<string, unknown>[] }).data;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
return [];
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
function normalizeGridRows(rows: Record<string, unknown>[], uniqueID: string): Record<string, unknown>[] {
|
|
707
|
-
return rows.map((row) => {
|
|
708
|
-
const rowId = row?.id;
|
|
709
|
-
const uniqueValue = row?.[uniqueID];
|
|
710
|
-
if (rowId != null || uniqueValue == null) return row;
|
|
711
|
-
return { ...row, id: uniqueValue };
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function extractTotalFromResponse(response: unknown, rows: Record<string, unknown>[]): number {
|
|
716
|
-
if (
|
|
717
|
-
response &&
|
|
718
|
-
typeof response === 'object' &&
|
|
719
|
-
typeof (response as { metadata?: { total?: unknown } }).metadata?.total === 'number'
|
|
720
|
-
) {
|
|
721
|
-
return (response as { metadata: { total: number } }).metadata.total;
|
|
704
|
+
if (response?.success !== false) {
|
|
705
|
+
const incoming = normalizeGridRows(extractRowsFromResponse(response), resolvedUniqueID);
|
|
706
|
+
data = [...data, ...incoming];
|
|
722
707
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
function openSearch() {
|
|
735
|
-
searchOpen = true;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function closeSearch() {
|
|
739
|
-
searchOpen = false;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
let selectedIdsInternal = $state<(string | number)[]>([]);
|
|
743
|
-
$effect(() => {
|
|
744
|
-
if (selectedItems === undefined) return;
|
|
745
|
-
selectedIdsInternal = resolvedSelectedIds;
|
|
746
|
-
});
|
|
747
|
-
function handleSelectedIdsChange(ids: Array<string | number>) {
|
|
708
|
+
} catch {} finally {
|
|
709
|
+
infiniteLoading = false;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// ── SVAR Grid init ─────────────────────────────────────────────────────────
|
|
713
|
+
function handleInit(api) {
|
|
714
|
+
gridApi = api;
|
|
715
|
+
// Row selection callback
|
|
716
|
+
selectedIdsInternal = api.getState().selectedRows ?? [];
|
|
717
|
+
api.on("select-row", () => {
|
|
718
|
+
const ids = api.getState().selectedRows ?? [];
|
|
748
719
|
selectedIdsInternal = ids;
|
|
720
|
+
onselect?.(ids);
|
|
749
721
|
emitSelectedItems(idsToSelectedItems(ids, data, resolvedUniqueID));
|
|
722
|
+
});
|
|
723
|
+
// Virtual scroll: SVAR fires request-data when the visible row window changes.
|
|
724
|
+
// We fetch only that window from the API and replace data.
|
|
725
|
+
if (virtualScroll) {
|
|
726
|
+
api.on("request-data", async (ev) => {
|
|
727
|
+
const _adapter = adapter;
|
|
728
|
+
if (!_adapter) return;
|
|
729
|
+
const { start, end } = ev.row;
|
|
730
|
+
const limit = end - start;
|
|
731
|
+
if (limit <= 0) return;
|
|
732
|
+
loading = true;
|
|
733
|
+
try {
|
|
734
|
+
const response = await _adapter.read(resolvedServerSide ? debouncedSort : [], resolvedServerSide ? debouncedFilter : {}, limit, start, query);
|
|
735
|
+
if (response?.success !== false) {
|
|
736
|
+
data = normalizeGridRows(extractRowsFromResponse(response), resolvedUniqueID);
|
|
737
|
+
}
|
|
738
|
+
} catch (e) {
|
|
739
|
+
const msg = e instanceof Error ? e.message : "Failed to load data";
|
|
740
|
+
error = msg;
|
|
741
|
+
onerror?.(msg);
|
|
742
|
+
} finally {
|
|
743
|
+
loading = false;
|
|
744
|
+
}
|
|
745
|
+
});
|
|
750
746
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
typeof userContextMenuProps?.items === 'function'
|
|
768
|
-
? (userContextMenuProps.items as any)(ctx)
|
|
769
|
-
: (userContextMenuProps?.items as any);
|
|
770
|
-
|
|
771
|
-
const userItems = Array.isArray(userItemsRaw) ? (userItemsRaw as SvarkContextMenuItem[]) : [];
|
|
772
|
-
if (userItems.length === 0) return defaults;
|
|
773
|
-
|
|
774
|
-
const hasMarker = userItems.some((it) => it?.id === SVARKGRID_DEFAULT_ITEMS_MARKER_ID);
|
|
775
|
-
if (hasMarker) {
|
|
776
|
-
const merged: SvarkContextMenuItem[] = [];
|
|
777
|
-
for (const it of userItems) {
|
|
778
|
-
if (it?.id === SVARKGRID_DEFAULT_ITEMS_MARKER_ID) merged.push(...defaults);
|
|
779
|
-
else merged.push(it);
|
|
780
|
-
}
|
|
781
|
-
return merged;
|
|
747
|
+
// Keep generic sortOrder in sync with SVAR, in both local and server-side modes.
|
|
748
|
+
// In server-side mode we block SVAR's default sorting (the server returns sorted rows).
|
|
749
|
+
api.intercept("sort-rows", (ev) => {
|
|
750
|
+
if (ev.key) {
|
|
751
|
+
if (ev.add) {
|
|
752
|
+
const idx = sortState.findIndex((s) => s.key === ev.key);
|
|
753
|
+
if (idx >= 0) {
|
|
754
|
+
sortState[idx] = {
|
|
755
|
+
key: ev.key,
|
|
756
|
+
order: ev.order ?? "asc"
|
|
757
|
+
};
|
|
758
|
+
} else {
|
|
759
|
+
sortState = [...sortState, {
|
|
760
|
+
key: ev.key,
|
|
761
|
+
order: ev.order ?? "asc"
|
|
762
|
+
}];
|
|
782
763
|
}
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
764
|
+
} else {
|
|
765
|
+
sortState = [{
|
|
766
|
+
key: ev.key,
|
|
767
|
+
order: ev.order ?? "asc"
|
|
768
|
+
}];
|
|
769
|
+
}
|
|
770
|
+
} else {
|
|
771
|
+
sortState = [];
|
|
772
|
+
}
|
|
773
|
+
emitSortOrder(svarkSortStateToSortOrder(sortState));
|
|
774
|
+
return resolvedServerSide ? false : true;
|
|
788
775
|
});
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
url: resolvedUrl,
|
|
799
|
-
token: resolvedToken,
|
|
800
|
-
schema: resolvedSchema,
|
|
801
|
-
entity: resolvedEntity
|
|
802
|
-
})
|
|
803
|
-
: new SvarkResolveSpecAdapter({
|
|
804
|
-
url: resolvedUrl,
|
|
805
|
-
token: resolvedToken,
|
|
806
|
-
schema: resolvedSchema,
|
|
807
|
-
entity: resolvedEntity
|
|
808
|
-
});
|
|
776
|
+
api.intercept("resize-column", (ev) => {
|
|
777
|
+
emitGridEvent("column_resized", undefined, resolveGenericColumn(ev?.id != null ? String(ev.id) : undefined), {
|
|
778
|
+
x: 0,
|
|
779
|
+
y: 0
|
|
780
|
+
}, {
|
|
781
|
+
width: ev?.width,
|
|
782
|
+
inProgress: ev?.inProgress
|
|
783
|
+
});
|
|
784
|
+
return true;
|
|
809
785
|
});
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
const gridDynamic = $derived(
|
|
813
|
-
virtualScroll && total > 0 ? { rowCount: total } : undefined
|
|
814
|
-
);
|
|
815
|
-
|
|
816
|
-
// ── Main data effect (initial load + sort/filter/page/reload triggers) ──────
|
|
817
|
-
|
|
818
|
-
$effect(() => {
|
|
786
|
+
api.on("add-row", async (ev) => {
|
|
787
|
+
if (resolvedReadonly) return;
|
|
819
788
|
const _adapter = adapter;
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
void reload;
|
|
826
|
-
|
|
827
|
-
if (!_adapter) {
|
|
828
|
-
activeServerRequestId += 1;
|
|
829
|
-
activeServerRequestKey = '';
|
|
830
|
-
loading = false;
|
|
831
|
-
error = resolvedServerSide
|
|
832
|
-
? 'Missing server configuration (dataSourceOptions.url/schema/entity)'
|
|
833
|
-
: null;
|
|
834
|
-
if (error) onerror?.(error);
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Guard: if something upstream keeps re-triggering the effect without changing the
|
|
839
|
-
// actual server query parameters, avoid a tight refetch loop.
|
|
840
|
-
let requestId = 0;
|
|
841
|
-
let reqKey = '';
|
|
842
|
-
if (resolvedServerSide) {
|
|
843
|
-
const sortKey = JSON.stringify(_sort ?? []);
|
|
844
|
-
const filterKey = JSON.stringify(
|
|
845
|
-
Object.keys(_filters ?? {})
|
|
846
|
-
.sort()
|
|
847
|
-
.map((k) => [k, (_filters as any)[k]?.column, (_filters as any)[k]?.operator, (_filters as any)[k]?.value])
|
|
848
|
-
);
|
|
849
|
-
const queryKey = JSON.stringify(query ?? {});
|
|
850
|
-
reqKey = JSON.stringify({
|
|
851
|
-
url: resolvedUrl,
|
|
852
|
-
schema: resolvedSchema,
|
|
853
|
-
entity: resolvedEntity,
|
|
854
|
-
adapter: resolvedAdapterType,
|
|
855
|
-
limit: _limit,
|
|
856
|
-
offset: _offset,
|
|
857
|
-
sortKey,
|
|
858
|
-
filterKey,
|
|
859
|
-
queryKey,
|
|
860
|
-
reload
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
if (reqKey === lastServerRequestKey) {
|
|
864
|
-
return;
|
|
789
|
+
try {
|
|
790
|
+
if (_adapter) {
|
|
791
|
+
await _adapter.create(ev.row);
|
|
792
|
+
} else {
|
|
793
|
+
await applyLocalDataChange([...localRawData, ev.row]);
|
|
865
794
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
activeServerRequestId += 1;
|
|
870
|
-
requestId = activeServerRequestId;
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
loading = true;
|
|
874
|
-
error = null;
|
|
875
|
-
|
|
876
|
-
_adapter
|
|
877
|
-
.read(_sort, _filters, _limit, _offset, query)
|
|
878
|
-
.then((response) => {
|
|
879
|
-
if (
|
|
880
|
-
resolvedServerSide &&
|
|
881
|
-
(requestId !== activeServerRequestId || reqKey !== activeServerRequestKey)
|
|
882
|
-
) {
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
if ((response as any)?.success !== false) {
|
|
886
|
-
const incoming = normalizeGridRows(
|
|
887
|
-
extractRowsFromResponse(response),
|
|
888
|
-
resolvedUniqueID
|
|
889
|
-
);
|
|
890
|
-
total = extractTotalFromResponse(response, incoming);
|
|
891
|
-
|
|
892
|
-
if (virtualScroll) {
|
|
893
|
-
data = [];
|
|
894
|
-
} else {
|
|
895
|
-
data = incoming;
|
|
896
|
-
}
|
|
897
|
-
emitGridEvent('load', incoming[0], undefined, { x: 0, y: 0 }, { total });
|
|
898
|
-
|
|
899
|
-
// If selection is currently id-based, refresh selectedItems mapping.
|
|
900
|
-
if (selectedIdsInternal.length > 0 && onSelectedItemsChange) {
|
|
901
|
-
const items = idsToSelectedItems(selectedIdsInternal, incoming as any, resolvedUniqueID);
|
|
902
|
-
emitSelectedItems(items);
|
|
903
|
-
}
|
|
904
|
-
} else {
|
|
905
|
-
const msg = (response as any).error?.message ?? 'Failed to load data';
|
|
906
|
-
error = msg;
|
|
907
|
-
onerror?.(msg);
|
|
908
|
-
}
|
|
909
|
-
})
|
|
910
|
-
.catch((e) => {
|
|
911
|
-
if (
|
|
912
|
-
resolvedServerSide &&
|
|
913
|
-
(requestId !== activeServerRequestId || reqKey !== activeServerRequestKey)
|
|
914
|
-
) {
|
|
915
|
-
return;
|
|
916
|
-
}
|
|
917
|
-
const msg = e instanceof Error ? e.message : 'Unknown error';
|
|
918
|
-
error = msg;
|
|
919
|
-
onerror?.(msg);
|
|
920
|
-
})
|
|
921
|
-
.finally(() => {
|
|
922
|
-
if (
|
|
923
|
-
!resolvedServerSide ||
|
|
924
|
-
(requestId === activeServerRequestId && reqKey === activeServerRequestKey)
|
|
925
|
-
) {
|
|
926
|
-
loading = false;
|
|
927
|
-
}
|
|
795
|
+
onchange?.({
|
|
796
|
+
action: "add",
|
|
797
|
+
row: ev.row
|
|
928
798
|
});
|
|
799
|
+
reload++;
|
|
800
|
+
} catch (e) {
|
|
801
|
+
onerror?.(e instanceof Error ? e.message : "Create failed");
|
|
802
|
+
}
|
|
929
803
|
});
|
|
930
|
-
|
|
931
|
-
|
|
804
|
+
api.on("delete-row", async (ev) => {
|
|
805
|
+
if (resolvedReadonly) return;
|
|
932
806
|
const _adapter = adapter;
|
|
933
|
-
if (!_adapter) return;
|
|
934
|
-
|
|
935
|
-
// Block if main fetch is running (sort/filter change in progress)
|
|
936
|
-
if (loading || infiniteLoading || data.length >= total) return;
|
|
937
|
-
|
|
938
|
-
// Snapshot debounced sort+filter so we can discard the response if they
|
|
939
|
-
// change while the request is in-flight (race condition guard).
|
|
940
|
-
const snapshotSort = resolvedServerSide ? JSON.stringify(debouncedSort) : '';
|
|
941
|
-
const snapshotFilter = resolvedServerSide ? JSON.stringify(debouncedFilter) : '';
|
|
942
|
-
const snapshotOffset = data.length;
|
|
943
|
-
|
|
944
|
-
infiniteLoading = true;
|
|
945
807
|
try {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
snapshotOffset,
|
|
951
|
-
query
|
|
952
|
-
);
|
|
953
|
-
// Discard if sort/filter changed while we were fetching
|
|
954
|
-
if (resolvedServerSide) {
|
|
955
|
-
if (
|
|
956
|
-
JSON.stringify(debouncedSort) !== snapshotSort ||
|
|
957
|
-
JSON.stringify(debouncedFilter) !== snapshotFilter
|
|
958
|
-
) {
|
|
959
|
-
return;
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
if ((response as any)?.success !== false) {
|
|
963
|
-
const incoming = normalizeGridRows(
|
|
964
|
-
extractRowsFromResponse(response),
|
|
965
|
-
resolvedUniqueID
|
|
966
|
-
);
|
|
967
|
-
data = [...data, ...incoming];
|
|
808
|
+
if (_adapter) {
|
|
809
|
+
await _adapter.delete(ev.id);
|
|
810
|
+
} else {
|
|
811
|
+
await applyLocalDataChange(localRawData.filter((row) => String(row?.[resolvedUniqueID]) !== String(ev.id)));
|
|
968
812
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
infiniteLoading = false;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
// ── SVAR Grid init ─────────────────────────────────────────────────────────
|
|
977
|
-
|
|
978
|
-
function handleInit(api: IApi) {
|
|
979
|
-
gridApi = api;
|
|
980
|
-
|
|
981
|
-
// Row selection callback
|
|
982
|
-
selectedIdsInternal = ((api.getState() as any).selectedRows ?? []) as (string | number)[];
|
|
983
|
-
api.on('select-row', () => {
|
|
984
|
-
const ids = ((api.getState() as any).selectedRows ?? []) as (string | number)[];
|
|
985
|
-
selectedIdsInternal = ids;
|
|
986
|
-
onselect?.(ids);
|
|
987
|
-
emitSelectedItems(idsToSelectedItems(ids, data, resolvedUniqueID));
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
// Virtual scroll: SVAR fires request-data when the visible row window changes.
|
|
991
|
-
// We fetch only that window from the API and replace data.
|
|
992
|
-
if (virtualScroll) {
|
|
993
|
-
api.on('request-data', async (ev: any) => {
|
|
994
|
-
const _adapter = adapter;
|
|
995
|
-
if (!_adapter) return;
|
|
996
|
-
|
|
997
|
-
const { start, end } = ev.row as { start: number; end: number };
|
|
998
|
-
const limit = end - start;
|
|
999
|
-
if (limit <= 0) return;
|
|
1000
|
-
|
|
1001
|
-
loading = true;
|
|
1002
|
-
try {
|
|
1003
|
-
const response = await _adapter.read(
|
|
1004
|
-
resolvedServerSide ? debouncedSort : [],
|
|
1005
|
-
resolvedServerSide ? debouncedFilter : {},
|
|
1006
|
-
limit,
|
|
1007
|
-
start,
|
|
1008
|
-
query
|
|
1009
|
-
);
|
|
1010
|
-
if ((response as any)?.success !== false) {
|
|
1011
|
-
data = normalizeGridRows(extractRowsFromResponse(response), resolvedUniqueID);
|
|
1012
|
-
}
|
|
1013
|
-
} catch (e) {
|
|
1014
|
-
const msg = e instanceof Error ? e.message : 'Failed to load data';
|
|
1015
|
-
error = msg;
|
|
1016
|
-
onerror?.(msg);
|
|
1017
|
-
} finally {
|
|
1018
|
-
loading = false;
|
|
1019
|
-
}
|
|
813
|
+
onchange?.({
|
|
814
|
+
action: "delete",
|
|
815
|
+
id: ev.id
|
|
1020
816
|
});
|
|
817
|
+
reload++;
|
|
818
|
+
} catch (e) {
|
|
819
|
+
onerror?.(e instanceof Error ? e.message : "Delete failed");
|
|
1021
820
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
if (idx >= 0) {
|
|
1030
|
-
sortState[idx] = { key: ev.key, order: ev.order ?? 'asc' };
|
|
1031
|
-
} else {
|
|
1032
|
-
sortState = [...sortState, { key: ev.key, order: ev.order ?? 'asc' }];
|
|
1033
|
-
}
|
|
1034
|
-
} else {
|
|
1035
|
-
sortState = [{ key: ev.key, order: ev.order ?? 'asc' }];
|
|
1036
|
-
}
|
|
821
|
+
});
|
|
822
|
+
api.on("update-cell", async (ev) => {
|
|
823
|
+
if (resolvedReadonly) return;
|
|
824
|
+
const _adapter = adapter;
|
|
825
|
+
try {
|
|
826
|
+
if (_adapter) {
|
|
827
|
+
await _adapter.update(ev.id, { [String(ev.column)]: ev.value });
|
|
1037
828
|
} else {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
|
-
api.intercept('resize-column', (ev: any) => {
|
|
1045
|
-
emitGridEvent(
|
|
1046
|
-
'column_resized',
|
|
1047
|
-
undefined,
|
|
1048
|
-
resolveGenericColumn(ev?.id != null ? String(ev.id) : undefined),
|
|
1049
|
-
{ x: 0, y: 0 },
|
|
1050
|
-
{ width: ev?.width, inProgress: ev?.inProgress }
|
|
1051
|
-
);
|
|
1052
|
-
return true;
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
api.on('add-row', async (ev: any) => {
|
|
1056
|
-
if (resolvedReadonly) return;
|
|
1057
|
-
const _adapter = adapter;
|
|
1058
|
-
try {
|
|
1059
|
-
if (_adapter) {
|
|
1060
|
-
await _adapter.create(ev.row);
|
|
1061
|
-
} else {
|
|
1062
|
-
await applyLocalDataChange([...localRawData, ev.row]);
|
|
1063
|
-
}
|
|
1064
|
-
onchange?.({ action: 'add', row: ev.row });
|
|
1065
|
-
reload++;
|
|
1066
|
-
} catch (e) {
|
|
1067
|
-
onerror?.(e instanceof Error ? e.message : 'Create failed');
|
|
1068
|
-
}
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
api.on('delete-row', async (ev: any) => {
|
|
1072
|
-
if (resolvedReadonly) return;
|
|
1073
|
-
const _adapter = adapter;
|
|
1074
|
-
try {
|
|
1075
|
-
if (_adapter) {
|
|
1076
|
-
await _adapter.delete(ev.id);
|
|
1077
|
-
} else {
|
|
1078
|
-
await applyLocalDataChange(
|
|
1079
|
-
localRawData.filter((row) => String(row?.[resolvedUniqueID]) !== String(ev.id))
|
|
1080
|
-
);
|
|
1081
|
-
}
|
|
1082
|
-
onchange?.({ action: 'delete', id: ev.id });
|
|
1083
|
-
reload++;
|
|
1084
|
-
} catch (e) {
|
|
1085
|
-
onerror?.(e instanceof Error ? e.message : 'Delete failed');
|
|
1086
|
-
}
|
|
1087
|
-
});
|
|
1088
|
-
|
|
1089
|
-
api.on('update-cell', async (ev: any) => {
|
|
1090
|
-
if (resolvedReadonly) return;
|
|
1091
|
-
const _adapter = adapter;
|
|
1092
|
-
try {
|
|
1093
|
-
if (_adapter) {
|
|
1094
|
-
await _adapter.update(ev.id, { [String(ev.column)]: ev.value });
|
|
1095
|
-
} else {
|
|
1096
|
-
await applyLocalDataChange(
|
|
1097
|
-
localRawData.map((row) =>
|
|
1098
|
-
String(row?.[resolvedUniqueID]) === String(ev.id)
|
|
1099
|
-
? { ...row, [String(ev.column)]: ev.value }
|
|
1100
|
-
: row
|
|
1101
|
-
)
|
|
1102
|
-
);
|
|
1103
|
-
}
|
|
1104
|
-
onchange?.({ action: 'update', id: ev.id, column: ev.column, value: ev.value });
|
|
1105
|
-
} catch (e) {
|
|
1106
|
-
onerror?.(e instanceof Error ? e.message : 'Update failed');
|
|
829
|
+
await applyLocalDataChange(localRawData.map((row) => String(row?.[resolvedUniqueID]) === String(ev.id) ? {
|
|
830
|
+
...row,
|
|
831
|
+
[String(ev.column)]: ev.value
|
|
832
|
+
} : row));
|
|
1107
833
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
let lastFilterSignature = $state('');
|
|
1120
|
-
$effect(() => {
|
|
1121
|
-
const signature = JSON.stringify(resolvedFilters ?? {});
|
|
1122
|
-
if (signature === lastFilterSignature) return;
|
|
1123
|
-
lastFilterSignature = signature;
|
|
1124
|
-
emitGridEvent('filter_changed', undefined, undefined, { x: 0, y: 0 }, { filters: resolvedFilters });
|
|
1125
|
-
});
|
|
1126
|
-
|
|
1127
|
-
let lastSearchValue = $state(searchValueState);
|
|
1128
|
-
$effect(() => {
|
|
1129
|
-
const next = searchValueState;
|
|
1130
|
-
if (next === lastSearchValue) return;
|
|
1131
|
-
lastSearchValue = next;
|
|
1132
|
-
emitGridEvent('search_changed', undefined, undefined, { x: 0, y: 0 }, { value: next });
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
let lastSelectionSignature = $state('');
|
|
1136
|
-
$effect(() => {
|
|
1137
|
-
const signature = JSON.stringify(selectedIdsInternal);
|
|
1138
|
-
if (signature === lastSelectionSignature) return;
|
|
1139
|
-
lastSelectionSignature = signature;
|
|
1140
|
-
emitGridEvent('selection_changed', resolvedSelectedItems[0], undefined, { x: 0, y: 0 }, {
|
|
1141
|
-
selectedIds: selectedIdsInternal,
|
|
1142
|
-
selectedItems: resolvedSelectedItems
|
|
1143
|
-
});
|
|
834
|
+
onchange?.({
|
|
835
|
+
action: "update",
|
|
836
|
+
id: ev.id,
|
|
837
|
+
column: ev.column,
|
|
838
|
+
value: ev.value
|
|
839
|
+
});
|
|
840
|
+
} catch (e) {
|
|
841
|
+
onerror?.(e instanceof Error ? e.message : "Update failed");
|
|
842
|
+
}
|
|
1144
843
|
});
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
844
|
+
}
|
|
845
|
+
let lastSortSignature = $state("");
|
|
846
|
+
$effect(() => {
|
|
847
|
+
const signature = JSON.stringify(resolvedSortOrder ?? {});
|
|
848
|
+
if (!signature || signature === lastSortSignature) return;
|
|
849
|
+
lastSortSignature = signature;
|
|
850
|
+
emitGridEvent("sort_changed", undefined, undefined, {
|
|
851
|
+
x: 0,
|
|
852
|
+
y: 0
|
|
853
|
+
}, { sortOrder: resolvedSortOrder });
|
|
854
|
+
});
|
|
855
|
+
let lastFilterSignature = $state("");
|
|
856
|
+
$effect(() => {
|
|
857
|
+
const signature = JSON.stringify(resolvedFilters ?? {});
|
|
858
|
+
if (signature === lastFilterSignature) return;
|
|
859
|
+
lastFilterSignature = signature;
|
|
860
|
+
emitGridEvent("filter_changed", undefined, undefined, {
|
|
861
|
+
x: 0,
|
|
862
|
+
y: 0
|
|
863
|
+
}, { filters: resolvedFilters });
|
|
864
|
+
});
|
|
865
|
+
// eslint-disable-next-line svelte/no-unused-svelte-ignore
|
|
866
|
+
// svelte-ignore state_referenced_locally -- intentional initial capture for change-detection
|
|
867
|
+
let lastSearchValue = $state(searchValueState);
|
|
868
|
+
$effect(() => {
|
|
869
|
+
const next = searchValueState;
|
|
870
|
+
if (next === lastSearchValue) return;
|
|
871
|
+
lastSearchValue = next;
|
|
872
|
+
emitGridEvent("search_changed", undefined, undefined, {
|
|
873
|
+
x: 0,
|
|
874
|
+
y: 0
|
|
875
|
+
}, { value: next });
|
|
876
|
+
});
|
|
877
|
+
let lastSelectionSignature = $state("");
|
|
878
|
+
$effect(() => {
|
|
879
|
+
const signature = JSON.stringify(selectedIdsInternal);
|
|
880
|
+
if (signature === lastSelectionSignature) return;
|
|
881
|
+
lastSelectionSignature = signature;
|
|
882
|
+
emitGridEvent("selection_changed", resolvedSelectedItems[0], undefined, {
|
|
883
|
+
x: 0,
|
|
884
|
+
y: 0
|
|
885
|
+
}, {
|
|
886
|
+
selectedIds: selectedIdsInternal,
|
|
887
|
+
selectedItems: resolvedSelectedItems
|
|
1152
888
|
});
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
889
|
+
});
|
|
890
|
+
let lastSettingsSignature = $state("");
|
|
891
|
+
$effect(() => {
|
|
892
|
+
const signature = JSON.stringify(settings ?? {});
|
|
893
|
+
if (signature === lastSettingsSignature) return;
|
|
894
|
+
lastSettingsSignature = signature;
|
|
895
|
+
emitGridEvent("settings_changed", undefined, undefined, {
|
|
896
|
+
x: 0,
|
|
897
|
+
y: 0
|
|
898
|
+
}, { settings });
|
|
899
|
+
});
|
|
900
|
+
let lastPageSignature = $state("");
|
|
901
|
+
$effect(() => {
|
|
902
|
+
if (!showPager) return;
|
|
903
|
+
const signature = `${page}:${totalPages}:${pageStart}:${pageEnd}`;
|
|
904
|
+
if (signature === lastPageSignature) return;
|
|
905
|
+
lastPageSignature = signature;
|
|
906
|
+
emitGridEvent("page_loaded", data[0], undefined, {
|
|
907
|
+
x: 0,
|
|
908
|
+
y: 0
|
|
909
|
+
}, {
|
|
910
|
+
page,
|
|
911
|
+
totalPages,
|
|
912
|
+
pageStart,
|
|
913
|
+
pageEnd
|
|
1166
914
|
});
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
915
|
+
});
|
|
916
|
+
let lastDataSignature = $state("");
|
|
917
|
+
$effect(() => {
|
|
918
|
+
const signature = `${data.length}:${total}:${loading ? 1 : 0}`;
|
|
919
|
+
if (signature === lastDataSignature || loading) return;
|
|
920
|
+
lastDataSignature = signature;
|
|
921
|
+
emitGridEvent("data_changed", data[0], undefined, {
|
|
922
|
+
x: 0,
|
|
923
|
+
y: 0
|
|
924
|
+
}, {
|
|
925
|
+
data,
|
|
926
|
+
total
|
|
1174
927
|
});
|
|
928
|
+
});
|
|
1175
929
|
</script>
|
|
1176
930
|
|
|
1177
931
|
<SvarkGridView
|