nuxt-ui-tools 1.26.0 → 1.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/module.json +1 -1
- package/dist/runtime/table/composables/use-filter-persistence.d.ts +14 -0
- package/dist/runtime/table/composables/use-filter-persistence.js +77 -0
- package/dist/runtime/table/composables/use-query-state.js +31 -26
- package/dist/runtime/table/composables/use-table-api.js +4 -0
- package/dist/runtime/table/composables/use-table-data.js +3 -2
- package/dist/runtime/table/composables/use-table-state.d.ts +1 -0
- package/dist/runtime/table/composables/use-table-state.js +13 -2
- package/dist/runtime/table/types/filters.d.ts +11 -0
- package/dist/runtime/table/types/table-api.d.ts +5 -0
- package/dist/runtime/table/utils/filter-persistence.d.ts +16 -0
- package/dist/runtime/table/utils/filter-persistence.js +42 -0
- package/dist/runtime/table/utils/index.d.ts +1 -0
- package/dist/runtime/table/utils/index.js +1 -0
- package/dist/runtime/table/utils/resolved-filters.d.ts +8 -0
- package/dist/runtime/table/utils/resolved-filters.js +6 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComputedRef, WritableComputedRef } from 'vue';
|
|
2
|
+
import type { TableFilterState, TableSchemaView } from '../types/index.js';
|
|
3
|
+
export interface UseFilterPersistenceParams {
|
|
4
|
+
schema: ComputedRef<TableSchemaView>;
|
|
5
|
+
filters: WritableComputedRef<TableFilterState>;
|
|
6
|
+
/** Writes the restored state without adding a browser history entry. */
|
|
7
|
+
restore: (state: TableFilterState) => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Keeps the value of every filter marked `behavior.persist` in `localStorage`. On setup, a stored
|
|
11
|
+
* value replaces the default of a filter the URL does not set; afterwards each change is written
|
|
12
|
+
* back, and a filter back at its default is forgotten.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useFilterPersistence(params: UseFilterPersistenceParams): void;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { watch } from "vue";
|
|
2
|
+
import {
|
|
3
|
+
isTableFilterRuleDefault,
|
|
4
|
+
parsePersistedFilterRule,
|
|
5
|
+
persistedFilterStorageKey,
|
|
6
|
+
serializePersistedFilterRule
|
|
7
|
+
} from "../utils/index.js";
|
|
8
|
+
export function useFilterPersistence(params) {
|
|
9
|
+
const storage = resolveStorage();
|
|
10
|
+
const persisted = (params.schema.value.filters?.ui ?? []).filter(
|
|
11
|
+
(definition) => definition.behavior?.persist === true
|
|
12
|
+
);
|
|
13
|
+
if (!storage || persisted.length === 0) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const storageKey = (definition) => persistedFilterStorageKey({
|
|
17
|
+
tableKey: params.schema.value.tableKey,
|
|
18
|
+
filterKey: definition.key
|
|
19
|
+
});
|
|
20
|
+
const current = params.filters.value.ui;
|
|
21
|
+
const restored = persisted.flatMap((definition) => {
|
|
22
|
+
const rule = current.find((candidate) => candidate.key === definition.key);
|
|
23
|
+
if (rule && !isTableFilterRuleDefault({ definition, rule })) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const stored = parsePersistedFilterRule({
|
|
27
|
+
definition,
|
|
28
|
+
raw: safeRead(storage, storageKey(definition))
|
|
29
|
+
});
|
|
30
|
+
return stored ? [stored] : [];
|
|
31
|
+
});
|
|
32
|
+
if (restored.length > 0) {
|
|
33
|
+
const keys = new Set(restored.map((rule) => rule.key));
|
|
34
|
+
params.restore({
|
|
35
|
+
...params.filters.value,
|
|
36
|
+
ui: [...current.filter((rule) => !keys.has(rule.key)), ...restored]
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
watch(
|
|
40
|
+
() => params.filters.value.ui,
|
|
41
|
+
(rules) => {
|
|
42
|
+
for (const definition of persisted) {
|
|
43
|
+
const rule = rules.find((candidate) => candidate.key === definition.key);
|
|
44
|
+
safeWrite(
|
|
45
|
+
storage,
|
|
46
|
+
storageKey(definition),
|
|
47
|
+
rule && !isTableFilterRuleDefault({ definition, rule }) ? serializePersistedFilterRule({ definition, rule }) : null
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function resolveStorage() {
|
|
54
|
+
try {
|
|
55
|
+
return typeof window === "undefined" ? null : window.localStorage;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function safeRead(storage, key) {
|
|
61
|
+
try {
|
|
62
|
+
return storage.getItem(key);
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function safeWrite(storage, key, value) {
|
|
68
|
+
try {
|
|
69
|
+
if (value === null) {
|
|
70
|
+
storage.removeItem(key);
|
|
71
|
+
} else {
|
|
72
|
+
storage.setItem(key, value);
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
resolveFilterSupportedOperators,
|
|
20
20
|
serializeTableFilterQueryState
|
|
21
21
|
} from "../utils/index.js";
|
|
22
|
+
import { useFilterPersistence } from "./use-filter-persistence.js";
|
|
22
23
|
export function useQueryState(params) {
|
|
23
24
|
const defaultPageSize = getDefaultPageSize({
|
|
24
25
|
layout: params.activeLayout.value,
|
|
@@ -96,32 +97,36 @@ export function useQueryState(params) {
|
|
|
96
97
|
sortingState.value = value ? { dir: value.dir, key: value.key } : { dir: void 0, key: "" };
|
|
97
98
|
}
|
|
98
99
|
});
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
100
|
+
const filterSchema = {
|
|
101
|
+
search: { codec: stringCodec, defaultValue: "" },
|
|
102
|
+
ui: dynamicQueryState({
|
|
103
|
+
defaultValue: resolveTableFilterDefaultRules(params.schema.value.filters?.ui ?? []),
|
|
104
|
+
definitions: () => params.schema.value.filters?.ui ?? [],
|
|
105
|
+
parse(entries, definitions) {
|
|
106
|
+
return parseTableFilterQueryState({ definitions, entries });
|
|
107
|
+
},
|
|
108
|
+
resolve(filter) {
|
|
109
|
+
const definition = normalizeFilterDefinition(filter);
|
|
110
|
+
const operators = resolveFilterSupportedOperators(definition);
|
|
111
|
+
const defaultOp = resolveFilterDefaultOperator(definition);
|
|
112
|
+
const codec = createTableFilterValueCodec(definition);
|
|
113
|
+
return operators.map((op) => ({
|
|
114
|
+
codec,
|
|
115
|
+
urlKey: op === defaultOp ? definition.key : `${definition.key}~${op}`
|
|
116
|
+
}));
|
|
117
|
+
},
|
|
118
|
+
serialize(rules, definitions) {
|
|
119
|
+
return serializeTableFilterQueryState({ definitions, rules });
|
|
120
|
+
},
|
|
121
|
+
urlPrefix: "ui"
|
|
122
|
+
})
|
|
123
|
+
};
|
|
124
|
+
const filters = useQueryStates({ historyMode: "push", prefix: "f", schema: filterSchema });
|
|
125
|
+
useFilterPersistence({
|
|
126
|
+
schema: params.schema,
|
|
127
|
+
filters,
|
|
128
|
+
restore(state) {
|
|
129
|
+
useQueryStates({ historyMode: "replace", prefix: "f", schema: filterSchema }).value = state;
|
|
125
130
|
}
|
|
126
131
|
});
|
|
127
132
|
return {
|
|
@@ -23,6 +23,10 @@ export function useTableApi(params) {
|
|
|
23
23
|
// SAFETY: raw rows follow the same schema source contract before client-side shaping.
|
|
24
24
|
rawRows: computed(() => params.queryContent.rawData.value.rows),
|
|
25
25
|
rawRowCount: computed(() => params.queryContent.rawData.value.rowCount),
|
|
26
|
+
// SAFETY: filtered rows are the raw source rows after client filtering and sorting.
|
|
27
|
+
filteredRows: computed(
|
|
28
|
+
() => params.queryContent.filteredRows.value
|
|
29
|
+
),
|
|
26
30
|
context: computed(
|
|
27
31
|
// SAFETY: context keys are decoded from the schema context items before publication.
|
|
28
32
|
() => params.queryContent.contextData.value
|
|
@@ -59,6 +59,7 @@ export function useTableData(params) {
|
|
|
59
59
|
() => params.state.queryState.sorting.value ? [params.state.queryState.sorting.value] : []
|
|
60
60
|
);
|
|
61
61
|
const requestFilters = computed(() => params.state.resolvedFilterState.value);
|
|
62
|
+
const rowFilters = computed(() => params.state.rowFilterState.value);
|
|
62
63
|
const requestSearch = computed(() => ({
|
|
63
64
|
fields: params.schema.value.filters?.search?.fields ?? [],
|
|
64
65
|
value: params.state.queryState.filters.value.search
|
|
@@ -214,7 +215,7 @@ export function useTableData(params) {
|
|
|
214
215
|
return rawData.value.rows;
|
|
215
216
|
}
|
|
216
217
|
return filterClientRows({
|
|
217
|
-
filters:
|
|
218
|
+
filters: rowFilters.value,
|
|
218
219
|
rows: rawData.value.rows,
|
|
219
220
|
search: requestSearch.value
|
|
220
221
|
});
|
|
@@ -269,7 +270,7 @@ export function useTableData(params) {
|
|
|
269
270
|
if (params.schema.value.source.mode === "client") {
|
|
270
271
|
return executeClientFacets({
|
|
271
272
|
facets: clientFacetDescriptors.value,
|
|
272
|
-
request: requestContext.value,
|
|
273
|
+
request: { ...requestContext.value, filters: rowFilters.value },
|
|
273
274
|
rows: rawData.value.rows
|
|
274
275
|
});
|
|
275
276
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { computed } from "vue";
|
|
2
|
-
import { createResolvedFilterState } from "../utils/index.js";
|
|
2
|
+
import { createResolvedFilterState, withoutQueryFilterRules } from "../utils/index.js";
|
|
3
3
|
import { useQueryState } from "./use-query-state.js";
|
|
4
4
|
export function useTableState(params) {
|
|
5
5
|
const queryState = useQueryState({
|
|
@@ -13,8 +13,19 @@ export function useTableState(params) {
|
|
|
13
13
|
staticFilters: params.schema.value.filters?.static
|
|
14
14
|
})
|
|
15
15
|
);
|
|
16
|
+
const rowFilterState = computed(
|
|
17
|
+
() => createResolvedFilterState({
|
|
18
|
+
definitions: params.schema.value.filters?.ui ?? [],
|
|
19
|
+
filters: withoutQueryFilterRules({
|
|
20
|
+
definitions: params.schema.value.filters?.ui ?? [],
|
|
21
|
+
filters: queryState.filters.value
|
|
22
|
+
}),
|
|
23
|
+
staticFilters: params.schema.value.filters?.static
|
|
24
|
+
})
|
|
25
|
+
);
|
|
16
26
|
return {
|
|
17
27
|
queryState,
|
|
18
|
-
resolvedFilterState
|
|
28
|
+
resolvedFilterState,
|
|
29
|
+
rowFilterState
|
|
19
30
|
};
|
|
20
31
|
}
|
|
@@ -31,6 +31,17 @@ export interface TableFilterBehaviorCommon<TOperator extends TableFilterOperator
|
|
|
31
31
|
commitMode?: TableFilterCommitMode;
|
|
32
32
|
clearOnOperatorChange?: boolean;
|
|
33
33
|
reopenOnOperatorChange?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* What the filter narrows. `rows` (default) matches every row against it. `query` only hands the
|
|
36
|
+
* value to the source query, through its `filters`, so a client source can ask the server for a
|
|
37
|
+
* different dataset (a period, a scope) while its rows are never matched against the rule.
|
|
38
|
+
*/
|
|
39
|
+
applyTo?: 'rows' | 'query';
|
|
40
|
+
/**
|
|
41
|
+
* Remembers the last value in `localStorage`, per table key and filter key. It is restored when
|
|
42
|
+
* the URL carries no value of its own, and forgotten once the filter returns to its default.
|
|
43
|
+
*/
|
|
44
|
+
persist?: boolean;
|
|
34
45
|
}
|
|
35
46
|
export interface TableFilterUiActionLabelsConfig {
|
|
36
47
|
actions?: {
|
|
@@ -87,6 +87,11 @@ export interface TableApi<TSchema = TableSchemaView> {
|
|
|
87
87
|
totalRowCount: ComputedRef<number | null>;
|
|
88
88
|
rawRows: ComputedRef<ExtractTableRow<TSchema>[]>;
|
|
89
89
|
rawRowCount: ComputedRef<number | null>;
|
|
90
|
+
/**
|
|
91
|
+
* Every row that passes the filters and the search, sorted, before pagination. Client sources
|
|
92
|
+
* only; `undefined` for remote sources, which never hold the whole result.
|
|
93
|
+
*/
|
|
94
|
+
filteredRows: ComputedRef<ExtractTableRow<TSchema>[] | undefined>;
|
|
90
95
|
context: ComputedRef<ExtractTableContextData<TSchema>>;
|
|
91
96
|
pageContext: ComputedRef<ExtractTablePageContextData<TSchema>>;
|
|
92
97
|
requestContext: ReturnType<typeof useTableData>['requestContext'];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TableQueryStateFilterRule, TableUiFilterDefinition } from '../types/index.js';
|
|
2
|
+
/** Storage key of one persisted filter, scoped by table key. */
|
|
3
|
+
export declare function persistedFilterStorageKey(options: {
|
|
4
|
+
tableKey: string;
|
|
5
|
+
filterKey: string;
|
|
6
|
+
}): string;
|
|
7
|
+
/** Serializes a rule with the filter's URL codec, or returns `null` when it has nothing to keep. */
|
|
8
|
+
export declare function serializePersistedFilterRule(options: {
|
|
9
|
+
definition: TableUiFilterDefinition;
|
|
10
|
+
rule: TableQueryStateFilterRule;
|
|
11
|
+
}): string | null;
|
|
12
|
+
/** Reads a stored rule back, dropping anything the filter no longer accepts. */
|
|
13
|
+
export declare function parsePersistedFilterRule(options: {
|
|
14
|
+
definition: TableUiFilterDefinition;
|
|
15
|
+
raw: string | null;
|
|
16
|
+
}): TableQueryStateFilterRule | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isObject, isString } from "../../shared/utils/predicate.js";
|
|
2
|
+
import {
|
|
3
|
+
createTableFilterValueCodec,
|
|
4
|
+
normalizeFilterDefinition,
|
|
5
|
+
resolveFilterDefaultOperator,
|
|
6
|
+
resolveFilterSupportedOperators
|
|
7
|
+
} from "./query-state.js";
|
|
8
|
+
const STORAGE_PREFIX = "nuxt-ui-tools:table-filter";
|
|
9
|
+
export function persistedFilterStorageKey(options) {
|
|
10
|
+
return `${STORAGE_PREFIX}:${options.tableKey}:${options.filterKey}`;
|
|
11
|
+
}
|
|
12
|
+
export function serializePersistedFilterRule(options) {
|
|
13
|
+
const normalized = normalizeFilterDefinition(options.definition);
|
|
14
|
+
const value = createTableFilterValueCodec(normalized).serialize(options.rule.value);
|
|
15
|
+
return value === null ? null : JSON.stringify({
|
|
16
|
+
operator: options.rule.operator ?? resolveFilterDefaultOperator(normalized),
|
|
17
|
+
value
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function parsePersistedFilterRule(options) {
|
|
21
|
+
if (!options.raw) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const stored = parseStoredRule(options.raw);
|
|
25
|
+
if (!stored) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const normalized = normalizeFilterDefinition(options.definition);
|
|
29
|
+
const operator = resolveFilterSupportedOperators(normalized).find(
|
|
30
|
+
(candidate) => candidate === stored.operator
|
|
31
|
+
);
|
|
32
|
+
const value = createTableFilterValueCodec(normalized).parse(stored.value);
|
|
33
|
+
return operator && value !== void 0 ? { key: options.definition.key, operator, value } : null;
|
|
34
|
+
}
|
|
35
|
+
function parseStoredRule(raw) {
|
|
36
|
+
try {
|
|
37
|
+
const stored = JSON.parse(raw);
|
|
38
|
+
return isObject(stored) && isString(stored.operator) && isString(stored.value) ? { operator: stored.operator, value: stored.value } : null;
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -4,6 +4,7 @@ export * from './columns/index.js';
|
|
|
4
4
|
export * from './cursor-query.js';
|
|
5
5
|
export * from './data-list-ui.js';
|
|
6
6
|
export * from './facets.js';
|
|
7
|
+
export * from './filter-persistence.js';
|
|
7
8
|
export * from './filters/index.js';
|
|
8
9
|
export * from './pagination.js';
|
|
9
10
|
export * from './prefetch.js';
|
|
@@ -4,6 +4,7 @@ export * from "./columns/index.js";
|
|
|
4
4
|
export * from "./cursor-query.js";
|
|
5
5
|
export * from "./data-list-ui.js";
|
|
6
6
|
export * from "./facets.js";
|
|
7
|
+
export * from "./filter-persistence.js";
|
|
7
8
|
export * from "./filters/index.js";
|
|
8
9
|
export * from "./pagination.js";
|
|
9
10
|
export * from "./prefetch.js";
|
|
@@ -5,3 +5,11 @@ export declare function createResolvedFilterState<TRow extends GenericObject = G
|
|
|
5
5
|
staticFilters?: TableStaticFilterNode<TRow, TContext, TKey>[];
|
|
6
6
|
context?: TContext;
|
|
7
7
|
}): TableResolvedFilterGroup<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Drops the rules of filters marked `behavior.applyTo: 'query'`, which feed the source query but
|
|
10
|
+
* never narrow client rows.
|
|
11
|
+
*/
|
|
12
|
+
export declare function withoutQueryFilterRules(options: {
|
|
13
|
+
definitions: TableUiFilterDefinition[];
|
|
14
|
+
filters: TableFilterState;
|
|
15
|
+
}): TableFilterState;
|
|
@@ -67,3 +67,9 @@ function normalizeResolvedFilterNode(node) {
|
|
|
67
67
|
function isResolvedFilterGroup(value) {
|
|
68
68
|
return !!value && isObject(value) && "type" in value && value.type === "group" && "children" in value && isArray(value.children);
|
|
69
69
|
}
|
|
70
|
+
export function withoutQueryFilterRules(options) {
|
|
71
|
+
const queryKeys = new Set(
|
|
72
|
+
options.definitions.filter((definition) => definition.behavior?.applyTo === "query").map((definition) => definition.key)
|
|
73
|
+
);
|
|
74
|
+
return queryKeys.size === 0 ? options.filters : { ...options.filters, ui: options.filters.ui.filter((rule) => !queryKeys.has(rule.key)) };
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-ui-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.0",
|
|
4
4
|
"description": "Nuxt module and runtime toolkit for UI-heavy Nuxt applications",
|
|
5
5
|
"homepage": "https://github.com/ChronicStone/nuxt-ui-tools#readme",
|
|
6
6
|
"bugs": "https://github.com/ChronicStone/nuxt-ui-tools/issues",
|