nuxt-ui-tools 1.11.1 → 1.12.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.11.1",
8
+ "version": "1.12.0",
9
9
  "builder": {
10
10
  "@nuxt/module-builder": "1.0.3",
11
11
  "unbuild": "unknown"
@@ -49,6 +49,7 @@ function resolveSchemaUi(schema) {
49
49
  >
50
50
  <FormRoot
51
51
  :form="overlay.form"
52
+ :runtime="overlay.runtime"
52
53
  shell="drawer"
53
54
  :show-close-button="drawerConfig?.showCloseButton !== false"
54
55
  :ui="formUi"
@@ -70,6 +71,7 @@ function resolveSchemaUi(schema) {
70
71
  >
71
72
  <FormRoot
72
73
  :form="overlay.form"
74
+ :runtime="overlay.runtime"
73
75
  shell="fullscreen"
74
76
  :show-close-button="fullscreenConfig?.showCloseButton !== false"
75
77
  :ui="formUi"
@@ -91,6 +93,7 @@ function resolveSchemaUi(schema) {
91
93
  >
92
94
  <FormRoot
93
95
  :form="overlay.form"
96
+ :runtime="overlay.runtime"
94
97
  shell="modal"
95
98
  :show-close-button="modalConfig?.showCloseButton !== false"
96
99
  :ui="formUi"
@@ -1,6 +1,7 @@
1
- import type { FormValue, FormObject, FormRendererController, FormRenderShell, FormUiConfig, FormValidationMode } from '../../types/index.js';
1
+ import type { FormValue, FormObject, FormRendererController, FormRuntime, FormRenderShell, FormUiConfig, FormValidationMode } from '../../types/index.js';
2
2
  type __VLS_Props = {
3
3
  form?: FormRendererController;
4
+ runtime?: FormRuntime;
4
5
  schema?: FormValue;
5
6
  input?: FormObject;
6
7
  shell?: FormRenderShell;
@@ -20,6 +20,7 @@ import FormFieldRenderer from "../renderer/form-field-renderer.vue";
20
20
  import FormDirectionalTransition from "../utils/form-directional-transition.vue";
21
21
  const props = defineProps({
22
22
  form: { type: Object, required: false },
23
+ runtime: { type: Object, required: false },
23
24
  schema: { type: null, required: false },
24
25
  input: { type: null, required: false },
25
26
  shell: { type: String, required: false },
@@ -42,6 +43,7 @@ const {
42
43
  submit
43
44
  } = useFormRoot({
44
45
  form: () => props.form,
46
+ runtime: () => props.runtime,
45
47
  input: () => props.input,
46
48
  onCancelled: (value) => emit("cancel", value),
47
49
  onSubmitted: (value, result) => emit("submit", value, result),
@@ -1,6 +1,7 @@
1
- import type { FormValue, FormObject, FormRendererController, FormRenderShell, FormUiConfig, FormValidationMode } from '../../types/index.js';
1
+ import type { FormValue, FormObject, FormRendererController, FormRuntime, FormRenderShell, FormUiConfig, FormValidationMode } from '../../types/index.js';
2
2
  type __VLS_Props = {
3
3
  form?: FormRendererController;
4
+ runtime?: FormRuntime;
4
5
  schema?: FormValue;
5
6
  input?: FormObject;
6
7
  shell?: FormRenderShell;
@@ -8,5 +8,6 @@ export declare function useFormOverlayController(instance: FormApiRuntimeInstanc
8
8
  handleSubmitted: (formData: FormObject, result: FormSubmitHandlerResult<FormValue>) => void;
9
9
  open: import("vue").Ref<boolean, boolean>;
10
10
  resolveAfterClose: () => void;
11
+ runtime: import("../types/index.js").FormRuntime;
11
12
  title: import("vue").ComputedRef<string | undefined>;
12
13
  };
@@ -3,6 +3,7 @@ import { getFormOverlayDescription, getFormOverlayTitle } from "../utils/overlay
3
3
  import { isRecord } from "../utils/path.js";
4
4
  import { useForm } from "./use-form.js";
5
5
  import { useFormApi } from "./use-form-api.js";
6
+ import { useFormRuntime } from "./use-form-runtime.js";
6
7
  export function useFormOverlayController(instance) {
7
8
  const formApi = useFormApi();
8
9
  const form = useForm({
@@ -17,6 +18,13 @@ export function useFormOverlayController(instance) {
17
18
  () => getFormOverlayDescription(instance.schema) ?? getFormOverlayTitle(instance.schema) ?? "Form"
18
19
  );
19
20
  const dismissible = computed(() => !form.isSubmitting.value);
21
+ const runtime = useFormRuntime({
22
+ input: form.input,
23
+ schema: form.schema,
24
+ syncInput: form.syncInput,
25
+ validationMode: form.validationMode
26
+ });
27
+ form.bind(runtime);
20
28
  formApi.setController(instance.id, form);
21
29
  const runtimeControls = {
22
30
  close: requestCancel,
@@ -24,6 +32,7 @@ export function useFormOverlayController(instance) {
24
32
  };
25
33
  formApi.setRuntimeControls(instance.id, runtimeControls);
26
34
  onBeforeUnmount(() => {
35
+ form.unbind(runtime);
27
36
  formApi.removeController(instance.id, form);
28
37
  formApi.removeRuntimeControls(instance.id, runtimeControls);
29
38
  });
@@ -85,6 +94,7 @@ export function useFormOverlayController(instance) {
85
94
  handleSubmitted,
86
95
  open,
87
96
  resolveAfterClose,
97
+ runtime,
88
98
  title
89
99
  };
90
100
  }
@@ -1,7 +1,9 @@
1
- import type { FormObject, FormRendererController, FormSubmitHandlerResult, FormUiConfig, FormValidationMode, FormValue } from '../types/index.js';
1
+ import type { FormObject, FormRendererController, FormRuntime, FormSubmitHandlerResult, FormUiConfig, FormValidationMode, FormValue } from '../types/index.js';
2
2
  export interface UseFormRootParams {
3
3
  /** Controller from `useForm`. Its schema, input, and policies win over the direct ones. */
4
4
  form: () => FormRendererController | undefined;
5
+ /** Runtime owned by a parent. The root renders it and neither creates, binds nor disposes it. */
6
+ runtime?: () => FormRuntime | undefined;
5
7
  schema: () => FormValue;
6
8
  input: () => FormObject | undefined;
7
9
  syncInput: () => boolean | readonly string[] | undefined;
@@ -27,7 +29,7 @@ export declare function useFormRoot(params: UseFormRootParams): {
27
29
  };
28
30
  input: import("vue").ComputedRef<import("../../shared/index.js").GenericObject | undefined>;
29
31
  refreshContext: () => Promise<void>;
30
- runtime: import("../types/index.js").FormRuntime;
32
+ runtime: FormRuntime;
31
33
  schema: import("vue").ComputedRef<{}>;
32
34
  submit: () => Promise<void>;
33
35
  validationMode: import("vue").ComputedRef<FormValidationMode>;
@@ -34,17 +34,20 @@ export function useFormRoot(params) {
34
34
  const validationMode = computed(
35
35
  () => params.form()?.validationMode.value ?? params.validate() ?? getSchemaValidationMode(schema.value)
36
36
  );
37
- const runtime = useFormRuntime({ input, schema, syncInput, validationMode });
37
+ const ownedRuntime = params.runtime?.();
38
+ const runtime = ownedRuntime ?? useFormRuntime({ input, schema, syncInput, validationMode });
38
39
  provideFormRuntime(runtime);
39
- watch(
40
- params.form,
41
- (controller, previousController) => {
42
- previousController?.unbind(runtime);
43
- controller?.bind(runtime);
44
- },
45
- { immediate: true }
46
- );
47
- onBeforeUnmount(() => params.form()?.unbind(runtime));
40
+ if (!ownedRuntime) {
41
+ watch(
42
+ params.form,
43
+ (controller, previousController) => {
44
+ previousController?.unbind(runtime);
45
+ controller?.bind(runtime);
46
+ },
47
+ { immediate: true }
48
+ );
49
+ onBeforeUnmount(() => params.form()?.unbind(runtime));
50
+ }
48
51
  onMounted(async () => {
49
52
  await nextTick();
50
53
  const target = getSchemaAutoFocus(schema.value);
@@ -41,6 +41,7 @@ const {
41
41
  itemPath,
42
42
  itemRenderKey,
43
43
  items,
44
+ removeItemLabel,
44
45
  runCustomAction,
45
46
  t,
46
47
  title,
@@ -229,7 +230,7 @@ function bodyId(index) {
229
230
  variant="ghost"
230
231
  size="xs"
231
232
  :class="ui?.action"
232
- :aria-label="t('form.fields.array.removeItem')"
233
+ :aria-label="removeItemLabel(item, index)"
233
234
  @click="removeItem(index)"
234
235
  />
235
236
  <UButton
@@ -43,6 +43,7 @@ const {
43
43
  itemPath,
44
44
  itemRenderKey,
45
45
  items,
46
+ removeItemLabel,
46
47
  runCustomAction,
47
48
  t,
48
49
  title,
@@ -230,7 +231,7 @@ function updateDraggedItems(value) {
230
231
  variant="ghost"
231
232
  size="xs"
232
233
  :class="ui?.action"
233
- :aria-label="t('form.fields.array.removeItem')"
234
+ :aria-label="removeItemLabel(item, index)"
234
235
  @click.stop="removeItem(index)"
235
236
  />
236
237
  </div>
@@ -291,76 +292,84 @@ function updateDraggedItems(value) {
291
292
  handle=".array-drag-handle"
292
293
  :animation="150"
293
294
  >
294
- <div
295
- v-for="(item, index) in items"
296
- :key="itemRenderKey(item, index)"
297
- :class="[
295
+ <TransitionGroup
296
+ enter-active-class="transition duration-200 ease-out motion-reduce:transition-none"
297
+ enter-from-class="translate-y-1 opacity-0"
298
+ leave-active-class="transition duration-150 ease-in motion-reduce:transition-none"
299
+ leave-to-class="scale-[0.98] opacity-0"
300
+ >
301
+ <div
302
+ v-for="(item, index) in items"
303
+ :key="itemRenderKey(item, index)"
304
+ :class="[
298
305
  mergeFormUiClass('grid rounded-lg border border-default bg-default', ui?.item),
299
306
  fieldProps.compact ? 'gap-3 p-3' : 'gap-4 p-4'
300
307
  ]"
301
- >
302
- <div :class="mergeFormUiClass('flex items-center justify-between gap-3', ui?.itemHeader)">
303
- <span
304
- :class="mergeFormUiClass('text-sm font-medium text-highlighted', ui?.itemTitle)"
305
- :data-form-array-heading="index"
306
- >
307
- {{ itemHeading(items[index] ?? {}, index) }}
308
- </span>
309
- <div :class="mergeFormUiClass('flex items-center gap-1', ui?.itemActions)">
310
- <UButton
311
- v-if="isDraggable"
312
- icon="i-lucide-grip-vertical"
313
- color="neutral"
314
- variant="ghost"
315
- size="xs"
316
- :class="mergeFormUiClass('array-drag-handle cursor-grab', ui?.action)"
317
- :aria-label="t('form.fields.array.dragItem')"
318
- />
319
- <UButton
320
- v-if="canDelete(index)"
321
- icon="i-lucide-trash-2"
322
- color="neutral"
323
- variant="ghost"
324
- size="xs"
325
- :class="ui?.action"
326
- :aria-label="t('form.fields.array.removeItem')"
327
- @click="removeItem(index)"
328
- />
329
- <UButton
330
- v-for="(action, actionIndex) in field.actions?.custom ?? []"
331
- v-show="customActionVisible(index, actionIndex)"
332
- :key="actionIndex"
333
- :icon="action.icon"
334
- color="neutral"
335
- variant="ghost"
336
- size="xs"
337
- :class="ui?.action"
338
- @click="runCustomAction(index, actionIndex)"
308
+ :data-form-array-item="index"
309
+ >
310
+ <div :class="mergeFormUiClass('flex items-center justify-between gap-3', ui?.itemHeader)">
311
+ <span
312
+ :class="mergeFormUiClass('text-sm font-medium text-highlighted', ui?.itemTitle)"
313
+ :data-form-array-heading="index"
339
314
  >
340
- {{ resolveFormText(action.label) }}
341
- </UButton>
315
+ {{ itemHeading(items[index] ?? {}, index) }}
316
+ </span>
317
+ <div :class="mergeFormUiClass('flex items-center gap-1', ui?.itemActions)">
318
+ <UButton
319
+ v-if="isDraggable"
320
+ icon="i-lucide-grip-vertical"
321
+ color="neutral"
322
+ variant="ghost"
323
+ size="xs"
324
+ :class="mergeFormUiClass('array-drag-handle cursor-grab', ui?.action)"
325
+ :aria-label="t('form.fields.array.dragItem')"
326
+ />
327
+ <UButton
328
+ v-if="canDelete(index)"
329
+ icon="i-lucide-trash-2"
330
+ color="neutral"
331
+ variant="ghost"
332
+ size="xs"
333
+ :class="ui?.action"
334
+ :aria-label="removeItemLabel(items[index] ?? {}, index)"
335
+ @click="removeItem(index)"
336
+ />
337
+ <UButton
338
+ v-for="(action, actionIndex) in field.actions?.custom ?? []"
339
+ v-show="customActionVisible(index, actionIndex)"
340
+ :key="actionIndex"
341
+ :icon="action.icon"
342
+ color="neutral"
343
+ variant="ghost"
344
+ size="xs"
345
+ :class="ui?.action"
346
+ @click="runCustomAction(index, actionIndex)"
347
+ >
348
+ {{ resolveFormText(action.label) }}
349
+ </UButton>
350
+ </div>
342
351
  </div>
343
- </div>
344
-
345
- <USelect
346
- v-if="field.type === 'array-variant'"
347
- :model-value="variantValue(items[index])"
348
- :items="variantItems"
349
- value-key="value"
350
- :size="formUi.controlSize.value"
351
- :class="mergeFormUiClass('w-full max-w-xs', ui?.variant)"
352
- @update:model-value="updateVariant(index, $event)"
353
- />
354
352
 
355
- <div :class="mergeFormUiClass('grid', ui?.fields)" :style="containerLayout.style.value">
356
- <FormFieldRenderer
357
- v-for="child in fieldsForItem(items[index] ?? {})"
358
- :key="child.key"
359
- :field="child"
360
- :parent-path="itemPath(index)"
353
+ <USelect
354
+ v-if="field.type === 'array-variant'"
355
+ :model-value="variantValue(items[index])"
356
+ :items="variantItems"
357
+ value-key="value"
358
+ :size="formUi.controlSize.value"
359
+ :class="mergeFormUiClass('w-full max-w-xs', ui?.variant)"
360
+ @update:model-value="updateVariant(index, $event)"
361
361
  />
362
+
363
+ <div :class="mergeFormUiClass('grid', ui?.fields)" :style="containerLayout.style.value">
364
+ <FormFieldRenderer
365
+ v-for="child in fieldsForItem(items[index] ?? {})"
366
+ :key="child.key"
367
+ :field="child"
368
+ :parent-path="itemPath(index)"
369
+ />
370
+ </div>
362
371
  </div>
363
- </div>
372
+ </TransitionGroup>
364
373
  </component>
365
374
 
366
375
  <UButton
@@ -42,6 +42,7 @@ export declare function useFormArrayItems(field: () => FormArrayItemsField, path
42
42
  itemRenderKey: (item: FormObject, index: number) => string;
43
43
  items: import("vue").ComputedRef<readonly import("../../../shared/index.js").GenericObject[]>;
44
44
  removeItem: (index: number) => boolean;
45
+ removeItemLabel: (item: FormObject, index: number) => string;
45
46
  resolveAction: (action: FormArrayAction | undefined, index: number) => (import("./types.js").FormArrayActionCondition<{}, {}> | undefined) & boolean;
46
47
  runCustomAction: (index: number, actionIndex: number) => Promise<void>;
47
48
  t: import("../../../i18n/index.js").UiToolsTranslator;
@@ -106,6 +106,9 @@ export function useFormArrayItems(field, path) {
106
106
  function itemHeading(item, index) {
107
107
  return resolveFormText(field().headerTemplate?.(item, index, actionParams(index).deps)) ?? `${itemLabel.value} ${index + 1}`;
108
108
  }
109
+ function removeItemLabel(item, index) {
110
+ return t("form.fields.array.removeNamedItem", { item: itemHeading(item, index) });
111
+ }
109
112
  function resolveAction(action, index) {
110
113
  const condition = isArrayActionConfig(action) ? action.condition : action;
111
114
  if (isBoolean(condition)) {
@@ -239,6 +242,7 @@ export function useFormArrayItems(field, path) {
239
242
  itemRenderKey,
240
243
  items,
241
244
  removeItem,
245
+ removeItemLabel,
242
246
  resolveAction,
243
247
  runCustomAction,
244
248
  t,
@@ -41,6 +41,7 @@ const {
41
41
  itemRenderKey,
42
42
  items,
43
43
  removeItem,
44
+ removeItemLabel,
44
45
  runCustomAction,
45
46
  t,
46
47
  title,
@@ -208,7 +209,7 @@ function fieldLabel(field) {
208
209
  variant="ghost"
209
210
  size="xs"
210
211
  :class="ui?.action"
211
- :aria-label="t('form.fields.array.removeItem')"
212
+ :aria-label="removeItemLabel(item, index)"
212
213
  @click="removeItem(index)"
213
214
  />
214
215
  <UButton
@@ -1,4 +1,4 @@
1
- import type { NestedPaths } from '../../shared/types/utils.js';
1
+ import type { DecrementDepth } from '../../shared/types/utils.js';
2
2
  import type { FormValue } from './/index.js';
3
3
  import type { FormContextData } from './context.js';
4
4
  import type { FormMaybePromise, FormObject } from './utils.js';
@@ -170,10 +170,17 @@ export interface FormFieldApi<TValue = FormValue, TOption = FormValue, TContext
170
170
  /** Focuses the mounted control for this field when it has a focusable input. */
171
171
  focus: () => Promise<boolean>;
172
172
  }
173
+ /**
174
+ * Dot path into submitted output. Array items are addressed by index (`contacts.0.email`); the
175
+ * unindexed form (`contacts.email`) stays accepted.
176
+ */
177
+ type FormOutputPath<T, MaxDepth extends number = 10> = [MaxDepth] extends [0] ? never : T extends readonly (infer TItem)[] ? `${number}` | `${number}.${FormOutputPath<TItem, DecrementDepth<MaxDepth>>}` | FormOutputPath<TItem, DecrementDepth<MaxDepth>> : T extends object ? {
178
+ [K in keyof T & string]: `${K}` | `${K}.${FormOutputPath<T[K], DecrementDepth<MaxDepth>>}`;
179
+ }[keyof T & string] : never;
173
180
  /**
174
181
  * Public form API available from form-level callbacks.
175
182
  */
176
- export type FormFieldPath<TOutput = FormObject> = TOutput extends FormObject ? FormObject extends TOutput ? string : NestedPaths<TOutput> : string;
183
+ export type FormFieldPath<TOutput = FormObject> = TOutput extends FormObject ? FormObject extends TOutput ? string : FormOutputPath<TOutput> : string;
177
184
  export interface FormApi<TOutput = FormObject> {
178
185
  /** Reads an internal form value by raw path. */
179
186
  get: (path: string) => FormValue;
@@ -212,3 +219,4 @@ export interface FormSubmitHandlerResult<TSubmitData = FormValue> {
212
219
  success: boolean;
213
220
  data?: TSubmitData;
214
221
  }
222
+ export {};
@@ -229,6 +229,7 @@ export default defineUiToolsLocale({
229
229
  expand: "Expand",
230
230
  item: "Item",
231
231
  removeItem: "Remove item",
232
+ removeNamedItem: "Remove {item}",
232
233
  unique: "This value is already in the list"
233
234
  },
234
235
  color: {
@@ -229,6 +229,7 @@ export default defineUiToolsLocale({
229
229
  expand: "D\xE9plier",
230
230
  item: "\xC9l\xE9ment",
231
231
  removeItem: "Supprimer l\u2019\xE9l\xE9ment",
232
+ removeNamedItem: "Supprimer {item}",
232
233
  unique: "Cette valeur est d\xE9j\xE0 dans la liste"
233
234
  },
234
235
  color: {
@@ -355,6 +355,7 @@ export interface UiToolsFormMessages {
355
355
  array: {
356
356
  addItem: string;
357
357
  removeItem: string;
358
+ removeNamedItem: string;
358
359
  dragItem: string;
359
360
  editItem: string;
360
361
  confirmDelete: string;
@@ -1,4 +1,5 @@
1
- import type { QueryFunction, QueryKey, QueryOptions, UseQueryOptions } from '@tanstack/vue-query';
1
+ import type { DataTag, QueryFunction, QueryKey, UseQueryOptions } from '@tanstack/vue-query';
2
+ import type { UnwrapRef } from 'vue';
2
3
  /**
3
4
  * TanStack query definition accepted by runtime domains that own the `useQuery` call themselves
4
5
  * (table sources, dashboard queries): an explicit `queryKey`, an optional `queryFn`, and any other
@@ -16,7 +17,16 @@ export interface QueryFnDefinition<TData = unknown> {
16
17
  queryKey: QueryKey;
17
18
  queryFn: QueryFunction<TData, QueryKey, string | null>;
18
19
  }
19
- /** Data returned by a query function, including generated options whose function may be a ref. */
20
+ type FunctionResult<TFunction> = TFunction extends (...args: never[]) => infer TResult ? TResult : never;
21
+ type QueryFnResult<TQuery> = TQuery extends {
22
+ queryFn?: infer TQueryFn;
23
+ } ? Awaited<FunctionResult<UnwrapRef<Exclude<TQueryFn, undefined>>>> : never;
24
+ /**
25
+ * Data returned by a query definition. A tagged `queryKey` (TanStack `queryOptions`, generated
26
+ * clients such as Tuyau) gives the data even when the options are ref-wrapped; otherwise the
27
+ * `queryFn` result is used, unwrapping a ref and ignoring `skipToken`.
28
+ */
20
29
  export type QueryFunctionResult<TQuery> = TQuery extends {
21
- queryFn: (...args: never[]) => infer TResult;
22
- } ? Awaited<TResult> : TQuery extends QueryOptions<infer TQueryFnData, infer _TError, infer _TData, infer _TQueryData, infer _TQueryKey> ? Awaited<TQueryFnData> : TQuery extends UseQueryOptions<infer _TQueryFnData, infer _TError, infer TData, infer _TQueryData, infer _TQueryKey> ? Awaited<TData> : never;
30
+ queryKey: DataTag<QueryKey, infer TTagged, infer _TError>;
31
+ } ? TTagged : QueryFnResult<TQuery>;
32
+ export {};
@@ -21,7 +21,7 @@ export declare function useTableActions(options: UseTableActionsParams): {
21
21
  actions: ComputedRef<TableActionController[]>;
22
22
  bulkActions: ComputedRef<TableActionController[]>;
23
23
  definitions: ComputedRef<TableActionDefinition[]>;
24
- execute: (definition: TableActionDefinition) => Promise<void>;
24
+ execute: (definition: TableActionDefinition) => Promise<boolean>;
25
25
  runningKeys: import("vue").Ref<string[], string[]>;
26
26
  toolbarActions: ComputedRef<TableActionController[]>;
27
27
  };
@@ -1,4 +1,5 @@
1
1
  import { computed, ref } from "vue";
2
+ import { isRecord } from "../../shared/utils/path.js";
2
3
  import { isFunction } from "../../shared/utils/predicate.js";
3
4
  import { resolveTableActionDefinitions } from "../utils/actions.js";
4
5
  export function useTableActions(options) {
@@ -46,10 +47,10 @@ export function useTableActions(options) {
46
47
  async function execute(definition) {
47
48
  const state = resolveState(definition);
48
49
  if (!state.visible || state.disabled || state.loading || state.running) {
49
- return;
50
+ return false;
50
51
  }
51
52
  if (!definition.action) {
52
- return;
53
+ return false;
53
54
  }
54
55
  const actionContext = context.value;
55
56
  runningKeys.value = [...runningKeys.value, definition.key];
@@ -58,10 +59,14 @@ export function useTableActions(options) {
58
59
  selectionApi.clear();
59
60
  }
60
61
  try {
61
- await definition.action(actionContext);
62
+ const result = await definition.action(actionContext);
63
+ if (isFailedActionResult(result)) {
64
+ return false;
65
+ }
62
66
  if (selectionClear === "success") {
63
67
  selectionApi.clear();
64
68
  }
69
+ return true;
65
70
  } finally {
66
71
  runningKeys.value = runningKeys.value.filter((key) => key !== definition.key);
67
72
  }
@@ -111,6 +116,9 @@ function resolveFlag(value, context) {
111
116
  function isBooleanResolver(value) {
112
117
  return isFunction(value);
113
118
  }
119
+ function isFailedActionResult(result) {
120
+ return result === false || isRecord(result) && result.success === false;
121
+ }
114
122
  function toPlainRecord(value) {
115
123
  return Object.fromEntries(Object.entries(value));
116
124
  }
@@ -22,7 +22,7 @@ declare function createTableInternals<TSchema>(options: {
22
22
  [x: string]: unknown;
23
23
  }, {
24
24
  [x: string]: unknown;
25
- }>) => Promise<void>;
25
+ }>) => Promise<boolean>;
26
26
  runningKeys: import("vue").Ref<string[], string[]>;
27
27
  toolbarActions: import("vue").ComputedRef<import("./use-table-actions.js").TableActionController[]>;
28
28
  };
@@ -618,7 +618,7 @@ declare function useProvideTableInternals<TSchema>(options: {
618
618
  [x: string]: unknown;
619
619
  }, {
620
620
  [x: string]: unknown;
621
- }>) => Promise<void>;
621
+ }>) => Promise<boolean>;
622
622
  runningKeys: import("vue").Ref<string[], string[]>;
623
623
  toolbarActions: import("vue").ComputedRef<import("./use-table-actions.js").TableActionController[]>;
624
624
  };
@@ -1211,7 +1211,7 @@ declare function useTableInternals(): {
1211
1211
  [x: string]: unknown;
1212
1212
  }, {
1213
1213
  [x: string]: unknown;
1214
- }>) => Promise<void>;
1214
+ }>) => Promise<boolean>;
1215
1215
  runningKeys: import("vue").Ref<string[], string[]>;
1216
1216
  toolbarActions: import("vue").ComputedRef<import("./use-table-actions.js").TableActionController[]>;
1217
1217
  };
@@ -18,10 +18,16 @@ export interface TableToolbarAction<TRow extends GenericObject = GenericObject,
18
18
  condition?: boolean | ((context: TContext) => boolean);
19
19
  disabled?: boolean | ((context: TableActionContext<TRow, TContext, TPageContext>) => boolean);
20
20
  loading?: boolean | ((context: TableActionContext<TRow, TContext, TPageContext>) => boolean);
21
+ /**
22
+ * Runs the action. Returning `false` or `{ success: false }` marks the run as failed without
23
+ * throwing, like a form submit handler: the selection is kept and `execute()` resolves `false`.
24
+ * A rejection also keeps the selection and propagates to the caller.
25
+ */
21
26
  action?: (context: TableActionContext<TRow, TContext, TPageContext>) => MaybePromise<unknown>;
22
27
  }
23
28
  export interface TableBulkAction<TRow extends GenericObject = GenericObject, TContext extends GenericObject = GenericObject, TPageContext extends GenericObject = GenericObject> extends TableToolbarAction<TRow, TContext, TPageContext> {
24
29
  requiresSelection?: boolean;
30
+ /** When the selection clears: before the action runs, after it succeeds, or never. */
25
31
  selectionClear?: 'trigger' | 'success' | 'never';
26
32
  }
27
33
  /** Runtime state exposed for a configured toolbar or bulk action. */
@@ -42,7 +48,8 @@ export interface TableActionSlotProps<TRow extends GenericObject = GenericObject
42
48
  state: TableActionState;
43
49
  running: boolean;
44
50
  selection: TableApi['selection'];
45
- execute: () => Promise<void>;
51
+ /** Runs the action and resolves `true` when it ran and succeeded. */
52
+ execute: () => Promise<boolean>;
46
53
  }
47
54
  export interface TableRowActionContext<TRow extends GenericObject = GenericObject, TContext extends GenericObject = GenericObject, TPageContext extends GenericObject = GenericObject> extends TableRowRenderParams<TRow, TContext, TPageContext> {
48
55
  layout: TableLayout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-ui-tools",
3
- "version": "1.11.1",
3
+ "version": "1.12.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",