quasar-ui-danx 0.4.42 → 0.4.45

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.42",
3
+ "version": "0.4.45",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -57,6 +57,7 @@
57
57
  :model-value="activePanel"
58
58
  :target="activeItem"
59
59
  :panels="controller.panels"
60
+ :drawer-class="drawerClass"
60
61
  @update:model-value="panel => controller.activatePanel(activeItem, panel)"
61
62
  @close="controller.setActiveItem(null)"
62
63
  >
@@ -91,6 +92,7 @@ export interface ActionTableLayoutProps {
91
92
  showFilters?: boolean;
92
93
  tableClass?: string;
93
94
  title?: string;
95
+ drawerClass?: string;
94
96
  }
95
97
 
96
98
  const props = withDefaults(defineProps<ActionTableLayoutProps>(), {
@@ -98,7 +100,8 @@ const props = withDefaults(defineProps<ActionTableLayoutProps>(), {
98
100
  panelTitleField: "",
99
101
  selection: "multiple",
100
102
  tableClass: "",
101
- title: ""
103
+ title: "",
104
+ drawerClass: ""
102
105
  });
103
106
 
104
107
  const activeFilter = computed(() => props.controller.activeFilter.value);
@@ -115,11 +115,11 @@ export function useControls(name: string, options: ListControlsOptions): ListCon
115
115
  * Loads the filter field options for the current filter.
116
116
  */
117
117
  async function loadFieldOptions() {
118
- if (!options.routes.fieldOptions || options.isFieldOptionsEnabled === false) return;
118
+ if (isLoadingFilters.value || !options.routes.fieldOptions || options.isFieldOptionsEnabled === false) return;
119
119
 
120
120
  isLoadingFilters.value = true;
121
121
  try {
122
- fieldOptions.value = await options.routes.fieldOptions(activeFilter.value) || {};
122
+ fieldOptions.value = await options.routes.fieldOptions() || {};
123
123
  } catch (e) {
124
124
  // Fail silently
125
125
  } finally {
@@ -9,7 +9,10 @@
9
9
  no-route-dismiss
10
10
  @update:show="$emit('close')"
11
11
  >
12
- <div class="flex flex-col flex-nowrap h-full">
12
+ <div
13
+ class="flex flex-col flex-nowrap h-full dx-panels-drawer-content"
14
+ :class="drawerClass"
15
+ >
13
16
  <div class="dx-panels-drawer-header flex items-center px-6 py-4">
14
17
  <div class="flex-grow">
15
18
  <slot name="header">
@@ -64,6 +67,12 @@
64
67
  <slot name="right-sidebar" />
65
68
  </div>
66
69
  </div>
70
+ <div
71
+ v-else
72
+ class="p-8"
73
+ >
74
+ <QSkeleton class="h-96" />
75
+ </div>
67
76
  </div>
68
77
  </div>
69
78
  </ContentDrawer>
@@ -76,22 +85,24 @@ import { ContentDrawer } from "../Utility";
76
85
  import PanelsDrawerPanels from "./PanelsDrawerPanels";
77
86
  import PanelsDrawerTabs from "./PanelsDrawerTabs";
78
87
 
79
- export interface Props {
88
+ export interface PanelsDrawerProps {
80
89
  title?: string,
81
90
  modelValue?: string | number,
82
91
  target: ActionTargetItem;
83
92
  tabsClass?: string | object,
84
93
  panelsClass?: string | object,
94
+ drawerClass?: string | object,
85
95
  position?: "standard" | "right" | "left";
86
96
  panels: ActionPanel[]
87
97
  }
88
98
 
89
99
  defineEmits(["update:model-value", "close"]);
90
- const props = withDefaults(defineProps<Props>(), {
100
+ const props = withDefaults(defineProps<PanelsDrawerProps>(), {
91
101
  title: "",
92
102
  modelValue: null,
93
103
  tabsClass: "w-[13.5rem] flex-shrink-0",
94
- panelsClass: "w-[80rem]",
104
+ panelsClass: "w-full",
105
+ drawerClass: "",
95
106
  position: "right"
96
107
  });
97
108
 
@@ -2,7 +2,7 @@
2
2
  <QBtn
3
3
  :loading="isSaving"
4
4
  class="shadow-none"
5
- :class="disabled ? 'text-slate-800 bg-slate-500 opacity-50' : colorClass"
5
+ :class="disabled ? disabledClass : colorClass"
6
6
  :disable="disabled"
7
7
  @click="()=> onAction()"
8
8
  >
@@ -79,6 +79,7 @@ export interface ActionButtonProps {
79
79
  target?: ActionTarget;
80
80
  input?: object;
81
81
  disabled?: boolean;
82
+ disabledClass?: string;
82
83
  confirm?: boolean;
83
84
  confirmText?: string;
84
85
  }
@@ -94,7 +95,8 @@ const props = withDefaults(defineProps<ActionButtonProps>(), {
94
95
  action: null,
95
96
  target: null,
96
97
  input: null,
97
- confirmText: "Are you sure?"
98
+ confirmText: "Are you sure?",
99
+ disabledClass: "text-slate-800 bg-slate-500 opacity-50"
98
100
  });
99
101
 
100
102
  const colorClass = computed(() => {
@@ -195,13 +197,14 @@ const isSaving = computed(() => {
195
197
 
196
198
  const isConfirming = ref(false);
197
199
  function onAction(isConfirmed = false) {
200
+ if (props.disabled) return;
201
+
198
202
  // Make sure this action is confirmed if the confirm prop is set
199
203
  if (props.confirm && !isConfirmed) {
200
204
  isConfirming.value = true;
201
205
  return false;
202
206
  }
203
207
  isConfirming.value = false;
204
- if (props.disabled) return;
205
208
  if (props.action) {
206
209
  props.action.trigger(props.target, props.input).then(async (response) => {
207
210
  emit("success", typeof response.json === "function" ? await response.json() : response);
@@ -41,7 +41,7 @@ export interface Props {
41
41
  hideIcon?: object | string;
42
42
  iconClass?: string;
43
43
  labelClass?: string;
44
- label?: string;
44
+ label?: string | number;
45
45
  tooltip?: string;
46
46
  disable?: boolean;
47
47
  }
@@ -138,7 +138,7 @@
138
138
  <script setup lang="ts">
139
139
  import { DocumentTextIcon as TextFileIcon, DownloadIcon, PlayIcon } from "@heroicons/vue/outline";
140
140
  import { computed, ComputedRef, onMounted, ref } from "vue";
141
- import { download, FileUpload } from "../../../helpers";
141
+ import { download, FileUpload, uniqueBy } from "../../../helpers";
142
142
  import { ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
143
143
  import { UploadedFile } from "../../../types";
144
144
  import { FullScreenCarouselDialog } from "../Dialogs";
@@ -202,10 +202,16 @@ const computedImage: ComputedRef<UploadedFile | null> = computed(() => {
202
202
  });
203
203
 
204
204
  const isUploading = computed(() => !props.file || props.file?.progress !== undefined);
205
- const previewableFiles: ComputedRef<[UploadedFile | null]> = computed(() => {
206
- return props.relatedFiles?.length > 0 ? props.relatedFiles : [computedImage.value];
205
+ const previewableFiles: ComputedRef<(UploadedFile | null)[] | null> = computed(() => {
206
+ return props.relatedFiles?.length > 0 ? uniqueBy([computedImage.value, ...props.relatedFiles], filesHaveSameUrl) : [computedImage.value];
207
207
  });
208
208
 
209
+ function filesHaveSameUrl(a: UploadedFile, b: UploadedFile) {
210
+ return a.id === b.id ||
211
+ [b.url, b.optimized?.url, b.thumb?.url].includes(a.url) ||
212
+ [a.url, a.optimized?.url, a.thumb?.url].includes(b.url);
213
+ }
214
+
209
215
  const filename = computed(() => computedImage.value?.name || computedImage.value?.filename || "");
210
216
  const mimeType = computed(
211
217
  () => computedImage.value?.type || computedImage.value?.mime || ""
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="dx-collapsable-sidebar overflow-y-auto overflow-x-hidden scroll-smooth flex-shrink-0 transition-all relative"
3
+ class="dx-collapsable-sidebar overflow-hidden scroll-smooth flex-shrink-0 flex-nowrap transition-all relative"
4
4
  :class="{
5
5
  'is-collapsed': isCollapsed,
6
6
  'is-right-side': rightSide,
@@ -8,99 +8,89 @@
8
8
  }"
9
9
  :style="style"
10
10
  >
11
- <div class="flex-grow max-w-full">
11
+ <div class="flex-grow max-w-full overflow-y-auto overflow-x-hidden">
12
12
  <slot :is-collapsed="isCollapsed" />
13
13
  </div>
14
14
  <template v-if="!disabled && (!hideToggleOnCollapse || !isCollapsed)">
15
15
  <div
16
16
  v-if="!toggleAtTop"
17
- class="flex w-full p-4"
18
- :class="rightSide ? 'justify-start' : 'justify-end'"
17
+ class="flex w-full p-4 flex-shrink-0"
18
+ :class="{'justify-start': rightSide, 'justify-end': !rightSide, ...resolveToggleClass}"
19
19
  >
20
20
  <slot name="toggle">
21
21
  <QBtn
22
22
  class="btn-secondary"
23
23
  @click="toggleCollapse"
24
24
  >
25
- <ToggleIcon
26
- class="w-5 transition-all"
27
- :class="{ 'rotate-180': rightSide ? !isCollapsed : isCollapsed }"
28
- />
25
+ <ToggleIcon :class="{ 'rotate-180': rightSide ? !isCollapsed : isCollapsed, ...resolvedToggleIconClass }" />
29
26
  </QBtn>
30
27
  </slot>
31
28
  </div>
32
29
  <div
33
30
  v-else
34
31
  class="absolute top-0 right-0 cursor-pointer p-2"
35
- :class="toggleClass"
32
+ :class="resolveToggleClass"
36
33
  @click="toggleCollapse"
37
34
  >
38
- <ToggleIcon
39
- class="w-5 transition-all"
40
- :class="{ 'rotate-180': rightSide ? !isCollapsed : isCollapsed }"
41
- />
35
+ <ToggleIcon :class="{ 'rotate-180': rightSide ? !isCollapsed : isCollapsed, ...resolvedToggleIconClass }" />
42
36
  </div>
43
37
  </template>
44
38
  </div>
45
39
  </template>
46
- <script setup>
40
+ <script setup lang="ts">
47
41
  import { ChevronLeftIcon as ToggleIcon } from "@heroicons/vue/outline";
48
42
  import { computed, onMounted, ref, watch } from "vue";
49
43
  import { getItem, setItem } from "../../../helpers";
44
+ import { AnyObject } from "../../../types";
50
45
 
51
46
  const emit = defineEmits(["collapse", "update:collapse"]);
52
- const props = defineProps({
53
- rightSide: Boolean,
54
- displayClass: {
55
- type: String,
56
- default: "flex flex-col"
57
- },
58
- maxWidth: {
59
- type: String,
60
- default: "13.5rem"
61
- },
62
- minWidth: {
63
- type: String,
64
- default: "5.5rem"
65
- },
66
- disabled: Boolean,
67
- collapse: Boolean,
68
- name: {
69
- type: String,
70
- default: "sidebar"
71
- },
72
- toggleAtTop: Boolean,
73
- toggleClass: {
74
- type: String,
75
- default: ""
76
- },
77
- hideToggleOnCollapse: Boolean
47
+ const props = withDefaults(defineProps<{
48
+ displayClass?: string;
49
+ toggleClass?: string | AnyObject;
50
+ toggleIconClass?: string | AnyObject;
51
+ rightSide?: boolean;
52
+ maxWidth?: string;
53
+ minWidth?: string;
54
+ disabled?: boolean;
55
+ collapse?: boolean;
56
+ name?: string;
57
+ toggleAtTop?: boolean;
58
+ hideToggleOnCollapse?: boolean;
59
+ }>(), {
60
+ displayClass: "flex flex-col",
61
+ maxWidth: "13.5rem",
62
+ minWidth: "5.5rem",
63
+ name: "sidebar",
64
+ toggleClass: "",
65
+ toggleIconClass: "w-5 transition-all"
78
66
  });
79
67
 
80
68
  const isCollapsed = ref(getItem(props.name + "-is-collapsed", props.collapse));
81
69
 
82
70
  function setCollapse(state) {
83
- isCollapsed.value = state;
84
- setItem(props.name + "-is-collapsed", !!isCollapsed.value);
71
+ isCollapsed.value = state;
72
+ setItem(props.name + "-is-collapsed", !!isCollapsed.value);
85
73
  }
86
74
 
87
75
  function toggleCollapse() {
88
- setCollapse(!isCollapsed.value);
89
- emit("collapse", isCollapsed.value);
90
- emit("update:collapse", isCollapsed.value);
76
+ setCollapse(!isCollapsed.value);
77
+ emit("collapse", isCollapsed.value);
78
+ emit("update:collapse", isCollapsed.value);
91
79
  }
92
80
 
93
81
  onMounted(() => {
94
- emit("collapse", isCollapsed.value);
95
- emit("update:collapse", isCollapsed.value);
82
+ emit("collapse", isCollapsed.value);
83
+ emit("update:collapse", isCollapsed.value);
96
84
  });
97
85
  const style = computed(() => {
98
- return {
99
- width: isCollapsed.value ? props.minWidth : props.maxWidth
100
- };
86
+ return {
87
+ width: isCollapsed.value ? props.minWidth : props.maxWidth
88
+ };
101
89
  });
102
90
 
91
+ const resolveToggleClass = computed(() => typeof props.toggleClass === "string" ? { [props.toggleClass]: true } : props.toggleClass);
92
+ const resolvedToggleIconClass = computed(() => typeof props.toggleIconClass === "string" ? { [props.toggleIconClass]: true } : props.toggleIconClass);
103
93
  watch(() => props.collapse, () => {
104
- setCollapse(props.collapse);
94
+ setCollapse(props.collapse);
105
95
  });
106
96
  </script>
@@ -0,0 +1,40 @@
1
+ <template>
2
+ <div :class="{[colorClass]: true, [sizeClass]: true, 'rounded-full': true}">
3
+ <slot>{{ label }}</slot>
4
+ </div>
5
+ </template>
6
+ <script setup lang="ts">
7
+ import { computed } from "vue";
8
+ import { LabelPillWidgetProps } from "../../../types";
9
+
10
+ const props = withDefaults(defineProps<LabelPillWidgetProps>(), {
11
+ label: "",
12
+ color: "none",
13
+ size: "md"
14
+ });
15
+
16
+ const colorClasses = {
17
+ sky: "bg-sky-950 text-sky-400",
18
+ green: "bg-green-950 text-green-400",
19
+ red: "bg-red-950 text-red-400",
20
+ amber: "bg-amber-950 text-amber-400",
21
+ yellow: "bg-yellow-950 text-yellow-400",
22
+ blue: "bg-blue-950 text-blue-400",
23
+ slate: "bg-slate-950 text-slate-400",
24
+ gray: "bg-slate-700 text-gray-300",
25
+ none: ""
26
+ };
27
+
28
+ const sizeClasses = {
29
+ xxs: "text-xs px-1 py-.5",
30
+ xs: "text-xs px-2 py-1",
31
+ sm: "text-sm px-3 py-1.5",
32
+ md: "text-base px-3 py-2",
33
+ lg: "text-lg px-4 py-2"
34
+ };
35
+
36
+ const colorClass = computed(() => {
37
+ return colorClasses[props.color];
38
+ });
39
+ const sizeClass = computed(() => sizeClasses[props.size]);
40
+ </script>
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <div class="flex items-stretch">
3
+ <div
4
+ class="rounded-l-lg bg-slate-400 text-slate-900 text-xs px-2 flex items-center justify-center"
5
+ :class="labelClass"
6
+ >
7
+ <slot name="label">
8
+ {{ label }}
9
+ </slot>
10
+ </div>
11
+ <div class="rounded-r-lg bg-slate-900 text-slate-400 px-2 py-1">
12
+ <slot name="value">
13
+ {{ value }}
14
+ </slot>
15
+ </div>
16
+ </div>
17
+ </template>
18
+ <script setup lang="ts">
19
+ withDefaults(defineProps<{
20
+ label: string;
21
+ value: string;
22
+ labelClass?: string;
23
+ }>(), {
24
+ labelClass: ""
25
+ });
26
+ </script>
@@ -0,0 +1,2 @@
1
+ export { default as LabelPillWidget } from "./LabelPillWidget.vue";
2
+ export { default as LabelValuePillWidget } from "./LabelValuePillWidget.vue";
@@ -8,3 +8,4 @@ export * from "./Popovers";
8
8
  export * from "./Tabs";
9
9
  export * from "./Tools";
10
10
  export * from "./Transitions";
11
+ export * from "./Widgets";
@@ -3,7 +3,14 @@ import { FaSolidCopy as CopyIcon, FaSolidPencil as EditIcon, FaSolidTrash as Del
3
3
  import { uid } from "quasar";
4
4
  import { h, isReactive, Ref, shallowReactive, shallowRef } from "vue";
5
5
  import { ConfirmActionDialog, CreateNewWithNameDialog } from "../components";
6
- import type { ActionGlobalOptions, ActionOptions, ActionTarget, ListController, ResourceAction } from "../types";
6
+ import type {
7
+ ActionGlobalOptions,
8
+ ActionOptions,
9
+ ActionTarget,
10
+ ActionTargetItem,
11
+ ListController,
12
+ ResourceAction
13
+ } from "../types";
7
14
  import { FlashMessages } from "./FlashMessages";
8
15
  import { storeObject } from "./objectStore";
9
16
 
@@ -233,12 +240,18 @@ async function onConfirmAction(action: ActionOptions, target: ActionTarget, inpu
233
240
  result = { error: `Action ${action.name} does not support batch actions` };
234
241
  }
235
242
  } else {
243
+ const __timestamp = Date.now();
244
+ if (action.optimisticDelete) {
245
+ storeObject({ ...target, __deleted_at: new Date().toISOString(), __timestamp } as ActionTargetItem);
246
+ }
247
+
236
248
  // If the action has an optimistic callback, we call it before the actual action to immediately
237
249
  // update the UI
238
250
  if (typeof action.optimistic === "function") {
239
251
  action.optimistic(action, target, input);
252
+ storeObject({ ...target, __timestamp } as ActionTargetItem);
240
253
  } else if (action.optimistic) {
241
- storeObject({ ...target, ...input });
254
+ storeObject({ ...target, ...input, __timestamp });
242
255
  }
243
256
 
244
257
  result = await action.onAction(action.alias || action.name, target, input);
@@ -2,31 +2,31 @@
2
2
  * Replace an item in an array with a new item
3
3
  */
4
4
  export function replace(array: any[], item: any, newItem = undefined, appendIfMissing = false) {
5
- const index: any = typeof item === "function" ? array.findIndex(item) : array.indexOf(item);
6
- if (index === false || index === -1) {
7
- return appendIfMissing ? [...array, newItem] : array;
8
- }
5
+ const index: any = typeof item === "function" ? array.findIndex(item) : array.indexOf(item);
6
+ if (index === false || index === -1) {
7
+ return appendIfMissing ? [...array, newItem] : array;
8
+ }
9
9
 
10
- const newArray = [...array];
11
- newItem !== undefined
12
- ? newArray.splice(index, 1, newItem)
13
- : newArray.splice(index, 1);
14
- return newArray;
10
+ const newArray = [...array];
11
+ newItem !== undefined
12
+ ? newArray.splice(index, 1, newItem)
13
+ : newArray.splice(index, 1);
14
+ return newArray;
15
15
  }
16
16
 
17
17
  /**
18
18
  * Remove an item from an array
19
19
  */
20
20
  export function remove(array: any[], item: any) {
21
- return replace(array, item);
21
+ return replace(array, item);
22
22
  }
23
23
 
24
24
  /**
25
25
  * Remove duplicate items from an array using a callback to compare 2 elements
26
26
  */
27
- export function uniqueBy(array: any[], cb: Function) {
28
- return array.filter((a, index, self) => {
29
- // Check if the current element 'a' is the first occurrence in the array
30
- return index === self.findIndex((b) => cb(a, b));
31
- });
27
+ export function uniqueBy(array: any[], cb: (a: any, b: any) => boolean) {
28
+ return array.filter((a, index, self) => {
29
+ // Check if the current element 'a' is the first occurrence in the array
30
+ return index === self.findIndex((b) => cb(a, b));
31
+ });
32
32
  }
@@ -22,7 +22,7 @@ export function storeObject<T extends TypedObject>(newObject: T, recentlyStoredO
22
22
  if (typeof newObject !== "object") {
23
23
  return newObject;
24
24
  }
25
-
25
+
26
26
  const id = newObject?.id || newObject?.name;
27
27
  const type = newObject?.__type;
28
28
  if (!id || !type) return shallowReactive(newObject);
@@ -49,6 +49,9 @@ export function storeObject<T extends TypedObject>(newObject: T, recentlyStoredO
49
49
  // @ts-expect-error __timestamp is guaranteed to be set in this case on both old and new
50
50
  if (oldObject && newObject.__timestamp < oldObject.__timestamp) {
51
51
  recentlyStoredObjects[objectKey] = oldObject;
52
+
53
+ // Recursively store all the children of the object as well
54
+ storeObjectChildren(newObject, recentlyStoredObjects, oldObject);
52
55
  return oldObject;
53
56
  }
54
57
 
@@ -59,19 +62,7 @@ export function storeObject<T extends TypedObject>(newObject: T, recentlyStoredO
59
62
  recentlyStoredObjects[objectKey] = reactiveObject;
60
63
 
61
64
  // Recursively store all the children of the object as well
62
- for (const key of Object.keys(newObject)) {
63
- const value = newObject[key];
64
- if (Array.isArray(value) && value.length > 0) {
65
- for (const index in value) {
66
- if (value[index] && typeof value[index] === "object") {
67
- newObject[key][index] = storeObject(value[index], recentlyStoredObjects);
68
- }
69
- }
70
- } else if (value?.__type) {
71
- // @ts-expect-error __type is guaranteed to be set in this case
72
- newObject[key] = storeObject(value as TypedObject, recentlyStoredObjects);
73
- }
74
- }
65
+ storeObjectChildren(newObject, recentlyStoredObjects);
75
66
 
76
67
  Object.assign(reactiveObject, newObject);
77
68
 
@@ -87,6 +78,33 @@ export function storeObject<T extends TypedObject>(newObject: T, recentlyStoredO
87
78
  return reactiveObject;
88
79
  }
89
80
 
81
+ /**
82
+ * A recursive way to store all the child TypedObjects of a TypedObject.
83
+ * recentlyStoredObjects is used to avoid infinite recursion
84
+ * applyToObject is used to apply the stored objects to a different object than the one being stored.
85
+ * The apply to object is the current object being returned by storeObject() - Normally, the oldObject if it has a more recent timestamp than the newObject being stored.
86
+ */
87
+ function storeObjectChildren<T extends TypedObject>(object: T, recentlyStoredObjects: AnyObject = {}, applyToObject: T | null = null) {
88
+ applyToObject = applyToObject || object;
89
+ for (const key of Object.keys(object)) {
90
+ const value = object[key];
91
+ if (Array.isArray(value) && value.length > 0) {
92
+ for (const index in value) {
93
+ if (value[index] && typeof value[index] === "object") {
94
+ if (!applyToObject[key]) {
95
+ // @ts-expect-error this is fine... T is generic, but not sure why the matter to write to an object?
96
+ applyToObject[key] = [];
97
+ }
98
+ applyToObject[key][index] = storeObject(value[index], recentlyStoredObjects);
99
+ }
100
+ }
101
+ } else if (value?.__type) {
102
+ // @ts-expect-error __type is guaranteed to be set in this case
103
+ applyToObject[key] = storeObject(value as TypedObject, recentlyStoredObjects);
104
+ }
105
+ }
106
+ }
107
+
90
108
  /**
91
109
  * Remove an object from all lists in the store
92
110
  */
@@ -111,7 +129,10 @@ function removeObjectFromLists<T extends TypedObject>(object: T) {
111
129
  */
112
130
  const registeredAutoRefreshes: AnyObject = {};
113
131
 
114
- export async function autoRefreshObject<T extends TypedObject>(object: T, condition: (object: T) => boolean, callback: (object: T) => Promise<T>, interval = 3000) {
132
+ export async function autoRefreshObject<T extends TypedObject>(name: string, object: T, condition: (object: T) => boolean, callback: (object: T) => Promise<T>, interval = 3000) {
133
+ // Always clear any previously registered auto refreshes before creating a new timeout
134
+ stopAutoRefreshObject(name);
135
+
115
136
  if (!object?.id || !object?.__type) {
116
137
  throw new Error("Invalid stored object. Cannot auto-refresh");
117
138
  }
@@ -127,13 +148,11 @@ export async function autoRefreshObject<T extends TypedObject>(object: T, condit
127
148
  storeObject(refreshedObject);
128
149
  }
129
150
 
130
- // Save the timeoutId to the object so it can be cleared when the object refresh is no longer needed
131
- const timeoutId = setTimeout(() => autoRefreshObject(object, condition, callback, interval), interval);
132
- registeredAutoRefreshes[object.__type + ":" + object.id] = timeoutId;
151
+ // Save the autoRefreshId for the object so it can be cleared when the object refresh is no longer needed
152
+ registeredAutoRefreshes[name] = setTimeout(() => autoRefreshObject(name, object, condition, callback, interval), interval);
133
153
  }
134
154
 
135
- export async function stopAutoRefreshObject<T extends TypedObject>(object: T) {
136
- const timeoutId = registeredAutoRefreshes[object.__type + ":" + object.id];
137
-
155
+ export function stopAutoRefreshObject(name: string) {
156
+ const timeoutId = registeredAutoRefreshes[name];
138
157
  timeoutId && clearTimeout(timeoutId);
139
158
  }
@@ -20,7 +20,7 @@ export const request: RequestApi = {
20
20
  async call(url, options) {
21
21
  options = options || {};
22
22
  const abortKey = options?.abortOn !== undefined ? options.abortOn : url + JSON.stringify(options.params || "");
23
- const timestamp = new Date().getTime();
23
+ const timestamp = Date.now();
24
24
 
25
25
  if (abortKey) {
26
26
  const abort = new AbortController();
@@ -47,7 +47,15 @@ export const request: RequestApi = {
47
47
  delete options.params;
48
48
  }
49
49
 
50
- const response = await fetch(request.url(url), options);
50
+ let response = null;
51
+ try {
52
+ response = await fetch(request.url(url), options);
53
+ } catch (e) {
54
+ if (options.ignoreAbort && (e + "").match(/Request was aborted/)) {
55
+ return { abort: true };
56
+ }
57
+ throw e;
58
+ }
51
59
 
52
60
  // Verify the app version of the client and server are matching
53
61
  checkAppVersion(response);
@@ -97,12 +105,13 @@ export const request: RequestApi = {
97
105
  async get(url, options) {
98
106
  return await request.call(url, {
99
107
  method: "get",
108
+ ...options,
100
109
  headers: {
101
110
  Accept: "application/json",
102
111
  "Content-Type": "application/json",
103
- ...danxOptions.value.request?.headers
104
- },
105
- ...options
112
+ ...danxOptions.value.request?.headers,
113
+ ...options?.headers
114
+ }
106
115
  });
107
116
  },
108
117
 
@@ -110,12 +119,13 @@ export const request: RequestApi = {
110
119
  return await request.call(url, {
111
120
  method: "post",
112
121
  body: data && JSON.stringify(data),
122
+ ...options,
113
123
  headers: {
114
124
  Accept: "application/json",
115
125
  "Content-Type": "application/json",
116
- ...danxOptions.value.request?.headers
117
- },
118
- ...options
126
+ ...danxOptions.value.request?.headers,
127
+ ...options?.headers
128
+ }
119
129
  });
120
130
  }
121
131
  };