quasar-ui-danx 0.0.9 → 0.0.11

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