quasar-ui-danx 0.4.16 → 0.4.17

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-danx",
3
- "version": "0.4.16",
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
  /**
@@ -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
  */