quasar-ui-danx 0.2.32 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -46,7 +46,8 @@ const columnStyle = computed(() => {
46
46
  const width = props.settings?.width || column.value.width;
47
47
  return {
48
48
  width: width ? `${width}px` : undefined,
49
- minWidth: column.value.minWidth ? `${column.value.minWidth}px` : undefined
49
+ minWidth: column.value.minWidth ? `${column.value.minWidth}px` : undefined,
50
+ ...(column.value.headerStyle || {})
50
51
  };
51
52
  });
52
53
 
@@ -2,7 +2,7 @@
2
2
  <div class="flex items-center transition-all" :class="{'w-72': showFilters, 'w-[6.5rem]': !showFilters}">
3
3
  <div class="flex-grow">
4
4
  <QBtn
5
- class="toggle-button border-blue-700"
5
+ class="btn-blue-highlight border-blue-700"
6
6
  :class="{'highlighted': showFilters}"
7
7
  @click="$emit('update:show-filters', !showFilters)"
8
8
  >
@@ -38,7 +38,6 @@ const props = defineProps({
38
38
  const activeCount = computed(() => Object.keys(props.filter).filter(key => props.filter[key] !== undefined).length);
39
39
  </script>
40
40
  <style lang="scss" scoped>
41
-
42
41
  .btn-blue-highlight {
43
42
  @apply rounded-lg border border-solid;
44
43
 
@@ -2,12 +2,13 @@
2
2
  <QToggle
3
3
  :data-testid="'boolean-field-' + field.id"
4
4
  :model-value="modelValue"
5
+ :disable="disable || readonly"
5
6
  :toggle-indeterminate="toggleIndeterminate"
6
7
  :indeterminate-value="undefined"
7
8
  @update:model-value="$emit('update:model-value', $event)"
8
9
  >
9
10
  <FieldLabel
10
- :field="field"
11
+ :field="{...field, label}"
11
12
  :show-name="showName"
12
13
  :class="labelClass"
13
14
  />
@@ -19,6 +20,10 @@ import FieldLabel from "./FieldLabel";
19
20
 
20
21
  defineEmits(["update:model-value"]);
21
22
  defineProps({
23
+ label: {
24
+ type: String,
25
+ default: null
26
+ },
22
27
  modelValue: {
23
28
  type: [Boolean],
24
29
  default: undefined
@@ -32,6 +37,8 @@ defineProps({
32
37
  default: "text-sm"
33
38
  },
34
39
  showName: Boolean,
35
- toggleIndeterminate: Boolean
40
+ toggleIndeterminate: Boolean,
41
+ disable: Boolean,
42
+ readonly: Boolean
36
43
  });
37
44
  </script>
@@ -1,5 +1,46 @@
1
1
  <template>
2
2
  <div class="rendered-form">
3
+ <div v-if="form.variations > 1" class="mb-4">
4
+ <QTabs v-model="currentVariation" class="text-xs">
5
+ <QTab
6
+ v-for="(name, index) in variationNames"
7
+ :key="name"
8
+ :name="name"
9
+ class="p-0"
10
+ >
11
+ <div class="flex flex-nowrap items-center text-sm">
12
+ <div>{{ name }}</div>
13
+ <template v-if="!disable && !readonly">
14
+ <a
15
+ @click="() => (variationToEdit = name) && (newVariationName = name)"
16
+ class="ml-1 p-1 hover:opacity-100 opacity-20 hover:bg-blue-200 rounded"
17
+ >
18
+ <EditIcon class="w-3 text-blue-900" />
19
+ </a>
20
+ <a
21
+ v-if="index > 0"
22
+ @click="variationToDelete = name"
23
+ class="ml-1 p-1 hover:opacity-100 opacity-20 hover:bg-red-200 rounded"
24
+ >
25
+ <RemoveIcon class="w-3 text-red-900" />
26
+ </a>
27
+ </template>
28
+ </div>
29
+ </QTab>
30
+ <QTab
31
+ v-if="canAddVariation"
32
+ name="add"
33
+ key="add-new-variation"
34
+ @click="onAddVariation"
35
+ class="bg-blue-600 rounded-t-lg !text-white"
36
+ >
37
+ <template v-if="saving">
38
+ <QSpinnerBall class="w-4" />
39
+ </template>
40
+ <template v-else>+ Add Variation</template>
41
+ </QTab>
42
+ </QTabs>
43
+ </div>
3
44
  <div
4
45
  v-for="(field, index) in mappedFields"
5
46
  :key="field.id"
@@ -7,7 +48,8 @@
7
48
  >
8
49
  <Component
9
50
  :is="field.component"
10
- v-model="fieldValues[field.name]"
51
+ :key="field.name + '-' + currentVariation"
52
+ :model-value="getFieldValue(field.name)"
11
53
  :field="field"
12
54
  :label="field.label || undefined"
13
55
  :no-label="noLabel"
@@ -17,10 +59,36 @@
17
59
  @update:model-value="onInput(field.name, $event)"
18
60
  />
19
61
  </div>
62
+ <ConfirmDialog
63
+ v-if="variationToEdit"
64
+ title="Change variation name"
65
+ @confirm="onChangeVariationName"
66
+ @close="variationToEdit = null"
67
+ >
68
+ <TextField
69
+ v-model="newVariationName"
70
+ label="Enter name"
71
+ placeholder="Variation Name"
72
+ input-class="bg-white"
73
+ />
74
+ </ConfirmDialog>
75
+ <ConfirmDialog
76
+ v-if="variationToDelete"
77
+ :title="`Remove variation ${variationToDelete}?`"
78
+ content="You cannot undo this action. If there was any analytics collected for this variation, it will still be attributed to the ad."
79
+ confirm-class="bg-red-900 text-white"
80
+ content-class="w-96"
81
+ @confirm="onRemoveVariation(variationToDelete)"
82
+ @close="variationToDelete = ''"
83
+ />
20
84
  </div>
21
85
  </template>
22
86
  <script setup>
23
- import { reactive } from "vue";
87
+ import { PencilIcon as EditIcon } from "@heroicons/vue/solid";
88
+ import { computed, ref } from "vue";
89
+ import { FlashMessages, incrementName, replace } from "../../../helpers";
90
+ import { TrashIcon as RemoveIcon } from "../../../svg";
91
+ import { ConfirmDialog } from "../../Utility";
24
92
  import {
25
93
  BooleanField,
26
94
  DateField,
@@ -36,17 +104,18 @@ import {
36
104
  const emit = defineEmits(["update:values"]);
37
105
  const props = defineProps({
38
106
  values: {
39
- type: Object,
107
+ type: Array,
40
108
  default: null
41
109
  },
42
- fields: {
43
- type: Array,
110
+ form: {
111
+ type: Object,
44
112
  required: true
45
113
  },
46
114
  noLabel: Boolean,
47
115
  showName: Boolean,
48
116
  disable: Boolean,
49
- readonly: Boolean
117
+ readonly: Boolean,
118
+ saving: Boolean
50
119
  });
51
120
 
52
121
  const FORM_FIELD_MAP = {
@@ -61,16 +130,83 @@ const FORM_FIELD_MAP = {
61
130
  WYSIWYG: WysiwygField
62
131
  };
63
132
 
64
- const mappedFields = props.fields.map((field) => ({
133
+ const mappedFields = props.form.fields.map((field) => ({
65
134
  placeholder: `Enter ${field.label}`,
66
135
  ...field,
67
136
  component: FORM_FIELD_MAP[field.type],
68
137
  default: field.type === "BOOLEAN" ? false : ""
69
138
  }));
70
139
 
71
- const fieldValues = reactive(props.values || {});
140
+ const variationNames = computed(() => {
141
+ return [...new Set(props.values.map(v => v.variation))].sort();
142
+ });
143
+
144
+ const currentVariation = ref(variationNames.value[0] || "default");
145
+ const newVariationName = ref("");
146
+ const variationToEdit = ref("");
147
+ const variationToDelete = ref("");
148
+ const canAddVariation = computed(() => variationNames.value.length < props.form.variations && !props.readonly && !props.disable);
149
+
150
+ function getFieldResponse(name) {
151
+ if (!props.values) return undefined;
152
+ return props.values.find((v) => v.variation === currentVariation.value && v.name === name);
153
+ }
154
+ function getFieldValue(name) {
155
+ return getFieldResponse(name)?.value;
156
+ }
157
+ function onInput(name, value) {
158
+ const fieldResponse = getFieldResponse(name);
159
+ const newFieldResponse = {
160
+ name,
161
+ variation: currentVariation.value,
162
+ value
163
+ };
164
+ const newValues = replace(props.values, fieldResponse, newFieldResponse, true);
165
+ emit("update:values", newValues);
166
+ }
167
+
168
+ function onAddVariation() {
169
+ if (props.saving) return;
170
+
171
+ const previousName = variationNames.value[variationNames.value.length - 1];
172
+ const newName = incrementName(previousName === "default" ? "Variation" : previousName);
173
+
174
+ const newVariation = props.form.fields.map((field) => ({
175
+ variation: newName,
176
+ name: field.name,
177
+ value: field.type === "BOOLEAN" ? false : null
178
+ }));
179
+ const newValues = [...props.values, ...newVariation];
180
+ emit("update:values", newValues);
181
+ currentVariation.value = newName;
182
+ }
183
+
184
+ function onChangeVariationName() {
185
+ if (!newVariationName.value) return;
186
+ if (variationNames.value.includes(newVariationName.value)) {
187
+ FlashMessages.error("Variation name already exists");
188
+ return;
189
+ }
190
+ const newValues = props.values.map((v) => {
191
+ if (v.variation === variationToEdit.value) {
192
+ return { ...v, variation: newVariationName.value };
193
+ }
194
+ return v;
195
+ });
196
+ emit("update:values", newValues);
197
+
198
+ currentVariation.value = newVariationName.value;
199
+ variationToEdit.value = "";
200
+ newVariationName.value = "";
201
+ }
202
+
203
+ function onRemoveVariation(name) {
204
+ const newValues = props.values.filter((v) => v.variation !== name);
205
+ emit("update:values", newValues);
72
206
 
73
- function onInput(key, value) {
74
- emit("update:values", { ...fieldValues, [key]: value });
207
+ if (currentVariation.value === name) {
208
+ currentVariation.value = variationNames.value[0];
209
+ }
210
+ variationToDelete.value = "";
75
211
  }
76
212
  </script>
@@ -1,8 +1,8 @@
1
- import { computed, ref, watch } from "vue";
2
- import { getItem, setItem, waitForRef } from "../../helpers";
1
+ import { computed, Ref, ref, ShallowRef, shallowRef, watch } from "vue";
2
+ import { ActionTargetItem, getItem, setItem, waitForRef } from "../../helpers";
3
3
  import { getFilterFromUrl } from "./listHelpers";
4
4
 
5
- interface ListActionsOptions {
5
+ export interface ListActionsOptions {
6
6
  listRoute: Function;
7
7
  summaryRoute?: Function | null;
8
8
  filterFieldOptionsRoute?: Function | null;
@@ -13,6 +13,14 @@ interface ListActionsOptions {
13
13
  refreshFilters?: boolean;
14
14
  }
15
15
 
16
+ export interface PagedItems {
17
+ data: any[] | undefined;
18
+ meta: {
19
+ total: number;
20
+ last_page?: number;
21
+ } | undefined;
22
+ }
23
+
16
24
  export function useListControls(name: string, {
17
25
  listRoute,
18
26
  summaryRoute = null,
@@ -25,14 +33,19 @@ export function useListControls(name: string, {
25
33
  }: ListActionsOptions) {
26
34
  let isInitialized = false;
27
35
  const PAGE_SETTINGS_KEY = `${name}-pagination-settings`;
28
- const pagedItems = ref(null);
29
- const filter = ref({});
36
+ const pagedItems: Ref<PagedItems | null> = shallowRef(null);
37
+ const filter: Ref<object | any> = ref({});
30
38
  const globalFilter = ref({});
31
39
  const showFilters = ref(false);
32
- const selectedRows = ref([]);
40
+ const selectedRows = shallowRef([]);
33
41
  const isLoadingList = ref(false);
34
42
  const isLoadingSummary = ref(false);
35
- const summary = ref(null);
43
+ const summary = shallowRef(null);
44
+
45
+ // The active ad for viewing / editing
46
+ const activeItem: ShallowRef<ActionTargetItem | null> = shallowRef(null);
47
+ // Controls the active panel (ie: tab) if rendering a panels drawer or similar
48
+ const activePanel = shallowRef(null);
36
49
 
37
50
  // Filter fields are the field values available for the currently applied filter on Creative Groups
38
51
  // (ie: all states available under the current filter)
@@ -86,7 +99,7 @@ export function useListControls(name: string, {
86
99
  isLoadingSummary.value = true;
87
100
  const summaryFilter = { id: null, ...filter.value, ...globalFilter.value };
88
101
  if (selectedRows.value.length) {
89
- summaryFilter.id = selectedRows.value.map((row) => row.id);
102
+ summaryFilter.id = selectedRows.value.map((row: { id: string }) => row.id);
90
103
  }
91
104
  summary.value = await summaryRoute(summaryFilter);
92
105
  isLoadingSummary.value = false;
@@ -108,7 +121,7 @@ export function useListControls(name: string, {
108
121
  * Watches for a filter URL parameter and applies the filter if it is set.
109
122
  */
110
123
  function applyFilterFromUrl(url: string, filterFields = null) {
111
- if (url.match(urlPattern)) {
124
+ if (urlPattern && url.match(urlPattern)) {
112
125
  // A flat list of valid filterable field names
113
126
  const validFilterKeys = filterFields?.value?.map(group => group.fields.map(field => field.name)).flat();
114
127
 
@@ -125,15 +138,32 @@ export function useListControls(name: string, {
125
138
 
126
139
  // Set the reactive pager to map from the Laravel pagination to Quasar pagination
127
140
  // and automatically update the list of ads
128
- function setPagedItems(items) {
141
+ function setPagedItems(items: any[] | PagedItems) {
142
+ let data, meta;
143
+
129
144
  if (Array.isArray(items)) {
130
- pagedItems.value = { data: items, meta: { total: items.length } };
145
+ data = items;
146
+ meta = { total: items.length };
147
+
131
148
  } else {
132
- pagedItems.value = items;
133
- if (items?.meta && items.meta.total !== quasarPagination.value.rowsNumber) {
134
- quasarPagination.value.rowsNumber = items.meta.total;
135
- }
149
+ data = items.data;
150
+ meta = items.meta;
136
151
  }
152
+
153
+ // Update the Quasar pagination rows number if it is different from the total
154
+ if (meta && meta.total !== quasarPagination.value.rowsNumber) {
155
+ quasarPagination.value.rowsNumber = meta.total;
156
+ }
157
+
158
+ // Add a reactive isSaving property to each item (for performance reasons in checking saving state)
159
+ data = data.map((item: any) => {
160
+ // We want to keep the isSaving state if it is already set, as optimizations prevent reloading the
161
+ // components, and therefore reactivity is not responding to the new isSaving state
162
+ const oldItem = pagedItems.value?.data?.find(i => i.id === item.id);
163
+ return { ...item, isSaving: oldItem?.isSaving || ref(false) };
164
+ });
165
+
166
+ pagedItems.value = { data, meta };
137
167
  }
138
168
 
139
169
  /**
@@ -148,9 +178,12 @@ export function useListControls(name: string, {
148
178
  *
149
179
  * @param updatedItem
150
180
  */
151
- function setItemInPagedList(updatedItem) {
181
+ function setItemInList(updatedItem: any) {
152
182
  const data = pagedItems.value?.data?.map(item => (item.id === updatedItem.id && (item.updated_at === null || item.updated_at <= updatedItem.updated_at)) ? updatedItem : item);
153
- pagedItems.value = { ...pagedItems.value, data };
183
+ setPagedItems({
184
+ data,
185
+ meta: { total: pagedItems.value.meta.total }
186
+ });
154
187
 
155
188
  // Update the active item as well if it is set
156
189
  if (activeItem.value?.id === updatedItem.id) {
@@ -160,10 +193,10 @@ export function useListControls(name: string, {
160
193
 
161
194
  /**
162
195
  * Loads more items into the list.
163
- * @param index
164
- * @param perPage
165
196
  */
166
- async function loadMore(index, perPage = undefined) {
197
+ async function loadMore(index: number, perPage = undefined) {
198
+ if (!moreRoute) return;
199
+
167
200
  const newItems = await moreRoute({
168
201
  page: index + 1,
169
202
  perPage,
@@ -171,7 +204,10 @@ export function useListControls(name: string, {
171
204
  });
172
205
 
173
206
  if (newItems && newItems.length > 0) {
174
- pagedItems.value.data = [...pagedItems.value.data, ...newItems];
207
+ setPagedItems({
208
+ data: [...pagedItems.value.data, ...newItems],
209
+ meta: { total: pagedItems.value.meta.total }
210
+ });
175
211
  return true;
176
212
  }
177
213
 
@@ -180,7 +216,6 @@ export function useListControls(name: string, {
180
216
 
181
217
  /**
182
218
  * Refreshes the list, summary, and filter field options.
183
- * @returns {Promise<Awaited<void>[]>}
184
219
  */
185
220
  async function refreshAll() {
186
221
  return Promise.all([loadList(), loadSummary(), loadFilterFieldOptions(), getActiveItemDetails()]);
@@ -188,10 +223,8 @@ export function useListControls(name: string, {
188
223
 
189
224
  /**
190
225
  * Updates the settings in local storage
191
- * @param key
192
- * @param value
193
226
  */
194
- function updateSettings(key, value) {
227
+ function updateSettings(key: string, value: any) {
195
228
  const settings = getItem(PAGE_SETTINGS_KEY) || {};
196
229
  settings[key] = value;
197
230
  setItem(PAGE_SETTINGS_KEY, settings);
@@ -242,11 +275,6 @@ export function useListControls(name: string, {
242
275
  setItem(PAGE_SETTINGS_KEY, settings);
243
276
  }
244
277
 
245
- // The active ad for viewing / editing
246
- const activeItem = ref(null);
247
- // Controls the active panel (ie: tab) if rendering a panels drawer or similar
248
- const activePanel = ref(null);
249
-
250
278
  /**
251
279
  * Gets the additional details for the currently active item.
252
280
  * (ie: data that is not normally loaded in the list because it is not needed for the list view)
@@ -261,7 +289,8 @@ export function useListControls(name: string, {
261
289
  // NOTE: race conditions might allow the finished loading item to be different to the currently
262
290
  // requested item
263
291
  if (result?.id === activeItem.value?.id) {
264
- activeItem.value = result;
292
+ const loadedItem = pagedItems.value?.data.find((i: { id: string }) => i.id === result.id);
293
+ activeItem.value = { ...result, isSaving: loadedItem.isSaving || ref(false) };
265
294
  }
266
295
  }
267
296
 
@@ -289,12 +318,13 @@ export function useListControls(name: string, {
289
318
  /**
290
319
  * Gets the next item in the list at the given offset (ie: 1 or -1) from the current position in the list of the
291
320
  * selected item. If the next item is on a previous or next page, it will load the page first then select the item
292
- * @param offset
293
- * @returns {Promise<void>}
294
321
  */
295
- async function getNextItem(offset) {
296
- const index = pagedItems.value?.data.findIndex(i => i.id === activeItem.value.id);
297
- if (index === undefined) return;
322
+ async function getNextItem(offset: number) {
323
+ if (!pagedItems.value) return;
324
+
325
+ const index = pagedItems.value.data.findIndex((i: { id: string }) => i.id === activeItem.value?.id);
326
+ if (index === undefined || index === null) return;
327
+
298
328
  let nextIndex = index + offset;
299
329
 
300
330
  // Load the previous page if the offset is before index 0
@@ -321,7 +351,7 @@ export function useListControls(name: string, {
321
351
  }
322
352
  }
323
353
 
324
- activeItem.value = pagedItems.value.data[nextIndex];
354
+ activeItem.value = pagedItems.value?.data[nextIndex];
325
355
  }
326
356
 
327
357
  // Initialize the list actions and load settings, lists, summaries, filter fields, etc.
@@ -358,6 +388,6 @@ export function useListControls(name: string, {
358
388
  getNextItem,
359
389
  activatePanel,
360
390
  applyFilterFromUrl,
361
- setItemInPagedList
391
+ setItemInList
362
392
  };
363
393
  }
@@ -9,9 +9,9 @@ export interface TableColumn {
9
9
  field: string,
10
10
  format?: Function,
11
11
  innerClass?: string | object,
12
- style?: string,
13
- headerStyle?: string,
14
- isSaving?: boolean | Function,
12
+ style?: string | object,
13
+ headerStyle?: string | object,
14
+ isSavingRow?: boolean | Function,
15
15
  label: string,
16
16
  maxWidth?: number,
17
17
  minWidth?: number,
@@ -6,7 +6,7 @@
6
6
  <QTabPanel v-for="panel in panels" :key="panel.name" :name="panel.name">
7
7
  <RenderVnode
8
8
  v-if="panel.vnode"
9
- :vnode="panel.vnode()"
9
+ :vnode="panel.vnode"
10
10
  />
11
11
  </QTabPanel>
12
12
  </QTabPanels>
@@ -1,5 +1,19 @@
1
1
  <script>
2
- const RenderVnode = (props) => props.vnode;
3
- RenderVnode.props = { vnode: { type: Object, required: true } };
2
+ const RenderVnode = (props) => {
3
+ if (props.vnode.__v_isVNode) {
4
+ return props.vnode;
5
+ }
6
+
7
+ if (props.vnode.__v_isRef) {
8
+ return props.vnode.value;
9
+ }
10
+
11
+ if (typeof props.vnode === "function") {
12
+ return props.vnode();
13
+ }
14
+
15
+ return null;
16
+ };
17
+ RenderVnode.props = { vnode: { type: [Function, Object], required: true } };
4
18
  export default RenderVnode;
5
19
  </script>
@@ -1,17 +1,23 @@
1
- import { ref, shallowRef, VNode } from "vue";
1
+ import { useDebounceFn } from "@vueuse/core";
2
+ import { Ref, shallowRef, VNode } from "vue";
2
3
  import { FlashMessages } from "./FlashMessages";
3
4
 
4
- type ActionTarget = object[] | object;
5
+ export type ActionTargetItem = {
6
+ id: number | string;
7
+ isSaving: Ref<boolean>;
8
+ [key: string]: any;
9
+ };
10
+ export type ActionTarget = ActionTargetItem[] | ActionTargetItem;
5
11
 
6
- interface ActionOptions {
12
+ export interface ActionOptions {
7
13
  name?: string;
8
14
  label?: string;
9
15
  menu?: boolean;
10
16
  batch?: boolean;
11
17
  category?: string;
12
18
  class?: string;
19
+ debounce?: number;
13
20
  trigger?: (target: ActionTarget, input: any) => Promise<any>;
14
- activeTarget?: any;
15
21
  vnode?: (target: ActionTarget) => VNode;
16
22
  enabled?: (target: object) => boolean;
17
23
  batchEnabled?: (targets: object[]) => boolean;
@@ -24,7 +30,7 @@ interface ActionOptions {
24
30
  onFinish?: (result: any, targets: ActionTarget, input: any) => any;
25
31
  }
26
32
 
27
- export const activeActionVnode: object = shallowRef(null);
33
+ export const activeActionVnode: Ref = shallowRef(null);
28
34
 
29
35
  /**
30
36
  * Hook to perform an action on a set of targets
@@ -36,31 +42,25 @@ export const activeActionVnode: object = shallowRef(null);
36
42
  export function useActions(actions: ActionOptions[], globalOptions: ActionOptions | null = null) {
37
43
  const mappedActions = actions.map(action => {
38
44
  const mappedAction: ActionOptions = { ...globalOptions, ...action };
39
- if (!mappedAction.trigger) {
40
- mappedAction.trigger = (target, input) => performAction(mappedAction, target, input);
41
- mappedAction.activeTarget = ref(null);
45
+ if (mappedAction.debounce) {
46
+ mappedAction.trigger = useDebounceFn((target, input) => performAction(mappedAction, target, input, true), mappedAction.debounce);
47
+ } else if (!mappedAction.trigger) {
48
+ mappedAction.trigger = (target, input) => performAction(mappedAction, target, input, true);
42
49
  }
43
50
  return mappedAction;
44
51
  });
45
52
 
46
53
  /**
47
- * Check if the provided target is currently being saved by any of the actions
54
+ * Set the reactive saving state of a target
48
55
  */
49
- function isSavingTarget(target: any): boolean {
50
- if (!target) return false;
51
-
52
- for (const action of mappedActions) {
53
- const activeTargets = (Array.isArray(action.activeTarget.value) ? action.activeTarget.value : [action.activeTarget.value]).filter(t => t);
54
- if (activeTargets.length === 0) continue;
55
-
56
- for (const activeTarget of activeTargets) {
57
- if (activeTarget === target || (activeTarget.id && activeTarget.id === target.id)) {
58
- return true;
59
- }
56
+ function setTargetSavingState(target: ActionTarget, saving: boolean) {
57
+ if (Array.isArray(target)) {
58
+ for (const t of target) {
59
+ t.isSaving.value = saving;
60
60
  }
61
+ } else {
62
+ target.isSaving.value = saving;
61
63
  }
62
-
63
- return false;
64
64
  }
65
65
 
66
66
  /**
@@ -69,22 +69,23 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionOption
69
69
  * @param {string} name - can either be a string or an action object
70
70
  * @param {object[]|object} target - an array of targets or a single target object
71
71
  * @param {any} input - The input data to pass to the action handler
72
+ * @param isTriggered - Whether the action was triggered by a trigger function
72
73
  */
73
- async function performAction(name: string | object, target: ActionTarget, input: any = null) {
74
- const action: ActionOptions = typeof name === "string" ? mappedActions.find(a => a.name === name) : name;
74
+ async function performAction(name: string | object, target: ActionTarget, input: any = null, isTriggered = false) {
75
+ const action: ActionOptions | null | undefined = typeof name === "string" ? mappedActions.find(a => a.name === name) : name;
75
76
  if (!action) {
76
77
  throw new Error(`Unknown action: ${name}`);
77
78
  }
78
79
 
79
- if (!action.activeTarget) {
80
- throw new Error(`Action ${action.name} does not have an activeTarget ref. Please use useActions() or manually set the activeTarget ref`);
80
+ // We always want to call the trigger function if it exists, unless it's already been triggered
81
+ // This provides behavior like debounce and custom action resolution
82
+ if (action.trigger && !isTriggered) {
83
+ return action.trigger(target, input);
81
84
  }
82
85
 
83
86
  const vnode = action.vnode && action.vnode(target);
84
87
  let result: any;
85
88
 
86
- action.activeTarget.value = target;
87
-
88
89
  // Run the onStart handler if it exists and quit the operation if it returns false
89
90
  if (action.onStart) {
90
91
  if (!action.onStart(action, target, input)) {
@@ -92,6 +93,8 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionOption
92
93
  }
93
94
  }
94
95
 
96
+ setTargetSavingState(target, true);
97
+
95
98
  // If additional input is required, first render the vnode and wait for the confirm or cancel action
96
99
  if (vnode) {
97
100
  // If the action requires an input, we set the activeActionVnode to the input component.
@@ -119,7 +122,8 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionOption
119
122
  result = await onConfirmAction(action, target, input);
120
123
  }
121
124
 
122
- action.activeTarget.value = null;
125
+ setTargetSavingState(target, false);
126
+
123
127
  return result;
124
128
  }
125
129
 
@@ -135,14 +139,13 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionOption
135
139
 
136
140
  for (const filter of Object.keys(filters)) {
137
141
  const filterValue = filters[filter];
138
- filteredActions = filteredActions.filter(a => a[filter] === filterValue || (Array.isArray(filterValue) && filterValue.includes(a[filter])));
142
+ filteredActions = filteredActions.filter((a: object) => a[filter] === filterValue || (Array.isArray(filterValue) && filterValue.includes(a[filter])));
139
143
  }
140
144
  return filteredActions;
141
145
  }
142
146
 
143
147
  return {
144
148
  actions: mappedActions,
145
- isSavingTarget,
146
149
  filterActions,
147
150
  performAction
148
151
  };