quasar-ui-danx 0.4.15 → 0.4.17

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-danx",
3
- "version": "0.4.15",
3
+ "version": "0.4.17",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -1,66 +1,66 @@
1
1
  <template>
2
- <div
3
- class="dx-action-table overflow-hidden"
4
- :class="{'dx-no-data': !hasData, 'dx-is-loading': loadingList || loadingSummary, 'dx-is-loading-list': loadingList}"
5
- >
6
- <ActionVnode />
7
- <QTable
8
- ref="actionTable"
9
- :selected="selectedRows"
10
- :pagination="pagination"
11
- :columns="tableColumns"
12
- :loading="loadingList || loadingSummary"
13
- :rows="pagedItems?.data || []"
14
- :binary-state-sort="false"
15
- :selection="selection"
16
- :rows-per-page-options="rowsPerPageOptions"
17
- class="sticky-column sticky-header w-full h-full !border-0"
18
- :color="color"
19
- @update:selected="$emit('update:selected-rows', $event)"
20
- @update:pagination="() => {}"
21
- @request="(e) => $emit('update:pagination', {...e.pagination, __sort: mapSortBy(e.pagination, tableColumns)})"
22
- >
23
- <template #no-data>
24
- <slot name="empty">
25
- <EmptyTableState :text="`There are no ${label.toLowerCase()} matching the applied filter`" />
26
- </slot>
27
- </template>
28
- <template #top-row>
29
- <TableSummaryRow
30
- v-if="hasData"
31
- :label="label"
32
- :item-count="summary?.count || 0"
33
- :selected-count="selectedRows.length"
34
- :sticky-colspan="summaryColSpan"
35
- :loading="loadingSummary"
36
- :summary="summary"
37
- :columns="tableColumns"
38
- @clear="$emit('update:selected-rows', [])"
39
- />
40
- </template>
41
- <template #header-cell="rowProps">
42
- <ActionTableHeaderColumn
43
- v-model="columnSettings"
44
- :row-props="rowProps"
45
- :name="name"
46
- @update:model-value="onUpdateColumnSettings"
47
- />
48
- </template>
49
- <template #body-cell="rowProps">
50
- <ActionTableColumn
51
- :key="rowProps.key"
52
- :row-props="rowProps"
53
- :settings="columnSettings[rowProps.col.name]"
54
- >
55
- <slot
56
- :column-name="rowProps.col.name"
57
- :row="rowProps.row"
58
- :value="rowProps.value"
59
- />
60
- </ActionTableColumn>
61
- </template>
62
- </QTable>
63
- </div>
2
+ <div
3
+ class="dx-action-table overflow-hidden"
4
+ :class="{'dx-no-data': !hasData, 'dx-is-loading': loadingList || loadingSummary, 'dx-is-loading-list': loadingList}"
5
+ >
6
+ <ActionVnode />
7
+ <QTable
8
+ ref="actionTable"
9
+ :selected="selectedRows"
10
+ :pagination="pagination"
11
+ :columns="tableColumns"
12
+ :loading="loadingList || loadingSummary"
13
+ :rows="pagedItems?.data || []"
14
+ :binary-state-sort="false"
15
+ :selection="selection"
16
+ :rows-per-page-options="rowsPerPageOptions"
17
+ class="sticky-column sticky-header w-full h-full !border-0"
18
+ :color="color"
19
+ @update:selected="$emit('update:selected-rows', $event)"
20
+ @update:pagination="() => {}"
21
+ @request="(e) => $emit('update:pagination', {...e.pagination, __sort: mapSortBy(e.pagination, tableColumns)})"
22
+ >
23
+ <template #no-data>
24
+ <slot name="empty">
25
+ <EmptyTableState :text="`There are no ${label.toLowerCase()} matching the applied filter`" />
26
+ </slot>
27
+ </template>
28
+ <template #top-row>
29
+ <TableSummaryRow
30
+ v-if="hasData"
31
+ :label="label"
32
+ :item-count="summary?.count || 0"
33
+ :selected-count="selectedRows.length"
34
+ :sticky-colspan="summaryColSpan"
35
+ :loading="loadingSummary"
36
+ :summary="summary"
37
+ :columns="tableColumns"
38
+ @clear="$emit('update:selected-rows', [])"
39
+ />
40
+ </template>
41
+ <template #header-cell="rowProps">
42
+ <ActionTableHeaderColumn
43
+ v-model="columnSettings"
44
+ :row-props="rowProps"
45
+ :name="name"
46
+ @update:model-value="onUpdateColumnSettings"
47
+ />
48
+ </template>
49
+ <template #body-cell="rowProps">
50
+ <ActionTableColumn
51
+ :key="rowProps.key"
52
+ :row-props="rowProps"
53
+ :settings="columnSettings[rowProps.col.name]"
54
+ >
55
+ <slot
56
+ :column-name="rowProps.col.name"
57
+ :row="rowProps.row"
58
+ :value="rowProps.value"
59
+ />
60
+ </ActionTableColumn>
61
+ </template>
62
+ </QTable>
63
+ </div>
64
64
  </template>
65
65
 
66
66
  <script setup lang="ts">
@@ -1,72 +1,72 @@
1
1
  <template>
2
- <div class="flex flex-grow flex-col flex-nowrap overflow-hidden h-full">
3
- <slot name="top" />
4
- <slot name="toolbar">
5
- <ActionToolbar
6
- v-if="!hideToolbar"
7
- :title="title"
8
- :refresh-button="refreshButton"
9
- :actions="actions?.filter(a => a.batch)"
10
- :action-target="controller.selectedRows.value"
11
- :exporter="controller.exportList"
12
- :loading="controller.isLoadingList.value || controller.isLoadingSummary.value"
13
- @refresh="controller.refreshAll"
14
- >
15
- <template #default>
16
- <slot name="action-toolbar" />
17
- </template>
18
- </ActionToolbar>
19
- </slot>
20
- <div class="flex flex-nowrap flex-grow overflow-hidden w-full">
21
- <slot name="filters">
22
- <CollapsableFiltersSidebar
23
- v-if="activeFilter"
24
- :name="controller.name"
25
- :show-filters="showFilters"
26
- :filters="filters"
27
- :active-filter="activeFilter"
28
- class="dx-action-table-filters"
29
- @update:active-filter="controller.setActiveFilter"
30
- />
31
- </slot>
32
- <slot>
33
- <ActionTable
34
- class="flex-grow"
35
- :pagination="controller.pagination.value"
36
- :selected-rows="controller.selectedRows.value"
37
- :label="controller.label"
38
- :name="controller.name"
39
- :class="tableClass"
40
- :summary="controller.summary.value"
41
- :loading-list="controller.isLoadingList.value"
42
- :loading-summary="controller.isLoadingSummary.value"
43
- :paged-items="controller.pagedItems.value"
44
- :columns="columns"
45
- :selection="selection"
46
- @update:selected-rows="controller.setSelectedRows"
47
- @update:pagination="controller.setPagination"
48
- />
49
- </slot>
50
- <slot name="panels">
51
- <PanelsDrawer
52
- v-if="activeItem && panels"
53
- :title="panelTitle"
54
- :model-value="activePanel"
55
- :active-item="activeItem"
56
- :panels="panels"
57
- @update:model-value="panel => controller.activatePanel(activeItem, panel)"
58
- @close="controller.setActiveItem(null)"
59
- >
60
- <template #controls>
61
- <PreviousNextControls
62
- :is-loading="controller.isLoadingList.value"
63
- @next="controller.getNextItem"
64
- />
65
- </template>
66
- </PanelsDrawer>
67
- </slot>
68
- </div>
69
- </div>
2
+ <div class="flex flex-grow flex-col flex-nowrap overflow-hidden h-full">
3
+ <slot name="top" />
4
+ <slot name="toolbar">
5
+ <ActionToolbar
6
+ v-if="!hideToolbar"
7
+ :title="title"
8
+ :refresh-button="refreshButton"
9
+ :actions="actions?.filter(a => a.batch)"
10
+ :action-target="controller.selectedRows.value"
11
+ :exporter="controller.exportList"
12
+ :loading="controller.isLoadingList.value || controller.isLoadingSummary.value"
13
+ @refresh="controller.refreshAll"
14
+ >
15
+ <template #default>
16
+ <slot name="action-toolbar" />
17
+ </template>
18
+ </ActionToolbar>
19
+ </slot>
20
+ <div class="flex flex-nowrap flex-grow overflow-hidden w-full">
21
+ <slot name="filters">
22
+ <CollapsableFiltersSidebar
23
+ v-if="activeFilter"
24
+ :name="controller.name"
25
+ :show-filters="showFilters"
26
+ :filters="filters"
27
+ :active-filter="activeFilter"
28
+ class="dx-action-table-filters"
29
+ @update:active-filter="controller.setActiveFilter"
30
+ />
31
+ </slot>
32
+ <slot>
33
+ <ActionTable
34
+ class="flex-grow"
35
+ :pagination="controller.pagination.value"
36
+ :selected-rows="controller.selectedRows.value"
37
+ :label="controller.label"
38
+ :name="controller.name"
39
+ :class="tableClass"
40
+ :summary="controller.summary.value"
41
+ :loading-list="controller.isLoadingList.value"
42
+ :loading-summary="controller.isLoadingSummary.value"
43
+ :paged-items="controller.pagedItems.value"
44
+ :columns="columns"
45
+ :selection="selection"
46
+ @update:selected-rows="controller.setSelectedRows"
47
+ @update:pagination="controller.setPagination"
48
+ />
49
+ </slot>
50
+ <slot name="panels">
51
+ <PanelsDrawer
52
+ v-if="activeItem && panels"
53
+ :title="panelTitle"
54
+ :model-value="activePanel"
55
+ :active-item="activeItem"
56
+ :panels="panels"
57
+ @update:model-value="panel => controller.activatePanel(activeItem, panel)"
58
+ @close="controller.setActiveItem(null)"
59
+ >
60
+ <template #controls>
61
+ <PreviousNextControls
62
+ :is-loading="controller.isLoadingList.value"
63
+ @next="controller.getNextItem"
64
+ />
65
+ </template>
66
+ </PanelsDrawer>
67
+ </slot>
68
+ </div>
69
+ </div>
70
70
  </template>
71
71
  <script setup lang="ts">
72
72
  import { computed } from "vue";
@@ -96,7 +96,12 @@ export interface ActionTableLayoutProps {
96
96
  const props = withDefaults(defineProps<ActionTableLayoutProps>(), {
97
97
  title: "",
98
98
  tableClass: "",
99
- selection: "multiple"
99
+ selection: "multiple",
100
+ filters: undefined,
101
+ panels: undefined,
102
+ actions: undefined,
103
+ exporter: undefined,
104
+ panelTitleField: ""
100
105
  });
101
106
 
102
107
  const activeFilter = computed(() => props.controller.activeFilter.value);
@@ -1,7 +1,7 @@
1
1
  import { computed, Ref, ref, shallowRef, watch } from "vue";
2
2
  import { RouteParams, Router } from "vue-router";
3
3
  import { danxOptions } from "../../config";
4
- import { getItem, setItem, storeObject, waitForRef } from "../../helpers";
4
+ import { getItem, latestCallOnly, setItem, storeObject, waitForRef } from "../../helpers";
5
5
  import {
6
6
  ActionController,
7
7
  ActionTargetItem,
@@ -290,10 +290,16 @@ export function useListControls(name: string, options: ListControlsOptions): Act
290
290
  * @returns {Promise<void>}
291
291
  */
292
292
  async function getActiveItemDetails() {
293
- if (!activeItem.value || !options.routes.details) return;
294
-
295
293
  try {
296
- const result = await options.routes.details(activeItem.value);
294
+ const latestResult = latestCallOnly("active-item", async () => {
295
+ if (!activeItem.value || !options.routes.details) return undefined;
296
+ return await options.routes.details(activeItem.value);
297
+ });
298
+
299
+ const result = await latestResult();
300
+
301
+ // Undefined means we were not the latest, or the request was invalid (ie: activeItem was already cleared)
302
+ if (result === undefined) return;
297
303
 
298
304
  if (!result || !result.__type || !result.id) {
299
305
  return console.error("Invalid response from details route: All responses must include a __type and id field. result =", result);
@@ -354,33 +360,41 @@ export function useListControls(name: string, options: ListControlsOptions): Act
354
360
  const index = pagedItems.value.data.findIndex((i: ActionTargetItem) => i.id === activeItem.value?.id);
355
361
  if (index === undefined || index === null) return;
356
362
 
357
- let nextIndex = index + offset;
363
+ const nextIndex = index + offset;
364
+
365
+ const latestNextIndex = latestCallOnly("getNextItem", async () => {
366
+ // Load the previous page if the offset is before index 0
367
+ if (nextIndex < 0) {
368
+ if (pagination.value.page > 1) {
369
+ pagination.value = { ...pagination.value, page: pagination.value.page - 1 };
370
+ await waitForRef(isLoadingList, false);
371
+ return pagedItems.value.data.length - 1;
372
+ }
358
373
 
359
- // Load the previous page if the offset is before index 0
360
- if (nextIndex < 0) {
361
- if (pagination.value.page > 1) {
362
- pagination.value = { ...pagination.value, page: pagination.value.page - 1 };
363
- await waitForRef(isLoadingList, false);
364
- nextIndex = pagedItems.value.data.length - 1;
365
- } else {
366
374
  // There are no more previous pages
367
- return;
375
+ return -1;
368
376
  }
369
- }
370
377
 
371
- // Load the next page if the offset is past the last index
372
- if (nextIndex >= pagedItems.value.data.length) {
373
- if (pagination.value.page < (pagedItems.value?.meta?.last_page || 1)) {
374
- pagination.value = { ...pagination.value, page: pagination.value.page + 1 };
375
- await waitForRef(isLoadingList, false);
376
- nextIndex = 0;
377
- } else {
378
+ // Load the next page if the offset is past the last index
379
+ if (nextIndex >= pagedItems.value.data.length) {
380
+ if (pagination.value.page < (pagedItems.value?.meta?.last_page || 1)) {
381
+ pagination.value = { ...pagination.value, page: pagination.value.page + 1 };
382
+ await waitForRef(isLoadingList, false);
383
+ return 0;
384
+ }
385
+
378
386
  // There are no more next pages
379
- return;
387
+ return -1;
380
388
  }
381
- }
382
389
 
383
- activeItem.value = pagedItems.value?.data[nextIndex];
390
+ return nextIndex;
391
+ });
392
+
393
+ const resolvedNextIndex = await latestNextIndex();
394
+
395
+ if (resolvedNextIndex !== undefined && resolvedNextIndex >= 0) {
396
+ activeItem.value = pagedItems.value?.data[resolvedNextIndex];
397
+ }
384
398
  }
385
399
 
386
400
  /**
@@ -14,6 +14,25 @@ export const activeActionVnode: Ref = shallowRef(null);
14
14
  export function useActions(actions: ActionOptions[], globalOptions: Partial<ActionOptions> | null = null) {
15
15
  const namespace = uid();
16
16
 
17
+ /**
18
+ * Extend an action so the base action can be modified without affecting other places the action is used.
19
+ * This isolates the action to the provided id, so it is still re-usable across the system, but does not affect behavior elsewhere.
20
+ *
21
+ * For example, when you have a list of items and you want to perform a callback on the action on a single item, you can extend the action
22
+ * with the id of the item you want to perform the action on, allowing you to perform behavior on a single item, instead of all instances
23
+ * in the list receiving the same callback.
24
+ */
25
+ function extendAction(actionName: string, extendedId: string | number, actionOptions: Partial<ActionOptions>): ResourceAction {
26
+ const action = getAction(actionName);
27
+ const extendedAction = { ...action, ...actionOptions, id: extendedId };
28
+ if (extendedAction.debounce) {
29
+ extendedAction.trigger = useDebounceFn((target, input) => performAction(extendedAction, target, input), extendedAction.debounce);
30
+ } else {
31
+ extendedAction.trigger = (target, input) => performAction(extendedAction, target, input);
32
+ }
33
+ return storeObject(extendedAction);
34
+ }
35
+
17
36
  /**
18
37
  * Resolve the action object based on the provided name (or return the object if the name is already an object)
19
38
  */
@@ -140,7 +159,8 @@ export function useActions(actions: ActionOptions[], globalOptions: Partial<Acti
140
159
 
141
160
  return {
142
161
  getAction,
143
- getActions
162
+ getActions,
163
+ extendAction
144
164
  };
145
165
  }
146
166
 
@@ -34,9 +34,11 @@ export function storeObject<T extends TypedObject>(newObject: T): ShallowReactiv
34
34
  // Recursively store all the children of the object as well
35
35
  for (const key of Object.keys(newObject)) {
36
36
  const value = newObject[key];
37
- if (Array.isArray(value) && value.length > 0 && typeof value[0] === "object") {
37
+ if (Array.isArray(value) && value.length > 0) {
38
38
  for (const index in value) {
39
- newObject[key][index] = storeObject(value[index]);
39
+ if (value[index] && typeof value[index] === "object") {
40
+ newObject[key][index] = storeObject(value[index]);
41
+ }
40
42
  }
41
43
  } else if (value?.__type) {
42
44
  // @ts-expect-error newObject[key] is guaranteed to be a TypedObject
@@ -1,3 +1,4 @@
1
+ import { AnyObject } from "src/types";
1
2
  import { Ref, watch } from "vue";
2
3
 
3
4
  export { useDebounceFn } from "@vueuse/core";
@@ -33,6 +34,21 @@ export function waitForRef(ref: Ref, value: any) {
33
34
  });
34
35
  }
35
36
 
37
+ const currentCalls: AnyObject = {};
38
+ export function latestCallOnly<T extends any[], R>(type: string, fn: (...args: T) => Promise<R>) {
39
+ if (!currentCalls[type]) {
40
+ currentCalls[type] = 0;
41
+ }
42
+ return async function (...args: T) {
43
+ const callId = ++currentCalls[type];
44
+ const result = await fn(...args);
45
+ if (callId === currentCalls[type]) {
46
+ return result;
47
+ }
48
+ return undefined;
49
+ };
50
+ }
51
+
36
52
  /**
37
53
  * Returns a number that is constrained to the given range.
38
54
  */