quasar-ui-danx 0.3.11 → 0.3.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. package/dist/danx.es.js +5978 -5901
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +5 -5
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/package.json +1 -1
  7. package/src/components/ActionTable/ActionMenu.vue +7 -7
  8. package/src/components/ActionTable/ActionTable.vue +55 -31
  9. package/src/components/ActionTable/ActionTableColumn.vue +37 -15
  10. package/src/components/ActionTable/ActionTableHeaderColumn.vue +9 -9
  11. package/src/components/ActionTable/Columns/ColumnListItem.vue +33 -10
  12. package/src/components/ActionTable/Columns/ColumnSettingsDialog.vue +23 -19
  13. package/src/components/ActionTable/Columns/TitleColumnFormat.vue +5 -3
  14. package/src/components/ActionTable/Columns/VisibleColumnsToggleButtons.vue +24 -22
  15. package/src/components/ActionTable/EmptyTableState.vue +4 -2
  16. package/src/components/ActionTable/Filters/CollapsableFiltersSidebar.vue +17 -9
  17. package/src/components/ActionTable/Filters/FilterFieldItem.vue +7 -5
  18. package/src/components/ActionTable/Filters/FilterFieldList.vue +21 -21
  19. package/src/components/ActionTable/Filters/FilterListToggle.vue +13 -10
  20. package/src/components/ActionTable/Filters/FilterToolbarLayout.vue +14 -7
  21. package/src/components/ActionTable/Filters/FilterableField.vue +75 -70
  22. package/src/components/ActionTable/Form/Fields/BooleanField.vue +9 -9
  23. package/src/components/ActionTable/Form/Fields/ConfirmPasswordField.vue +7 -7
  24. package/src/components/ActionTable/Form/Fields/DateField.vue +4 -4
  25. package/src/components/ActionTable/Form/Fields/DateRangeField.vue +10 -12
  26. package/src/components/ActionTable/Form/Fields/DateTimeField.vue +13 -10
  27. package/src/components/ActionTable/Form/Fields/DateTimePicker.vue +36 -14
  28. package/src/components/ActionTable/Form/Fields/EditableDiv.vue +3 -3
  29. package/src/components/ActionTable/Form/Fields/FieldLabel.vue +2 -2
  30. package/src/components/ActionTable/Form/Fields/FileUploadButton.vue +23 -20
  31. package/src/components/ActionTable/Form/Fields/InlineDateTimeField.vue +10 -10
  32. package/src/components/ActionTable/Form/Fields/IntegerField.vue +6 -6
  33. package/src/components/ActionTable/Form/Fields/LabeledInput.vue +20 -17
  34. package/src/components/ActionTable/Form/Fields/MultiFileField.vue +35 -35
  35. package/src/components/ActionTable/Form/Fields/MultiKeywordField.vue +15 -15
  36. package/src/components/ActionTable/Form/Fields/NewPasswordField.vue +4 -4
  37. package/src/components/ActionTable/Form/Fields/NumberField.vue +11 -11
  38. package/src/components/ActionTable/Form/Fields/NumberRangeField.vue +11 -11
  39. package/src/components/ActionTable/Form/Fields/SelectDrawer.vue +38 -33
  40. package/src/components/ActionTable/Form/Fields/SelectField.vue +37 -35
  41. package/src/components/ActionTable/Form/Fields/SelectWithChildrenField.vue +19 -15
  42. package/src/components/ActionTable/Form/Fields/SingleFileField.vue +29 -28
  43. package/src/components/ActionTable/Form/Fields/TextField.vue +22 -22
  44. package/src/components/ActionTable/Form/RenderedForm.vue +5 -1
  45. package/src/components/ActionTable/Layouts/ActionTableLayout.vue +1 -1
  46. package/src/components/ActionTable/TableSummaryRow.vue +15 -15
  47. package/src/components/ActionTable/listControls.ts +378 -383
  48. package/src/components/AuditHistory/AuditHistoryItem.vue +12 -6
  49. package/src/components/AuditHistory/AuditHistoryItemValue.vue +6 -6
  50. package/src/components/DragAndDrop/HandleDraggable.vue +21 -21
  51. package/src/components/DragAndDrop/ListItemDraggable.vue +19 -15
  52. package/src/components/PanelsDrawer/PanelsDrawer.vue +18 -11
  53. package/src/components/PanelsDrawer/PanelsDrawerPanels.vue +9 -5
  54. package/src/components/PanelsDrawer/PanelsDrawerTabs.vue +18 -13
  55. package/src/components/Utility/Buttons/ExportButton.vue +5 -1
  56. package/src/components/Utility/Buttons/RefreshButton.vue +2 -2
  57. package/src/components/Utility/Controls/PreviousNextControls.vue +8 -8
  58. package/src/components/Utility/Dialogs/ConfirmDialog.vue +32 -29
  59. package/src/components/Utility/Dialogs/FullScreenDialog.vue +12 -9
  60. package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +30 -26
  61. package/src/components/Utility/Dialogs/InfoDialog.vue +25 -22
  62. package/src/components/Utility/Dialogs/InputDialog.vue +7 -7
  63. package/src/components/Utility/Files/FilePreview.vue +64 -47
  64. package/src/components/Utility/Files/SvgImg.vue +7 -4
  65. package/src/components/Utility/Formats/GpsCoordinatesFormat.vue +11 -3
  66. package/src/components/Utility/Layouts/CollapsableSidebar.vue +16 -16
  67. package/src/components/Utility/Layouts/ContentDrawer.vue +8 -8
  68. package/src/components/Utility/Popovers/InteractiveTooltip.vue +7 -7
  69. package/src/components/Utility/Popovers/PopoverMenu.vue +21 -21
  70. package/src/components/Utility/Tabs/BadgeTab.vue +9 -7
  71. package/src/components/Utility/Tools/ActionVnode.vue +5 -5
  72. package/src/components/Utility/Tools/RenderComponent.vue +11 -9
  73. package/src/components/Utility/Transitions/StaggeredListTransition.vue +7 -7
  74. package/src/styles/quasar-reset.scss +63 -19
  75. package/src/styles/themes/danx/action-table.scss +19 -0
  76. package/src/styles/themes/danx/index.scss +1 -0
@@ -2,392 +2,387 @@ import { computed, Ref, ref, ShallowRef, shallowRef, watch } from "vue";
2
2
  import { ActionTargetItem, getItem, setItem, waitForRef } from "../../helpers";
3
3
  import { getFilterFromUrl } from "./listHelpers";
4
4
 
5
- export interface ListActionsOptions {
6
- listRoute: Function;
7
- summaryRoute?: Function | null;
8
- filterFieldOptionsRoute?: Function | null;
9
- moreRoute?: Function | null;
10
- itemDetailsRoute?: Function | null;
11
- urlPattern?: RegExp | null;
12
- filterDefaults?: Record<string, any>;
13
- refreshFilters?: boolean;
5
+ export interface ListControlsRoutes {
6
+ list: (pager: object) => Promise<ActionTargetItem[]>;
7
+ details?: (item: object) => Promise<ActionTargetItem> | null;
8
+ summary?: (filter: object | null) => Promise<object> | null;
9
+ filterFieldOptions?: (filter: object | null) => Promise<object> | null;
10
+ more?: (pager: object) => Promise<ActionTargetItem[]> | null;
11
+ }
12
+
13
+ export interface ListControlsOptions {
14
+ routes: ListControlsRoutes;
15
+ urlPattern?: RegExp | null;
16
+ filterDefaults?: Record<string, any>;
17
+ refreshFilters?: boolean;
14
18
  }
15
19
 
16
20
  export interface PagedItems {
17
- data: any[] | undefined;
18
- meta: {
19
- total: number;
20
- last_page?: number;
21
- } | undefined;
21
+ data: ActionTargetItem[] | undefined;
22
+ meta: {
23
+ total: number;
24
+ last_page?: number;
25
+ } | undefined;
22
26
  }
23
27
 
24
- export function useListControls(name: string, {
25
- listRoute,
26
- summaryRoute = null,
27
- filterFieldOptionsRoute = null,
28
- moreRoute = null,
29
- itemDetailsRoute = null,
30
- refreshFilters = false,
31
- urlPattern = null,
32
- filterDefaults = {}
33
- }: ListActionsOptions) {
34
- let isInitialized = false;
35
- const PAGE_SETTINGS_KEY = `${name}-pagination-settings`;
36
- const pagedItems: Ref<PagedItems | null> = shallowRef(null);
37
- const filter: Ref<object | any> = ref({});
38
- const globalFilter = ref({});
39
- const showFilters = ref(false);
40
- const selectedRows = shallowRef([]);
41
- const isLoadingList = ref(false);
42
- const isLoadingSummary = ref(false);
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);
49
-
50
- // Filter fields are the field values available for the currently applied filter on Creative Groups
51
- // (ie: all states available under the current filter)
52
- const filterFieldOptions = ref({});
53
- const isLoadingFilters = ref(false);
54
-
55
- const filterActiveCount = computed(() => Object.keys(filter.value).filter(key => filter.value[key] !== undefined).length);
56
-
57
- const PAGING_DEFAULT = {
58
- __sort: null,
59
- sortBy: null,
60
- descending: false,
61
- page: 1,
62
- rowsNumber: 0,
63
- rowsPerPage: 50
64
- };
65
- const quasarPagination = ref(PAGING_DEFAULT);
66
-
67
- const pager = computed(() => ({
68
- perPage: quasarPagination.value.rowsPerPage,
69
- page: quasarPagination.value.page,
70
- filter: { ...filter.value, ...globalFilter.value },
71
- sort: quasarPagination.value.__sort || undefined
72
- }));
73
-
74
- // When any part of the filter changes, get the new list of creatives
75
- watch(pager, () => {
76
- saveSettings();
77
- loadList();
78
- });
79
- watch(filter, () => {
80
- saveSettings();
81
- loadSummary();
82
- });
83
- watch(selectedRows, loadSummary);
84
-
85
- if (refreshFilters) {
86
- watch(filter, loadFilterFieldOptions);
87
- }
88
-
89
- async function loadList() {
90
- if (!isInitialized) return;
91
- isLoadingList.value = true;
92
- setPagedItems(await listRoute(pager.value));
93
- isLoadingList.value = false;
94
- }
95
-
96
- async function loadSummary() {
97
- if (!summaryRoute || !isInitialized) return;
98
-
99
- isLoadingSummary.value = true;
100
- const summaryFilter = { id: null, ...filter.value, ...globalFilter.value };
101
- if (selectedRows.value.length) {
102
- summaryFilter.id = selectedRows.value.map((row: { id: string }) => row.id);
103
- }
104
- summary.value = await summaryRoute(summaryFilter);
105
- isLoadingSummary.value = false;
106
- }
107
-
108
- /**
109
- * Loads the filter field options for the current filter.
110
- *
111
- * @returns {Promise<void>}
112
- */
113
- async function loadFilterFieldOptions() {
114
- if (!filterFieldOptionsRoute || !isInitialized) return;
115
- isLoadingFilters.value = true;
116
- filterFieldOptions.value = await filterFieldOptionsRoute(filter.value);
117
- isLoadingFilters.value = false;
118
- }
119
-
120
- /**
121
- * Watches for a filter URL parameter and applies the filter if it is set.
122
- */
123
- function applyFilterFromUrl(url: string, filterFields = null) {
124
- if (urlPattern && url.match(urlPattern)) {
125
- // A flat list of valid filterable field names
126
- const validFilterKeys = filterFields?.value?.map(group => group.fields.map(field => field.name)).flat();
127
-
128
- const urlFilter = getFilterFromUrl(url, validFilterKeys);
129
-
130
- if (Object.keys(urlFilter).length > 0) {
131
- filter.value = urlFilter;
132
-
133
- // Override whatever is in local storage with this new filter
134
- updateSettings("filter", filter.value);
135
- }
136
- }
137
- }
138
-
139
- // Set the reactive pager to map from the Laravel pagination to Quasar pagination
140
- // and automatically update the list of ads
141
- function setPagedItems(items: any[] | PagedItems) {
142
- let data, meta;
143
-
144
- if (Array.isArray(items)) {
145
- data = items;
146
- meta = { total: items.length };
147
-
148
- } else {
149
- data = items.data;
150
- meta = items.meta;
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 };
167
- }
168
-
169
- /**
170
- * Resets the filter and pagination settings to their defaults.
171
- */
172
- function resetPaging() {
173
- quasarPagination.value = PAGING_DEFAULT;
174
- }
175
-
176
- /**
177
- * Updates a row in the paged items list with the new item data. Uses the item's id to find the row.
178
- *
179
- * @param updatedItem
180
- */
181
- function setItemInList(updatedItem: any) {
182
- const data = pagedItems.value?.data?.map(item => (item.id === updatedItem.id && (item.updated_at === null || item.updated_at <= updatedItem.updated_at)) ? updatedItem : item);
183
- setPagedItems({
184
- data,
185
- meta: { total: pagedItems.value.meta.total }
186
- });
187
-
188
- // Update the active item as well if it is set
189
- if (activeItem.value?.id === updatedItem.id) {
190
- activeItem.value = { ...activeItem.value, ...updatedItem };
191
- }
192
- }
193
-
194
- /**
195
- * Loads more items into the list.
196
- */
197
- async function loadMore(index: number, perPage = undefined) {
198
- if (!moreRoute) return;
199
-
200
- const newItems = await moreRoute({
201
- page: index + 1,
202
- perPage,
203
- filter: { ...filter.value, ...globalFilter.value }
204
- });
205
-
206
- if (newItems && newItems.length > 0) {
207
- setPagedItems({
208
- data: [...pagedItems.value.data, ...newItems],
209
- meta: { total: pagedItems.value.meta.total }
210
- });
211
- return true;
212
- }
213
-
214
- return false;
215
- }
216
-
217
- /**
218
- * Refreshes the list, summary, and filter field options.
219
- */
220
- async function refreshAll() {
221
- return Promise.all([loadList(), loadSummary(), loadFilterFieldOptions(), getActiveItemDetails()]);
222
- }
223
-
224
- /**
225
- * Updates the settings in local storage
226
- */
227
- function updateSettings(key: string, value: any) {
228
- const settings = getItem(PAGE_SETTINGS_KEY) || {};
229
- settings[key] = value;
230
- setItem(PAGE_SETTINGS_KEY, settings);
231
- }
232
-
233
- /**
234
- * Loads the filter and pagination settings from local storage.
235
- */
236
- function loadSettings() {
237
- // Only load settings when the class is fully initialized
238
- if (!isInitialized) return;
239
-
240
- const settings = getItem(PAGE_SETTINGS_KEY);
241
-
242
- // Load the filter settings from local storage
243
- if (settings) {
244
- filter.value = { ...settings.filter, ...filter.value };
245
- quasarPagination.value = settings.quasarPagination;
246
- } else {
247
- // If no local storage settings, apply the default filters
248
- filter.value = { ...filterDefaults, ...filter.value };
249
- }
250
-
251
- setTimeout(() => {
252
- if (!isLoadingList.value) {
253
- loadList();
254
- }
255
-
256
- if (!isLoadingSummary.value) {
257
- loadSummary();
258
- }
259
-
260
- if (!isLoadingFilters.value) {
261
- loadFilterFieldOptions();
262
- }
263
- }, 1);
264
- }
265
-
266
- /**
267
- * Saves the current filter and pagination settings to local storage.
268
- */
269
- async function saveSettings() {
270
- const settings = {
271
- filter: filter.value,
272
- quasarPagination: { ...quasarPagination.value, page: 1 }
273
- };
274
- // save in local storage
275
- setItem(PAGE_SETTINGS_KEY, settings);
276
- }
277
-
278
- /**
279
- * Gets the additional details for the currently active item.
280
- * (ie: data that is not normally loaded in the list because it is not needed for the list view)
281
- * @returns {Promise<void>}
282
- */
283
- async function getActiveItemDetails() {
284
- if (!activeItem.value || !itemDetailsRoute) return;
285
-
286
- const result = await itemDetailsRoute(activeItem.value);
287
-
288
- // Only set the ad details if we are the response for the currently loaded item
289
- // NOTE: race conditions might allow the finished loading item to be different to the currently
290
- // requested item
291
- if (result?.id === activeItem.value?.id) {
292
- const loadedItem = pagedItems.value?.data.find((i: { id: string }) => i.id === result.id);
293
- activeItem.value = { ...result, isSaving: loadedItem.isSaving || ref(false) };
294
- }
295
- }
296
-
297
- // Whenever the active item changes, fill the additional item details
298
- // (ie: tasks, verifications, creatives, etc.)
299
- if (itemDetailsRoute) {
300
- watch(() => activeItem.value, async (newItem, oldItem) => {
301
- if (newItem && oldItem?.id !== newItem.id) {
302
- await getActiveItemDetails();
303
- }
304
- });
305
- }
306
-
307
- /**
308
- * Opens the item's form with the given item and tab
309
- *
310
- * @param item
311
- * @param panel
312
- */
313
- function activatePanel(item, panel) {
314
- activeItem.value = item;
315
- activePanel.value = panel;
316
- }
317
-
318
- /**
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
320
- * selected item. If the next item is on a previous or next page, it will load the page first then select the item
321
- */
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
-
328
- let nextIndex = index + offset;
329
-
330
- // Load the previous page if the offset is before index 0
331
- if (nextIndex < 0) {
332
- if (quasarPagination.value.page > 1) {
333
- quasarPagination.value = { ...quasarPagination.value, page: quasarPagination.value.page - 1 };
334
- await waitForRef(isLoadingList, false);
335
- nextIndex = pagedItems.value.data.length - 1;
336
- } else {
337
- // There are no more previous pages
338
- return;
339
- }
340
- }
341
-
342
- // Load the next page if the offset is past the last index
343
- if (nextIndex >= pagedItems.value.data.length) {
344
- if (quasarPagination.value.page < pagedItems.value.meta.last_page) {
345
- quasarPagination.value = { ...quasarPagination.value, page: quasarPagination.value.page + 1 };
346
- await waitForRef(isLoadingList, false);
347
- nextIndex = 0;
348
- } else {
349
- // There are no more next pages
350
- return;
351
- }
352
- }
353
-
354
- activeItem.value = pagedItems.value?.data[nextIndex];
355
- }
356
-
357
- // Initialize the list actions and load settings, lists, summaries, filter fields, etc.
358
- function initialize() {
359
- isInitialized = true;
360
- loadSettings();
361
- }
362
-
363
- return {
364
- // State
365
- pagedItems,
366
- filter,
367
- globalFilter,
368
- filterActiveCount,
369
- showFilters,
370
- summary,
371
- filterFieldOptions,
372
- selectedRows,
373
- isLoadingList,
374
- isLoadingFilters,
375
- isLoadingSummary,
376
- pager,
377
- quasarPagination,
378
- activeItem,
379
- activePanel,
380
-
381
- // Actions
382
- initialize,
383
- loadSummary,
384
- resetPaging,
385
- loadList,
386
- loadMore,
387
- refreshAll,
388
- getNextItem,
389
- activatePanel,
390
- applyFilterFromUrl,
391
- setItemInList
392
- };
28
+ export function useListControls(name: string, options: ListControlsOptions) {
29
+ let isInitialized = false;
30
+ const PAGE_SETTINGS_KEY = `${name}-pagination-settings`;
31
+ const pagedItems: Ref<PagedItems | null> = shallowRef(null);
32
+ const filter: Ref<object | any> = ref({});
33
+ const globalFilter = ref({});
34
+ const showFilters = ref(false);
35
+ const selectedRows = shallowRef([]);
36
+ const isLoadingList = ref(false);
37
+ const isLoadingSummary = ref(false);
38
+ const summary: ShallowRef<object | null> = shallowRef(null);
39
+
40
+ // The active ad for viewing / editing
41
+ const activeItem: ShallowRef<ActionTargetItem | null> = shallowRef(null);
42
+ // Controls the active panel (ie: tab) if rendering a panels drawer or similar
43
+ const activePanel = shallowRef(null);
44
+
45
+ // Filter fields are the field values available for the currently applied filter on Creative Groups
46
+ // (ie: all states available under the current filter)
47
+ const filterFieldOptions: Ref<object> = ref({});
48
+ const isLoadingFilters = ref(false);
49
+
50
+ const filterActiveCount = computed(() => Object.keys(filter.value).filter(key => filter.value[key] !== undefined).length);
51
+
52
+ const PAGING_DEFAULT = {
53
+ __sort: null,
54
+ sortBy: null,
55
+ descending: false,
56
+ page: 1,
57
+ rowsNumber: 0,
58
+ rowsPerPage: 50
59
+ };
60
+ const quasarPagination = ref(PAGING_DEFAULT);
61
+
62
+ const pager = computed(() => ({
63
+ perPage: quasarPagination.value.rowsPerPage,
64
+ page: quasarPagination.value.page,
65
+ filter: { ...filter.value, ...globalFilter.value },
66
+ sort: quasarPagination.value.__sort || undefined
67
+ }));
68
+
69
+ // When any part of the filter changes, get the new list of creatives
70
+ watch(pager, () => {
71
+ saveSettings();
72
+ loadList();
73
+ });
74
+ watch(filter, () => {
75
+ saveSettings();
76
+ loadSummary();
77
+ });
78
+ watch(selectedRows, loadSummary);
79
+
80
+ if (options.refreshFilters) {
81
+ watch(filter, loadFilterFieldOptions);
82
+ }
83
+
84
+ async function loadList() {
85
+ if (!isInitialized) return;
86
+ isLoadingList.value = true;
87
+ setPagedItems(await options.routes.list(pager.value));
88
+ isLoadingList.value = false;
89
+ }
90
+
91
+ async function loadSummary() {
92
+ if (!options.routes.summary || !isInitialized) return;
93
+
94
+ isLoadingSummary.value = true;
95
+ const summaryFilter = { id: null, ...filter.value, ...globalFilter.value };
96
+ if (selectedRows.value.length) {
97
+ summaryFilter.id = selectedRows.value.map((row: { id: string }) => row.id);
98
+ }
99
+ summary.value = await options.routes.summary(summaryFilter);
100
+ isLoadingSummary.value = false;
101
+ }
102
+
103
+ /**
104
+ * Loads the filter field options for the current filter.
105
+ *
106
+ * @returns {Promise<void>}
107
+ */
108
+ async function loadFilterFieldOptions() {
109
+ if (!options.routes.filterFieldOptions || !isInitialized) return;
110
+ isLoadingFilters.value = true;
111
+ filterFieldOptions.value = await options.routes.filterFieldOptions(filter.value) || {};
112
+ isLoadingFilters.value = false;
113
+ }
114
+
115
+ /**
116
+ * Watches for a filter URL parameter and applies the filter if it is set.
117
+ */
118
+ function applyFilterFromUrl(url: string, filterFields: Ref<object[]> | null = null) {
119
+ if (options.urlPattern && url.match(options.urlPattern)) {
120
+ // A flat list of valid filterable field names
121
+ const validFilterKeys = filterFields?.value?.map(group => group.fields.map(field => field.name)).flat();
122
+
123
+ const urlFilter = getFilterFromUrl(url, validFilterKeys);
124
+
125
+ if (Object.keys(urlFilter).length > 0) {
126
+ filter.value = urlFilter;
127
+
128
+ // Override whatever is in local storage with this new filter
129
+ updateSettings("filter", filter.value);
130
+ }
131
+ }
132
+ }
133
+
134
+ // Set the reactive pager to map from the Laravel pagination to Quasar pagination
135
+ // and automatically update the list of ads
136
+ function setPagedItems(items: object[] | PagedItems) {
137
+ let data, meta;
138
+
139
+ if (Array.isArray(items)) {
140
+ data = items;
141
+ meta = { total: items.length };
142
+
143
+ } else {
144
+ data = items.data;
145
+ meta = items.meta;
146
+ }
147
+
148
+ // Update the Quasar pagination rows number if it is different from the total
149
+ if (meta && meta.total !== quasarPagination.value.rowsNumber) {
150
+ quasarPagination.value.rowsNumber = meta.total;
151
+ }
152
+
153
+ // Add a reactive isSaving property to each item (for performance reasons in checking saving state)
154
+ data = data.map((item: object) => {
155
+ // We want to keep the isSaving state if it is already set, as optimizations prevent reloading the
156
+ // components, and therefore reactivity is not responding to the new isSaving state
157
+ const oldItem = pagedItems.value?.data?.find(i => i.id === item.id);
158
+ return { ...item, isSaving: oldItem?.isSaving || ref(false) };
159
+ });
160
+
161
+ pagedItems.value = { data, meta };
162
+ }
163
+
164
+ /**
165
+ * Resets the filter and pagination settings to their defaults.
166
+ */
167
+ function resetPaging() {
168
+ quasarPagination.value = PAGING_DEFAULT;
169
+ }
170
+
171
+ /**
172
+ * Updates a row in the paged items list with the new item data. Uses the item's id to find the row.
173
+ *
174
+ * @param updatedItem
175
+ */
176
+ function setItemInList(updatedItem: ActionTargetItem) {
177
+ const data = pagedItems.value?.data?.map(item => (item.id === updatedItem.id && (item.updated_at === null || item.updated_at <= updatedItem.updated_at)) ? updatedItem : item);
178
+ setPagedItems({
179
+ data,
180
+ meta: { total: pagedItems.value?.meta?.total || 0 }
181
+ });
182
+
183
+ // Update the active item as well if it is set
184
+ if (activeItem.value?.id === updatedItem.id) {
185
+ activeItem.value = { ...activeItem.value, ...updatedItem };
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Loads more items into the list.
191
+ */
192
+ async function loadMore(index: number, perPage = undefined) {
193
+ if (!options.routes.more) return;
194
+
195
+ const newItems = await options.routes.more({
196
+ page: index + 1,
197
+ perPage,
198
+ filter: { ...filter.value, ...globalFilter.value }
199
+ });
200
+
201
+ if (newItems && newItems.length > 0) {
202
+ setPagedItems({
203
+ data: [...(pagedItems.value?.data || []), ...newItems],
204
+ meta: { total: pagedItems.value?.meta?.total || 0 }
205
+ });
206
+ return true;
207
+ }
208
+
209
+ return false;
210
+ }
211
+
212
+ /**
213
+ * Refreshes the list, summary, and filter field options.
214
+ */
215
+ async function refreshAll() {
216
+ return Promise.all([loadList(), loadSummary(), loadFilterFieldOptions(), getActiveItemDetails()]);
217
+ }
218
+
219
+ /**
220
+ * Updates the settings in local storage
221
+ */
222
+ function updateSettings(key: string, value: any) {
223
+ const settings = getItem(PAGE_SETTINGS_KEY) || {};
224
+ settings[key] = value;
225
+ setItem(PAGE_SETTINGS_KEY, settings);
226
+ }
227
+
228
+ /**
229
+ * Loads the filter and pagination settings from local storage.
230
+ */
231
+ function loadSettings() {
232
+ // Only load settings when the class is fully initialized
233
+ if (!isInitialized) return;
234
+
235
+ const settings = getItem(PAGE_SETTINGS_KEY);
236
+
237
+ // Load the filter settings from local storage
238
+ if (settings) {
239
+ filter.value = { ...settings.filter, ...filter.value };
240
+ quasarPagination.value = settings.quasarPagination;
241
+ } else {
242
+ // If no local storage settings, apply the default filters
243
+ filter.value = { ...options.filterDefaults, ...filter.value };
244
+ }
245
+
246
+ setTimeout(() => {
247
+ if (!isLoadingList.value) {
248
+ loadList();
249
+ }
250
+
251
+ if (!isLoadingSummary.value) {
252
+ loadSummary();
253
+ }
254
+
255
+ if (!isLoadingFilters.value) {
256
+ loadFilterFieldOptions();
257
+ }
258
+ }, 1);
259
+ }
260
+
261
+ /**
262
+ * Saves the current filter and pagination settings to local storage.
263
+ */
264
+ async function saveSettings() {
265
+ const settings = {
266
+ filter: filter.value,
267
+ quasarPagination: { ...quasarPagination.value, page: 1 }
268
+ };
269
+ // save in local storage
270
+ setItem(PAGE_SETTINGS_KEY, settings);
271
+ }
272
+
273
+ /**
274
+ * Gets the additional details for the currently active item.
275
+ * (ie: data that is not normally loaded in the list because it is not needed for the list view)
276
+ * @returns {Promise<void>}
277
+ */
278
+ async function getActiveItemDetails() {
279
+ if (!activeItem.value || !options.routes.details) return;
280
+
281
+ const result = await options.routes.details(activeItem.value);
282
+
283
+ // Only set the ad details if we are the response for the currently loaded item
284
+ // NOTE: race conditions might allow the finished loading item to be different to the currently
285
+ // requested item
286
+ if (result?.id === activeItem.value?.id) {
287
+ const loadedItem = pagedItems.value?.data?.find((i: ActionTargetItem) => i.id === result.id);
288
+ activeItem.value = { ...result, isSaving: loadedItem?.isSaving || ref(false) };
289
+ }
290
+ }
291
+
292
+ // Whenever the active item changes, fill the additional item details
293
+ // (ie: tasks, verifications, creatives, etc.)
294
+ if (options.routes.details) {
295
+ watch(() => activeItem.value, async (newItem, oldItem) => {
296
+ if (newItem && oldItem?.id !== newItem.id) {
297
+ await getActiveItemDetails();
298
+ }
299
+ });
300
+ }
301
+
302
+ /**
303
+ * Opens the item's form with the given item and tab
304
+ *
305
+ * @param item
306
+ * @param panel
307
+ */
308
+ function activatePanel(item: ActionTargetItem, panel) {
309
+ activeItem.value = item;
310
+ activePanel.value = panel;
311
+ }
312
+
313
+ /**
314
+ * Gets the next item in the list at the given offset (ie: 1 or -1) from the current position in the list of the
315
+ * selected item. If the next item is on a previous or next page, it will load the page first then select the item
316
+ */
317
+ async function getNextItem(offset: number) {
318
+ if (!pagedItems.value?.data) return;
319
+
320
+ const index = pagedItems.value.data.findIndex((i: ActionTargetItem) => i.id === activeItem.value?.id);
321
+ if (index === undefined || index === null) return;
322
+
323
+ let nextIndex = index + offset;
324
+
325
+ // Load the previous page if the offset is before index 0
326
+ if (nextIndex < 0) {
327
+ if (quasarPagination.value.page > 1) {
328
+ quasarPagination.value = { ...quasarPagination.value, page: quasarPagination.value.page - 1 };
329
+ await waitForRef(isLoadingList, false);
330
+ nextIndex = pagedItems.value.data.length - 1;
331
+ } else {
332
+ // There are no more previous pages
333
+ return;
334
+ }
335
+ }
336
+
337
+ // Load the next page if the offset is past the last index
338
+ if (nextIndex >= pagedItems.value.data.length) {
339
+ if (quasarPagination.value.page < (pagedItems.value?.meta?.last_page || 1)) {
340
+ quasarPagination.value = { ...quasarPagination.value, page: quasarPagination.value.page + 1 };
341
+ await waitForRef(isLoadingList, false);
342
+ nextIndex = 0;
343
+ } else {
344
+ // There are no more next pages
345
+ return;
346
+ }
347
+ }
348
+
349
+ activeItem.value = pagedItems.value?.data[nextIndex];
350
+ }
351
+
352
+ // Initialize the list actions and load settings, lists, summaries, filter fields, etc.
353
+ function initialize() {
354
+ isInitialized = true;
355
+ loadSettings();
356
+ }
357
+
358
+ return {
359
+ // State
360
+ pagedItems,
361
+ filter,
362
+ globalFilter,
363
+ filterActiveCount,
364
+ showFilters,
365
+ summary,
366
+ filterFieldOptions,
367
+ selectedRows,
368
+ isLoadingList,
369
+ isLoadingFilters,
370
+ isLoadingSummary,
371
+ pager,
372
+ quasarPagination,
373
+ activeItem,
374
+ activePanel,
375
+
376
+ // Actions
377
+ initialize,
378
+ loadSummary,
379
+ resetPaging,
380
+ loadList,
381
+ loadMore,
382
+ refreshAll,
383
+ getNextItem,
384
+ activatePanel,
385
+ applyFilterFromUrl,
386
+ setItemInList
387
+ };
393
388
  }