quasar-ui-danx 0.4.27 → 0.4.29
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +35 -35
- package/dist/danx.es.js +24686 -24119
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +109 -109
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/ActionTable/ActionTable.vue +29 -7
- package/src/components/ActionTable/Filters/FilterableField.vue +14 -2
- package/src/components/ActionTable/Form/ActionForm.vue +17 -12
- package/src/components/ActionTable/Form/Fields/DateField.vue +24 -20
- package/src/components/ActionTable/Form/Fields/DateRangeField.vue +57 -53
- package/src/components/ActionTable/Form/Fields/EditOnClickTextField.vue +9 -2
- package/src/components/ActionTable/Form/Fields/EditableDiv.vue +51 -21
- package/src/components/ActionTable/Form/Fields/FieldLabel.vue +1 -1
- package/src/components/ActionTable/Form/Fields/SelectField.vue +27 -6
- package/src/components/ActionTable/Form/Fields/SelectOrCreateField.vue +56 -0
- package/src/components/ActionTable/Form/Fields/TextField.vue +2 -0
- package/src/components/ActionTable/Form/Fields/index.ts +1 -0
- package/src/components/ActionTable/Form/RenderedForm.vue +7 -20
- package/src/components/ActionTable/Form/Utilities/MaxLengthCounter.vue +1 -1
- package/src/components/ActionTable/Form/Utilities/SaveStateIndicator.vue +37 -0
- package/src/components/ActionTable/Form/Utilities/index.ts +1 -0
- package/src/components/ActionTable/Layouts/ActionTableLayout.vue +20 -23
- package/src/components/ActionTable/Toolbars/ActionToolbar.vue +44 -36
- package/src/components/ActionTable/{listControls.ts → controls.ts} +13 -9
- package/src/components/ActionTable/index.ts +1 -1
- package/src/components/DragAndDrop/ListItemDraggable.vue +45 -31
- package/src/components/DragAndDrop/dragAndDrop.ts +221 -220
- package/src/components/DragAndDrop/listDragAndDrop.ts +269 -227
- package/src/components/PanelsDrawer/PanelsDrawer.vue +7 -7
- package/src/components/PanelsDrawer/PanelsDrawerTabs.vue +3 -3
- package/src/components/Utility/Buttons/ShowHideButton.vue +86 -0
- package/src/components/Utility/Buttons/index.ts +1 -0
- package/src/components/Utility/Dialogs/ActionFormDialog.vue +30 -0
- package/src/components/Utility/Dialogs/CreateNewWithNameDialog.vue +26 -0
- package/src/components/Utility/Dialogs/RenderedFormDialog.vue +50 -0
- package/src/components/Utility/Dialogs/index.ts +3 -0
- package/src/helpers/FileUpload.ts +4 -4
- package/src/helpers/actions.ts +84 -20
- package/src/helpers/files.ts +56 -43
- package/src/helpers/formats.ts +23 -20
- package/src/helpers/objectStore.ts +24 -12
- package/src/types/actions.d.ts +50 -26
- package/src/types/controls.d.ts +23 -25
- package/src/types/fields.d.ts +1 -0
- package/src/types/files.d.ts +2 -2
- package/src/types/index.d.ts +5 -0
- package/src/types/shared.d.ts +9 -0
- package/src/types/tables.d.ts +3 -3
- package/types/vue-shims.d.ts +3 -2
@@ -0,0 +1,30 @@
|
|
1
|
+
<template>
|
2
|
+
<ConfirmDialog
|
3
|
+
:title="title"
|
4
|
+
:confirm-text="confirmText || title"
|
5
|
+
@confirm="$emit('confirm', {name})"
|
6
|
+
@close="$emit('close')"
|
7
|
+
>
|
8
|
+
<ActionForm
|
9
|
+
:form="form"
|
10
|
+
:action="action"
|
11
|
+
:target="target"
|
12
|
+
/>
|
13
|
+
</ConfirmDialog>
|
14
|
+
</template>
|
15
|
+
<script setup lang="ts">
|
16
|
+
import { ref } from "vue";
|
17
|
+
import { ActionTargetItem, Form, ResourceAction } from "../../../types";
|
18
|
+
import { ActionForm } from "../../ActionTable";
|
19
|
+
import ConfirmDialog from "./ConfirmDialog";
|
20
|
+
|
21
|
+
defineEmits(["confirm", "close"]);
|
22
|
+
defineProps<{
|
23
|
+
title: string;
|
24
|
+
confirmText?: string;
|
25
|
+
form: Form;
|
26
|
+
action: ResourceAction;
|
27
|
+
target: ActionTargetItem;
|
28
|
+
}>();
|
29
|
+
const name = ref("");
|
30
|
+
</script>
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<template>
|
2
|
+
<ConfirmDialog
|
3
|
+
:title="title"
|
4
|
+
:confirm-text="confirmText || title"
|
5
|
+
@confirm="$emit('confirm', {name})"
|
6
|
+
@close="$emit('close')"
|
7
|
+
>
|
8
|
+
<TextField
|
9
|
+
v-model="name"
|
10
|
+
label="Name"
|
11
|
+
/>
|
12
|
+
<slot />
|
13
|
+
</ConfirmDialog>
|
14
|
+
</template>
|
15
|
+
<script setup lang="ts">
|
16
|
+
import { ref } from "vue";
|
17
|
+
import { TextField } from "../../ActionTable";
|
18
|
+
import ConfirmDialog from "./ConfirmDialog";
|
19
|
+
|
20
|
+
defineEmits(["confirm", "close"]);
|
21
|
+
defineProps<{
|
22
|
+
title: string;
|
23
|
+
confirmText?: string;
|
24
|
+
}>();
|
25
|
+
const name = ref("");
|
26
|
+
</script>
|
@@ -0,0 +1,50 @@
|
|
1
|
+
<template>
|
2
|
+
<ConfirmDialog
|
3
|
+
:title="title"
|
4
|
+
:confirm-text="confirmText || title"
|
5
|
+
:content-class="contentClass"
|
6
|
+
@confirm="$emit('confirm', input)"
|
7
|
+
@close="$emit('close')"
|
8
|
+
>
|
9
|
+
<RenderedForm
|
10
|
+
v-bind="renderedFormProps"
|
11
|
+
v-model:values="input"
|
12
|
+
empty-value=""
|
13
|
+
>
|
14
|
+
<slot />
|
15
|
+
</RenderedForm>
|
16
|
+
</ConfirmDialog>
|
17
|
+
</template>
|
18
|
+
<script setup lang="ts">
|
19
|
+
import { AnyObject, Form } from "../../../types";
|
20
|
+
import { RenderedForm } from "../../ActionTable/Form";
|
21
|
+
import ConfirmDialog from "./ConfirmDialog";
|
22
|
+
|
23
|
+
defineEmits(["confirm", "close"]);
|
24
|
+
const props = defineProps<{
|
25
|
+
title: string;
|
26
|
+
confirmText?: string;
|
27
|
+
form: Form;
|
28
|
+
noLabel?: boolean;
|
29
|
+
showName?: boolean;
|
30
|
+
disable?: boolean;
|
31
|
+
readonly?: boolean;
|
32
|
+
clearable?: boolean;
|
33
|
+
fieldClass?: string;
|
34
|
+
savingClass?: string;
|
35
|
+
hideSavedAt?: boolean;
|
36
|
+
contentClass?: string;
|
37
|
+
}>();
|
38
|
+
const input = defineModel<AnyObject>();
|
39
|
+
|
40
|
+
const renderedFormProps = {
|
41
|
+
form: props.form,
|
42
|
+
noLabel: props.noLabel,
|
43
|
+
showName: props.showName,
|
44
|
+
disable: props.disable,
|
45
|
+
readonly: props.readonly,
|
46
|
+
clearable: props.clearable,
|
47
|
+
fieldClass: props.fieldClass,
|
48
|
+
savingClass: props.savingClass
|
49
|
+
};
|
50
|
+
</script>
|
@@ -1,6 +1,9 @@
|
|
1
|
+
export { default as ActionFormDialog } from "./ActionFormDialog.vue";
|
1
2
|
export { default as ConfirmActionDialog } from "./ConfirmActionDialog.vue";
|
2
3
|
export { default as ConfirmDialog } from "./ConfirmDialog.vue";
|
4
|
+
export { default as CreateNewWithNameDialog } from "./CreateNewWithNameDialog.vue";
|
3
5
|
export { default as FullScreenCarouselDialog } from "./FullscreenCarouselDialog.vue";
|
4
6
|
export { default as FullScreenDialog } from "./FullScreenDialog.vue";
|
5
7
|
export { default as InfoDialog } from "./InfoDialog.vue";
|
6
8
|
export { default as InputDialog } from "./InputDialog.vue";
|
9
|
+
export { default as RenderedFormDialog } from "./RenderedFormDialog.vue";
|
@@ -243,11 +243,11 @@ export class FileUpload {
|
|
243
243
|
/**
|
244
244
|
* Mark the presigned upload as completed and return the file resource from the platform server
|
245
245
|
*/
|
246
|
-
async completePresignedUpload(
|
246
|
+
async completePresignedUpload(xhrFile: XHRFileUpload) {
|
247
247
|
// Show 95% as the last 5% will be to complete the presigned upload
|
248
|
-
this.fireProgressCallback(
|
248
|
+
this.fireProgressCallback(xhrFile, .95);
|
249
249
|
|
250
|
-
if (!
|
250
|
+
if (!xhrFile.file.resource_id) {
|
251
251
|
throw new Error("File resource ID is required to complete presigned upload");
|
252
252
|
}
|
253
253
|
|
@@ -256,7 +256,7 @@ export class FileUpload {
|
|
256
256
|
}
|
257
257
|
|
258
258
|
// Let the platform know the presigned upload is complete
|
259
|
-
return await this.options.completePresignedUpload(
|
259
|
+
return await this.options.completePresignedUpload(xhrFile.file.resource_id, xhrFile);
|
260
260
|
}
|
261
261
|
|
262
262
|
/**
|
package/src/helpers/actions.ts
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
import { useDebounceFn } from "@vueuse/core";
|
2
|
+
import { FaSolidCopy as CopyIcon, FaSolidPencil as EditIcon, FaSolidTrash as DeleteIcon } from "danx-icon";
|
2
3
|
import { uid } from "quasar";
|
3
|
-
import { isReactive, Ref, shallowRef } from "vue";
|
4
|
-
import
|
4
|
+
import { h, isReactive, Ref, shallowRef } from "vue";
|
5
|
+
import { ConfirmActionDialog, CreateNewWithNameDialog } from "../components";
|
6
|
+
import type { ActionGlobalOptions, ActionOptions, ActionTarget, ListController, ResourceAction } from "../types";
|
5
7
|
import { FlashMessages } from "./FlashMessages";
|
6
8
|
import { storeObject } from "./objectStore";
|
7
9
|
|
@@ -11,7 +13,7 @@ export const activeActionVnode: Ref = shallowRef(null);
|
|
11
13
|
* Hook to perform an action on a set of targets
|
12
14
|
* This helper allows you to perform actions by name on a set of targets using a provided list of actions
|
13
15
|
*/
|
14
|
-
export function useActions(actions: ActionOptions[], globalOptions:
|
16
|
+
export function useActions(actions: ActionOptions[], globalOptions: ActionGlobalOptions | null = null) {
|
15
17
|
const namespace = uid();
|
16
18
|
|
17
19
|
/**
|
@@ -33,6 +35,14 @@ export function useActions(actions: ActionOptions[], globalOptions: Partial<Acti
|
|
33
35
|
return storeObject(extendedAction);
|
34
36
|
}
|
35
37
|
|
38
|
+
/**
|
39
|
+
* Updates an action replacing the old options with the new options
|
40
|
+
*/
|
41
|
+
function modifyAction(actionName: string, actionOptions: Partial<ActionOptions>): ResourceAction {
|
42
|
+
const action = getAction(actionName);
|
43
|
+
return storeObject({ ...action, ...actionOptions });
|
44
|
+
}
|
45
|
+
|
36
46
|
/**
|
37
47
|
* Resolve the action object based on the provided name (or return the object if the name is already an object)
|
38
48
|
*/
|
@@ -48,6 +58,9 @@ export function useActions(actions: ActionOptions[], globalOptions: Partial<Acti
|
|
48
58
|
if (isReactive(baseOptions) && "__type" in baseOptions) return baseOptions as ResourceAction;
|
49
59
|
|
50
60
|
const resourceAction: ResourceAction = storeObject({
|
61
|
+
onAction: globalOptions?.routes?.applyAction,
|
62
|
+
onBatchAction: globalOptions?.routes?.batchAction,
|
63
|
+
onBatchSuccess: globalOptions?.controls?.clearSelectedRows,
|
51
64
|
...globalOptions,
|
52
65
|
...baseOptions,
|
53
66
|
trigger: (target, input) => performAction(resourceAction, target, input),
|
@@ -60,27 +73,22 @@ export function useActions(actions: ActionOptions[], globalOptions: Partial<Acti
|
|
60
73
|
resourceAction.trigger = useDebounceFn((target, input) => performAction(resourceAction, target, input), baseOptions.debounce);
|
61
74
|
}
|
62
75
|
|
76
|
+
// Splice the resourceAction in place of the action in the actions list
|
77
|
+
actions.splice(actions.findIndex(a => a.name === actionName), 1, resourceAction);
|
78
|
+
|
63
79
|
return resourceAction;
|
64
80
|
}
|
65
81
|
|
66
82
|
/**
|
67
|
-
*
|
68
|
-
*
|
69
|
-
*
|
70
|
-
* @param filters
|
71
|
-
* @returns {ActionOptions[]}
|
83
|
+
* Returns a filtered list of actions. Useful for building ordered menus.
|
84
|
+
* NOTE: If an action doesn't already exist, it will be created.
|
72
85
|
*/
|
73
|
-
function getActions(
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
for (const filterKey of Object.keys(filters)) {
|
78
|
-
const filterValue = filters[filterKey];
|
79
|
-
filteredActions = filteredActions.filter((a: AnyObject) => a[filterKey] === filterValue || (Array.isArray(filterValue) && filterValue.includes(a[filterKey])));
|
80
|
-
}
|
86
|
+
function getActions(names: string[]): ResourceAction[] {
|
87
|
+
const filteredActions = [];
|
88
|
+
for (const name of names) {
|
89
|
+
filteredActions.push(getAction(name));
|
81
90
|
}
|
82
|
-
|
83
|
-
return filteredActions.map((a: ActionOptions) => getAction(a.name));
|
91
|
+
return filteredActions;
|
84
92
|
}
|
85
93
|
|
86
94
|
/**
|
@@ -90,11 +98,15 @@ export function useActions(actions: ActionOptions[], globalOptions: Partial<Acti
|
|
90
98
|
* @param {object[]|object} target - an array of targets or a single target object
|
91
99
|
* @param {any} input - The input data to pass to the action handler
|
92
100
|
*/
|
93
|
-
async function performAction(action: ResourceAction, target: ActionTarget = null, input: any = null) {
|
101
|
+
async function performAction(action: ResourceAction | string, target: ActionTarget = null, input: any = null): Promise<any | void> {
|
102
|
+
if (typeof action === "string") {
|
103
|
+
action = getAction(action);
|
104
|
+
}
|
105
|
+
|
94
106
|
// Resolve the original action, if the current action is an alias
|
95
107
|
const aliasedAction = action.alias ? getAction(action.alias) : null;
|
96
108
|
|
97
|
-
const vnode = action.vnode && action.vnode(target);
|
109
|
+
const vnode = action.vnode && action.vnode(target, input);
|
98
110
|
let result: any;
|
99
111
|
|
100
112
|
// Run the onStart handler if it exists and quit the operation if it returns false
|
@@ -160,6 +172,8 @@ export function useActions(actions: ActionOptions[], globalOptions: Partial<Acti
|
|
160
172
|
return {
|
161
173
|
getAction,
|
162
174
|
getActions,
|
175
|
+
action: performAction,
|
176
|
+
modifyAction,
|
163
177
|
extendAction
|
164
178
|
};
|
165
179
|
}
|
@@ -271,3 +285,53 @@ async function onConfirmAction(action: ActionOptions, target: ActionTarget, inpu
|
|
271
285
|
|
272
286
|
return result;
|
273
287
|
}
|
288
|
+
|
289
|
+
export function withDefaultActions(label: string, listController?: ListController): ActionOptions[] {
|
290
|
+
return [
|
291
|
+
{
|
292
|
+
name: "create",
|
293
|
+
label: "Create " + label,
|
294
|
+
vnode: () => h(CreateNewWithNameDialog, { title: "Create " + label }),
|
295
|
+
onFinish: listController && ((result) => {
|
296
|
+
listController.activatePanel(result.item, "edit");
|
297
|
+
listController.loadListAndSummary();
|
298
|
+
})
|
299
|
+
},
|
300
|
+
{
|
301
|
+
name: "update",
|
302
|
+
optimistic: true
|
303
|
+
},
|
304
|
+
{
|
305
|
+
name: "update-debounced",
|
306
|
+
alias: "update",
|
307
|
+
debounce: 1000,
|
308
|
+
optimistic: true
|
309
|
+
},
|
310
|
+
{
|
311
|
+
name: "copy",
|
312
|
+
label: "Copy",
|
313
|
+
icon: CopyIcon,
|
314
|
+
onSuccess: listController?.loadListAndSummary
|
315
|
+
},
|
316
|
+
{
|
317
|
+
name: "edit",
|
318
|
+
label: "Edit",
|
319
|
+
icon: EditIcon,
|
320
|
+
onAction: (action, target) => listController?.activatePanel(target, "edit")
|
321
|
+
},
|
322
|
+
{
|
323
|
+
name: "delete",
|
324
|
+
label: "Delete",
|
325
|
+
class: "text-red-500",
|
326
|
+
iconClass: "text-red-500",
|
327
|
+
icon: DeleteIcon,
|
328
|
+
onFinish: listController?.loadListAndSummary,
|
329
|
+
vnode: (target: ActionTarget) => h(ConfirmActionDialog, {
|
330
|
+
action: "Delete",
|
331
|
+
label,
|
332
|
+
target,
|
333
|
+
confirmClass: "bg-red-900"
|
334
|
+
})
|
335
|
+
}
|
336
|
+
];
|
337
|
+
}
|
package/src/helpers/files.ts
CHANGED
@@ -1,53 +1,66 @@
|
|
1
1
|
import ExifReader from "exifreader";
|
2
|
+
import { UploadedFile } from "src/types";
|
2
3
|
import { useCompatibility } from "./compatibility";
|
3
4
|
import { FlashMessages } from "./FlashMessages";
|
4
5
|
|
5
|
-
export async function resolveFileLocation(file, waitMessage = null) {
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
export async function resolveFileLocation(file: UploadedFile, waitMessage: string | null = null) {
|
7
|
+
if (file.location) {
|
8
|
+
return file.location;
|
9
|
+
}
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
11
|
+
try {
|
12
|
+
const tags = await ExifReader.load(file.blobUrl || file.url || "", {
|
13
|
+
expanded: true
|
14
|
+
});
|
15
|
+
if (tags.gps) {
|
16
|
+
return {
|
17
|
+
latitude: tags.gps.Latitude,
|
18
|
+
longitude: tags.gps.Longitude
|
19
|
+
};
|
20
|
+
}
|
21
|
+
} catch (error) {
|
22
|
+
console.error("Failed to load EXIF data from file:", error);
|
23
|
+
}
|
20
24
|
|
21
|
-
|
25
|
+
try {
|
26
|
+
const { waitForLocation, location } = useCompatibility();
|
22
27
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
// Show a waiting for location message if we have not returned within 1 second
|
29
|
+
if (waitMessage) {
|
30
|
+
setTimeout(() => {
|
31
|
+
if (!location.value && waitMessage) {
|
32
|
+
FlashMessages.warning(waitMessage);
|
33
|
+
}
|
34
|
+
}, 1000);
|
35
|
+
}
|
31
36
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
// Wait for the browser to return the location (https only as http will not return a location)
|
38
|
+
if (window.location.protocol === "https:") {
|
39
|
+
await waitForLocation();
|
40
|
+
} else if (window.location.href.match("localhost")) {
|
41
|
+
// XXX: Special case for local development so we can test without https
|
42
|
+
return {
|
43
|
+
latitude: 37.7749,
|
44
|
+
longitude: -122.4194,
|
45
|
+
accuracy: 1,
|
46
|
+
altitude: 0,
|
47
|
+
altitudeAccuracy: 0
|
48
|
+
};
|
49
|
+
}
|
41
50
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
51
|
+
if (!location.value) {
|
52
|
+
return null;
|
53
|
+
}
|
54
|
+
|
55
|
+
return {
|
56
|
+
latitude: location.value.latitude,
|
57
|
+
longitude: location.value.longitude,
|
58
|
+
accuracy: location.value.accuracy,
|
59
|
+
altitude: location.value.altitude,
|
60
|
+
altitudeAccuracy: location.value.altitudeAccuracy
|
61
|
+
};
|
62
|
+
} catch (error) {
|
63
|
+
console.error(error);
|
64
|
+
return null;
|
65
|
+
}
|
53
66
|
}
|
package/src/helpers/formats.ts
CHANGED
@@ -32,32 +32,35 @@ export function remoteDateTime(dateTimeString: string) {
|
|
32
32
|
/**
|
33
33
|
* Parses a date string into a Luxon DateTime object
|
34
34
|
*/
|
35
|
-
export function parseDateTime(dateTime: string | DateTime | null): DateTime {
|
35
|
+
export function parseDateTime(dateTime: string | DateTime | null): DateTime<boolean> | null {
|
36
36
|
if (typeof dateTime === "string") {
|
37
|
-
dateTime
|
38
|
-
return DateTime.fromSQL(dateTime);
|
37
|
+
return parseSqlDateTime(dateTime) || parseQDate(dateTime) || parseQDateTime(dateTime);
|
39
38
|
}
|
40
39
|
return dateTime || DateTime.fromSQL("0000-00-00 00:00:00");
|
41
40
|
}
|
42
41
|
|
42
|
+
/**
|
43
|
+
* Parses a SQL formatted date string into a Luxon DateTime object
|
44
|
+
*/
|
45
|
+
export function parseSqlDateTime(dateTime: string) {
|
46
|
+
const parsed = DateTime.fromSQL(dateTime.replace("T", " ").replace(/\//g, "-"));
|
47
|
+
return parsed.isValid ? parsed : null;
|
48
|
+
}
|
49
|
+
|
43
50
|
/**
|
44
51
|
* Parses a Quasar formatted date string into a Luxon DateTime object
|
45
|
-
* @param date
|
46
|
-
* @param format
|
47
|
-
* @returns {DateTime}
|
48
52
|
*/
|
49
|
-
export function parseQDate(date: string, format = "yyyy/MM/dd") {
|
50
|
-
|
53
|
+
export function parseQDate(date: string, format = "yyyy/MM/dd"): DateTime<boolean> | null {
|
54
|
+
const parsed = DateTime.fromFormat(date, format);
|
55
|
+
return parsed.isValid ? parsed : null;
|
51
56
|
}
|
52
57
|
|
53
58
|
/**
|
54
59
|
* Parses a Quasar formatted date/time string into a Luxon DateTime object
|
55
|
-
* @param date
|
56
|
-
* @param format
|
57
|
-
* @returns {DateTime}
|
58
60
|
*/
|
59
|
-
export function parseQDateTime(date: string, format = "yyyy/MM/dd HH:mm:ss") {
|
60
|
-
|
61
|
+
export function parseQDateTime(date: string, format = "yyyy/MM/dd HH:mm:ss"): DateTime<boolean> | null {
|
62
|
+
const parsed = DateTime.fromFormat(date, format);
|
63
|
+
return parsed.isValid ? parsed : null;
|
61
64
|
}
|
62
65
|
|
63
66
|
/**
|
@@ -91,8 +94,8 @@ export function fDateTime(
|
|
91
94
|
dateTime: string | DateTime | null = null,
|
92
95
|
{ format = "M/d/yy h:mma", empty = "- -" }: fDateOptions = {}
|
93
96
|
) {
|
94
|
-
const formatted = parseDateTime(dateTime)
|
95
|
-
return
|
97
|
+
const formatted = parseDateTime(dateTime)?.toFormat(format).toLowerCase();
|
98
|
+
return formatted || empty;
|
96
99
|
}
|
97
100
|
|
98
101
|
/**
|
@@ -111,9 +114,9 @@ export function dbDateTime(dateTime: string | DateTime | null = null) {
|
|
111
114
|
* @param format
|
112
115
|
* @returns {string}
|
113
116
|
*/
|
114
|
-
export function fDate(dateTime: string, { empty = "--", format = "M/d/yy" }: fDateOptions = {}) {
|
115
|
-
const formatted = parseDateTime(dateTime)
|
116
|
-
return
|
117
|
+
export function fDate(dateTime: string | DateTime | null, { empty = "--", format = "M/d/yy" }: fDateOptions = {}) {
|
118
|
+
const formatted = parseDateTime(dateTime)?.toFormat(format || "M/d/yy");
|
119
|
+
return formatted || empty;
|
117
120
|
}
|
118
121
|
|
119
122
|
/**
|
@@ -130,8 +133,8 @@ export function fSecondsToTime(second: number) {
|
|
130
133
|
|
131
134
|
export function fElapsedTime(start: string, end?: string) {
|
132
135
|
const endDateTime = end ? parseDateTime(end) : DateTime.now();
|
133
|
-
const diff = endDateTime
|
134
|
-
if (!diff
|
136
|
+
const diff = endDateTime?.diff(parseDateTime(start) || DateTime.now(), ["hours", "minutes", "seconds"]);
|
137
|
+
if (!diff?.isValid) {
|
135
138
|
return "-";
|
136
139
|
}
|
137
140
|
const hours = Math.floor(diff.hours);
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { uid } from "quasar";
|
2
2
|
import { ShallowReactive, shallowReactive } from "vue";
|
3
|
-
import { TypedObject } from "../types";
|
3
|
+
import { AnyObject, TypedObject } from "../types";
|
4
4
|
import { FlashMessages } from "./FlashMessages";
|
5
5
|
|
6
6
|
const store = new Map<string, any>();
|
@@ -18,7 +18,7 @@ export function storeObjects<T extends TypedObject>(newObjects: T[]) {
|
|
18
18
|
* Store an object in the object store via type + id
|
19
19
|
* Returns the stored object that should be used instead of the passed object as the returned object is shared across the system
|
20
20
|
*/
|
21
|
-
export function storeObject<T extends TypedObject>(newObject: T): ShallowReactive<T> {
|
21
|
+
export function storeObject<T extends TypedObject>(newObject: T, recentlyStoredObjects: AnyObject = {}): ShallowReactive<T> {
|
22
22
|
const id = newObject?.id || newObject?.name;
|
23
23
|
const type = newObject?.__type;
|
24
24
|
if (!id || !type) return shallowReactive(newObject);
|
@@ -32,38 +32,50 @@ export function storeObject<T extends TypedObject>(newObject: T): ShallowReactiv
|
|
32
32
|
|
33
33
|
const objectKey = `${type}:${id}`;
|
34
34
|
|
35
|
+
// If the object was recently stored, return the recently stored object to avoid infinite recursion
|
36
|
+
if (recentlyStoredObjects[objectKey]) {
|
37
|
+
return recentlyStoredObjects[objectKey];
|
38
|
+
}
|
39
|
+
|
35
40
|
// Retrieve the existing object if it already exists in the store
|
36
41
|
const oldObject = store.get(objectKey);
|
37
42
|
|
38
43
|
// If an old object exists, and it is newer than the new object, do not store the new object, just return the old
|
44
|
+
// NOTE: If the timestamp is the same, its possible the intention is to update the existing object, so DO NOT return old object in this case
|
39
45
|
// @ts-expect-error __timestamp is guaranteed to be set in this case on both old and new
|
40
|
-
if (oldObject && newObject.__timestamp
|
46
|
+
if (oldObject && newObject.__timestamp < oldObject.__timestamp) {
|
47
|
+
recentlyStoredObjects[objectKey] = oldObject;
|
41
48
|
return oldObject;
|
42
49
|
}
|
43
50
|
|
51
|
+
// Reference to the reactive version of the object so we can update all the child relationships and return the reactive object
|
52
|
+
const reactiveObject = oldObject || shallowReactive(newObject);
|
53
|
+
|
54
|
+
// Make sure to store the object in the recently stored objects to avoid infinite recursion
|
55
|
+
recentlyStoredObjects[objectKey] = reactiveObject;
|
56
|
+
|
44
57
|
// Recursively store all the children of the object as well
|
45
58
|
for (const key of Object.keys(newObject)) {
|
46
59
|
const value = newObject[key];
|
47
60
|
if (Array.isArray(value) && value.length > 0) {
|
48
61
|
for (const index in value) {
|
49
62
|
if (value[index] && typeof value[index] === "object") {
|
50
|
-
newObject[key][index] = storeObject(value[index]);
|
63
|
+
newObject[key][index] = storeObject(value[index], recentlyStoredObjects);
|
51
64
|
}
|
52
65
|
}
|
53
66
|
} else if (value?.__type) {
|
54
|
-
// @ts-expect-error
|
55
|
-
newObject[key] = storeObject(value);
|
67
|
+
// @ts-expect-error __type is guaranteed to be set in this case
|
68
|
+
newObject[key] = storeObject(value as TypedObject, recentlyStoredObjects);
|
56
69
|
}
|
57
70
|
}
|
58
71
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
72
|
+
Object.assign(reactiveObject, newObject);
|
73
|
+
|
74
|
+
if (!oldObject) {
|
75
|
+
// Store the reactive object in the store if there was not already one existing
|
76
|
+
store.set(objectKey, reactiveObject);
|
63
77
|
}
|
64
78
|
|
65
|
-
const reactiveObject = shallowReactive(newObject);
|
66
|
-
store.set(objectKey, reactiveObject);
|
67
79
|
return reactiveObject;
|
68
80
|
}
|
69
81
|
|