@svadmin/ui 0.78.1 → 0.78.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.
@@ -0,0 +1,29 @@
1
+ import type { ColumnVisibilityState } from '@tanstack/svelte-table';
2
+ import type { FieldDefinition } from '@svadmin/core';
3
+ import { type ListPreferenceScope, type SavedListViewState } from './saved-list-views.js';
4
+ type ColumnPreferences = Pick<SavedListViewState, 'columnVisibility' | 'columnOrder'>;
5
+ interface ListColumnPreferencesOptions {
6
+ scope: () => ListPreferenceScope;
7
+ columns: () => Set<string>;
8
+ fields: () => FieldDefinition[];
9
+ isScopeLoaded: () => boolean;
10
+ readPreference: (scope: ListPreferenceScope, key: string, legacyKey: string) => string | null;
11
+ initialViewState: ColumnPreferences | undefined;
12
+ onDirty: () => void;
13
+ }
14
+ export declare function createListColumnPreferences(options: ListColumnPreferencesOptions): {
15
+ visibility: {
16
+ readonly current: NoInfer<ColumnVisibilityState>;
17
+ };
18
+ order: {
19
+ readonly current: NoInfer<string[]>;
20
+ };
21
+ readScope: (scope: ListPreferenceScope) => ColumnPreferences;
22
+ captureState: () => ColumnPreferences;
23
+ applyState: (state: ColumnPreferences) => void;
24
+ setVisibility: (columnId: string, visible: boolean) => void;
25
+ setOrder: (columns: Array<{
26
+ id: string;
27
+ }>) => void;
28
+ };
29
+ export {};
@@ -0,0 +1,92 @@
1
+ import { untrack } from 'svelte';
2
+ import { createAtom, useSelector } from '@tanstack/svelte-store';
3
+ import { decodeBaseRecord } from '@svadmin/core/schema';
4
+ import { columnOrderStorageKey, columnVisibilityStorageKey, legacyColumnOrderStorageKey, legacyColumnVisibilityStorageKey, } from './saved-list-views.js';
5
+ export function createListColumnPreferences(options) {
6
+ const visibilityAtom = createAtom(untrack(() => (options.initialViewState?.columnVisibility ?? readVisibility(options.scope()))));
7
+ const orderAtom = createAtom(untrack(() => (options.initialViewState?.columnOrder.length
8
+ ? options.initialViewState.columnOrder : readOrder(options.scope()))));
9
+ const visibility = useSelector(visibilityAtom);
10
+ const order = useSelector(orderAtom);
11
+ function readVisibility(scope) {
12
+ const stored = options.readPreference(scope, columnVisibilityStorageKey(scope), legacyColumnVisibilityStorageKey(scope.resourceName));
13
+ if (stored) {
14
+ try {
15
+ const parsed = JSON.parse(stored);
16
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
17
+ const visibility = {};
18
+ for (const [columnId, visible] of Object.entries(decodeBaseRecord(parsed))) {
19
+ if (options.columns().has(columnId) && typeof visible === 'boolean')
20
+ visibility[columnId] = visible;
21
+ }
22
+ return visibility;
23
+ }
24
+ }
25
+ catch { /* fall through to resource defaults */ }
26
+ }
27
+ const visibility = {};
28
+ for (const field of options.fields()) {
29
+ if (field.showInList === false)
30
+ visibility[field.key] = false;
31
+ }
32
+ return visibility;
33
+ }
34
+ function readOrder(scope) {
35
+ const stored = options.readPreference(scope, columnOrderStorageKey(scope), legacyColumnOrderStorageKey(scope.resourceName));
36
+ if (!stored)
37
+ return [];
38
+ try {
39
+ const parsed = JSON.parse(stored);
40
+ return Array.isArray(parsed)
41
+ ? [...new Set(parsed.filter((columnId) => (typeof columnId === 'string' && options.columns().has(columnId))))]
42
+ : [];
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ }
48
+ function persistOrder(ids) {
49
+ if (typeof window === 'undefined' || !options.isScopeLoaded())
50
+ return;
51
+ try {
52
+ localStorage.setItem(columnOrderStorageKey(options.scope()), JSON.stringify(ids));
53
+ }
54
+ catch { /* keep in-memory preferences when storage is unavailable */ }
55
+ }
56
+ $effect(() => {
57
+ if (!options.isScopeLoaded())
58
+ return;
59
+ const storageKey = columnVisibilityStorageKey(options.scope());
60
+ if (typeof window !== 'undefined') {
61
+ try {
62
+ localStorage.setItem(storageKey, JSON.stringify(visibility.current));
63
+ }
64
+ catch { /* keep in-memory preferences when storage is unavailable */ }
65
+ }
66
+ });
67
+ $effect(() => {
68
+ if (!options.isScopeLoaded())
69
+ return;
70
+ persistOrder(order.current);
71
+ });
72
+ function readScope(scope) {
73
+ return { columnVisibility: readVisibility(scope), columnOrder: readOrder(scope) };
74
+ }
75
+ function captureState() {
76
+ return { columnVisibility: { ...visibility.current }, columnOrder: [...order.current] };
77
+ }
78
+ function applyState(state) {
79
+ visibilityAtom.set(state.columnVisibility);
80
+ orderAtom.set(state.columnOrder);
81
+ persistOrder(state.columnOrder);
82
+ }
83
+ function setVisibility(columnId, visible) {
84
+ options.onDirty();
85
+ visibilityAtom.set({ ...visibility.current, [columnId]: visible });
86
+ }
87
+ function setOrder(columns) {
88
+ options.onDirty();
89
+ orderAtom.set(columns.map(column => column.id));
90
+ }
91
+ return { visibility, order, readScope, captureState, applyState, setVisibility, setOrder };
92
+ }
@@ -0,0 +1,38 @@
1
+ import type { createListSelection } from './list-selection.svelte.js';
2
+ interface DeleteRequest {
3
+ ids: (string | number)[];
4
+ batch: boolean;
5
+ }
6
+ interface ListDeletionOptions {
7
+ canRequestSingle: () => boolean;
8
+ canRequestBatch: () => boolean;
9
+ canConfirm: () => boolean;
10
+ permission: () => {
11
+ isLoading: boolean;
12
+ allowed: boolean;
13
+ };
14
+ selection: () => Pick<ReturnType<typeof createListSelection>, 'selectedIds' | 'captureExplicit' | 'restoreExplicit' | 'retainFailedIds'>;
15
+ clearSelection: () => void;
16
+ captureScope: () => () => boolean;
17
+ mutate: (ids: (string | number)[]) => Promise<unknown>;
18
+ reportError: (message: string | null) => void;
19
+ messages: {
20
+ single: () => string;
21
+ batch: (count: number) => string;
22
+ failure: () => string;
23
+ partial: (failed: number, total: number) => string;
24
+ };
25
+ }
26
+ export declare function createListDeletion(options: ListDeletionOptions): {
27
+ readonly request: DeleteRequest | null;
28
+ readonly open: boolean;
29
+ readonly pending: boolean;
30
+ readonly message: string;
31
+ requestDelete: (id: string | number) => void;
32
+ requestBatchDelete: () => void;
33
+ confirm: () => Promise<void>;
34
+ cancel: () => void;
35
+ cancelBatchOnCriteriaChange: () => void;
36
+ reset: () => void;
37
+ };
38
+ export {};
@@ -0,0 +1,111 @@
1
+ import { tick } from 'svelte';
2
+ import { DeleteManyPartialError, getAdminOptions } from '@svadmin/core';
3
+ export function createListDeletion(options) {
4
+ let request = $state(null);
5
+ let open = $state(false);
6
+ let message = $state('');
7
+ let pending = $state(false);
8
+ let activeDelete;
9
+ $effect(() => {
10
+ if (request && !options.permission().isLoading && !options.permission().allowed) {
11
+ open = false;
12
+ request = null;
13
+ options.reportError(options.messages.failure());
14
+ }
15
+ });
16
+ $effect(() => () => { activeDelete = undefined; });
17
+ function requestDelete(id) {
18
+ if (!options.canRequestSingle() || pending)
19
+ return;
20
+ message = options.messages.single();
21
+ request = { ids: [id], batch: false };
22
+ options.reportError(null);
23
+ open = true;
24
+ }
25
+ function requestBatchDelete() {
26
+ const ids = [...options.selection().selectedIds];
27
+ if (!options.canRequestBatch() || ids.length === 0 || pending)
28
+ return;
29
+ message = options.messages.batch(ids.length);
30
+ request = { ids, batch: true };
31
+ options.reportError(null);
32
+ open = true;
33
+ }
34
+ function cancel() {
35
+ if (pending)
36
+ return;
37
+ open = false;
38
+ request = null;
39
+ }
40
+ function cancelBatchOnCriteriaChange() {
41
+ if (request?.batch && !pending)
42
+ cancel();
43
+ }
44
+ function reset() {
45
+ activeDelete = undefined;
46
+ open = false;
47
+ pending = false;
48
+ request = null;
49
+ options.reportError(null);
50
+ }
51
+ async function confirm() {
52
+ if (!open || !options.canConfirm() || pending || !request)
53
+ return;
54
+ const confirmedRequest = request;
55
+ const selection = options.selection();
56
+ if (confirmedRequest.batch && JSON.stringify(confirmedRequest.ids) !== JSON.stringify(selection.selectedIds)) {
57
+ cancel();
58
+ return;
59
+ }
60
+ const scopeIsCurrent = options.captureScope();
61
+ const token = {};
62
+ activeDelete = token;
63
+ pending = true;
64
+ open = false;
65
+ const clearBeforeMutation = confirmedRequest.batch && getAdminOptions().mutationMode === 'undoable';
66
+ const selectionBeforeDelete = clearBeforeMutation ? selection.captureExplicit() : undefined;
67
+ if (clearBeforeMutation)
68
+ options.clearSelection();
69
+ const current = () => activeDelete === token && scopeIsCurrent();
70
+ try {
71
+ await options.mutate([...confirmedRequest.ids]);
72
+ if (!current())
73
+ return;
74
+ options.clearSelection();
75
+ await tick();
76
+ if (!current())
77
+ return;
78
+ open = false;
79
+ }
80
+ catch (error) {
81
+ if (!current())
82
+ return;
83
+ if (error instanceof DeleteManyPartialError) {
84
+ selection.retainFailedIds(error.failedIds);
85
+ options.reportError(options.messages.partial(error.failedIds.length, confirmedRequest.ids.length));
86
+ }
87
+ else {
88
+ if (selectionBeforeDelete)
89
+ selection.restoreExplicit(selectionBeforeDelete);
90
+ options.reportError(options.messages.failure());
91
+ }
92
+ open = false;
93
+ }
94
+ finally {
95
+ if (activeDelete === token) {
96
+ activeDelete = undefined;
97
+ pending = false;
98
+ request = null;
99
+ }
100
+ await tick();
101
+ await tick();
102
+ }
103
+ }
104
+ return {
105
+ get request() { return request; },
106
+ get open() { return open; },
107
+ get pending() { return pending; },
108
+ get message() { return message; },
109
+ requestDelete, requestBatchDelete, confirm, cancel, cancelBatchOnCriteriaChange, reset,
110
+ };
111
+ }
@@ -0,0 +1,31 @@
1
+ import type { Filter, Sort } from '@svadmin/core';
2
+ import type { RowSelectionState } from '@tanstack/svelte-table';
3
+ import { type BatchSelection } from './table-contract.js';
4
+ interface ListSelectionOptions {
5
+ filters: () => Filter[];
6
+ sorters: () => Sort[];
7
+ total: () => number;
8
+ selectable: () => boolean;
9
+ allowAllMatching: () => boolean;
10
+ onCriteriaChange: () => void;
11
+ }
12
+ export declare function createListSelection(options: ListSelectionOptions): {
13
+ rowSelection: RowSelectionState;
14
+ readonly allMatchingSelected: boolean;
15
+ readonly selectedIds: (string | number)[];
16
+ readonly selectedCount: number;
17
+ rowIsSelected: (key: string) => boolean;
18
+ trackRow: (key: string, id: string | number, selected: boolean) => void;
19
+ toggleExcluded: (key: string, id: string | number) => void;
20
+ selectAllMatching: () => void;
21
+ snapshot: (page: ReadonlyArray<{
22
+ key: string;
23
+ id: string | number;
24
+ }>) => BatchSelection;
25
+ reset: () => void;
26
+ clearExplicit: () => void;
27
+ captureExplicit: () => Map<string, string | number>;
28
+ restoreExplicit: (ids: ReadonlyMap<string, string | number>) => void;
29
+ retainFailedIds: (ids: (string | number)[]) => void;
30
+ };
31
+ export {};
@@ -0,0 +1,100 @@
1
+ import { untrack } from 'svelte';
2
+ import { SvelteMap } from 'svelte/reactivity';
3
+ import { tableRowKey } from './table-contract.js';
4
+ import { cloneListFilter } from './list-state.svelte.js';
5
+ export function createListSelection(options) {
6
+ let rowSelection = $state({});
7
+ let allMatchingSelected = $state(false);
8
+ const selectedIdValueByKey = new SvelteMap();
9
+ const excludedMatchingIds = new SvelteMap();
10
+ const selectedIds = $derived(Object.keys(rowSelection).filter(key => rowSelection[key] === true).flatMap(key => {
11
+ const id = selectedIdValueByKey.get(key);
12
+ return id === undefined ? [] : [id];
13
+ }));
14
+ const selectedCount = $derived(allMatchingSelected
15
+ ? Math.max(0, options.total() - excludedMatchingIds.size) : selectedIds.length);
16
+ const criteria = $derived(JSON.stringify(options.filters()));
17
+ let previousCriteria = untrack(() => criteria);
18
+ $effect.pre(() => {
19
+ const nextCriteria = criteria;
20
+ if (nextCriteria !== previousCriteria) {
21
+ reset();
22
+ options.onCriteriaChange();
23
+ }
24
+ previousCriteria = nextCriteria;
25
+ });
26
+ $effect.pre(() => {
27
+ if (!options.selectable() || !options.allowAllMatching())
28
+ clearMatching();
29
+ if (!options.selectable())
30
+ clearExplicit();
31
+ });
32
+ function clearMatching() {
33
+ allMatchingSelected = false;
34
+ excludedMatchingIds.clear();
35
+ }
36
+ function clearExplicit() {
37
+ selectedIdValueByKey.clear();
38
+ rowSelection = {};
39
+ }
40
+ function reset() {
41
+ clearMatching();
42
+ clearExplicit();
43
+ }
44
+ function rowIsSelected(key) {
45
+ return allMatchingSelected ? !excludedMatchingIds.has(key) : rowSelection[key] === true;
46
+ }
47
+ function trackRow(key, id, selected) {
48
+ if (selected)
49
+ selectedIdValueByKey.set(key, id);
50
+ else
51
+ selectedIdValueByKey.delete(key);
52
+ }
53
+ function toggleExcluded(key, id) {
54
+ if (excludedMatchingIds.has(key))
55
+ excludedMatchingIds.delete(key);
56
+ else
57
+ excludedMatchingIds.set(key, id);
58
+ }
59
+ function selectAllMatching() {
60
+ clearExplicit();
61
+ excludedMatchingIds.clear();
62
+ allMatchingSelected = true;
63
+ }
64
+ function snapshot(page) {
65
+ if (allMatchingSelected)
66
+ return {
67
+ scope: 'all',
68
+ filters: options.filters().map(cloneListFilter),
69
+ sorters: options.sorters().map(sorter => ({ ...sorter })),
70
+ total: options.total(),
71
+ excludedIds: [...excludedMatchingIds.values()],
72
+ };
73
+ return {
74
+ scope: 'selected',
75
+ ids: [...selectedIds],
76
+ currentPageIds: page.filter(row => rowSelection[row.key] === true).map(row => row.id),
77
+ };
78
+ }
79
+ function captureExplicit() {
80
+ return new Map(selectedIdValueByKey);
81
+ }
82
+ function restoreExplicit(ids) {
83
+ selectedIdValueByKey.clear();
84
+ for (const [key, id] of ids)
85
+ selectedIdValueByKey.set(key, id);
86
+ rowSelection = Object.fromEntries([...ids.keys()].map(key => [key, true]));
87
+ }
88
+ function retainFailedIds(ids) {
89
+ restoreExplicit(new Map(ids.map(id => [tableRowKey(id), id])));
90
+ }
91
+ return {
92
+ get rowSelection() { return rowSelection; },
93
+ set rowSelection(value) { rowSelection = value; },
94
+ get allMatchingSelected() { return allMatchingSelected; },
95
+ get selectedIds() { return selectedIds; },
96
+ get selectedCount() { return selectedCount; },
97
+ rowIsSelected, trackRow, toggleExcluded, selectAllMatching, snapshot,
98
+ reset, clearExplicit, captureExplicit, restoreExplicit, retainFailedIds,
99
+ };
100
+ }
@@ -0,0 +1,49 @@
1
+ import type { FieldDefinition, Filter, Pagination, Sort, readURLState, writeURLState } from '@svadmin/core';
2
+ import type { SavedListViewState } from './saved-list-views.js';
3
+ type URLState = ReturnType<typeof readURLState>;
4
+ type ListStateSnapshot = Pick<SavedListViewState, 'search' | 'filters' | 'sorters' | 'pagination'>;
5
+ interface ListStateOptions {
6
+ fields: () => FieldDefinition[];
7
+ defaultPageSize: () => number;
8
+ defaultSort: () => Sort | undefined;
9
+ externalPagination: () => Pagination | undefined;
10
+ externalSorters: () => Sort[] | undefined;
11
+ initialURLState: URLState;
12
+ initialViewState: SavedListViewState | undefined;
13
+ syncWithLocation: () => boolean;
14
+ writeURLState: (state: Parameters<typeof writeURLState>[0]) => void;
15
+ onDirty: () => void;
16
+ }
17
+ export declare function cloneListFilter(filter: Filter): Filter;
18
+ export declare function createListState(options: ListStateOptions): {
19
+ pagination: Pagination;
20
+ sorters: Sort[];
21
+ readonly searchText: string;
22
+ readonly appliedSearchText: string;
23
+ readonly filterValues: Record<string, string>;
24
+ readonly searchableFields: FieldDefinition[];
25
+ readonly filterableFields: FieldDefinition[];
26
+ readonly activeFilterCount: number;
27
+ readonly activeFilterItems: {
28
+ index: number;
29
+ label: string;
30
+ }[];
31
+ readonly activeFilters: Filter[];
32
+ readonly queryPagination: Pagination;
33
+ readonly querySorters: {
34
+ field: string;
35
+ order: "desc" | "asc";
36
+ }[];
37
+ readonly queryFilters: Filter[];
38
+ scheduleSearch: (value: string) => void;
39
+ flushSearch: () => void;
40
+ cancelSearch: () => void;
41
+ clearSearch: () => void;
42
+ clearFilters: () => void;
43
+ setFilterValue: (field: string, value: string) => void;
44
+ removeActiveFilter: (index: number) => void;
45
+ captureState: () => ListStateSnapshot;
46
+ applyState: (state: ListStateSnapshot) => void;
47
+ resetToDefaults: () => void;
48
+ };
49
+ export {};
@@ -0,0 +1,225 @@
1
+ import { untrack } from 'svelte';
2
+ import { definedOptions } from '@svadmin/core/options';
3
+ import { snapshotPlainData } from '@svadmin/core/schema';
4
+ export function cloneListFilter(filter) {
5
+ if ('field' in filter) {
6
+ return {
7
+ field: filter.field,
8
+ operator: filter.operator,
9
+ value: filter.value === undefined ? undefined : snapshotPlainData(filter.value),
10
+ };
11
+ }
12
+ return { operator: filter.operator, value: filter.value.map(cloneListFilter) };
13
+ }
14
+ function editableValues(filters, fields) {
15
+ const keys = new Set(fields.filter(field => field.filterable).map(field => field.key));
16
+ const counts = new Map();
17
+ const values = {};
18
+ for (const filter of filters) {
19
+ if ('field' in filter && filter.operator === 'contains' && typeof filter.value === 'string' && keys.has(filter.field)) {
20
+ counts.set(filter.field, (counts.get(filter.field) ?? 0) + 1);
21
+ }
22
+ }
23
+ for (const filter of filters) {
24
+ if ('field' in filter && filter.operator === 'contains' && typeof filter.value === 'string' && counts.get(filter.field) === 1) {
25
+ values[filter.field] = filter.value;
26
+ }
27
+ }
28
+ return values;
29
+ }
30
+ export function createListState(options) {
31
+ const url = options.initialURLState;
32
+ const initialView = options.initialViewState;
33
+ let pagination = $state(untrack(() => options.externalPagination() ?? {
34
+ current: url.page ?? initialView?.pagination.current ?? 1,
35
+ pageSize: url.pageSize ?? initialView?.pagination.pageSize ?? options.defaultPageSize(),
36
+ }));
37
+ let sorters = $state(untrack(() => {
38
+ const defaultSort = options.defaultSort();
39
+ return options.externalSorters() ?? (url.sortField
40
+ ? [{ field: url.sortField, order: url.sortOrder ?? 'asc' }]
41
+ : initialView?.sorters ?? (defaultSort ? [defaultSort] : []));
42
+ }));
43
+ let filters = $state(url.filters ?? initialView?.filters ?? []);
44
+ let filterValues = $state(untrack(() => editableValues(filters, options.fields())));
45
+ let searchText = $state(url.search ?? initialView?.search ?? '');
46
+ let appliedSearchText = $state(untrack(() => searchText));
47
+ let searchTimer;
48
+ const searchableFields = $derived(options.fields().filter(field => field.searchable));
49
+ const filterableFields = $derived(options.fields().filter(field => field.filterable));
50
+ const activeFilterItems = $derived.by(() => {
51
+ const labels = new Map(options.fields().map(field => [field.key, field.label]));
52
+ return filters.map((filter, index) => ({
53
+ index,
54
+ label: 'field' in filter
55
+ ? `${labels.get(filter.field) ?? filter.field}: ${String(filter.value)}`
56
+ : filter.operator,
57
+ }));
58
+ });
59
+ const activeFilters = $derived.by(() => {
60
+ const result = [...filters];
61
+ if (appliedSearchText.trim() && searchableFields.length > 0) {
62
+ const first = searchableFields[0];
63
+ if (searchableFields.length === 1 && first) {
64
+ result.push({ field: first.key, operator: 'contains', value: appliedSearchText });
65
+ }
66
+ else {
67
+ result.push({
68
+ operator: 'or',
69
+ value: searchableFields.map(field => ({
70
+ field: field.key, operator: 'contains', value: appliedSearchText,
71
+ })),
72
+ });
73
+ }
74
+ }
75
+ return result;
76
+ });
77
+ const queryPagination = $derived(definedOptions({
78
+ current: pagination.current, pageSize: pagination.pageSize, mode: pagination.mode,
79
+ }));
80
+ const querySorters = $derived(sorters.map(sorter => ({ field: sorter.field, order: sorter.order })));
81
+ const queryFilters = $derived(activeFilters.map(cloneListFilter));
82
+ $effect(() => {
83
+ const external = options.externalPagination();
84
+ if (external)
85
+ pagination = external;
86
+ });
87
+ $effect(() => {
88
+ const external = options.externalSorters();
89
+ if (external)
90
+ sorters = external;
91
+ });
92
+ $effect(() => {
93
+ if (!options.syncWithLocation())
94
+ return;
95
+ options.writeURLState({
96
+ page: pagination.current,
97
+ pageSize: pagination.pageSize,
98
+ sortField: sorters[0]?.field,
99
+ sortOrder: sorters[0]?.order,
100
+ search: appliedSearchText || undefined,
101
+ filters,
102
+ });
103
+ });
104
+ $effect(() => cancelSearch);
105
+ function cancelSearch() {
106
+ if (searchTimer !== undefined)
107
+ clearTimeout(searchTimer);
108
+ searchTimer = undefined;
109
+ }
110
+ function scheduleSearch(value) {
111
+ options.onDirty();
112
+ searchText = value;
113
+ cancelSearch();
114
+ searchTimer = setTimeout(() => {
115
+ appliedSearchText = searchText;
116
+ options.onDirty();
117
+ pagination = { ...pagination, current: 1 };
118
+ searchTimer = undefined;
119
+ }, 300);
120
+ }
121
+ function flushSearch() {
122
+ if (searchTimer === undefined)
123
+ return;
124
+ cancelSearch();
125
+ appliedSearchText = searchText;
126
+ }
127
+ function clearSearch() {
128
+ searchText = '';
129
+ appliedSearchText = '';
130
+ pagination = { ...pagination, current: 1 };
131
+ }
132
+ function clearFilters() {
133
+ options.onDirty();
134
+ cancelSearch();
135
+ clearSearch();
136
+ filters = [];
137
+ filterValues = {};
138
+ }
139
+ function setFilterValue(field, value) {
140
+ options.onDirty();
141
+ const trimmedValue = value.trim();
142
+ const nextFilter = { field, operator: 'contains', value: trimmedValue };
143
+ let replaced = false;
144
+ filters = filters.flatMap(filter => {
145
+ if (!('field' in filter) || filter.field !== field || filter.operator !== 'contains')
146
+ return [filter];
147
+ if (!trimmedValue || replaced)
148
+ return [];
149
+ replaced = true;
150
+ return [nextFilter];
151
+ });
152
+ if (trimmedValue && !replaced)
153
+ filters = [...filters, nextFilter];
154
+ filterValues[field] = value;
155
+ pagination = { ...pagination, current: 1 };
156
+ }
157
+ function removeActiveFilter(index) {
158
+ options.onDirty();
159
+ const filter = filters[index];
160
+ if (!filter)
161
+ return;
162
+ const nextFilters = filters.filter((_, filterIndex) => filterIndex !== index);
163
+ filters = nextFilters;
164
+ if ('field' in filter && filter.operator === 'contains') {
165
+ const remaining = nextFilters.filter(candidate => ('field' in candidate && candidate.field === filter.field
166
+ && candidate.operator === 'contains' && typeof candidate.value === 'string'));
167
+ const first = remaining[0];
168
+ filterValues[filter.field] = remaining.length === 1 && first && 'field' in first
169
+ ? String(first.value) : '';
170
+ }
171
+ pagination = { ...pagination, current: 1 };
172
+ }
173
+ function captureState() {
174
+ return {
175
+ search: appliedSearchText,
176
+ filters: filters.map(cloneListFilter),
177
+ sorters: sorters.map(sorter => ({ ...sorter })),
178
+ pagination: {
179
+ current: pagination.current ?? 1,
180
+ pageSize: pagination.pageSize ?? options.defaultPageSize(),
181
+ },
182
+ };
183
+ }
184
+ function applyState(state) {
185
+ cancelSearch();
186
+ searchText = state.search;
187
+ appliedSearchText = state.search;
188
+ filters = state.filters;
189
+ filterValues = editableValues(state.filters, options.fields());
190
+ pagination = { ...pagination, ...state.pagination };
191
+ sorters = state.sorters;
192
+ }
193
+ function resetToDefaults() {
194
+ cancelSearch();
195
+ searchText = '';
196
+ appliedSearchText = '';
197
+ filters = [];
198
+ filterValues = {};
199
+ if (!options.externalPagination())
200
+ pagination = { current: 1, pageSize: options.defaultPageSize() };
201
+ if (!options.externalSorters()) {
202
+ const defaultSort = options.defaultSort();
203
+ sorters = defaultSort ? [{ ...defaultSort }] : [];
204
+ }
205
+ }
206
+ return {
207
+ get pagination() { return pagination; },
208
+ set pagination(value) { pagination = value; },
209
+ get sorters() { return sorters; },
210
+ set sorters(value) { sorters = value; },
211
+ get searchText() { return searchText; },
212
+ get appliedSearchText() { return appliedSearchText; },
213
+ get filterValues() { return filterValues; },
214
+ get searchableFields() { return searchableFields; },
215
+ get filterableFields() { return filterableFields; },
216
+ get activeFilterCount() { return filters.length; },
217
+ get activeFilterItems() { return activeFilterItems; },
218
+ get activeFilters() { return activeFilters; },
219
+ get queryPagination() { return queryPagination; },
220
+ get querySorters() { return querySorters; },
221
+ get queryFilters() { return queryFilters; },
222
+ scheduleSearch, flushSearch, cancelSearch, clearSearch, clearFilters, setFilterValue, removeActiveFilter,
223
+ captureState, applyState, resetToDefaults,
224
+ };
225
+ }