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/dist/danx.es.js +6587 -6321
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +85 -85
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ActionTable/Layouts/ActionTableLayout.vue +4 -1
- package/src/components/ActionTable/controls.ts +2 -2
- package/src/components/PanelsDrawer/PanelsDrawer.vue +15 -4
- package/src/components/Utility/Buttons/ActionButton.vue +6 -3
- package/src/components/Utility/Buttons/ShowHideButton.vue +1 -1
- package/src/components/Utility/Files/FilePreview.vue +9 -3
- package/src/components/Utility/Layouts/CollapsableSidebar.vue +41 -51
- package/src/components/Utility/Widgets/LabelPillWidget.vue +40 -0
- package/src/components/Utility/Widgets/LabelValuePillWidget.vue +26 -0
- package/src/components/Utility/Widgets/index.ts +2 -0
- package/src/components/Utility/index.ts +1 -0
- package/src/helpers/actions.ts +15 -2
- package/src/helpers/array.ts +15 -15
- package/src/helpers/objectStore.ts +40 -21
- package/src/helpers/request.ts +18 -8
- package/src/helpers/routes.ts +76 -13
- package/src/helpers/utils.ts +3 -0
- package/src/types/actions.d.ts +1 -0
- package/src/types/controls.d.ts +10 -4
- package/src/types/index.d.ts +1 -0
- package/src/types/requests.d.ts +1 -0
- package/src/types/widgets.d.ts +5 -0
package/package.json
CHANGED
@@ -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(
|
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
|
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
|
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<
|
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-
|
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 ?
|
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);
|
@@ -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<
|
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-
|
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="
|
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="
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
84
|
-
|
71
|
+
isCollapsed.value = state;
|
72
|
+
setItem(props.name + "-is-collapsed", !!isCollapsed.value);
|
85
73
|
}
|
86
74
|
|
87
75
|
function toggleCollapse() {
|
88
|
-
|
89
|
-
|
90
|
-
|
76
|
+
setCollapse(!isCollapsed.value);
|
77
|
+
emit("collapse", isCollapsed.value);
|
78
|
+
emit("update:collapse", isCollapsed.value);
|
91
79
|
}
|
92
80
|
|
93
81
|
onMounted(() => {
|
94
|
-
|
95
|
-
|
82
|
+
emit("collapse", isCollapsed.value);
|
83
|
+
emit("update:collapse", isCollapsed.value);
|
96
84
|
});
|
97
85
|
const style = computed(() => {
|
98
|
-
|
99
|
-
|
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
|
-
|
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>
|
package/src/helpers/actions.ts
CHANGED
@@ -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 {
|
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);
|
package/src/helpers/array.ts
CHANGED
@@ -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
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
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:
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
131
|
-
|
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
|
136
|
-
const timeoutId = registeredAutoRefreshes[
|
137
|
-
|
155
|
+
export function stopAutoRefreshObject(name: string) {
|
156
|
+
const timeoutId = registeredAutoRefreshes[name];
|
138
157
|
timeoutId && clearTimeout(timeoutId);
|
139
158
|
}
|
package/src/helpers/request.ts
CHANGED
@@ -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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
126
|
+
...danxOptions.value.request?.headers,
|
127
|
+
...options?.headers
|
128
|
+
}
|
119
129
|
});
|
120
130
|
}
|
121
131
|
};
|