nuxt-ui-tools 1.3.0 → 1.5.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.
Files changed (94) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +10 -2
  3. package/dist/runtime/dashboard/composables/use-dashboard-remote-options.js +8 -3
  4. package/dist/runtime/dashboard/types/filters.d.ts +7 -1
  5. package/dist/runtime/dashboard/utils/builders/dashboard-filters.js +19 -2
  6. package/dist/runtime/form/components/actions/form-actions.vue +13 -127
  7. package/dist/runtime/form/components/page/form-page-actions.d.vue.ts +3 -0
  8. package/dist/runtime/form/components/page/form-page-actions.vue +40 -0
  9. package/dist/runtime/form/components/page/form-page-actions.vue.d.ts +3 -0
  10. package/dist/runtime/form/components/page/form-page-header.d.vue.ts +24 -0
  11. package/dist/runtime/form/components/page/form-page-header.vue +99 -0
  12. package/dist/runtime/form/components/page/form-page-header.vue.d.ts +24 -0
  13. package/dist/runtime/form/components/page/form-page-navigation.d.vue.ts +34 -0
  14. package/dist/runtime/form/components/page/form-page-navigation.vue +229 -0
  15. package/dist/runtime/form/components/page/form-page-navigation.vue.d.ts +34 -0
  16. package/dist/runtime/form/components/page/form-page-section.d.vue.ts +19 -0
  17. package/dist/runtime/form/components/page/form-page-section.vue +114 -0
  18. package/dist/runtime/form/components/page/form-page-section.vue.d.ts +19 -0
  19. package/dist/runtime/form/components/page/form-page-sections.d.vue.ts +21 -0
  20. package/dist/runtime/form/components/page/form-page-sections.vue +75 -0
  21. package/dist/runtime/form/components/page/form-page-sections.vue.d.ts +21 -0
  22. package/dist/runtime/form/components/page/form-page.d.vue.ts +80 -0
  23. package/dist/runtime/form/components/page/form-page.vue +91 -0
  24. package/dist/runtime/form/components/page/form-page.vue.d.ts +80 -0
  25. package/dist/runtime/form/components/renderer/form-field-shell.vue +7 -11
  26. package/dist/runtime/form/components/root/form.vue +25 -194
  27. package/dist/runtime/form/composables/use-field-options.js +19 -0
  28. package/dist/runtime/form/composables/use-form-action-buttons.d.ts +32 -0
  29. package/dist/runtime/form/composables/use-form-action-buttons.js +89 -0
  30. package/dist/runtime/form/composables/use-form-actions.d.ts +8 -5
  31. package/dist/runtime/form/composables/use-form-actions.js +1 -1
  32. package/dist/runtime/form/composables/use-form-page-scroll.d.ts +22 -0
  33. package/dist/runtime/form/composables/use-form-page-scroll.js +178 -0
  34. package/dist/runtime/form/composables/use-form-page.d.ts +114 -0
  35. package/dist/runtime/form/composables/use-form-page.js +83 -0
  36. package/dist/runtime/form/composables/use-form-root.d.ts +34 -0
  37. package/dist/runtime/form/composables/use-form-root.js +171 -0
  38. package/dist/runtime/form/composables/use-form-runtime.js +13 -4
  39. package/dist/runtime/form/composables/use-form.js +2 -22
  40. package/dist/runtime/form/composables/use-remote-field-options.js +21 -2
  41. package/dist/runtime/form/fields/checkbox-card/component.vue +28 -19
  42. package/dist/runtime/form/fields/checkbox-card/types.d.ts +2 -5
  43. package/dist/runtime/form/fields/choice-card/choice-card-label.d.vue.ts +17 -0
  44. package/dist/runtime/form/fields/choice-card/choice-card-label.vue +50 -0
  45. package/dist/runtime/form/fields/choice-card/choice-card-label.vue.d.ts +17 -0
  46. package/dist/runtime/form/fields/choice-card/types.d.ts +31 -0
  47. package/dist/runtime/form/fields/choice-card/types.js +0 -0
  48. package/dist/runtime/form/fields/choice-card/use-choice-card.d.ts +22 -0
  49. package/dist/runtime/form/fields/choice-card/use-choice-card.js +52 -0
  50. package/dist/runtime/form/fields/radio-card/component.vue +17 -18
  51. package/dist/runtime/form/fields/radio-card/types.d.ts +2 -4
  52. package/dist/runtime/form/index.d.ts +1 -1
  53. package/dist/runtime/form/index.js +7 -1
  54. package/dist/runtime/form/schema/index.d.ts +1 -1
  55. package/dist/runtime/form/schema/index.js +1 -0
  56. package/dist/runtime/form/schema/page.d.ts +50 -0
  57. package/dist/runtime/form/schema/page.js +25 -0
  58. package/dist/runtime/form/types/index.d.ts +1 -0
  59. package/dist/runtime/form/types/options.d.ts +22 -2
  60. package/dist/runtime/form/types/page.d.ts +172 -0
  61. package/dist/runtime/form/types/page.js +0 -0
  62. package/dist/runtime/form/types/ui.d.ts +60 -0
  63. package/dist/runtime/form/utils/controls.d.ts +15 -0
  64. package/dist/runtime/form/utils/controls.js +38 -0
  65. package/dist/runtime/form/utils/layout.js +3 -1
  66. package/dist/runtime/form/utils/page.d.ts +20 -0
  67. package/dist/runtime/form/utils/page.js +107 -0
  68. package/dist/runtime/form/utils/state.d.ts +2 -0
  69. package/dist/runtime/form/utils/state.js +1 -1
  70. package/dist/runtime/form/utils/ui.js +1 -0
  71. package/dist/runtime/i18n/locales/en.js +18 -0
  72. package/dist/runtime/i18n/locales/fr.js +18 -0
  73. package/dist/runtime/i18n/types.d.ts +29 -0
  74. package/dist/runtime/shared/index.d.ts +1 -0
  75. package/dist/runtime/shared/index.js +1 -0
  76. package/dist/runtime/shared/types/query.d.ts +5 -1
  77. package/dist/runtime/shared/types/remote-options.d.ts +56 -0
  78. package/dist/runtime/shared/utils/define-remote-options.d.ts +23 -0
  79. package/dist/runtime/shared/utils/define-remote-options.js +24 -0
  80. package/dist/runtime/shared/utils/remote-options-query.d.ts +6 -0
  81. package/dist/runtime/shared/utils/remote-options-query.js +18 -0
  82. package/dist/runtime/table/components/data-list/data-list-filter-tags.d.vue.ts +1 -1
  83. package/dist/runtime/table/components/data-list/data-list-filter-tags.vue +1 -1
  84. package/dist/runtime/table/components/data-list/data-list-filter-tags.vue.d.ts +1 -1
  85. package/dist/runtime/table/components/filters/panel/filter-panel.vue +14 -2
  86. package/dist/runtime/table/components/filters/shared/filter-option-multiple-list.vue +2 -2
  87. package/dist/runtime/table/components/filters/shared/filter-option-single-list.vue +2 -2
  88. package/dist/runtime/table/components/filters/shared/filter-option-tree-content.vue +5 -5
  89. package/dist/runtime/table/components/filters/shared/filter-trigger-tag.vue +7 -1
  90. package/dist/runtime/table/composables/use-table-filter-selected-options.js +16 -1
  91. package/dist/runtime/table/types/filters.d.ts +7 -0
  92. package/dist/runtime/table/types/remote-options.d.ts +3 -16
  93. package/dist/runtime/table/utils/remote-table-options.js +37 -37
  94. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ import type { DefineRemoteOptionsConfig, RemoteOption, RemoteOptionsLoader, RemoteOptionsQueries, RemoteOptionsQueryDefinition } from '../types/remote-options.js';
2
+ /**
3
+ * Defines a query-backed option loader once for dashboard filters, table filters, and form fields.
4
+ * The endpoint's query key separates requests; `key` separates mapped option data from other
5
+ * consumers of that endpoint. Inline remote definitions continue to work in each domain.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const users = defineRemoteOptions({
10
+ * load: ({ search, page }) => api.users.list.queryOptions({ search, cursor: page.cursor }),
11
+ * resolveSelected: ({ values }) => api.users.byIds.queryOptions({ ids: values }),
12
+ * }, {
13
+ * key: 'users',
14
+ * mapPage: ({ rows, nextCursor }) => ({
15
+ * options: rows.map((user) => ({ value: user.id, label: user.name })),
16
+ * nextCursor,
17
+ * }),
18
+ * mapSelected: ({ rows }) => rows.map((user) => ({ value: user.id, label: user.name })),
19
+ * pagination: { type: 'cursor', size: 25 },
20
+ * })
21
+ * ```
22
+ */
23
+ export declare function defineRemoteOptions<TPageQuery extends RemoteOptionsQueryDefinition, TSelectedQuery extends RemoteOptionsQueryDefinition, const TOption extends RemoteOption>(queries: RemoteOptionsQueries<TPageQuery, TSelectedQuery>, config: DefineRemoteOptionsConfig<TPageQuery, TSelectedQuery, TOption>): RemoteOptionsLoader<TOption>;
@@ -0,0 +1,24 @@
1
+ import { mapRemoteOptionsQuery } from "./remote-options-query.js";
2
+ export function defineRemoteOptions(queries, config) {
3
+ const suffix = ["remote-options", config.key];
4
+ return {
5
+ load: (request) => mapRemoteOptionsQuery(
6
+ queries.load(request),
7
+ config.mapPage,
8
+ [...suffix, "page"]
9
+ ),
10
+ pagination: config.pagination,
11
+ queryKeyFor: (request) => [...queries.load(request).queryKey, ...suffix, "page"],
12
+ resolveSelected: (request) => mapRemoteOptionsQuery(
13
+ queries.resolveSelected(request),
14
+ config.mapSelected,
15
+ [...suffix, "selected"]
16
+ ),
17
+ selectedQueryKeyFor: (request) => [
18
+ ...queries.resolveSelected(request).queryKey,
19
+ ...suffix,
20
+ "selected"
21
+ ],
22
+ search: config.search
23
+ };
24
+ }
@@ -0,0 +1,6 @@
1
+ import type { QueryKey } from '@tanstack/vue-query';
2
+ import type { QueryFnDefinition } from '../types/query.js';
3
+ /** Maps an endpoint query while keeping its own key and cancellation context. */
4
+ export declare function mapRemoteOptionsQuery<TData, TResult>(definition: {
5
+ queryKey: QueryKey;
6
+ }, map: (result: TData) => TResult, suffix: readonly unknown[]): QueryFnDefinition<TResult>;
@@ -0,0 +1,18 @@
1
+ import { isFunction } from "./predicate.js";
2
+ export function mapRemoteOptionsQuery(definition, map, suffix) {
3
+ return {
4
+ async queryFn(context) {
5
+ if (!hasQueryFunction(definition)) {
6
+ throw new Error(
7
+ "[remote options] `load` and `resolveSelected` must return a query with a queryFn."
8
+ );
9
+ }
10
+ const result = await definition.queryFn({ ...context, queryKey: definition.queryKey });
11
+ return map(result);
12
+ },
13
+ queryKey: [...definition.queryKey, ...suffix]
14
+ };
15
+ }
16
+ function hasQueryFunction(definition) {
17
+ return "queryFn" in definition && isFunction(definition.queryFn);
18
+ }
@@ -2,7 +2,7 @@ import type { DataListControlSize, DataListFilterTagsUi } from '../../types/inde
2
2
  type __VLS_Props = {
3
3
  showAdd?: boolean;
4
4
  showClear?: boolean;
5
- /** `sheet` collapses the tags into a bottom sheet on mobile; `tags` keeps inline tags everywhere. */
5
+ /** `tags` keeps inline tags on mobile (default); `sheet` moves all filters into a bottom sheet. */
6
6
  mobile?: 'sheet' | 'tags';
7
7
  size?: DataListControlSize;
8
8
  ui?: DataListFilterTagsUi;
@@ -9,7 +9,7 @@ import FilterTagsBar from "../filters/tags/filter-tags-bar.vue";
9
9
  const props = defineProps({
10
10
  showAdd: { type: Boolean, required: false, default: false },
11
11
  showClear: { type: Boolean, required: false, default: false },
12
- mobile: { type: String, required: false, default: "sheet" },
12
+ mobile: { type: String, required: false, default: "tags" },
13
13
  size: { type: String, required: false },
14
14
  ui: { type: Object, required: false }
15
15
  });
@@ -2,7 +2,7 @@ import type { DataListControlSize, DataListFilterTagsUi } from '../../types/inde
2
2
  type __VLS_Props = {
3
3
  showAdd?: boolean;
4
4
  showClear?: boolean;
5
- /** `sheet` collapses the tags into a bottom sheet on mobile; `tags` keeps inline tags everywhere. */
5
+ /** `tags` keeps inline tags on mobile (default); `sheet` moves all filters into a bottom sheet. */
6
6
  mobile?: 'sheet' | 'tags';
7
7
  size?: DataListControlSize;
8
8
  ui?: DataListFilterTagsUi;
@@ -5,6 +5,7 @@ import USlideover from "@nuxt/ui/components/Slideover.vue";
5
5
  import { computed, onMounted, watch } from "vue";
6
6
  import { useUiToolsLocale } from "#ui-tools/i18n";
7
7
  import { isNullish } from "../../../../shared/utils/predicate";
8
+ import { useDataListBreakpoint } from "../../../composables/use-data-list-breakpoint";
8
9
  import { useDataListUi } from "../../../composables/use-data-list-ui";
9
10
  import { useTableInternals } from "../../../composables/use-table-internals";
10
11
  import { mergeDataListProps, mergeDataListUiClass } from "../../../utils";
@@ -19,6 +20,7 @@ const props = defineProps({
19
20
  });
20
21
  const internals = useTableInternals();
21
22
  const dataListUi = useDataListUi();
23
+ const { isMobile } = useDataListBreakpoint();
22
24
  const { locale, t } = useUiToolsLocale();
23
25
  const config = computed(() => dataListUi.ui.value.filterPanel);
24
26
  const resolvedUi = computed(() => ({ ...config.value?.ui, ...props.ui }));
@@ -39,6 +41,7 @@ const triggerBind = computed(() => {
39
41
  return rest;
40
42
  });
41
43
  const triggerLabel = computed(() => triggerProps.value.label ?? t("table.filters.panel.trigger"));
44
+ const hasTriggerIcon = computed(() => Boolean(triggerProps.value.icon));
42
45
  const countProps = computed(
43
46
  () => controlProps.value.count === false ? null : mergeDataListProps(
44
47
  { color: "neutral", size: "xs", variant: "solid" },
@@ -212,6 +215,10 @@ onMounted(() => {
212
215
  <UButton
213
216
  v-bind="triggerBind"
214
217
  :aria-expanded="open"
218
+ :aria-label="triggerLabel"
219
+ :square="
220
+ triggerProps.square ?? (isMobile && hasTriggerIcon && (!countProps || activeCount === 0))
221
+ "
215
222
  :ui="{
216
223
  base: mergeDataListUiClass(
217
224
  'nut-dl-fpanel-trigger shrink-0',
@@ -221,11 +228,16 @@ onMounted(() => {
221
228
  }"
222
229
  >
223
230
  <span
231
+ v-if="!isMobile || !hasTriggerIcon || countProps && activeCount > 0"
224
232
  :class="
225
- mergeDataListUiClass('flex items-center gap-2', void 0, resolvedUi.triggerContent)
233
+ mergeDataListUiClass(
234
+ 'nut-dl-fpanel-trigger__content flex items-center gap-2',
235
+ void 0,
236
+ resolvedUi.triggerContent
237
+ )
226
238
  "
227
239
  >
228
- <span>{{ triggerLabel }}</span>
240
+ <span v-if="!isMobile || !hasTriggerIcon">{{ triggerLabel }}</span>
229
241
  <UBadge
230
242
  v-if="countProps && activeCount > 0"
231
243
  v-bind="countProps"
@@ -48,11 +48,12 @@ const geometry = computed(() => resolveDataListControlGeometry(size.value));
48
48
  :key="entry.value == null ? entry.label : String(entry.value)"
49
49
  :class="
50
50
  mergeDataListUiClass(
51
- `nut-dl-option flex items-center rounded-md text-left outline-none transition-colors hover:bg-elevated ${sizeClasses.option} ${entry.selected ? 'text-highlighted' : 'text-default'}`,
51
+ `nut-dl-option flex cursor-pointer items-center rounded-md text-left outline-none transition-colors hover:bg-elevated ${sizeClasses.option} ${entry.selected ? 'text-highlighted' : 'text-default'}`,
52
52
  void 0,
53
53
  ui?.option
54
54
  )
55
55
  "
56
+ @click="emit('select', { event: $event, entry, index, sectionKey: section.key })"
56
57
  >
57
58
  <UCheckbox
58
59
  :model-value="entry.selected ?? false"
@@ -67,7 +68,6 @@ const geometry = computed(() => resolveDataListControlGeometry(size.value));
67
68
  <button
68
69
  type="button"
69
70
  :class="['flex min-w-0 flex-1 items-center text-left', geometry.toolbarGap]"
70
- @click="emit('select', { event: $event, entry, index, sectionKey: section.key })"
71
71
  >
72
72
  <span
73
73
  v-if="entry.color"
@@ -39,7 +39,7 @@ const modelValue = defineModel({ type: null, ...{
39
39
  root: 'w-full',
40
40
  fieldset: mergeDataListUiClass('grid gap-0.5', void 0, ui?.list),
41
41
  item: mergeDataListUiClass(
42
- `flex items-center rounded-md transition-colors hover:bg-elevated data-[state=checked]:bg-elevated ${sizeClasses.option}`,
42
+ `relative flex cursor-pointer items-center rounded-md transition-colors hover:bg-elevated data-[state=checked]:bg-elevated ${sizeClasses.option}`,
43
43
  void 0,
44
44
  ui?.option
45
45
  ),
@@ -47,7 +47,7 @@ const modelValue = defineModel({ type: null, ...{
47
47
  base: 'cursor-pointer',
48
48
  wrapper: 'min-w-0 flex-1',
49
49
  label: mergeDataListUiClass(
50
- `w-full cursor-pointer text-default ${sizeClasses.optionLabel}`,
50
+ `w-full cursor-pointer text-default before:absolute before:inset-0 ${sizeClasses.optionLabel}`,
51
51
  void 0,
52
52
  ui?.optionLabel
53
53
  )
@@ -61,12 +61,13 @@ function activateTreeEntry(entry) {
61
61
  :key="entry.id"
62
62
  :class="
63
63
  mergeDataListUiClass(
64
- `flex items-center rounded-md text-left transition-colors hover:bg-elevated ${sizeClasses.option} ${entry.selected ? 'bg-elevated text-highlighted' : 'text-default'}`,
64
+ `flex cursor-pointer items-center rounded-md text-left transition-colors hover:bg-elevated ${sizeClasses.option} ${entry.selected ? 'bg-elevated text-highlighted' : 'text-default'}`,
65
65
  void 0,
66
66
  ui?.option
67
67
  )
68
68
  "
69
69
  :style="getTreeIndentStyle(entry.depth)"
70
+ @click="activateTreeEntry(entry)"
70
71
  >
71
72
  <button
72
73
  v-if="entry.expandable"
@@ -111,7 +112,6 @@ function activateTreeEntry(entry) {
111
112
  type="button"
112
113
  class="flex min-w-0 flex-1 items-center text-left"
113
114
  :class="geometry.toolbarGap"
114
- @click="activateTreeEntry(entry)"
115
115
  >
116
116
  <UIcon
117
117
  v-if="entry.icon"
@@ -161,7 +161,7 @@ function activateTreeEntry(entry) {
161
161
  root: 'w-full',
162
162
  fieldset: mergeDataListUiClass('grid gap-0.5', void 0, ui?.list),
163
163
  item: mergeDataListUiClass(
164
- `flex items-center rounded-md transition-colors hover:bg-elevated data-[state=checked]:bg-elevated ${sizeClasses.option}`,
164
+ `relative flex cursor-pointer items-center rounded-md transition-colors hover:bg-elevated data-[state=checked]:bg-elevated ${sizeClasses.option}`,
165
165
  void 0,
166
166
  ui?.option
167
167
  ),
@@ -169,7 +169,7 @@ function activateTreeEntry(entry) {
169
169
  base: 'cursor-pointer',
170
170
  wrapper: 'min-w-0 flex-1',
171
171
  label: mergeDataListUiClass(
172
- `w-full cursor-pointer text-default ${sizeClasses.optionLabel}`,
172
+ `w-full cursor-pointer text-default before:absolute before:inset-0 ${sizeClasses.optionLabel}`,
173
173
  void 0,
174
174
  ui?.optionLabel
175
175
  )
@@ -187,7 +187,7 @@ function activateTreeEntry(entry) {
187
187
  :aria-label="item.expanded ? `Collapse ${item.label}` : `Expand ${item.label}`"
188
188
  :aria-expanded="item.expanded"
189
189
  :class="[
190
- 'flex shrink-0 items-center justify-center rounded-sm text-muted transition-transform hover:bg-accented',
190
+ 'relative z-10 flex shrink-0 items-center justify-center rounded-sm text-muted transition-transform hover:bg-accented',
191
191
  sizeClasses.optionIcon,
192
192
  item.expanded ? 'rotate-90' : ''
193
193
  ]"
@@ -68,7 +68,13 @@ const labelClass = computed(() => `flex min-w-0 items-center ${geometry.value.to
68
68
 
69
69
  <template>
70
70
  <div
71
- :class="mergeDataListUiClass('inline-flex min-w-0 max-w-full align-top', void 0, ui?.root)"
71
+ :class="
72
+ mergeDataListUiClass(
73
+ 'nut-dl-tag-wrap inline-flex min-w-0 max-w-full align-top',
74
+ void 0,
75
+ ui?.root
76
+ )
77
+ "
72
78
  >
73
79
  <template v-if="showOperatorPickerFirst">
74
80
  <UButton
@@ -1,4 +1,4 @@
1
- import { useQuery, useQueryClient } from "@tanstack/vue-query";
1
+ import { hashKey, useQuery, useQueryClient } from "@tanstack/vue-query";
2
2
  import { computed, shallowRef, watch } from "vue";
3
3
  import { isBoolean, isNullish, isNumber, isString } from "../../shared/utils/predicate.js";
4
4
  import { QUERY_DEFAULTS } from "../constants/query-state.js";
@@ -11,6 +11,21 @@ export function useTableFilterSelectedOptions(params) {
11
11
  return remote ? [{ key: definition.key, remote }] : [];
12
12
  })
13
13
  );
14
+ const selectedScopes = computed(
15
+ () => remoteDefinitions.value.map(({ key, remote }) => ({
16
+ identity: remote.selectedQueryKeyFor ? hashKey(remote.selectedQueryKeyFor({ values: [] })) : null,
17
+ key
18
+ }))
19
+ );
20
+ watch(selectedScopes, (current, previous) => {
21
+ const earlier = new Map(previous.map((entry) => [entry.key, entry.identity]));
22
+ const next = new Map(known.value);
23
+ for (const entry of current) {
24
+ if (earlier.has(entry.key) && earlier.get(entry.key) !== entry.identity)
25
+ next.delete(entry.key);
26
+ }
27
+ if (next.size !== known.value.size) known.value = next;
28
+ });
14
29
  function selectedValues(key) {
15
30
  const rule = params.rules.value.find((entry) => entry.key === key);
16
31
  const values = Array.isArray(rule?.value) ? rule.value : isNullish(rule?.value) ? [] : [rule.value];
@@ -1,3 +1,4 @@
1
+ import type { QueryKey } from '@tanstack/vue-query';
1
2
  import type { RemoteOptionsPageRequest, RemoteOptionsPagination, RemoteOptionsResult, RemoteOptionsSearch } from '../../shared/types/remote-options.js';
2
3
  import type { TableFilterOperator, TableQueryStateFilterRule, TableQueryStateFilterValue } from './query-state.js';
3
4
  import type { TableFacetExecutionResult, TableFacetRequestDescriptor, TableFacetsContext, TableQueryDefinition } from './source.js';
@@ -566,6 +567,12 @@ export interface TableFilterRemoteOptions<TValue = TableFilterPrimitiveValue, TP
566
567
  resolveSelected?: {
567
568
  bivarianceHack(request: TableFilterRemoteSelectedRequest<TValue>): TableQueryDefinition<readonly TableOptionEntryForPresentation<TValue, TPresentation>[]>;
568
569
  }['bivarianceHack'];
570
+ /** Identity of a selected lookup for reusable loaders with changing external scope. */
571
+ selectedQueryKeyFor?: {
572
+ bivarianceHack(request: {
573
+ values: readonly TValue[];
574
+ }): QueryKey;
575
+ }['bivarianceHack'];
569
576
  /**
570
577
  * Page size, and how far before the list end the next page loads. Defaults to
571
578
  * `{ type: 'page', size: 25 }` with the next page requested three viewport heights ahead.
@@ -1,6 +1,5 @@
1
1
  import type { QueryKey } from '@tanstack/vue-query';
2
- import type { QueryFnDefinition } from '../../shared/types/query.js';
3
- import type { RemoteOptionsPageRequest, RemoteOptionsPagination, RemoteOptionsResult, RemoteOptionsSearch } from '../../shared/types/remote-options.js';
2
+ import type { RemoteOptionsLoader, RemoteOptionsPagination, RemoteOptionsSearch } from '../../shared/types/remote-options.js';
4
3
  import type { TableResolvedFilterGroup } from './filters.js';
5
4
  import type { TableCursorPageResult, TableOffsetPageResult, TableRemoteSourceRequest, TableSourceExecutionResult, TableSourceQueryResult, TableSourceRow } from './source.js';
6
5
  import type { GenericObject, TableKnownFieldPath, TableSortingRule, TableSortKey } from './utils.js';
@@ -47,18 +46,6 @@ export interface RemoteTableOptionsConfig<TRow extends GenericObject, TOption ex
47
46
  }
48
47
  /**
49
48
  * Remote option source built by `remoteTableOptions()`. It fits dashboard `p.remote()`, table
50
- * option filters (`source.remote`), and form remote options (`source: options.load`).
49
+ * option filters (`source.remote`), and form remote options (`loader: options`).
51
50
  */
52
- export interface RemoteTableOptions<TOption> {
53
- /** Query definition of one page of options for a search term. */
54
- load: (request: {
55
- search: string;
56
- page: RemoteOptionsPageRequest;
57
- }) => QueryFnDefinition<RemoteOptionsResult<TOption>>;
58
- /** Query definition of the options of selected values. */
59
- resolveSelected: (request: {
60
- values: readonly (string | number | boolean)[];
61
- }) => QueryFnDefinition<readonly TOption[]>;
62
- pagination: RemoteOptionsPagination;
63
- search?: RemoteOptionsSearch;
64
- }
51
+ export type RemoteTableOptions<TOption> = RemoteOptionsLoader<TOption>;
@@ -1,4 +1,5 @@
1
- import { isArray, isFunction, isObject } from "../../shared/utils/predicate.js";
1
+ import { isArray, isObject } from "../../shared/utils/predicate.js";
2
+ import { mapRemoteOptionsQuery } from "../../shared/utils/remote-options-query.js";
2
3
  const DEFAULT_PAGINATION = { size: 25, type: "cursor" };
3
4
  export function remoteTableOptions(query, config) {
4
5
  const pagination = config.pagination ?? DEFAULT_PAGINATION;
@@ -13,55 +14,54 @@ export function remoteTableOptions(query, config) {
13
14
  sorting: params.filters ? [] : [...sorting]
14
15
  };
15
16
  }
17
+ function selectedRequest(values) {
18
+ return request({
19
+ filters: [
20
+ {
21
+ children: [
22
+ {
23
+ key: config.valueKey ?? "id",
24
+ operator: "isAnyOf",
25
+ type: "condition",
26
+ value: [...values]
27
+ }
28
+ ],
29
+ combinator: "and",
30
+ type: "group"
31
+ }
32
+ ],
33
+ page: { cursor: null, index: 1, size: Math.max(1, values.length) },
34
+ search: ""
35
+ });
36
+ }
16
37
  return {
17
38
  load: ({ page, search: term }) => mapQuery(query(request({ page, search: term })), (result) => ({
18
39
  ...resolvePage(result),
19
40
  options: result.rows.map(config.option)
20
41
  })),
21
42
  pagination,
22
- resolveSelected: ({ values }) => mapQuery(
23
- query(
24
- request({
25
- filters: [
26
- {
27
- children: [
28
- {
29
- key: config.valueKey ?? "id",
30
- operator: "isAnyOf",
31
- type: "condition",
32
- value: [...values]
33
- }
34
- ],
35
- combinator: "and",
36
- type: "group"
37
- }
38
- ],
39
- page: { cursor: null, index: 1, size: Math.max(1, values.length) },
40
- search: ""
41
- })
42
- ),
43
- (result) => result.rows.map(config.option)
44
- ),
45
- search
43
+ queryKeyFor: ({ page, search: term }) => [
44
+ ...query(request({ page, search: term })).queryKey,
45
+ "remote-options"
46
+ ],
47
+ resolveSelected: ({ values }) => mapQuery(query(selectedRequest(values)), (result) => result.rows.map(config.option)),
48
+ search,
49
+ selectedQueryKeyFor: ({ values }) => [
50
+ ...query(selectedRequest(values)).queryKey,
51
+ "remote-options"
52
+ ]
46
53
  };
47
54
  }
48
55
  function mapQuery(definition, map) {
49
- const { queryKey } = definition;
50
- const queryFn = "queryFn" in definition ? definition.queryFn : void 0;
51
- return {
52
- async queryFn(context) {
53
- if (!isQueryFn(queryFn))
54
- throw new Error("[remoteTableOptions] `query` must return a query with a queryFn.");
55
- const result = await queryFn({ ...context, queryKey });
56
+ return mapRemoteOptionsQuery(
57
+ definition,
58
+ (result) => {
56
59
  if (!isTableResult(result))
57
60
  throw new Error("[remoteTableOptions] the query must resolve to a table response (`rows`).");
58
61
  return map(result);
59
62
  },
60
- queryKey: [...queryKey, "remote-options"]
61
- };
62
- }
63
- function isQueryFn(value) {
64
- return isFunction(value);
63
+ ["remote-options"]
64
+ );
65
65
  }
66
66
  function isTableResult(value) {
67
67
  return isObject(value) && isArray(value.rows);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-ui-tools",
3
- "version": "1.3.0",
3
+ "version": "1.5.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",