quasar-ui-danx 0.0.10 → 0.0.12

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