quasar-ui-danx 0.3.21 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. package/.eslintrc.cjs +32 -30
  2. package/danx-local.sh +1 -1
  3. package/dist/danx.es.js +7490 -7520
  4. package/dist/danx.es.js.map +1 -1
  5. package/dist/danx.umd.js +5 -5
  6. package/dist/danx.umd.js.map +1 -1
  7. package/dist/style.css +1 -1
  8. package/package.json +1 -1
  9. package/src/components/ActionTable/ActionMenu.vue +1 -1
  10. package/src/components/ActionTable/ActionTable.vue +64 -45
  11. package/src/components/ActionTable/{ActionTableColumn.vue → Columns/ActionTableColumn.vue} +4 -3
  12. package/src/components/ActionTable/{ActionTableHeaderColumn.vue → Columns/ActionTableHeaderColumn.vue} +2 -2
  13. package/src/components/ActionTable/Columns/index.ts +2 -0
  14. package/src/components/ActionTable/Filters/CollapsableFiltersSidebar.vue +22 -21
  15. package/src/components/ActionTable/Form/Fields/DateRangeField.vue +3 -5
  16. package/src/components/ActionTable/Form/Fields/FileUploadButton.vue +33 -35
  17. package/src/components/ActionTable/Form/Fields/TextField.vue +36 -36
  18. package/src/components/ActionTable/Form/RenderedForm.vue +137 -112
  19. package/src/components/ActionTable/Form/form.d.ts +31 -0
  20. package/src/components/ActionTable/Layouts/ActionTableLayout.vue +88 -4
  21. package/src/components/ActionTable/TableSummaryRow.vue +4 -4
  22. package/src/components/ActionTable/Toolbars/ActionToolbar.vue +46 -0
  23. package/src/components/ActionTable/Toolbars/index.ts +1 -0
  24. package/src/components/ActionTable/index.ts +1 -2
  25. package/src/components/ActionTable/listControls.ts +512 -385
  26. package/src/components/ActionTable/listHelpers.ts +46 -44
  27. package/src/components/PanelsDrawer/PanelsDrawer.vue +37 -26
  28. package/src/components/PanelsDrawer/PanelsDrawerPanels.vue +1 -1
  29. package/src/components/PanelsDrawer/PanelsDrawerTabs.vue +1 -6
  30. package/src/components/Utility/Buttons/ExportButton.vue +1 -1
  31. package/src/components/Utility/Buttons/RefreshButton.vue +5 -5
  32. package/src/components/Utility/Controls/PreviousNextControls.vue +4 -4
  33. package/src/components/Utility/Layouts/CollapsableSidebar.vue +2 -8
  34. package/src/components/Utility/Popovers/PopoverMenu.vue +3 -3
  35. package/src/helpers/actions.ts +197 -187
  36. package/src/styles/general.scss +12 -11
  37. package/src/styles/quasar-reset.scss +59 -11
  38. package/src/styles/themes/danx/action-table.scss +19 -0
  39. package/src/styles/themes/danx/buttons.scss +13 -0
  40. package/src/styles/themes/danx/forms.scss +5 -0
  41. package/src/styles/themes/danx/index.scss +3 -0
  42. package/src/styles/themes/danx/panels.scss +19 -0
  43. package/src/styles/themes/danx/sidebar.scss +3 -0
  44. package/src/styles/themes/danx/toolbar.scss +3 -0
  45. package/types/index.d.ts +1 -0
  46. package/src/styles/actions.scss +0 -10
@@ -1,393 +1,520 @@
1
- import { computed, Ref, ref, ShallowRef, shallowRef, watch } from "vue";
2
- import { ActionTargetItem, getItem, setItem, waitForRef } from "../../helpers";
1
+ import { computed, ComputedRef, Ref, ref, ShallowRef, shallowRef, VNode, watch } from "vue";
2
+ import { ActionTargetItem, AnyObject, 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 ActionController {
6
+ name: string;
7
+ label: string;
8
+ pagedItems: Ref<PagedItems | null>;
9
+ activeFilter: Ref<ListControlsFilter>;
10
+ globalFilter: Ref<ListControlsFilter>;
11
+ filterActiveCount: ComputedRef<number>;
12
+ showFilters: Ref<boolean>;
13
+ summary: ShallowRef<object | null>;
14
+ filterFieldOptions: Ref<AnyObject>;
15
+ selectedRows: ShallowRef<ActionTargetItem[]>;
16
+ isLoadingList: Ref<boolean>;
17
+ isLoadingFilters: Ref<boolean>;
18
+ isLoadingSummary: Ref<boolean>;
19
+ pager: ComputedRef<{
20
+ perPage: number;
21
+ page: number;
22
+ filter: ListControlsFilter;
23
+ sort: object[] | undefined;
24
+ }>;
25
+ pagination: ShallowRef<ListControlsPagination>;
26
+ activeItem: ShallowRef<ActionTargetItem | null>;
27
+ activePanel: ShallowRef<string | null>;
28
+
29
+ // Actions
30
+ initialize: () => void;
31
+ loadSummary: () => Promise<void>;
32
+ resetPaging: () => void;
33
+ setPagination: (updated: ListControlsPagination) => void;
34
+ setSelectedRows: (selection: ActionTargetItem[]) => void;
35
+ loadList: () => Promise<void>;
36
+ loadMore: (index: number, perPage?: number) => Promise<boolean>;
37
+ refreshAll: () => Promise<void[]>;
38
+ exportList: () => Promise<void>;
39
+ setActiveItem: (item: ActionTargetItem | null) => void;
40
+ getNextItem: (offset: number) => Promise<void>;
41
+ activatePanel: (item: ActionTargetItem | null, panel: string | null) => void;
42
+ setActiveFilter: (filter: ListControlsFilter) => void;
43
+ applyFilterFromUrl: (url: string, filterFields: Ref<FilterGroup[]> | null) => void;
44
+ setItemInList: (updatedItem: ActionTargetItem) => void;
45
+ }
46
+
47
+ export interface LabelValueItem {
48
+ label: string;
49
+ value: string | number | boolean;
50
+ }
51
+
52
+ export interface FilterField {
53
+ name: string;
54
+ label: string;
55
+ type: string;
56
+ options?: string[] | number[] | LabelValueItem[];
57
+ inline?: boolean;
58
+ }
59
+
60
+ export interface FilterGroup {
61
+ name?: string;
62
+ flat?: boolean;
63
+ fields: FilterField[];
64
+ }
65
+
66
+ export interface ActionPanel {
67
+ name: string;
68
+ label: string;
69
+ category?: string;
70
+ enabled: boolean | (() => boolean);
71
+ tabVnode: () => VNode;
72
+ vnode: () => VNode;
73
+ }
74
+
75
+ export interface ListControlsFilter {
76
+ [key: string]: object | object[] | null | undefined | string | number | boolean;
77
+ }
78
+
79
+ export interface ListControlsRoutes {
80
+ list: (pager: object) => Promise<ActionTargetItem[]>;
81
+ details?: (item: object) => Promise<ActionTargetItem> | null;
82
+ summary?: (filter: object | null) => Promise<object> | null;
83
+ filterFieldOptions?: (filter: object | null) => Promise<object> | null;
84
+ more?: (pager: object) => Promise<ActionTargetItem[]> | null;
85
+ export: (filter: object) => Promise<void>;
86
+ }
87
+
88
+ export interface ListControlsOptions {
89
+ label?: string,
90
+ routes: ListControlsRoutes;
91
+ urlPattern?: RegExp | null;
92
+ filterDefaults?: Record<string, object>;
93
+ refreshFilters?: boolean;
94
+ }
95
+
96
+ export interface ListControlsPagination {
97
+ __sort: object[] | null;
98
+ sortBy: string | null;
99
+ descending: boolean;
100
+ page: number;
101
+ rowsNumber: number;
102
+ rowsPerPage: number;
14
103
  }
15
104
 
16
105
  export interface PagedItems {
17
- data: any[] | undefined;
18
- meta: {
19
- total: number;
20
- last_page?: number;
21
- } | undefined;
106
+ data: ActionTargetItem[] | undefined;
107
+ meta: {
108
+ total: number;
109
+ last_page?: number;
110
+ } | undefined;
22
111
  }
23
112
 
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
- };
113
+ export function useListControls(name: string, options: ListControlsOptions) {
114
+ let isInitialized = false;
115
+ const PAGE_SETTINGS_KEY = `dx-${name}-pager`;
116
+ const pagedItems: Ref<PagedItems | null> = shallowRef(null);
117
+ const activeFilter: Ref<ListControlsFilter> = ref({});
118
+ const globalFilter = ref({});
119
+ const showFilters = ref(false);
120
+ const selectedRows: ShallowRef<ActionTargetItem[]> = shallowRef([]);
121
+ const isLoadingList = ref(false);
122
+ const isLoadingSummary = ref(false);
123
+ const summary: ShallowRef<object | null> = shallowRef(null);
124
+
125
+ // The active ad for viewing / editing
126
+ const activeItem: ShallowRef<ActionTargetItem | null> = shallowRef(null);
127
+ // Controls the active panel (ie: tab) if rendering a panels drawer or similar
128
+ const activePanel: ShallowRef<string> = shallowRef("");
129
+
130
+ // Filter fields are the field values available for the currently applied filter on Creative Groups
131
+ // (ie: all states available under the current filter)
132
+ const filterFieldOptions: Ref<AnyObject> = ref({});
133
+ const isLoadingFilters = ref(false);
134
+
135
+ const filterActiveCount = computed(() => Object.keys(activeFilter.value).filter(key => activeFilter.value[key] !== undefined).length);
136
+
137
+ const PAGING_DEFAULT = {
138
+ __sort: null,
139
+ sortBy: null,
140
+ descending: false,
141
+ page: 0,
142
+ rowsNumber: 0,
143
+ rowsPerPage: 50
144
+ };
145
+ const pagination = shallowRef(PAGING_DEFAULT);
146
+
147
+ const pager = computed(() => ({
148
+ perPage: pagination.value.rowsPerPage,
149
+ page: pagination.value.page,
150
+ filter: { ...activeFilter.value, ...globalFilter.value },
151
+ sort: pagination.value.__sort || undefined
152
+ }));
153
+
154
+ // When any part of the filter changes, get the new list of creatives
155
+ watch(pager, () => {
156
+ saveSettings();
157
+ loadList();
158
+ });
159
+ watch(activeFilter, () => {
160
+ saveSettings();
161
+ loadSummary();
162
+ });
163
+ watch(selectedRows, loadSummary);
164
+
165
+ if (options.refreshFilters) {
166
+ watch(activeFilter, loadFilterFieldOptions);
167
+ }
168
+
169
+ async function loadList() {
170
+ if (!isInitialized) return;
171
+ isLoadingList.value = true;
172
+ setPagedItems(await options.routes.list(pager.value));
173
+ isLoadingList.value = false;
174
+ }
175
+
176
+ async function loadSummary() {
177
+ if (!options.routes.summary || !isInitialized) return;
178
+
179
+ isLoadingSummary.value = true;
180
+ const summaryFilter: ListControlsFilter = { id: null, ...activeFilter.value, ...globalFilter.value };
181
+ if (selectedRows.value.length) {
182
+ summaryFilter.id = selectedRows.value.map((row) => row.id);
183
+ }
184
+ summary.value = await options.routes.summary(summaryFilter);
185
+ isLoadingSummary.value = false;
186
+ }
187
+
188
+ /**
189
+ * Gets the field options for the given field name.
190
+ */
191
+ function getFieldOptions(field: string) {
192
+ return filterFieldOptions.value[field] || [];
193
+ }
194
+
195
+ /**
196
+ * Loads the filter field options for the current filter.
197
+ *
198
+ * @returns {Promise<void>}
199
+ */
200
+ async function loadFilterFieldOptions() {
201
+ if (!options.routes.filterFieldOptions || !isInitialized) return;
202
+ isLoadingFilters.value = true;
203
+ filterFieldOptions.value = await options.routes.filterFieldOptions(activeFilter.value) || {};
204
+ isLoadingFilters.value = false;
205
+ }
206
+
207
+ /**
208
+ * Watches for a filter URL parameter and applies the filter if it is set.
209
+ */
210
+ function applyFilterFromUrl(url: string, filterFields: Ref<FilterGroup[]> | null = null) {
211
+ if (options.urlPattern && url.match(options.urlPattern)) {
212
+ // A flat list of valid filterable field names
213
+ const validFilterKeys = filterFields?.value?.map(group => group.fields.map(field => field.name)).flat();
214
+
215
+ const urlFilter = getFilterFromUrl(url, validFilterKeys);
216
+
217
+ if (Object.keys(urlFilter).length > 0) {
218
+ activeFilter.value = urlFilter;
219
+
220
+ // Override whatever is in local storage with this new filter
221
+ updateSettings("filter", activeFilter.value);
222
+ }
223
+ }
224
+ }
225
+
226
+ // Set the reactive pager to map from the Laravel pagination to Quasar pagination
227
+ // and automatically update the list of ads
228
+ function setPagedItems(items: ActionTargetItem[] | PagedItems) {
229
+ let data: ActionTargetItem[] = [], meta;
230
+
231
+ if (Array.isArray(items)) {
232
+ data = items;
233
+ meta = { total: items.length };
234
+
235
+ } else if (items.data) {
236
+ data = items.data;
237
+ meta = items.meta;
238
+ }
239
+
240
+ // Update the Quasar pagination rows number if it is different from the total
241
+ if (meta && meta.total !== pagination.value.rowsNumber) {
242
+ pagination.value.rowsNumber = meta.total;
243
+ }
244
+
245
+ // Add a reactive isSaving property to each item (for performance reasons in checking saving state)
246
+ data = data.map((item) => {
247
+ // We want to keep the isSaving state if it is already set, as optimizations prevent reloading the
248
+ // components, and therefore reactivity is not responding to the new isSaving state
249
+ const oldItem = pagedItems.value?.data?.find(i => i.id === item.id);
250
+ return { ...item, isSaving: oldItem?.isSaving || ref(false) };
251
+ });
252
+
253
+ pagedItems.value = { data, meta };
254
+ }
255
+
256
+ /**
257
+ * Resets the filter and pagination settings to their defaults.
258
+ */
259
+ function resetPaging() {
260
+ pagination.value = PAGING_DEFAULT;
261
+ }
262
+
263
+ /**
264
+ * Sets the pagination settings to the given values.
265
+ */
266
+ function setPagination(updated: ListControlsPagination) {
267
+ // @ts-expect-error Seems like a bug in the typescript linting?
268
+ pagination.value = updated;
269
+ }
270
+
271
+ /**
272
+ * Sets the selected rows in the list for batch actions or other operations.
273
+ */
274
+ function setSelectedRows(selection: ActionTargetItem[]) {
275
+ selectedRows.value = selection;
276
+ }
277
+
278
+ /**
279
+ * Updates a row in the paged items list with the new item data. Uses the item's id to find the row.
280
+ *
281
+ * @param updatedItem
282
+ */
283
+ function setItemInList(updatedItem: ActionTargetItem) {
284
+ if (updatedItem && updatedItem.id) {
285
+ const data = pagedItems.value?.data?.map(item => (item.id === updatedItem.id && (item.updated_at === null || item.updated_at <= updatedItem.updated_at)) ? updatedItem : item);
286
+ setPagedItems({
287
+ data,
288
+ meta: { total: pagedItems.value?.meta?.total || 0 }
289
+ });
290
+
291
+ // Update the active item as well if it is set
292
+ if (activeItem.value?.id === updatedItem.id) {
293
+ activeItem.value = { ...activeItem.value, ...updatedItem };
294
+ }
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Loads more items into the list.
300
+ */
301
+ async function loadMore(index: number, perPage = undefined) {
302
+ if (!options.routes.more) return;
303
+
304
+ const newItems = await options.routes.more({
305
+ page: index + 1,
306
+ perPage,
307
+ filter: { ...activeFilter.value, ...globalFilter.value }
308
+ });
309
+
310
+ if (newItems && newItems.length > 0) {
311
+ setPagedItems({
312
+ data: [...(pagedItems.value?.data || []), ...newItems],
313
+ meta: { total: pagedItems.value?.meta?.total || 0 }
314
+ });
315
+ return true;
316
+ }
317
+
318
+ return false;
319
+ }
320
+
321
+ /**
322
+ * Refreshes the list, summary, and filter field options.
323
+ */
324
+ async function refreshAll() {
325
+ return Promise.all([loadList(), loadSummary(), loadFilterFieldOptions(), getActiveItemDetails()]);
326
+ }
327
+
328
+ /**
329
+ * Updates the settings in local storage
330
+ */
331
+ function updateSettings(key: string, value: any) {
332
+ const settings = getItem(PAGE_SETTINGS_KEY) || {};
333
+ settings[key] = value;
334
+ setItem(PAGE_SETTINGS_KEY, settings);
335
+ }
336
+
337
+ /**
338
+ * Loads the filter and pagination settings from local storage.
339
+ */
340
+ function loadSettings() {
341
+ // Only load settings when the class is fully initialized
342
+ if (!isInitialized) return;
343
+
344
+ const settings = getItem(PAGE_SETTINGS_KEY);
345
+
346
+ // Load the filter settings from local storage
347
+ if (settings) {
348
+ activeFilter.value = { ...settings.filter, ...activeFilter.value };
349
+ pagination.value = settings.pagination;
350
+ } else {
351
+ // If no local storage settings, apply the default filters
352
+ activeFilter.value = { ...options.filterDefaults, ...activeFilter.value };
353
+ }
354
+
355
+ setTimeout(() => {
356
+ if (!isLoadingList.value) {
357
+ loadList();
358
+ }
359
+
360
+ if (!isLoadingSummary.value) {
361
+ loadSummary();
362
+ }
363
+
364
+ if (!isLoadingFilters.value) {
365
+ loadFilterFieldOptions();
366
+ }
367
+ }, 1);
368
+ }
369
+
370
+ /**
371
+ * Saves the current filter and pagination settings to local storage.
372
+ */
373
+ async function saveSettings() {
374
+ const settings = {
375
+ filter: activeFilter.value,
376
+ pagination: { ...pagination.value, page: 1 }
377
+ };
378
+ // save in local storage
379
+ setItem(PAGE_SETTINGS_KEY, settings);
380
+ }
381
+
382
+ /**
383
+ * Gets the additional details for the currently active item.
384
+ * (ie: data that is not normally loaded in the list because it is not needed for the list view)
385
+ * @returns {Promise<void>}
386
+ */
387
+ async function getActiveItemDetails() {
388
+ if (!activeItem.value || !options.routes.details) return;
389
+
390
+ const result = await options.routes.details(activeItem.value);
391
+
392
+ // Only set the ad details if we are the response for the currently loaded item
393
+ // NOTE: race conditions might allow the finished loading item to be different to the currently
394
+ // requested item
395
+ if (result?.id === activeItem.value?.id) {
396
+ const loadedItem = pagedItems.value?.data?.find((i: ActionTargetItem) => i.id === result.id);
397
+ activeItem.value = { ...result, isSaving: loadedItem?.isSaving || ref(false) };
398
+ }
399
+ }
400
+
401
+ // Whenever the active item changes, fill the additional item details
402
+ // (ie: tasks, verifications, creatives, etc.)
403
+ if (options.routes.details) {
404
+ watch(() => activeItem.value, async (newItem, oldItem) => {
405
+ if (newItem && oldItem?.id !== newItem.id) {
406
+ await getActiveItemDetails();
407
+ }
408
+ });
409
+ }
410
+
411
+ /**
412
+ * Opens the item's form with the given item and tab
413
+ */
414
+ function activatePanel(item: ActionTargetItem | null, panel: string) {
415
+ activeItem.value = item;
416
+ activePanel.value = panel;
417
+ }
418
+
419
+ /**
420
+ * Sets the currently active item in the list.
421
+ */
422
+ function setActiveItem(item: ActionTargetItem | null) {
423
+ activeItem.value = item;
424
+ }
425
+
426
+ /**
427
+ * Gets the next item in the list at the given offset (ie: 1 or -1) from the current position in the list of the
428
+ * selected item. If the next item is on a previous or next page, it will load the page first then select the item
429
+ */
430
+ async function getNextItem(offset: number) {
431
+ if (!pagedItems.value?.data) return;
432
+
433
+ const index = pagedItems.value.data.findIndex((i: ActionTargetItem) => i.id === activeItem.value?.id);
434
+ if (index === undefined || index === null) return;
435
+
436
+ let nextIndex = index + offset;
437
+
438
+ // Load the previous page if the offset is before index 0
439
+ if (nextIndex < 0) {
440
+ if (pagination.value.page > 1) {
441
+ pagination.value = { ...pagination.value, page: pagination.value.page - 1 };
442
+ await waitForRef(isLoadingList, false);
443
+ nextIndex = pagedItems.value.data.length - 1;
444
+ } else {
445
+ // There are no more previous pages
446
+ return;
447
+ }
448
+ }
449
+
450
+ // Load the next page if the offset is past the last index
451
+ if (nextIndex >= pagedItems.value.data.length) {
452
+ if (pagination.value.page < (pagedItems.value?.meta?.last_page || 1)) {
453
+ pagination.value = { ...pagination.value, page: pagination.value.page + 1 };
454
+ await waitForRef(isLoadingList, false);
455
+ nextIndex = 0;
456
+ } else {
457
+ // There are no more next pages
458
+ return;
459
+ }
460
+ }
461
+
462
+ activeItem.value = pagedItems.value?.data[nextIndex];
463
+ }
464
+
465
+ /**
466
+ * Sets the active filter to the given filter.
467
+ */
468
+ function setActiveFilter(filter: ListControlsFilter) {
469
+ activeFilter.value = filter;
470
+ }
471
+
472
+ async function exportList(filter: object) {
473
+ return options.routes.export(filter);
474
+ }
475
+
476
+ // Initialize the list actions and load settings, lists, summaries, filter fields, etc.
477
+ function initialize() {
478
+ isInitialized = true;
479
+ loadSettings();
480
+ }
481
+
482
+ return {
483
+ // State
484
+ name,
485
+ label: options.label || name,
486
+ pagedItems,
487
+ activeFilter,
488
+ globalFilter,
489
+ filterActiveCount,
490
+ showFilters,
491
+ summary,
492
+ filterFieldOptions,
493
+ selectedRows,
494
+ isLoadingList,
495
+ isLoadingFilters,
496
+ isLoadingSummary,
497
+ pager,
498
+ pagination,
499
+ activeItem,
500
+ activePanel,
501
+
502
+ // Actions
503
+ initialize,
504
+ loadSummary,
505
+ resetPaging,
506
+ setPagination,
507
+ setSelectedRows,
508
+ loadList,
509
+ loadMore,
510
+ refreshAll,
511
+ exportList,
512
+ setActiveItem,
513
+ getNextItem,
514
+ activatePanel,
515
+ setActiveFilter,
516
+ applyFilterFromUrl,
517
+ setItemInList,
518
+ getFieldOptions
519
+ };
393
520
  }