quasar-ui-danx 0.0.10 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) 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/Utility/CollapsableSidebar.vue +119 -0
  42. package/src/components/Utility/ContentDrawer.vue +70 -0
  43. package/src/components/Utility/Dialogs/ConfirmDialog.vue +132 -0
  44. package/src/components/Utility/Dialogs/FullScreenDialog.vue +46 -0
  45. package/src/components/Utility/Dialogs/InfoDialog.vue +92 -0
  46. package/src/components/Utility/Dialogs/InputDialog.vue +35 -0
  47. package/src/components/Utility/Transitions/ListTransition.vue +50 -0
  48. package/src/components/Utility/Transitions/SlideTransition.vue +63 -0
  49. package/src/components/Utility/Transitions/StaggeredListTransition.vue +97 -0
  50. package/src/components/Utility/index.ts +9 -0
  51. package/src/components/index.ts +3 -0
  52. package/src/helpers/FileUpload.ts +294 -0
  53. package/src/helpers/FlashMessages.ts +79 -0
  54. package/src/helpers/array.ts +37 -0
  55. package/src/helpers/compatibility.ts +64 -0
  56. package/src/helpers/date.ts +5 -0
  57. package/src/helpers/download.ts +192 -0
  58. package/src/helpers/downloadPdf.ts +92 -0
  59. package/src/helpers/files.ts +52 -0
  60. package/src/helpers/formats.ts +183 -0
  61. package/src/helpers/http.ts +62 -0
  62. package/src/helpers/index.ts +10 -1
  63. package/src/helpers/multiFileUpload.ts +68 -0
  64. package/src/helpers/singleFileUpload.ts +54 -0
  65. 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
+ }