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.
- package/dist/danx.es.js +6496 -6291
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +5 -5
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ActionTable/ActionTableColumn.vue +5 -2
- package/src/components/ActionTable/ActionTableHeaderColumn.vue +2 -1
- package/src/components/ActionTable/Filters/FilterListToggle.vue +1 -2
- package/src/components/ActionTable/Form/Fields/BooleanField.vue +9 -2
- package/src/components/ActionTable/Form/RenderedForm.vue +146 -10
- package/src/components/ActionTable/listControls.ts +68 -38
- package/src/components/ActionTable/tableColumns.ts +3 -3
- package/src/components/PanelsDrawer/PanelsDrawerPanels.vue +1 -1
- package/src/components/Utility/Tools/RenderVnode.vue +16 -2
- package/src/helpers/actions.ts +34 -31
- package/src/helpers/array.ts +11 -16
- package/src/helpers/utils.ts +20 -25
- package/src/styles/quasar-reset.scss +4 -0
@@ -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="
|
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
|
-
|
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 {
|
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:
|
107
|
+
type: Array,
|
40
108
|
default: null
|
41
109
|
},
|
42
|
-
|
43
|
-
type:
|
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
|
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
|
-
|
74
|
-
|
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 =
|
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 =
|
40
|
+
const selectedRows = shallowRef([]);
|
33
41
|
const isLoadingList = ref(false);
|
34
42
|
const isLoadingSummary = ref(false);
|
35
|
-
const summary =
|
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
|
-
|
145
|
+
data = items;
|
146
|
+
meta = { total: items.length };
|
147
|
+
|
131
148
|
} else {
|
132
|
-
|
133
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
297
|
-
|
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
|
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
|
-
|
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
|
-
|
12
|
+
style?: string | object,
|
13
|
+
headerStyle?: string | object,
|
14
|
+
isSavingRow?: boolean | Function,
|
15
15
|
label: string,
|
16
16
|
maxWidth?: number,
|
17
17
|
minWidth?: number,
|
@@ -1,5 +1,19 @@
|
|
1
1
|
<script>
|
2
|
-
const RenderVnode = (props) =>
|
3
|
-
|
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>
|
package/src/helpers/actions.ts
CHANGED
@@ -1,17 +1,23 @@
|
|
1
|
-
import {
|
1
|
+
import { useDebounceFn } from "@vueuse/core";
|
2
|
+
import { Ref, shallowRef, VNode } from "vue";
|
2
3
|
import { FlashMessages } from "./FlashMessages";
|
3
4
|
|
4
|
-
type
|
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:
|
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 (
|
40
|
-
mappedAction.trigger = (target, input) => performAction(mappedAction, target, input);
|
41
|
-
|
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
|
-
*
|
54
|
+
* Set the reactive saving state of a target
|
48
55
|
*/
|
49
|
-
function
|
50
|
-
if (
|
51
|
-
|
52
|
-
|
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
|
80
|
-
|
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
|
-
|
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
|
};
|