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 CHANGED
@@ -5,7 +5,7 @@
5
5
  "configKey": "nuxtUiTools",
6
6
  "docs": "https://ui.nuxt.com/docs/getting-started/installation/nuxt",
7
7
  "name": "nuxt-ui-tools",
8
- "version": "1.26.0",
8
+ "version": "1.27.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
@@ -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 filters = useQueryStates({
100
- historyMode: "push",
101
- prefix: "f",
102
- schema: {
103
- search: { codec: stringCodec, defaultValue: "" },
104
- ui: dynamicQueryState({
105
- defaultValue: resolveTableFilterDefaultRules(params.schema.value.filters?.ui ?? []),
106
- definitions: () => params.schema.value.filters?.ui ?? [],
107
- parse(entries, definitions) {
108
- return parseTableFilterQueryState({ definitions, entries });
109
- },
110
- resolve(filter) {
111
- const definition = normalizeFilterDefinition(filter);
112
- const operators = resolveFilterSupportedOperators(definition);
113
- const defaultOp = resolveFilterDefaultOperator(definition);
114
- const codec = createTableFilterValueCodec(definition);
115
- return operators.map((op) => ({
116
- codec,
117
- urlKey: op === defaultOp ? definition.key : `${definition.key}~${op}`
118
- }));
119
- },
120
- serialize(rules, definitions) {
121
- return serializeTableFilterQueryState({ definitions, rules });
122
- },
123
- urlPrefix: "ui"
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: requestFilters.value,
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
  }
@@ -49,4 +49,5 @@ export declare function useTableState(params: UseTableStateParams): {
49
49
  } | null>;
50
50
  };
51
51
  resolvedFilterState: ComputedRef<TableResolvedFilterGroup<string>>;
52
+ rowFilterState: ComputedRef<TableResolvedFilterGroup<string>>;
52
53
  };
@@ -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.26.0",
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",