quasar-ui-danx 0.2.32 → 0.3.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.
@@ -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
  };