@alaarab/ogrid-vue 2.0.2
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/dist/esm/components/SideBar.js +1 -0
- package/dist/esm/composables/index.js +27 -0
- package/dist/esm/composables/useActiveCell.js +58 -0
- package/dist/esm/composables/useCellEditing.js +20 -0
- package/dist/esm/composables/useCellSelection.js +281 -0
- package/dist/esm/composables/useClipboard.js +147 -0
- package/dist/esm/composables/useColumnChooserState.js +77 -0
- package/dist/esm/composables/useColumnHeaderFilterState.js +186 -0
- package/dist/esm/composables/useColumnResize.js +73 -0
- package/dist/esm/composables/useContextMenu.js +23 -0
- package/dist/esm/composables/useDataGridState.js +308 -0
- package/dist/esm/composables/useDateFilterState.js +36 -0
- package/dist/esm/composables/useDebounce.js +42 -0
- package/dist/esm/composables/useFillHandle.js +175 -0
- package/dist/esm/composables/useFilterOptions.js +39 -0
- package/dist/esm/composables/useInlineCellEditorState.js +42 -0
- package/dist/esm/composables/useKeyboardNavigation.js +353 -0
- package/dist/esm/composables/useMultiSelectFilterState.js +59 -0
- package/dist/esm/composables/useOGrid.js +406 -0
- package/dist/esm/composables/usePeopleFilterState.js +66 -0
- package/dist/esm/composables/useRichSelectState.js +59 -0
- package/dist/esm/composables/useRowSelection.js +75 -0
- package/dist/esm/composables/useSideBarState.js +41 -0
- package/dist/esm/composables/useTableLayout.js +85 -0
- package/dist/esm/composables/useTextFilterState.js +26 -0
- package/dist/esm/composables/useUndoRedo.js +75 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/types/columnTypes.js +1 -0
- package/dist/esm/types/dataGridTypes.js +1 -0
- package/dist/esm/types/index.js +1 -0
- package/dist/esm/utils/dataGridViewModel.js +195 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/types/components/SideBar.d.ts +26 -0
- package/dist/types/composables/index.d.ts +47 -0
- package/dist/types/composables/useActiveCell.d.ts +14 -0
- package/dist/types/composables/useCellEditing.d.ts +16 -0
- package/dist/types/composables/useCellSelection.d.ts +20 -0
- package/dist/types/composables/useClipboard.d.ts +25 -0
- package/dist/types/composables/useColumnChooserState.d.ts +22 -0
- package/dist/types/composables/useColumnHeaderFilterState.d.ts +68 -0
- package/dist/types/composables/useColumnResize.d.ts +21 -0
- package/dist/types/composables/useContextMenu.d.ts +19 -0
- package/dist/types/composables/useDataGridState.d.ts +129 -0
- package/dist/types/composables/useDateFilterState.d.ts +16 -0
- package/dist/types/composables/useDebounce.d.ts +10 -0
- package/dist/types/composables/useFillHandle.d.ts +30 -0
- package/dist/types/composables/useFilterOptions.d.ts +15 -0
- package/dist/types/composables/useInlineCellEditorState.d.ts +20 -0
- package/dist/types/composables/useKeyboardNavigation.d.ts +46 -0
- package/dist/types/composables/useMultiSelectFilterState.d.ts +20 -0
- package/dist/types/composables/useOGrid.d.ts +52 -0
- package/dist/types/composables/usePeopleFilterState.d.ts +20 -0
- package/dist/types/composables/useRichSelectState.d.ts +21 -0
- package/dist/types/composables/useRowSelection.d.ts +21 -0
- package/dist/types/composables/useSideBarState.d.ts +19 -0
- package/dist/types/composables/useTableLayout.d.ts +27 -0
- package/dist/types/composables/useTextFilterState.d.ts +13 -0
- package/dist/types/composables/useUndoRedo.d.ts +21 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/types/columnTypes.d.ts +25 -0
- package/dist/types/types/dataGridTypes.d.ts +151 -0
- package/dist/types/types/index.d.ts +3 -0
- package/dist/types/utils/dataGridViewModel.d.ts +137 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/package.json +38 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { ref, computed, watch } from 'vue';
|
|
2
|
+
import { mergeFilter, deriveFilterOptionsFromData, getMultiSelectFilterFields, flattenColumns, processClientSideData, } from '@alaarab/ogrid-core';
|
|
3
|
+
import { useFilterOptions } from './useFilterOptions';
|
|
4
|
+
import { useSideBarState } from './useSideBarState';
|
|
5
|
+
const DEFAULT_PAGE_SIZE = 25;
|
|
6
|
+
const EMPTY_LOADING_OPTIONS = {};
|
|
7
|
+
/**
|
|
8
|
+
* Top-level orchestration composable for OGrid: manages pagination, sorting, filtering,
|
|
9
|
+
* column visibility, and sidebar.
|
|
10
|
+
*/
|
|
11
|
+
export function useOGrid(props) {
|
|
12
|
+
// --- Destructure props reactively ---
|
|
13
|
+
const columnsProp = computed(() => props.value.columns);
|
|
14
|
+
const getRowId = computed(() => props.value.getRowId);
|
|
15
|
+
const data = computed(() => ('data' in props.value ? props.value.data : undefined));
|
|
16
|
+
const dataSource = computed(() => ('dataSource' in props.value ? props.value.dataSource : undefined));
|
|
17
|
+
const controlledPage = computed(() => props.value.page);
|
|
18
|
+
const controlledPageSize = computed(() => props.value.pageSize);
|
|
19
|
+
const controlledSort = computed(() => props.value.sort);
|
|
20
|
+
const controlledFilters = computed(() => props.value.filters);
|
|
21
|
+
const controlledVisibleColumns = computed(() => props.value.visibleColumns);
|
|
22
|
+
const controlledLoading = computed(() => props.value.isLoading);
|
|
23
|
+
const onPageChange = computed(() => props.value.onPageChange);
|
|
24
|
+
const onPageSizeChange = computed(() => props.value.onPageSizeChange);
|
|
25
|
+
const onSortChange = computed(() => props.value.onSortChange);
|
|
26
|
+
const onFiltersChange = computed(() => props.value.onFiltersChange);
|
|
27
|
+
const onVisibleColumnsChange = computed(() => props.value.onVisibleColumnsChange);
|
|
28
|
+
const columnOrder = computed(() => props.value.columnOrder);
|
|
29
|
+
const onColumnOrderChange = computed(() => props.value.onColumnOrderChange);
|
|
30
|
+
const onColumnResized = computed(() => props.value.onColumnResized);
|
|
31
|
+
const onColumnPinned = computed(() => props.value.onColumnPinned);
|
|
32
|
+
const defaultPageSize = computed(() => props.value.defaultPageSize ?? DEFAULT_PAGE_SIZE);
|
|
33
|
+
const defaultSortBy = computed(() => props.value.defaultSortBy);
|
|
34
|
+
const defaultSortDirection = computed(() => props.value.defaultSortDirection ?? 'asc');
|
|
35
|
+
const entityLabelPlural = computed(() => props.value.entityLabelPlural ?? 'items');
|
|
36
|
+
const columnChooserProp = computed(() => props.value.columnChooser);
|
|
37
|
+
const onFirstDataRendered = computed(() => props.value.onFirstDataRendered);
|
|
38
|
+
const onError = computed(() => props.value.onError);
|
|
39
|
+
// Resolve column chooser placement
|
|
40
|
+
const columnChooserPlacement = computed(() => columnChooserProp.value === false ? 'none'
|
|
41
|
+
: columnChooserProp.value === 'sidebar' ? 'sidebar'
|
|
42
|
+
: 'toolbar');
|
|
43
|
+
const columns = computed(() => flattenColumns(columnsProp.value));
|
|
44
|
+
const isServerSide = computed(() => dataSource.value != null);
|
|
45
|
+
const isClientSide = computed(() => !isServerSide.value);
|
|
46
|
+
const internalData = ref([]);
|
|
47
|
+
const internalLoading = ref(false);
|
|
48
|
+
const displayData = computed(() => data.value ?? internalData.value);
|
|
49
|
+
const displayLoading = computed(() => controlledLoading.value ?? internalLoading.value);
|
|
50
|
+
const defaultSortField = computed(() => defaultSortBy.value ?? columns.value[0]?.columnId ?? '');
|
|
51
|
+
const internalPage = ref(1);
|
|
52
|
+
const internalPageSize = ref(defaultPageSize.value);
|
|
53
|
+
const internalSort = ref({
|
|
54
|
+
field: defaultSortField.value,
|
|
55
|
+
direction: defaultSortDirection.value,
|
|
56
|
+
});
|
|
57
|
+
const internalFilters = ref({});
|
|
58
|
+
const internalVisibleColumns = ref((() => {
|
|
59
|
+
const visible = columns.value
|
|
60
|
+
.filter((c) => c.defaultVisible !== false)
|
|
61
|
+
.map((c) => c.columnId);
|
|
62
|
+
return new Set(visible.length > 0 ? visible : columns.value.map((c) => c.columnId));
|
|
63
|
+
})());
|
|
64
|
+
const columnWidthOverrides = ref({});
|
|
65
|
+
const pinnedOverrides = ref({});
|
|
66
|
+
const page = computed(() => controlledPage.value ?? internalPage.value);
|
|
67
|
+
const pageSize = computed(() => controlledPageSize.value ?? internalPageSize.value);
|
|
68
|
+
const sort = computed(() => controlledSort.value ?? internalSort.value);
|
|
69
|
+
const filters = computed(() => controlledFilters.value ?? internalFilters.value);
|
|
70
|
+
const visibleColumns = computed(() => controlledVisibleColumns.value ?? internalVisibleColumns.value);
|
|
71
|
+
const setPage = (p) => {
|
|
72
|
+
if (controlledPage.value === undefined)
|
|
73
|
+
internalPage.value = p;
|
|
74
|
+
onPageChange.value?.(p);
|
|
75
|
+
};
|
|
76
|
+
const setPageSize = (size) => {
|
|
77
|
+
if (controlledPageSize.value === undefined)
|
|
78
|
+
internalPageSize.value = size;
|
|
79
|
+
onPageSizeChange.value?.(size);
|
|
80
|
+
setPage(1);
|
|
81
|
+
};
|
|
82
|
+
const setSort = (s) => {
|
|
83
|
+
if (controlledSort.value === undefined)
|
|
84
|
+
internalSort.value = s;
|
|
85
|
+
onSortChange.value?.(s);
|
|
86
|
+
setPage(1);
|
|
87
|
+
};
|
|
88
|
+
const setFilters = (f) => {
|
|
89
|
+
if (controlledFilters.value === undefined)
|
|
90
|
+
internalFilters.value = f;
|
|
91
|
+
onFiltersChange.value?.(f);
|
|
92
|
+
setPage(1);
|
|
93
|
+
};
|
|
94
|
+
const setVisibleColumns = (cols) => {
|
|
95
|
+
if (controlledVisibleColumns.value === undefined)
|
|
96
|
+
internalVisibleColumns.value = cols;
|
|
97
|
+
onVisibleColumnsChange.value?.(cols);
|
|
98
|
+
};
|
|
99
|
+
const handleSort = (columnKey) => {
|
|
100
|
+
setSort({
|
|
101
|
+
field: columnKey,
|
|
102
|
+
direction: sort.value.field === columnKey && sort.value.direction === 'asc' ? 'desc' : 'asc',
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
const handleFilterChange = (key, value) => {
|
|
106
|
+
setFilters(mergeFilter(filters.value, key, value));
|
|
107
|
+
};
|
|
108
|
+
const handleVisibilityChange = (columnKey, isVisible) => {
|
|
109
|
+
const next = new Set(visibleColumns.value);
|
|
110
|
+
if (isVisible)
|
|
111
|
+
next.add(columnKey);
|
|
112
|
+
else
|
|
113
|
+
next.delete(columnKey);
|
|
114
|
+
setVisibleColumns(next);
|
|
115
|
+
};
|
|
116
|
+
const internalSelectedRows = ref(new Set());
|
|
117
|
+
const selectedRowsProp = computed(() => props.value.selectedRows);
|
|
118
|
+
const effectiveSelectedRows = computed(() => selectedRowsProp.value ?? internalSelectedRows.value);
|
|
119
|
+
const handleSelectionChange = (event) => {
|
|
120
|
+
if (selectedRowsProp.value === undefined) {
|
|
121
|
+
internalSelectedRows.value = new Set(event.selectedRowIds);
|
|
122
|
+
}
|
|
123
|
+
props.value.onSelectionChange?.(event);
|
|
124
|
+
};
|
|
125
|
+
const multiSelectFilterFields = computed(() => getMultiSelectFilterFields(columns.value));
|
|
126
|
+
const filterOptionsSource = computed(() => dataSource.value ?? { fetchFilterOptions: undefined });
|
|
127
|
+
const { filterOptions: serverFilterOptions, loadingOptions: loadingFilterOptions } = useFilterOptions(filterOptionsSource, multiSelectFilterFields);
|
|
128
|
+
const hasServerFilterOptions = computed(() => dataSource.value?.fetchFilterOptions != null);
|
|
129
|
+
const clientFilterOptions = computed(() => {
|
|
130
|
+
if (hasServerFilterOptions.value)
|
|
131
|
+
return serverFilterOptions.value;
|
|
132
|
+
return deriveFilterOptionsFromData(displayData.value, columns.value);
|
|
133
|
+
});
|
|
134
|
+
// --- Client-side filtering & sorting ---
|
|
135
|
+
const clientItemsAndTotal = computed(() => {
|
|
136
|
+
if (!isClientSide.value)
|
|
137
|
+
return null;
|
|
138
|
+
const rows = processClientSideData(displayData.value, columns.value, filters.value, sort.value.field, sort.value.direction);
|
|
139
|
+
const total = rows.length;
|
|
140
|
+
const start = (page.value - 1) * pageSize.value;
|
|
141
|
+
const paged = rows.slice(start, start + pageSize.value);
|
|
142
|
+
return { items: paged, totalCount: total };
|
|
143
|
+
});
|
|
144
|
+
// --- Server-side fetching ---
|
|
145
|
+
const serverItems = ref([]);
|
|
146
|
+
const serverTotalCount = ref(0);
|
|
147
|
+
const loading = ref(true);
|
|
148
|
+
let fetchId = 0;
|
|
149
|
+
const refreshCounter = ref(0);
|
|
150
|
+
watch([isServerSide, dataSource, page, pageSize, () => sort.value.field, () => sort.value.direction, filters, refreshCounter], () => {
|
|
151
|
+
if (!isServerSide.value || !dataSource.value) {
|
|
152
|
+
if (!isServerSide.value)
|
|
153
|
+
loading.value = false;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const id = ++fetchId;
|
|
157
|
+
loading.value = true;
|
|
158
|
+
dataSource.value
|
|
159
|
+
.fetchPage({
|
|
160
|
+
page: page.value,
|
|
161
|
+
pageSize: pageSize.value,
|
|
162
|
+
sort: { field: sort.value.field, direction: sort.value.direction },
|
|
163
|
+
filters: filters.value,
|
|
164
|
+
})
|
|
165
|
+
.then((res) => {
|
|
166
|
+
if (id !== fetchId)
|
|
167
|
+
return;
|
|
168
|
+
serverItems.value = res.items;
|
|
169
|
+
serverTotalCount.value = res.totalCount;
|
|
170
|
+
})
|
|
171
|
+
.catch((err) => {
|
|
172
|
+
if (id !== fetchId)
|
|
173
|
+
return;
|
|
174
|
+
onError.value?.(err);
|
|
175
|
+
serverItems.value = [];
|
|
176
|
+
serverTotalCount.value = 0;
|
|
177
|
+
})
|
|
178
|
+
.finally(() => {
|
|
179
|
+
if (id === fetchId)
|
|
180
|
+
loading.value = false;
|
|
181
|
+
});
|
|
182
|
+
}, { immediate: true });
|
|
183
|
+
const displayItems = computed(() => isClientSide.value && clientItemsAndTotal.value
|
|
184
|
+
? clientItemsAndTotal.value.items
|
|
185
|
+
: serverItems.value);
|
|
186
|
+
const displayTotalCount = computed(() => isClientSide.value && clientItemsAndTotal.value
|
|
187
|
+
? clientItemsAndTotal.value.totalCount
|
|
188
|
+
: serverTotalCount.value);
|
|
189
|
+
// Fire onFirstDataRendered once
|
|
190
|
+
let firstDataRendered = false;
|
|
191
|
+
watch(displayItems, (items) => {
|
|
192
|
+
if (!firstDataRendered && items.length > 0) {
|
|
193
|
+
firstDataRendered = true;
|
|
194
|
+
onFirstDataRendered.value?.();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
// With discriminated union, any defined value is active
|
|
198
|
+
const hasActiveFilters = computed(() => Object.values(filters.value).some((v) => v !== undefined));
|
|
199
|
+
const columnChooserColumns = computed(() => columns.value.map((c) => ({
|
|
200
|
+
columnId: c.columnId,
|
|
201
|
+
name: c.name,
|
|
202
|
+
required: c.required === true,
|
|
203
|
+
})));
|
|
204
|
+
const statusBarConfig = computed(() => {
|
|
205
|
+
const sb = props.value.statusBar;
|
|
206
|
+
if (!sb)
|
|
207
|
+
return undefined;
|
|
208
|
+
if (typeof sb === 'object')
|
|
209
|
+
return sb;
|
|
210
|
+
const totalData = isClientSide.value ? (data.value?.length ?? 0) : serverTotalCount.value;
|
|
211
|
+
const filteredData = displayTotalCount.value;
|
|
212
|
+
return {
|
|
213
|
+
totalCount: totalData,
|
|
214
|
+
filteredCount: hasActiveFilters.value ? filteredData : undefined,
|
|
215
|
+
selectedCount: effectiveSelectedRows.value.size,
|
|
216
|
+
suppressRowCount: true,
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
const handleColumnResized = (columnId, width) => {
|
|
220
|
+
columnWidthOverrides.value = { ...columnWidthOverrides.value, [columnId]: width };
|
|
221
|
+
onColumnResized.value?.(columnId, width);
|
|
222
|
+
};
|
|
223
|
+
const handleColumnPinned = (columnId, pinned) => {
|
|
224
|
+
if (pinned === null) {
|
|
225
|
+
const { [columnId]: _, ...rest } = pinnedOverrides.value;
|
|
226
|
+
pinnedOverrides.value = rest;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
pinnedOverrides.value = { ...pinnedOverrides.value, [columnId]: pinned };
|
|
230
|
+
}
|
|
231
|
+
onColumnPinned.value?.(columnId, pinned);
|
|
232
|
+
};
|
|
233
|
+
// --- Side bar ---
|
|
234
|
+
const sideBarState = useSideBarState({ config: props.value.sideBar });
|
|
235
|
+
const filterableColumns = computed(() => columns.value
|
|
236
|
+
.filter((c) => c.filterable && c.filterable.type)
|
|
237
|
+
.map((c) => ({
|
|
238
|
+
columnId: c.columnId,
|
|
239
|
+
name: c.name,
|
|
240
|
+
filterField: c.filterable.filterField ?? c.columnId,
|
|
241
|
+
filterType: c.filterable.type,
|
|
242
|
+
})));
|
|
243
|
+
const sideBarProps = computed(() => {
|
|
244
|
+
if (!sideBarState.isEnabled)
|
|
245
|
+
return null;
|
|
246
|
+
return {
|
|
247
|
+
activePanel: sideBarState.activePanel.value,
|
|
248
|
+
onPanelChange: sideBarState.setActivePanel,
|
|
249
|
+
panels: sideBarState.panels,
|
|
250
|
+
position: sideBarState.position,
|
|
251
|
+
columns: columnChooserColumns.value,
|
|
252
|
+
visibleColumns: visibleColumns.value,
|
|
253
|
+
onVisibilityChange: handleVisibilityChange,
|
|
254
|
+
onSetVisibleColumns: setVisibleColumns,
|
|
255
|
+
filterableColumns: filterableColumns.value,
|
|
256
|
+
filters: filters.value,
|
|
257
|
+
onFilterChange: handleFilterChange,
|
|
258
|
+
filterOptions: clientFilterOptions.value,
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
const clearAllFilters = () => setFilters({});
|
|
262
|
+
const isLoadingResolved = computed(() => (isServerSide.value && loading.value) || displayLoading.value);
|
|
263
|
+
// --- Build result objects ---
|
|
264
|
+
const dataGridProps = computed(() => ({
|
|
265
|
+
items: displayItems.value,
|
|
266
|
+
columns: columnsProp.value,
|
|
267
|
+
getRowId: getRowId.value,
|
|
268
|
+
sortBy: sort.value.field,
|
|
269
|
+
sortDirection: sort.value.direction,
|
|
270
|
+
onColumnSort: handleSort,
|
|
271
|
+
visibleColumns: visibleColumns.value,
|
|
272
|
+
columnOrder: columnOrder.value,
|
|
273
|
+
onColumnOrderChange: onColumnOrderChange.value,
|
|
274
|
+
onColumnResized: handleColumnResized,
|
|
275
|
+
onColumnPinned: handleColumnPinned,
|
|
276
|
+
pinnedColumns: pinnedOverrides.value,
|
|
277
|
+
initialColumnWidths: columnWidthOverrides.value,
|
|
278
|
+
freezeRows: props.value.freezeRows,
|
|
279
|
+
freezeCols: props.value.freezeCols,
|
|
280
|
+
editable: props.value.editable,
|
|
281
|
+
cellSelection: props.value.cellSelection,
|
|
282
|
+
onCellValueChanged: props.value.onCellValueChanged,
|
|
283
|
+
onUndo: props.value.onUndo,
|
|
284
|
+
onRedo: props.value.onRedo,
|
|
285
|
+
canUndo: props.value.canUndo,
|
|
286
|
+
canRedo: props.value.canRedo,
|
|
287
|
+
rowSelection: props.value.rowSelection ?? 'none',
|
|
288
|
+
selectedRows: effectiveSelectedRows.value,
|
|
289
|
+
onSelectionChange: handleSelectionChange,
|
|
290
|
+
statusBar: statusBarConfig.value,
|
|
291
|
+
isLoading: isLoadingResolved.value,
|
|
292
|
+
filters: filters.value,
|
|
293
|
+
onFilterChange: handleFilterChange,
|
|
294
|
+
filterOptions: clientFilterOptions.value,
|
|
295
|
+
loadingFilterOptions: dataSource.value?.fetchFilterOptions ? loadingFilterOptions.value : EMPTY_LOADING_OPTIONS,
|
|
296
|
+
peopleSearch: dataSource.value?.searchPeople,
|
|
297
|
+
getUserByEmail: dataSource.value?.getUserByEmail,
|
|
298
|
+
layoutMode: props.value.layoutMode,
|
|
299
|
+
suppressHorizontalScroll: props.value.suppressHorizontalScroll,
|
|
300
|
+
'aria-label': props.value['aria-label'],
|
|
301
|
+
'aria-labelledby': props.value['aria-labelledby'],
|
|
302
|
+
emptyState: {
|
|
303
|
+
hasActiveFilters: hasActiveFilters.value,
|
|
304
|
+
onClearAll: clearAllFilters,
|
|
305
|
+
message: props.value.emptyState?.message,
|
|
306
|
+
render: props.value.emptyState?.render,
|
|
307
|
+
},
|
|
308
|
+
}));
|
|
309
|
+
const pagination = computed(() => ({
|
|
310
|
+
page: page.value,
|
|
311
|
+
pageSize: pageSize.value,
|
|
312
|
+
displayTotalCount: displayTotalCount.value,
|
|
313
|
+
setPage,
|
|
314
|
+
setPageSize,
|
|
315
|
+
pageSizeOptions: props.value.pageSizeOptions,
|
|
316
|
+
entityLabelPlural: entityLabelPlural.value,
|
|
317
|
+
}));
|
|
318
|
+
const columnChooser = computed(() => ({
|
|
319
|
+
columns: columnChooserColumns.value,
|
|
320
|
+
visibleColumns: visibleColumns.value,
|
|
321
|
+
onVisibilityChange: handleVisibilityChange,
|
|
322
|
+
placement: columnChooserPlacement.value,
|
|
323
|
+
}));
|
|
324
|
+
const layout = computed(() => ({
|
|
325
|
+
toolbar: props.value.toolbar,
|
|
326
|
+
toolbarBelow: props.value.toolbarBelow,
|
|
327
|
+
className: props.value.className,
|
|
328
|
+
emptyState: props.value.emptyState,
|
|
329
|
+
sideBarProps: sideBarProps.value,
|
|
330
|
+
}));
|
|
331
|
+
const filtersResult = computed(() => ({
|
|
332
|
+
hasActiveFilters: hasActiveFilters.value,
|
|
333
|
+
setFilters,
|
|
334
|
+
}));
|
|
335
|
+
// --- Imperative API ---
|
|
336
|
+
const api = computed(() => ({
|
|
337
|
+
setRowData: (d) => {
|
|
338
|
+
if (!isServerSide.value)
|
|
339
|
+
internalData.value = d;
|
|
340
|
+
},
|
|
341
|
+
setLoading: (v) => { internalLoading.value = v; },
|
|
342
|
+
getColumnState: () => ({
|
|
343
|
+
visibleColumns: Array.from(visibleColumns.value),
|
|
344
|
+
sort: sort.value,
|
|
345
|
+
columnOrder: columnOrder.value ?? undefined,
|
|
346
|
+
columnWidths: Object.keys(columnWidthOverrides.value).length > 0 ? columnWidthOverrides.value : undefined,
|
|
347
|
+
filters: Object.keys(filters.value).length > 0 ? filters.value : undefined,
|
|
348
|
+
pinnedColumns: Object.keys(pinnedOverrides.value).length > 0 ? pinnedOverrides.value : undefined,
|
|
349
|
+
}),
|
|
350
|
+
applyColumnState: (state) => {
|
|
351
|
+
if (state.visibleColumns)
|
|
352
|
+
setVisibleColumns(new Set(state.visibleColumns));
|
|
353
|
+
if (state.sort)
|
|
354
|
+
setSort(state.sort);
|
|
355
|
+
if (state.columnOrder && onColumnOrderChange.value)
|
|
356
|
+
onColumnOrderChange.value(state.columnOrder);
|
|
357
|
+
if (state.columnWidths)
|
|
358
|
+
columnWidthOverrides.value = state.columnWidths;
|
|
359
|
+
if (state.filters)
|
|
360
|
+
setFilters(state.filters);
|
|
361
|
+
if (state.pinnedColumns)
|
|
362
|
+
pinnedOverrides.value = state.pinnedColumns;
|
|
363
|
+
},
|
|
364
|
+
setFilterModel: setFilters,
|
|
365
|
+
getSelectedRows: () => Array.from(effectiveSelectedRows.value),
|
|
366
|
+
setSelectedRows: (rowIds) => {
|
|
367
|
+
if (selectedRowsProp.value === undefined)
|
|
368
|
+
internalSelectedRows.value = new Set(rowIds);
|
|
369
|
+
},
|
|
370
|
+
selectAll: () => {
|
|
371
|
+
const allIds = new Set(displayItems.value.map((item) => getRowId.value(item)));
|
|
372
|
+
if (selectedRowsProp.value === undefined)
|
|
373
|
+
internalSelectedRows.value = allIds;
|
|
374
|
+
props.value.onSelectionChange?.({ selectedRowIds: Array.from(allIds), selectedItems: displayItems.value });
|
|
375
|
+
},
|
|
376
|
+
deselectAll: () => {
|
|
377
|
+
if (selectedRowsProp.value === undefined)
|
|
378
|
+
internalSelectedRows.value = new Set();
|
|
379
|
+
props.value.onSelectionChange?.({ selectedRowIds: [], selectedItems: [] });
|
|
380
|
+
},
|
|
381
|
+
clearFilters: () => setFilters({}),
|
|
382
|
+
clearSort: () => setSort({ field: defaultSortField.value, direction: defaultSortDirection.value }),
|
|
383
|
+
resetGridState: (options) => {
|
|
384
|
+
setFilters({});
|
|
385
|
+
setSort({ field: defaultSortField.value, direction: defaultSortDirection.value });
|
|
386
|
+
if (!options?.keepSelection) {
|
|
387
|
+
if (selectedRowsProp.value === undefined)
|
|
388
|
+
internalSelectedRows.value = new Set();
|
|
389
|
+
props.value.onSelectionChange?.({ selectedRowIds: [], selectedItems: [] });
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
getDisplayedRows: () => displayItems.value,
|
|
393
|
+
refreshData: () => {
|
|
394
|
+
if (isServerSide.value)
|
|
395
|
+
refreshCounter.value++;
|
|
396
|
+
},
|
|
397
|
+
}));
|
|
398
|
+
return {
|
|
399
|
+
dataGridProps,
|
|
400
|
+
pagination,
|
|
401
|
+
columnChooser,
|
|
402
|
+
layout,
|
|
403
|
+
filters: filtersResult,
|
|
404
|
+
api,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ref, watch, onUnmounted } from 'vue';
|
|
2
|
+
const PEOPLE_SEARCH_DEBOUNCE_MS = 300;
|
|
3
|
+
export function usePeopleFilterState(params) {
|
|
4
|
+
const { onUserChange, filterType } = params;
|
|
5
|
+
const peopleInputRef = ref(null);
|
|
6
|
+
let peopleSearchTimeout;
|
|
7
|
+
const peopleSuggestions = ref([]);
|
|
8
|
+
const isPeopleLoading = ref(false);
|
|
9
|
+
const peopleSearchText = ref('');
|
|
10
|
+
const setPeopleSearchText = (v) => {
|
|
11
|
+
peopleSearchText.value = v;
|
|
12
|
+
};
|
|
13
|
+
// Sync temp state when popover opens
|
|
14
|
+
watch(() => params.isFilterOpen(), (open) => {
|
|
15
|
+
if (open) {
|
|
16
|
+
peopleSearchText.value = '';
|
|
17
|
+
peopleSuggestions.value = [];
|
|
18
|
+
if (filterType === 'people') {
|
|
19
|
+
setTimeout(() => peopleInputRef.value?.focus(), 50);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
// People search with debounce
|
|
24
|
+
watch([peopleSearchText, () => params.peopleSearch, () => params.isFilterOpen()], ([searchText, search, isOpen]) => {
|
|
25
|
+
if (peopleSearchTimeout)
|
|
26
|
+
clearTimeout(peopleSearchTimeout);
|
|
27
|
+
if (!search || !isOpen || filterType !== 'people')
|
|
28
|
+
return;
|
|
29
|
+
if (!searchText.trim()) {
|
|
30
|
+
peopleSuggestions.value = [];
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
isPeopleLoading.value = true;
|
|
34
|
+
peopleSearchTimeout = setTimeout(async () => {
|
|
35
|
+
try {
|
|
36
|
+
const results = await search(searchText);
|
|
37
|
+
peopleSuggestions.value = results.slice(0, 10);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
peopleSuggestions.value = [];
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
isPeopleLoading.value = false;
|
|
44
|
+
}
|
|
45
|
+
}, PEOPLE_SEARCH_DEBOUNCE_MS);
|
|
46
|
+
});
|
|
47
|
+
onUnmounted(() => {
|
|
48
|
+
if (peopleSearchTimeout)
|
|
49
|
+
clearTimeout(peopleSearchTimeout);
|
|
50
|
+
});
|
|
51
|
+
const handleUserSelect = (user) => {
|
|
52
|
+
onUserChange?.(user);
|
|
53
|
+
};
|
|
54
|
+
const handleClearUser = () => {
|
|
55
|
+
onUserChange?.(undefined);
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
peopleSuggestions,
|
|
59
|
+
isPeopleLoading,
|
|
60
|
+
peopleSearchText,
|
|
61
|
+
setPeopleSearchText,
|
|
62
|
+
peopleInputRef,
|
|
63
|
+
handleUserSelect,
|
|
64
|
+
handleClearUser,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ref, computed } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Manages searchable rich select editor state with keyboard navigation.
|
|
4
|
+
*/
|
|
5
|
+
export function useRichSelectState(params) {
|
|
6
|
+
const { values, formatValue, onCommit, onCancel } = params;
|
|
7
|
+
const searchText = ref('');
|
|
8
|
+
const highlightedIndex = ref(0);
|
|
9
|
+
const setSearchText = (text) => {
|
|
10
|
+
searchText.value = text;
|
|
11
|
+
};
|
|
12
|
+
const getDisplayText = (value) => {
|
|
13
|
+
if (formatValue)
|
|
14
|
+
return formatValue(value);
|
|
15
|
+
return value != null ? String(value) : '';
|
|
16
|
+
};
|
|
17
|
+
const filteredValues = computed(() => {
|
|
18
|
+
if (!searchText.value.trim())
|
|
19
|
+
return values;
|
|
20
|
+
const lower = searchText.value.toLowerCase();
|
|
21
|
+
return values.filter((v) => getDisplayText(v).toLowerCase().includes(lower));
|
|
22
|
+
});
|
|
23
|
+
const selectValue = (value) => {
|
|
24
|
+
onCommit(value);
|
|
25
|
+
};
|
|
26
|
+
const handleKeyDown = (e) => {
|
|
27
|
+
switch (e.key) {
|
|
28
|
+
case 'ArrowDown':
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
highlightedIndex.value = Math.min(highlightedIndex.value + 1, filteredValues.value.length - 1);
|
|
31
|
+
break;
|
|
32
|
+
case 'ArrowUp':
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
highlightedIndex.value = Math.max(highlightedIndex.value - 1, 0);
|
|
35
|
+
break;
|
|
36
|
+
case 'Enter':
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
e.stopPropagation();
|
|
39
|
+
if (filteredValues.value.length > 0 && highlightedIndex.value < filteredValues.value.length) {
|
|
40
|
+
selectValue(filteredValues.value[highlightedIndex.value]);
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
case 'Escape':
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
e.stopPropagation();
|
|
46
|
+
onCancel();
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
searchText,
|
|
52
|
+
setSearchText,
|
|
53
|
+
filteredValues,
|
|
54
|
+
highlightedIndex,
|
|
55
|
+
handleKeyDown,
|
|
56
|
+
selectValue,
|
|
57
|
+
getDisplayText,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ref, computed } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Manages row selection state for single or multiple selection modes with shift-click range support.
|
|
4
|
+
*/
|
|
5
|
+
export function useRowSelection(params) {
|
|
6
|
+
const { items, getRowId, rowSelection, controlledSelectedRows, onSelectionChange, } = params;
|
|
7
|
+
const internalSelectedRows = ref(new Set());
|
|
8
|
+
let lastClickedRow = -1;
|
|
9
|
+
const selectedRowIds = computed(() => {
|
|
10
|
+
const controlled = controlledSelectedRows.value;
|
|
11
|
+
if (controlled != null) {
|
|
12
|
+
return controlled instanceof Set
|
|
13
|
+
? controlled
|
|
14
|
+
: new Set(controlled);
|
|
15
|
+
}
|
|
16
|
+
return internalSelectedRows.value;
|
|
17
|
+
});
|
|
18
|
+
const updateSelection = (newSelectedIds) => {
|
|
19
|
+
if (controlledSelectedRows.value === undefined) {
|
|
20
|
+
internalSelectedRows.value = newSelectedIds;
|
|
21
|
+
}
|
|
22
|
+
onSelectionChange?.({
|
|
23
|
+
selectedRowIds: Array.from(newSelectedIds),
|
|
24
|
+
selectedItems: items.value.filter((item) => newSelectedIds.has(getRowId(item))),
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const handleRowCheckboxChange = (rowId, checked, rowIndex, shiftKey) => {
|
|
28
|
+
if (rowSelection.value === 'single') {
|
|
29
|
+
updateSelection(checked ? new Set([rowId]) : new Set());
|
|
30
|
+
lastClickedRow = rowIndex;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const next = new Set(selectedRowIds.value);
|
|
34
|
+
const currentItems = items.value;
|
|
35
|
+
if (shiftKey && lastClickedRow >= 0 && lastClickedRow !== rowIndex) {
|
|
36
|
+
const start = Math.min(lastClickedRow, rowIndex);
|
|
37
|
+
const end = Math.max(lastClickedRow, rowIndex);
|
|
38
|
+
for (let i = start; i <= end; i++) {
|
|
39
|
+
if (i < currentItems.length) {
|
|
40
|
+
const id = getRowId(currentItems[i]);
|
|
41
|
+
if (checked)
|
|
42
|
+
next.add(id);
|
|
43
|
+
else
|
|
44
|
+
next.delete(id);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
if (checked)
|
|
50
|
+
next.add(rowId);
|
|
51
|
+
else
|
|
52
|
+
next.delete(rowId);
|
|
53
|
+
}
|
|
54
|
+
lastClickedRow = rowIndex;
|
|
55
|
+
updateSelection(next);
|
|
56
|
+
};
|
|
57
|
+
const handleSelectAll = (checked) => {
|
|
58
|
+
if (checked) {
|
|
59
|
+
updateSelection(new Set(items.value.map((item) => getRowId(item))));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
updateSelection(new Set());
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const allSelected = computed(() => items.value.length > 0 && items.value.every((item) => selectedRowIds.value.has(getRowId(item))));
|
|
66
|
+
const someSelected = computed(() => !allSelected.value && items.value.some((item) => selectedRowIds.value.has(getRowId(item))));
|
|
67
|
+
return {
|
|
68
|
+
selectedRowIds,
|
|
69
|
+
updateSelection,
|
|
70
|
+
handleRowCheckboxChange,
|
|
71
|
+
handleSelectAll,
|
|
72
|
+
allSelected,
|
|
73
|
+
someSelected,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ref, computed } from 'vue';
|
|
2
|
+
const DEFAULT_PANELS = ['columns', 'filters'];
|
|
3
|
+
/**
|
|
4
|
+
* Manages side bar panel state: enabled panels, active panel, position, and toggle/close handlers.
|
|
5
|
+
*/
|
|
6
|
+
export function useSideBarState(params) {
|
|
7
|
+
const { config } = params;
|
|
8
|
+
const isEnabled = config != null && config !== false;
|
|
9
|
+
const parsed = (() => {
|
|
10
|
+
if (!isEnabled || config === true) {
|
|
11
|
+
return { panels: DEFAULT_PANELS, position: 'right', defaultPanel: null };
|
|
12
|
+
}
|
|
13
|
+
const def = config;
|
|
14
|
+
return {
|
|
15
|
+
panels: def.panels ?? DEFAULT_PANELS,
|
|
16
|
+
position: def.position ?? 'right',
|
|
17
|
+
defaultPanel: def.defaultPanel ?? null,
|
|
18
|
+
};
|
|
19
|
+
})();
|
|
20
|
+
const activePanel = ref(parsed.defaultPanel);
|
|
21
|
+
const setActivePanel = (panel) => {
|
|
22
|
+
activePanel.value = panel;
|
|
23
|
+
};
|
|
24
|
+
const toggle = (panel) => {
|
|
25
|
+
activePanel.value = activePanel.value === panel ? null : panel;
|
|
26
|
+
};
|
|
27
|
+
const close = () => {
|
|
28
|
+
activePanel.value = null;
|
|
29
|
+
};
|
|
30
|
+
const isOpen = computed(() => activePanel.value !== null);
|
|
31
|
+
return {
|
|
32
|
+
isEnabled,
|
|
33
|
+
activePanel,
|
|
34
|
+
setActivePanel,
|
|
35
|
+
panels: parsed.panels,
|
|
36
|
+
position: parsed.position,
|
|
37
|
+
isOpen,
|
|
38
|
+
toggle,
|
|
39
|
+
close,
|
|
40
|
+
};
|
|
41
|
+
}
|