quasar-ui-danx 0.0.9 → 0.0.11
Sign up to get free protection for your applications and to get access to all the features.
- package/package.json +3 -2
- package/src/components/ActionTable/ActionTable.vue +135 -0
- package/src/components/ActionTable/BatchActionMenu.vue +60 -0
- package/src/components/ActionTable/EmptyTableState.vue +33 -0
- package/src/components/ActionTable/Filters/CollapsableFiltersSidebar.vue +36 -0
- package/src/components/ActionTable/Filters/FilterGroupItem.vue +28 -0
- package/src/components/ActionTable/Filters/FilterGroupList.vue +76 -0
- package/src/components/ActionTable/Filters/FilterListToggle.vue +50 -0
- package/src/components/ActionTable/Filters/FilterableField.vue +141 -0
- package/src/components/ActionTable/Form/Fields/BooleanField.vue +37 -0
- package/src/components/ActionTable/Form/Fields/ConfirmPasswordField.vue +46 -0
- package/src/components/ActionTable/Form/Fields/DateField.vue +59 -0
- package/src/components/ActionTable/Form/Fields/DateRangeField.vue +110 -0
- package/src/components/ActionTable/Form/Fields/DateTimeField.vue +50 -0
- package/src/components/ActionTable/Form/Fields/DateTimePicker.vue +59 -0
- package/src/components/ActionTable/Form/Fields/EditableDiv.vue +39 -0
- package/src/components/ActionTable/Form/Fields/FieldLabel.vue +32 -0
- package/src/components/ActionTable/Form/Fields/FileUploadButton.vue +78 -0
- package/src/components/ActionTable/Form/Fields/InlineDateTimeField.vue +44 -0
- package/src/components/ActionTable/Form/Fields/IntegerField.vue +26 -0
- package/src/components/ActionTable/Form/Fields/LabeledInput.vue +63 -0
- package/src/components/ActionTable/Form/Fields/MultiFileField.vue +91 -0
- package/src/components/ActionTable/Form/Fields/MultiKeywordField.vue +57 -0
- package/src/components/ActionTable/Form/Fields/NewPasswordField.vue +39 -0
- package/src/components/ActionTable/Form/Fields/NumberField.vue +94 -0
- package/src/components/ActionTable/Form/Fields/NumberRangeField.vue +140 -0
- package/src/components/ActionTable/Form/Fields/SelectDrawer.vue +136 -0
- package/src/components/ActionTable/Form/Fields/SelectField.vue +318 -0
- package/src/components/ActionTable/Form/Fields/SelectWithChildrenField.vue +81 -0
- package/src/components/ActionTable/Form/Fields/SingleFileField.vue +78 -0
- package/src/components/ActionTable/Form/Fields/TextField.vue +82 -0
- package/src/components/ActionTable/Form/Fields/WysiwygField.vue +46 -0
- package/src/components/ActionTable/Form/Fields/index.ts +23 -0
- package/src/components/ActionTable/Form/RenderedForm.vue +74 -0
- package/src/components/ActionTable/RenderComponentColumn.vue +22 -0
- package/src/components/ActionTable/TableSummaryRow.vue +95 -0
- package/src/components/ActionTable/index.ts +15 -0
- package/src/components/ActionTable/listActions.ts +361 -0
- package/src/components/ActionTable/tableColumns.ts +72 -0
- package/src/components/ActionTable/tableHelpers.ts +83 -0
- package/src/components/DragAndDrop/listDragAndDrop.ts +210 -210
- package/src/components/Utility/CollapsableSidebar.vue +119 -0
- package/src/components/Utility/ContentDrawer.vue +70 -0
- package/src/components/Utility/Dialogs/ConfirmDialog.vue +132 -0
- package/src/components/Utility/Dialogs/FullScreenDialog.vue +46 -0
- package/src/components/Utility/Dialogs/InfoDialog.vue +92 -0
- package/src/components/Utility/Dialogs/InputDialog.vue +35 -0
- package/src/components/Utility/SvgImg.vue +10 -5
- package/src/components/Utility/Transitions/ListTransition.vue +50 -0
- package/src/components/Utility/Transitions/SlideTransition.vue +63 -0
- package/src/components/Utility/Transitions/StaggeredListTransition.vue +97 -0
- package/src/components/Utility/index.ts +9 -0
- package/src/components/index.ts +3 -0
- package/src/helpers/FileUpload.ts +294 -0
- package/src/helpers/FlashMessages.ts +79 -0
- package/src/helpers/array.ts +37 -0
- package/src/helpers/compatibility.ts +64 -0
- package/src/helpers/date.ts +5 -0
- package/src/helpers/download.ts +192 -0
- package/src/helpers/downloadPdf.ts +92 -0
- package/src/helpers/files.ts +52 -0
- package/src/helpers/formats.ts +183 -0
- package/src/helpers/http.ts +62 -0
- package/src/helpers/index.ts +10 -1
- package/src/helpers/multiFileUpload.ts +68 -0
- package/src/helpers/singleFileUpload.ts +54 -0
- package/src/helpers/storage.ts +8 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
<template>
|
2
|
+
<Component
|
3
|
+
:is="component.is"
|
4
|
+
v-bind="component.props"
|
5
|
+
@action="$emit('action', $event)"
|
6
|
+
>{{ component.value || component.props?.text || rowProps.value }}</Component>
|
7
|
+
</template>
|
8
|
+
<script setup>
|
9
|
+
import { computed } from "vue";
|
10
|
+
|
11
|
+
defineEmits(["action"]);
|
12
|
+
const props = defineProps({
|
13
|
+
rowProps: {
|
14
|
+
type: Object,
|
15
|
+
required: true
|
16
|
+
}
|
17
|
+
});
|
18
|
+
|
19
|
+
const component = computed(() => {
|
20
|
+
return props.rowProps.col.component(props.rowProps.row);
|
21
|
+
});
|
22
|
+
</script>
|
@@ -0,0 +1,95 @@
|
|
1
|
+
<template>
|
2
|
+
<QTr
|
3
|
+
class="sticky-column-1 transition-all sticky-row"
|
4
|
+
:class="{'!bg-neutral-plus-7': !selectedCount, '!bg-blue-base text-white selected': selectedCount, 'opacity-50': loading}"
|
5
|
+
>
|
6
|
+
<QTd
|
7
|
+
:colspan="stickyColspan"
|
8
|
+
class="font-bold transition-all"
|
9
|
+
:class="{'!bg-neutral-plus-7 !pl-5': !selectedCount, '!bg-blue-base text-white !pl-4': selectedCount}"
|
10
|
+
>
|
11
|
+
<div class="flex flex-nowrap items-center">
|
12
|
+
<div
|
13
|
+
v-if="selectedCount"
|
14
|
+
class="flex items-center"
|
15
|
+
>
|
16
|
+
<ClearIcon
|
17
|
+
class="w-6 mr-3"
|
18
|
+
@click="$emit('clear')"
|
19
|
+
/>
|
20
|
+
{{ fNumber(selectedCount) }} {{ selectedLabel }}
|
21
|
+
</div>
|
22
|
+
<div v-else-if="itemCount">
|
23
|
+
{{ fNumber(itemCount) }} {{ label }}
|
24
|
+
</div>
|
25
|
+
<QSpinner
|
26
|
+
v-if="loading"
|
27
|
+
class="ml-3"
|
28
|
+
size="18"
|
29
|
+
/>
|
30
|
+
</div>
|
31
|
+
</QTd>
|
32
|
+
<QTd
|
33
|
+
v-for="column in summaryColumns"
|
34
|
+
:key="column.name"
|
35
|
+
:align="column.align || 'left'"
|
36
|
+
>
|
37
|
+
<template v-if="summary">
|
38
|
+
{{ formatValue(column) }}
|
39
|
+
</template>
|
40
|
+
</QTd>
|
41
|
+
</QTr>
|
42
|
+
</template>
|
43
|
+
<script setup>
|
44
|
+
import { XCircleIcon as ClearIcon } from "@heroicons/vue/solid";
|
45
|
+
import { fNumber } from "danx/src/helpers/formats";
|
46
|
+
import { computed } from "vue";
|
47
|
+
|
48
|
+
defineEmits(["clear"]);
|
49
|
+
const props = defineProps({
|
50
|
+
loading: Boolean,
|
51
|
+
label: {
|
52
|
+
type: String,
|
53
|
+
default: "Rows"
|
54
|
+
},
|
55
|
+
selectedLabel: {
|
56
|
+
type: String,
|
57
|
+
default: "Selected"
|
58
|
+
},
|
59
|
+
selectedCount: {
|
60
|
+
type: Number,
|
61
|
+
default: 0
|
62
|
+
},
|
63
|
+
itemCount: {
|
64
|
+
type: Number,
|
65
|
+
default: 0
|
66
|
+
},
|
67
|
+
summary: {
|
68
|
+
type: Object,
|
69
|
+
default: null
|
70
|
+
},
|
71
|
+
columns: {
|
72
|
+
type: Array,
|
73
|
+
required: true
|
74
|
+
},
|
75
|
+
stickyColspan: {
|
76
|
+
type: Number,
|
77
|
+
default: 2
|
78
|
+
}
|
79
|
+
});
|
80
|
+
|
81
|
+
const summaryColumns = computed(() => {
|
82
|
+
// The sticky columns are where we display the selection count and should not be included in the summary columns
|
83
|
+
return props.columns.slice(props.stickyColspan - 1);
|
84
|
+
});
|
85
|
+
|
86
|
+
function formatValue(column) {
|
87
|
+
const value = props.summary[column.name];
|
88
|
+
if (value === undefined) return "";
|
89
|
+
|
90
|
+
if (column.format) {
|
91
|
+
return column.format(value);
|
92
|
+
}
|
93
|
+
return value;
|
94
|
+
}
|
95
|
+
</script>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
export * from "./Form/Fields";
|
2
|
+
export * from "danx/src/components/ActionTable/tableHelpers";
|
3
|
+
export * from "./listActions";
|
4
|
+
export * from "./tableColumns";
|
5
|
+
export { default as ActionTable } from "./ActionTable";
|
6
|
+
export { default as BatchActionMenu } from "./BatchActionMenu";
|
7
|
+
export {
|
8
|
+
default as CollapsableFiltersSidebar
|
9
|
+
} from "danx/src/components/ActionTable/Filters/CollapsableFiltersSidebar";
|
10
|
+
export { default as EmptyTableState } from "./EmptyTableState";
|
11
|
+
export { default as FilterGroupList } from "./Filters/FilterGroupList";
|
12
|
+
export { default as FilterListToggle } from "./Filters/FilterListToggle";
|
13
|
+
export { default as RenderComponentColumn } from "./RenderComponentColumn";
|
14
|
+
export { default as RenderedForm } from "./Form/RenderedForm";
|
15
|
+
export { default as TableSummaryRow } from "./TableSummaryRow";
|
@@ -0,0 +1,361 @@
|
|
1
|
+
import { getFilterFromUrl, mapSortBy, waitForRef } from "danx/src/components/ActionTable/tableHelpers";
|
2
|
+
import { getItem, setItem } from "danx/src/helpers";
|
3
|
+
import { computed, ref, watch } from "vue";
|
4
|
+
|
5
|
+
export function useListActions(name, {
|
6
|
+
listRoute,
|
7
|
+
filterFieldOptionsRoute,
|
8
|
+
summaryRoute = null,
|
9
|
+
moreRoute = null,
|
10
|
+
applyActionRoute = null,
|
11
|
+
applyBatchActionRoute = null,
|
12
|
+
itemDetailsRoute = null,
|
13
|
+
columns = null,
|
14
|
+
filterGroups = null,
|
15
|
+
refreshFilters = false,
|
16
|
+
urlPattern = null,
|
17
|
+
filterDefaults = {}
|
18
|
+
}) {
|
19
|
+
const PAGE_SETTINGS_KEY = `${name}-pagination-settings`;
|
20
|
+
const pagedItems = ref(null);
|
21
|
+
const filter = ref({});
|
22
|
+
const showFilters = ref(getItem(`${name}-show-filters`, true));
|
23
|
+
const selectedRows = ref([]);
|
24
|
+
const isLoadingList = ref(false);
|
25
|
+
const isLoadingSummary = ref(false);
|
26
|
+
const summary = ref(null);
|
27
|
+
|
28
|
+
const filterActiveCount = computed(() => Object.keys(filter.value).filter(key => filter.value[key] !== undefined).length);
|
29
|
+
|
30
|
+
const PAGING_DEFAULT = {
|
31
|
+
sortBy: null,
|
32
|
+
descending: false,
|
33
|
+
page: 1,
|
34
|
+
rowsNumber: 0,
|
35
|
+
rowsPerPage: 50
|
36
|
+
};
|
37
|
+
const quasarPagination = ref(PAGING_DEFAULT);
|
38
|
+
|
39
|
+
const pager = computed(() => ({
|
40
|
+
perPage: quasarPagination.value.rowsPerPage,
|
41
|
+
page: quasarPagination.value.page,
|
42
|
+
filter: filter.value,
|
43
|
+
sort: columns ? mapSortBy(quasarPagination.value, columns) : undefined
|
44
|
+
}));
|
45
|
+
|
46
|
+
// When any part of the filter changes, get the new list of creatives
|
47
|
+
watch(pager, () => {
|
48
|
+
saveSettings();
|
49
|
+
loadList();
|
50
|
+
});
|
51
|
+
watch(filter, () => {
|
52
|
+
saveSettings();
|
53
|
+
loadSummary();
|
54
|
+
});
|
55
|
+
watch(selectedRows, loadSummary);
|
56
|
+
|
57
|
+
if (refreshFilters) {
|
58
|
+
watch(filter, loadFilterFieldOptions);
|
59
|
+
}
|
60
|
+
|
61
|
+
async function loadList() {
|
62
|
+
isLoadingList.value = true;
|
63
|
+
setPagedItems(await listRoute(pager.value));
|
64
|
+
isLoadingList.value = false;
|
65
|
+
}
|
66
|
+
|
67
|
+
async function loadSummary() {
|
68
|
+
if (summaryRoute) {
|
69
|
+
isLoadingSummary.value = true;
|
70
|
+
const summaryFilter = { id: null, ...filter.value };
|
71
|
+
if (selectedRows.value.length) {
|
72
|
+
summaryFilter.id = selectedRows.value.map((row) => row.id);
|
73
|
+
}
|
74
|
+
summary.value = await summaryRoute(summaryFilter);
|
75
|
+
isLoadingSummary.value = false;
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
// Filter fields are the field values available for the currently applied filter on Creative Groups
|
80
|
+
// (ie: all states available under the current filter)
|
81
|
+
const filterFieldOptions = ref({});
|
82
|
+
const isLoadingFilters = ref(false);
|
83
|
+
|
84
|
+
watch(() => showFilters.value, (show) => {
|
85
|
+
setItem(`${name}-show-filters`, show);
|
86
|
+
});
|
87
|
+
|
88
|
+
async function loadFilterFieldOptions() {
|
89
|
+
isLoadingFilters.value = true;
|
90
|
+
filterFieldOptions.value = await filterFieldOptionsRoute(filter.value);
|
91
|
+
isLoadingFilters.value = false;
|
92
|
+
}
|
93
|
+
|
94
|
+
// A flat list of valid filterable field names
|
95
|
+
const validFilterKeys = computed(() => filterGroups?.value?.map(group => group.fields.map(field => field.name)).flat());
|
96
|
+
|
97
|
+
/**
|
98
|
+
* Watches for a filter URL parameter and applies the filter if it is set.
|
99
|
+
*/
|
100
|
+
function applyFilterFromUrl(url) {
|
101
|
+
if (url.match(urlPattern)) {
|
102
|
+
const urlFilter = getFilterFromUrl(url, validFilterKeys.value);
|
103
|
+
|
104
|
+
if (Object.keys(urlFilter).length > 0) {
|
105
|
+
filter.value = urlFilter;
|
106
|
+
}
|
107
|
+
}
|
108
|
+
}
|
109
|
+
|
110
|
+
// Set the reactive pager to map from the Laravel pagination to Quasar pagination
|
111
|
+
// and automatically update the list of ads
|
112
|
+
function setPagedItems(items) {
|
113
|
+
if (Array.isArray(items)) {
|
114
|
+
pagedItems.value = { data: items, meta: { total: items.length } };
|
115
|
+
} else {
|
116
|
+
pagedItems.value = items;
|
117
|
+
if (items?.meta && items.meta.total !== quasarPagination.value.rowsNumber) {
|
118
|
+
quasarPagination.value.rowsNumber = items.meta.total;
|
119
|
+
}
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
/**
|
124
|
+
* Resets the filter and pagination settings to their defaults.
|
125
|
+
*/
|
126
|
+
function resetPaging() {
|
127
|
+
quasarPagination.value = PAGING_DEFAULT;
|
128
|
+
}
|
129
|
+
|
130
|
+
/**
|
131
|
+
* Updates a row in the paged items list with the new item data. Uses the item's id to find the row.
|
132
|
+
*
|
133
|
+
* @param updatedItem
|
134
|
+
*/
|
135
|
+
function setItemInPagedList(updatedItem) {
|
136
|
+
const data = pagedItems.value?.data?.map(item => (item.id === updatedItem.id && (item.updated_at === null || item.updated_at <= updatedItem.updated_at)) ? updatedItem : item);
|
137
|
+
pagedItems.value = { ...pagedItems.value, data };
|
138
|
+
}
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Loads more items into the list.
|
142
|
+
* @param index
|
143
|
+
* @param perPage
|
144
|
+
*/
|
145
|
+
async function loadMore(index, perPage = undefined) {
|
146
|
+
const newItems = await moreRoute({
|
147
|
+
page: index + 1,
|
148
|
+
perPage,
|
149
|
+
filter: filter.value
|
150
|
+
});
|
151
|
+
|
152
|
+
if (newItems && newItems.length > 0) {
|
153
|
+
pagedItems.value.data = [...pagedItems.value.data, ...newItems];
|
154
|
+
return true;
|
155
|
+
}
|
156
|
+
|
157
|
+
return false;
|
158
|
+
}
|
159
|
+
|
160
|
+
/**
|
161
|
+
* Refreshes the list, summary, and filter field options.
|
162
|
+
* @returns {Promise<Awaited<void>[]>}
|
163
|
+
*/
|
164
|
+
async function refreshAll() {
|
165
|
+
return Promise.all([loadList(), loadSummary(), loadFilterFieldOptions()]);
|
166
|
+
}
|
167
|
+
|
168
|
+
/**
|
169
|
+
* Loads the filter and pagination settings from local storage.
|
170
|
+
*/
|
171
|
+
function loadSettings() {
|
172
|
+
const settings = getItem(PAGE_SETTINGS_KEY);
|
173
|
+
|
174
|
+
// Load the filter settings from local storage
|
175
|
+
if (settings) {
|
176
|
+
filter.value = { ...settings.filter, ...filter.value };
|
177
|
+
quasarPagination.value = settings.quasarPagination;
|
178
|
+
} else {
|
179
|
+
// If no local storage settings, apply the default filters
|
180
|
+
filter.value = { ...filterDefaults, ...filter.value };
|
181
|
+
}
|
182
|
+
|
183
|
+
// Load the URL filters if they are set
|
184
|
+
applyFilterFromUrl(window.location.href);
|
185
|
+
|
186
|
+
setTimeout(() => {
|
187
|
+
if (!isLoadingList.value) {
|
188
|
+
loadList();
|
189
|
+
}
|
190
|
+
|
191
|
+
if (!isLoadingSummary.value) {
|
192
|
+
loadSummary();
|
193
|
+
}
|
194
|
+
|
195
|
+
if (!isLoadingFilters.value) {
|
196
|
+
loadFilterFieldOptions();
|
197
|
+
}
|
198
|
+
}, 1);
|
199
|
+
}
|
200
|
+
|
201
|
+
/**
|
202
|
+
* Saves the current filter and pagination settings to local storage.
|
203
|
+
*/
|
204
|
+
async function saveSettings() {
|
205
|
+
const settings = {
|
206
|
+
filter: filter.value,
|
207
|
+
quasarPagination: { ...quasarPagination.value, page: 1 }
|
208
|
+
};
|
209
|
+
// save in local storage
|
210
|
+
setItem(PAGE_SETTINGS_KEY, settings);
|
211
|
+
}
|
212
|
+
|
213
|
+
/**
|
214
|
+
* Applies an action to an item.
|
215
|
+
*/
|
216
|
+
const isApplyingActionToItem = ref(null);
|
217
|
+
let actionResultCount = 0;
|
218
|
+
|
219
|
+
async function applyAction(item, input, itemData = {}) {
|
220
|
+
isApplyingActionToItem.value = item;
|
221
|
+
const resultNumber = ++actionResultCount;
|
222
|
+
setItemInPagedList({ ...item, ...input, ...itemData });
|
223
|
+
const result = await applyActionRoute(item, input);
|
224
|
+
if (result.success) {
|
225
|
+
// Only render the most recent campaign changes
|
226
|
+
if (resultNumber !== actionResultCount) return;
|
227
|
+
|
228
|
+
// Update the updated item in the previously loaded list if it exists
|
229
|
+
setItemInPagedList(result.item);
|
230
|
+
|
231
|
+
// Update the active item if it is the same as the updated item
|
232
|
+
if (activeItem.value?.id === result.item.id) {
|
233
|
+
activeItem.value = { ...activeItem.value, ...result.item };
|
234
|
+
}
|
235
|
+
}
|
236
|
+
isApplyingActionToItem.value = null;
|
237
|
+
return result;
|
238
|
+
}
|
239
|
+
|
240
|
+
/**
|
241
|
+
* Applies an action to all selected items.
|
242
|
+
*/
|
243
|
+
const isApplyingBatchAction = ref(false);
|
244
|
+
|
245
|
+
async function applyBatchAction(input) {
|
246
|
+
isApplyingBatchAction.value = true;
|
247
|
+
const batchFilter = { id: selectedRows.value.map(r => r.id) };
|
248
|
+
const result = await applyBatchActionRoute(batchFilter, input);
|
249
|
+
isApplyingBatchAction.value = false;
|
250
|
+
await refreshAll();
|
251
|
+
|
252
|
+
return result;
|
253
|
+
}
|
254
|
+
|
255
|
+
// The active ad for viewing / editing in the Ad Form
|
256
|
+
const activeItem = ref(null);
|
257
|
+
// Controls the tab on the Ad Form
|
258
|
+
const formTab = ref("general");
|
259
|
+
|
260
|
+
// Whenever the active item changes, fill the additional item details
|
261
|
+
// (ie: tasks, verifications, creatives, etc.)
|
262
|
+
if (itemDetailsRoute) {
|
263
|
+
watch(() => activeItem.value, async (newItem, oldItem) => {
|
264
|
+
if (newItem && oldItem?.id !== newItem.id) {
|
265
|
+
const result = await itemDetailsRoute(newItem);
|
266
|
+
|
267
|
+
// Only set the ad details if we are the response for the currently loaded item
|
268
|
+
// NOTE: race conditions might allow the finished loading item to be different to the currently requested item
|
269
|
+
if (result?.id === activeItem.value?.id) {
|
270
|
+
activeItem.value = result;
|
271
|
+
}
|
272
|
+
}
|
273
|
+
});
|
274
|
+
}
|
275
|
+
|
276
|
+
/**
|
277
|
+
* Opens the item's form with the given item and tab
|
278
|
+
*
|
279
|
+
* @param item
|
280
|
+
* @param tab
|
281
|
+
*/
|
282
|
+
function openItemForm(item, tab) {
|
283
|
+
activeItem.value = item;
|
284
|
+
formTab.value = tab;
|
285
|
+
}
|
286
|
+
|
287
|
+
/**
|
288
|
+
* Gets the next item in the list at the given offset (ie: 1 or -1) from the current position in the list of the
|
289
|
+
* selected item. If the next item is on a previous or next page, it will load the page first then select the item
|
290
|
+
* @param offset
|
291
|
+
* @returns {Promise<void>}
|
292
|
+
*/
|
293
|
+
async function getNextItem(offset) {
|
294
|
+
const index = pagedItems.value?.data.findIndex(i => i.id === activeItem.value.id);
|
295
|
+
if (index === undefined) return;
|
296
|
+
let nextIndex = index + offset;
|
297
|
+
|
298
|
+
// Load the previous page if the offset is before index 0
|
299
|
+
if (nextIndex < 0) {
|
300
|
+
if (quasarPagination.value.page > 1) {
|
301
|
+
quasarPagination.value = { ...quasarPagination.value, page: quasarPagination.value.page - 1 };
|
302
|
+
await waitForRef(isLoadingList, false);
|
303
|
+
nextIndex = pagedItems.value.data.length - 1;
|
304
|
+
} else {
|
305
|
+
// There are no more previous pages
|
306
|
+
return;
|
307
|
+
}
|
308
|
+
}
|
309
|
+
|
310
|
+
// Load the next page if the offset is past the last index
|
311
|
+
if (nextIndex >= pagedItems.value.data.length) {
|
312
|
+
if (quasarPagination.value.page < pagedItems.value.meta.last_page) {
|
313
|
+
quasarPagination.value = { ...quasarPagination.value, page: quasarPagination.value.page + 1 };
|
314
|
+
await waitForRef(isLoadingList, false);
|
315
|
+
nextIndex = 0;
|
316
|
+
} else {
|
317
|
+
// There are no more next pages
|
318
|
+
return;
|
319
|
+
}
|
320
|
+
}
|
321
|
+
|
322
|
+
activeItem.value = pagedItems.value.data[nextIndex];
|
323
|
+
}
|
324
|
+
|
325
|
+
// Async load the settings for this Action List
|
326
|
+
setTimeout(loadSettings, 1);
|
327
|
+
|
328
|
+
return {
|
329
|
+
// State
|
330
|
+
pagedItems,
|
331
|
+
filter,
|
332
|
+
filterActiveCount,
|
333
|
+
showFilters,
|
334
|
+
summary,
|
335
|
+
filterFieldOptions,
|
336
|
+
selectedRows,
|
337
|
+
isLoadingList,
|
338
|
+
isLoadingFilters,
|
339
|
+
isLoadingSummary,
|
340
|
+
pager,
|
341
|
+
quasarPagination,
|
342
|
+
isApplyingActionToItem,
|
343
|
+
isApplyingBatchAction,
|
344
|
+
activeItem,
|
345
|
+
formTab,
|
346
|
+
columns,
|
347
|
+
filterGroups,
|
348
|
+
|
349
|
+
// Actions
|
350
|
+
loadSummary,
|
351
|
+
resetPaging,
|
352
|
+
loadList,
|
353
|
+
loadMore,
|
354
|
+
refreshAll,
|
355
|
+
applyAction,
|
356
|
+
applyBatchAction,
|
357
|
+
getNextItem,
|
358
|
+
openItemForm,
|
359
|
+
applyFilterFromUrl
|
360
|
+
};
|
361
|
+
}
|
@@ -0,0 +1,72 @@
|
|
1
|
+
import { getItem, setItem } from "danx/src/helpers";
|
2
|
+
import { computed, ref, watch } from "vue";
|
3
|
+
|
4
|
+
export function useTableColumns(name, columns, options = { titleMinWidth: 120, titleMaxWidth: 200 }) {
|
5
|
+
const COLUMN_ORDER_KEY = `${name}-column-order`;
|
6
|
+
const VISIBLE_COLUMNS_KEY = `${name}-visible-columns`;
|
7
|
+
const TITLE_COLUMNS_KEY = `${name}-title-columns`;
|
8
|
+
const TITLE_WIDTH_KEY = `${name}-title-width`;
|
9
|
+
|
10
|
+
// The list that defines the order the columns should appear in
|
11
|
+
const columnOrder = ref(getItem(COLUMN_ORDER_KEY) || []);
|
12
|
+
|
13
|
+
// Manages visible columns on the table
|
14
|
+
const hiddenColumnNames = ref(getItem(VISIBLE_COLUMNS_KEY, columns.filter(c => c.category !== "General" || c.name === "status").map(c => c.name)));
|
15
|
+
|
16
|
+
// Title columns will have their name appear on the first column of the table as part of the records' title
|
17
|
+
const titleColumnNames = ref(getItem(TITLE_COLUMNS_KEY, []));
|
18
|
+
|
19
|
+
// The width of the title column
|
20
|
+
const titleWidth = ref(getItem(TITLE_WIDTH_KEY, options.titleMinWidth));
|
21
|
+
|
22
|
+
/**
|
23
|
+
* When the title column is resized, update the titleWidth
|
24
|
+
* @param val
|
25
|
+
*/
|
26
|
+
function onResizeTitleColumn(val) {
|
27
|
+
titleWidth.value = Math.max(Math.min(val.distance + val.startDropZoneSize, options.titleMaxWidth), options.titleMinWidth);
|
28
|
+
}
|
29
|
+
|
30
|
+
// Columns that should be locked to the left side of the table
|
31
|
+
const lockedColumns = computed(() => orderedColumns.value.slice(0, 1));
|
32
|
+
|
33
|
+
// The resolved list of columns in the order they should appear in
|
34
|
+
const orderedColumns = computed(() => [...columns].sort((a, b) => {
|
35
|
+
const aIndex = columnOrder.value.indexOf(a.name);
|
36
|
+
const bIndex = columnOrder.value.indexOf(b.name);
|
37
|
+
return aIndex === -1 ? 1 : bIndex === -1 ? -1 : aIndex - bIndex;
|
38
|
+
}));
|
39
|
+
|
40
|
+
// The ordered list of columns. The ordering of this list is editable and will be stored in localStorage
|
41
|
+
const sortableColumns = computed({
|
42
|
+
get() {
|
43
|
+
return orderedColumns.value.slice(1);
|
44
|
+
},
|
45
|
+
set(newColumns) {
|
46
|
+
columnOrder.value = [...lockedColumns.value.map(c => c.name), ...newColumns.map(c => c.name)];
|
47
|
+
setItem(COLUMN_ORDER_KEY, columnOrder.value);
|
48
|
+
}
|
49
|
+
});
|
50
|
+
|
51
|
+
// The list of columns that are visible. To edit the visible columns, edit the hiddenColumnNames list
|
52
|
+
const visibleColumns = computed(() => orderedColumns.value.filter(c => !hiddenColumnNames.value.includes(c.name)));
|
53
|
+
|
54
|
+
// The list of columns that should be included in the title of a row
|
55
|
+
const orderedTitleColumns = computed(() => orderedColumns.value.filter(c => titleColumnNames.value.includes(c.name)));
|
56
|
+
|
57
|
+
// Save changes to the list of hidden columns in localStorage
|
58
|
+
watch(() => hiddenColumnNames.value, () => setItem(VISIBLE_COLUMNS_KEY, hiddenColumnNames.value));
|
59
|
+
watch(() => titleColumnNames.value, () => setItem(TITLE_COLUMNS_KEY, titleColumnNames.value));
|
60
|
+
watch(() => titleWidth.value, () => setItem(TITLE_WIDTH_KEY, titleWidth.value));
|
61
|
+
|
62
|
+
return {
|
63
|
+
sortableColumns,
|
64
|
+
lockedColumns,
|
65
|
+
visibleColumns,
|
66
|
+
hiddenColumnNames,
|
67
|
+
titleColumnNames,
|
68
|
+
titleWidth,
|
69
|
+
orderedTitleColumns,
|
70
|
+
onResizeTitleColumn
|
71
|
+
};
|
72
|
+
}
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import { onMounted, watch } from "vue";
|
2
|
+
|
3
|
+
export function registerStickyScrolling(tableRef) {
|
4
|
+
onMounted(() => {
|
5
|
+
const scrollEl = tableRef.value.$el.getElementsByClassName("q-table__middle")[0];
|
6
|
+
scrollEl.addEventListener("scroll", onScroll);
|
7
|
+
|
8
|
+
function onScroll({ target }) {
|
9
|
+
// Add / remove scroll y class based on whether we're scrolling vertically
|
10
|
+
if (target.scrollTop > 0) {
|
11
|
+
scrollEl.classList.add("is-scrolling-y");
|
12
|
+
} else {
|
13
|
+
scrollEl.classList.remove("is-scrolling-y");
|
14
|
+
}
|
15
|
+
|
16
|
+
// Add / remove scroll x class based on whether we're scrolling horizontally
|
17
|
+
if (target.scrollLeft > 0) {
|
18
|
+
scrollEl.classList.add("is-scrolling-x");
|
19
|
+
} else {
|
20
|
+
scrollEl.classList.remove("is-scrolling-x");
|
21
|
+
}
|
22
|
+
}
|
23
|
+
});
|
24
|
+
}
|
25
|
+
|
26
|
+
export function mapSortBy(pagination, columns) {
|
27
|
+
if (!pagination.sortBy) return null;
|
28
|
+
|
29
|
+
const column = columns.find(c => c.name === pagination.sortBy);
|
30
|
+
return [
|
31
|
+
{
|
32
|
+
column: column.sortBy || column.name,
|
33
|
+
expression: column.sortByExpression || undefined,
|
34
|
+
order: pagination.descending ? "desc" : "asc"
|
35
|
+
}
|
36
|
+
];
|
37
|
+
}
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Wait for a ref to have a value and then resolve the promise
|
41
|
+
*
|
42
|
+
* @param ref
|
43
|
+
* @param value
|
44
|
+
* @returns {Promise<unknown>}
|
45
|
+
*/
|
46
|
+
export function waitForRef(ref, value) {
|
47
|
+
return new Promise((resolve) => {
|
48
|
+
watch(ref, (newValue) => {
|
49
|
+
if (newValue === value) {
|
50
|
+
resolve();
|
51
|
+
}
|
52
|
+
});
|
53
|
+
});
|
54
|
+
}
|
55
|
+
|
56
|
+
/**
|
57
|
+
* Returns the value of the URL parameter (if it is set)
|
58
|
+
* @param key
|
59
|
+
* @param url
|
60
|
+
*/
|
61
|
+
export function getUrlParam(key, url = undefined) {
|
62
|
+
const params = new URLSearchParams(url?.replace(/.*\?/, "") || window.location.search);
|
63
|
+
return params.get(key);
|
64
|
+
}
|
65
|
+
|
66
|
+
/**
|
67
|
+
* Returns the filter from the URL if it is set
|
68
|
+
* @param url
|
69
|
+
* @param allowedKeys
|
70
|
+
*/
|
71
|
+
export function getFilterFromUrl(url, allowedKeys = null) {
|
72
|
+
const filter = {};
|
73
|
+
const urlFilter = getUrlParam("filter", url);
|
74
|
+
if (urlFilter) {
|
75
|
+
const fields = JSON.parse(urlFilter);
|
76
|
+
Object.keys(fields).forEach((key) => {
|
77
|
+
if (!allowedKeys || allowedKeys.includes(key)) {
|
78
|
+
filter[key] = fields[key];
|
79
|
+
}
|
80
|
+
});
|
81
|
+
}
|
82
|
+
return filter;
|
83
|
+
}
|