quasar-ui-danx 0.3.21 → 0.4.1

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.
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
  }