quasar-ui-danx 0.4.10 → 0.4.13
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/danx.es.js +12389 -7677
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +137 -7
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/index.d.ts +7 -0
- package/index.ts +1 -0
- package/package.json +10 -4
- package/src/components/ActionTable/ActionMenu.vue +26 -31
- package/src/components/ActionTable/ActionTable.vue +4 -1
- package/src/components/ActionTable/Columns/ActionTableColumn.vue +14 -6
- package/src/components/ActionTable/Columns/ActionTableHeaderColumn.vue +63 -42
- package/src/components/ActionTable/Form/ActionForm.vue +55 -0
- package/src/components/ActionTable/Form/Fields/EditOnClickTextField.vue +11 -5
- package/src/components/ActionTable/Form/Fields/FileUploadButton.vue +1 -0
- package/src/components/ActionTable/Form/Fields/MultiFileField.vue +1 -1
- package/src/components/ActionTable/Form/Fields/NumberField.vue +0 -1
- package/src/components/ActionTable/Form/RenderedForm.vue +57 -50
- package/src/components/ActionTable/Form/index.ts +1 -0
- package/src/components/ActionTable/Layouts/ActionTableLayout.vue +3 -3
- package/src/components/ActionTable/TableSummaryRow.vue +48 -37
- package/src/components/ActionTable/Toolbars/ActionToolbar.vue +2 -2
- package/src/components/ActionTable/listControls.ts +3 -2
- package/src/components/PanelsDrawer/PanelsDrawer.vue +15 -5
- package/src/components/PanelsDrawer/PanelsDrawerPanels.vue +3 -1
- package/src/components/PanelsDrawer/PanelsDrawerTabs.vue +17 -4
- package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +30 -5
- package/src/components/Utility/Files/FilePreview.vue +72 -12
- package/src/components/Utility/Popovers/PopoverMenu.vue +34 -29
- package/src/components/Utility/Tools/RenderVnode.vue +5 -1
- package/src/config/index.ts +2 -1
- package/src/helpers/FileUpload.ts +59 -8
- package/src/helpers/actions.ts +27 -27
- package/src/helpers/date.ts +2 -2
- package/src/helpers/download.ts +8 -2
- package/src/helpers/formats.ts +52 -5
- package/src/helpers/multiFileUpload.ts +6 -4
- package/src/helpers/objectStore.ts +14 -17
- package/src/helpers/request.ts +12 -0
- package/src/helpers/singleFileUpload.ts +63 -55
- package/src/helpers/utils.ts +12 -3
- package/src/index.ts +1 -0
- package/src/styles/danx.scss +5 -0
- package/src/styles/index.scss +1 -0
- package/src/styles/themes/danx/action-table.scss +24 -13
- package/src/types/actions.d.ts +16 -7
- package/src/types/controls.d.ts +4 -4
- package/src/types/files.d.ts +10 -5
- package/src/types/forms.d.ts +19 -1
- package/src/types/index.d.ts +0 -1
- package/src/types/requests.d.ts +2 -0
- package/src/types/tables.d.ts +28 -22
- package/src/{vue-plugin.js → vue-plugin.ts} +5 -4
- package/tsconfig.json +1 -0
- package/types/index.d.ts +2 -0
@@ -11,6 +11,8 @@ import {
|
|
11
11
|
} from "../types";
|
12
12
|
import { resolveFileLocation } from "./files";
|
13
13
|
import { FlashMessages } from "./FlashMessages";
|
14
|
+
import { storeObject } from "./objectStore";
|
15
|
+
import { sleep } from "./utils";
|
14
16
|
|
15
17
|
|
16
18
|
export class FileUpload {
|
@@ -22,7 +24,8 @@ export class FileUpload {
|
|
22
24
|
onAllCompleteCb: FileUploadAllCompleteCallback | null = null;
|
23
25
|
options: FileUploadOptions;
|
24
26
|
|
25
|
-
constructor(files: UploadedFile[] | UploadedFile, options?: FileUploadOptions) {
|
27
|
+
constructor(files: UploadedFile[] | UploadedFile, options?: FileUploadOptions | null) {
|
28
|
+
/* @ts-expect-error Files is an array */
|
26
29
|
this.files = !Array.isArray(files) && !(files instanceof FileList) ? [files] : files;
|
27
30
|
this.fileUploads = [];
|
28
31
|
this.onErrorCb = null;
|
@@ -33,6 +36,7 @@ export class FileUpload {
|
|
33
36
|
this.options = {
|
34
37
|
createPresignedUpload: null,
|
35
38
|
completePresignedUpload: null,
|
39
|
+
refreshFile: null,
|
36
40
|
...danxOptions.value.fileUpload,
|
37
41
|
...options
|
38
42
|
};
|
@@ -40,7 +44,6 @@ export class FileUpload {
|
|
40
44
|
if (!this.options.createPresignedUpload || !this.options.completePresignedUpload) {
|
41
45
|
throw new Error("Please configure danxOptions.fileUpload: import { configure } from 'quasar-ui-danx';");
|
42
46
|
}
|
43
|
-
this.prepare();
|
44
47
|
}
|
45
48
|
|
46
49
|
/**
|
@@ -69,6 +72,8 @@ export class FileUpload {
|
|
69
72
|
isComplete: false
|
70
73
|
});
|
71
74
|
}
|
75
|
+
|
76
|
+
return this;
|
72
77
|
}
|
73
78
|
|
74
79
|
/**
|
@@ -110,7 +115,7 @@ export class FileUpload {
|
|
110
115
|
/**
|
111
116
|
* Handles the error events / fires the callback if it is set
|
112
117
|
*/
|
113
|
-
errorHandler(e: InputEvent, file: UploadedFile, error = null) {
|
118
|
+
errorHandler(e: InputEvent | ProgressEvent, file: UploadedFile, error = null) {
|
114
119
|
if (this.onErrorCb) {
|
115
120
|
this.onErrorCb({ e, file, error });
|
116
121
|
}
|
@@ -179,7 +184,8 @@ export class FileUpload {
|
|
179
184
|
progress: file.progress,
|
180
185
|
location: file.location,
|
181
186
|
blobUrl: file.blobUrl,
|
182
|
-
url: ""
|
187
|
+
url: "",
|
188
|
+
__type: "BrowserFile"
|
183
189
|
};
|
184
190
|
}
|
185
191
|
|
@@ -219,12 +225,13 @@ export class FileUpload {
|
|
219
225
|
async (e) => {
|
220
226
|
try {
|
221
227
|
// First complete the presigned upload to get the updated file resource data
|
222
|
-
|
223
|
-
|
228
|
+
let storedFile = await this.completePresignedUpload(fileUpload);
|
229
|
+
storedFile = storeObject(storedFile);
|
224
230
|
// Fire the file complete callbacks
|
225
|
-
this.fireCompleteCallback(fileUpload,
|
231
|
+
this.fireCompleteCallback(fileUpload, storedFile);
|
226
232
|
this.checkAllComplete();
|
227
|
-
|
233
|
+
await this.waitForTranscode(storedFile);
|
234
|
+
} catch (error: any) {
|
228
235
|
this.errorHandler(e, fileUpload.file, error);
|
229
236
|
}
|
230
237
|
},
|
@@ -252,6 +259,49 @@ export class FileUpload {
|
|
252
259
|
return await this.options.completePresignedUpload(fileUpload.file.resource_id);
|
253
260
|
}
|
254
261
|
|
262
|
+
/**
|
263
|
+
* Refresh the file data, in case transcoding or some transient state is needed to be refreshed on the file
|
264
|
+
*/
|
265
|
+
async refreshFile(file: UploadedFile): Promise<UploadedFile | null> {
|
266
|
+
if (!this.options.refreshFile) return null;
|
267
|
+
|
268
|
+
const storedFile = await this.options.refreshFile(file.id);
|
269
|
+
|
270
|
+
if (storedFile) {
|
271
|
+
return storeObject(storedFile);
|
272
|
+
}
|
273
|
+
return storedFile;
|
274
|
+
}
|
275
|
+
|
276
|
+
/**
|
277
|
+
* Checks if the file has a transcode in progress or pending
|
278
|
+
*/
|
279
|
+
isTranscoding(file: UploadedFile) {
|
280
|
+
const metaTranscodes = file?.meta?.transcodes || [];
|
281
|
+
|
282
|
+
for (const transcodeName of Object.keys(metaTranscodes)) {
|
283
|
+
const transcode = metaTranscodes[transcodeName];
|
284
|
+
if (transcode.status === "Pending" || transcode.status === "In Progress") {
|
285
|
+
return true;
|
286
|
+
}
|
287
|
+
}
|
288
|
+
return false;
|
289
|
+
}
|
290
|
+
|
291
|
+
/**
|
292
|
+
* Keeps refreshing the file while there is transcoding in progress
|
293
|
+
*/
|
294
|
+
async waitForTranscode(file: UploadedFile) {
|
295
|
+
// Only allow waiting for transcode 1 time per file
|
296
|
+
if (!file.meta || file.meta.is_waiting_transcode) return;
|
297
|
+
file.meta.is_waiting_transcode = true;
|
298
|
+
let currentFile: UploadedFile | null = file;
|
299
|
+
while (currentFile && this.isTranscoding(currentFile)) {
|
300
|
+
await sleep(1000);
|
301
|
+
currentFile = await this.refreshFile(currentFile);
|
302
|
+
}
|
303
|
+
}
|
304
|
+
|
255
305
|
/**
|
256
306
|
* Start uploading all files
|
257
307
|
*/
|
@@ -297,6 +347,7 @@ export class FileUpload {
|
|
297
347
|
|
298
348
|
// Send all the XHR file uploads
|
299
349
|
for (const fileUpload of this.fileUploads) {
|
350
|
+
// @ts-expect-error XHRFileUpload has a xhr property
|
300
351
|
fileUpload.xhr?.send(fileUpload.body);
|
301
352
|
}
|
302
353
|
}
|
package/src/helpers/actions.ts
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
import { useDebounceFn } from "@vueuse/core";
|
2
2
|
import { uid } from "quasar";
|
3
3
|
import { isReactive, Ref, shallowRef } from "vue";
|
4
|
-
import { ActionOptions, ActionTarget, AnyObject } from "../types";
|
4
|
+
import type { ActionOptions, ActionOptionsPartial, ActionTarget, AnyObject, ResourceAction } from "../types";
|
5
5
|
import { FlashMessages } from "./FlashMessages";
|
6
6
|
import { storeObject } from "./objectStore";
|
7
7
|
|
@@ -10,39 +10,40 @@ export const activeActionVnode: Ref = shallowRef(null);
|
|
10
10
|
/**
|
11
11
|
* Hook to perform an action on a set of targets
|
12
12
|
* This helper allows you to perform actions by name on a set of targets using a provided list of actions
|
13
|
-
*
|
14
|
-
* @param actions
|
15
|
-
* @param {ActionOptions | null} globalOptions
|
16
13
|
*/
|
17
|
-
export function useActions(actions: ActionOptions[], globalOptions:
|
14
|
+
export function useActions(actions: ActionOptions[], globalOptions: ActionOptionsPartial | null = null) {
|
18
15
|
const namespace = uid();
|
19
16
|
|
20
17
|
/**
|
21
18
|
* Resolve the action object based on the provided name (or return the object if the name is already an object)
|
22
19
|
*/
|
23
|
-
function getAction(
|
24
|
-
|
25
|
-
|
20
|
+
function getAction(actionName: string | ActionOptions | ResourceAction): ResourceAction {
|
21
|
+
let actionOptions: ActionOptions | ResourceAction;
|
22
|
+
|
23
|
+
/// Resolve the action options or resource action based on the provided input
|
24
|
+
if (typeof actionName === "string") {
|
25
|
+
actionOptions = actions.find(a => a.name === actionName) || { name: actionName };
|
26
|
+
} else {
|
27
|
+
actionOptions = actionName;
|
26
28
|
}
|
27
29
|
|
28
30
|
// If the action is already reactive, return it
|
29
|
-
if (isReactive(
|
31
|
+
if (isReactive(actionOptions) && "__type" in actionOptions) return actionOptions as ResourceAction;
|
30
32
|
|
31
|
-
|
33
|
+
const resourceAction: ResourceAction = storeObject({
|
34
|
+
...globalOptions,
|
35
|
+
...actionOptions,
|
36
|
+
trigger: (target, input) => performAction(resourceAction, target, input),
|
37
|
+
isApplying: false,
|
38
|
+
__type: "__Action:" + namespace
|
39
|
+
});
|
32
40
|
|
33
41
|
// Assign Trigger function if it doesn't exist
|
34
|
-
if (
|
35
|
-
|
36
|
-
action.trigger = useDebounceFn((target, input) => performAction(action, target, input), action.debounce);
|
37
|
-
} else {
|
38
|
-
action.trigger = (target, input) => performAction(action, target, input);
|
39
|
-
}
|
42
|
+
if (actionOptions.debounce) {
|
43
|
+
resourceAction.trigger = useDebounceFn((target, input) => performAction(resourceAction, target, input), actionOptions.debounce);
|
40
44
|
}
|
41
45
|
|
42
|
-
|
43
|
-
action.isApplying = false;
|
44
|
-
|
45
|
-
return storeObject({ ...action, __type: "__Action:" + namespace });
|
46
|
+
return resourceAction;
|
46
47
|
}
|
47
48
|
|
48
49
|
/**
|
@@ -52,17 +53,17 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionOption
|
|
52
53
|
* @param filters
|
53
54
|
* @returns {ActionOptions[]}
|
54
55
|
*/
|
55
|
-
function getActions(filters?: AnyObject):
|
56
|
+
function getActions(filters?: AnyObject): ResourceAction[] {
|
56
57
|
let filteredActions = [...actions];
|
57
58
|
|
58
59
|
if (filters) {
|
59
|
-
for (const
|
60
|
-
const filterValue = filters[
|
61
|
-
filteredActions = filteredActions.filter((a: AnyObject) => a[
|
60
|
+
for (const filterKey of Object.keys(filters)) {
|
61
|
+
const filterValue = filters[filterKey];
|
62
|
+
filteredActions = filteredActions.filter((a: AnyObject) => a[filterKey] === filterValue || (Array.isArray(filterValue) && filterValue.includes(a[filterKey])));
|
62
63
|
}
|
63
64
|
}
|
64
65
|
|
65
|
-
return filteredActions.map((a:
|
66
|
+
return filteredActions.map((a: ActionOptions) => getAction(a));
|
66
67
|
}
|
67
68
|
|
68
69
|
/**
|
@@ -72,8 +73,7 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionOption
|
|
72
73
|
* @param {object[]|object} target - an array of targets or a single target object
|
73
74
|
* @param {any} input - The input data to pass to the action handler
|
74
75
|
*/
|
75
|
-
async function performAction(action:
|
76
|
-
action = getAction(action);
|
76
|
+
async function performAction(action: ResourceAction, target: ActionTarget = null, input: any = null) {
|
77
77
|
// Resolve the original action, if the current action is an alias
|
78
78
|
const aliasedAction = action.alias ? getAction(action.alias) : null;
|
79
79
|
|
package/src/helpers/date.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import { parseDateTime } from "./formats";
|
2
2
|
|
3
|
-
export function diffInDays(date1, date2) {
|
4
|
-
|
3
|
+
export function diffInDays(date1: string, date2: string) {
|
4
|
+
return parseDateTime(date2).diff(parseDateTime(date1), ["days"]).days;
|
5
5
|
}
|
package/src/helpers/download.ts
CHANGED
@@ -23,6 +23,7 @@ export function download(data: any, strFileName?: string, strMimeType?: string)
|
|
23
23
|
|
24
24
|
var anchor = document.createElement("a");
|
25
25
|
|
26
|
+
// @ts-ignore
|
26
27
|
var toString = function (a) {
|
27
28
|
return String(a);
|
28
29
|
};
|
@@ -35,8 +36,10 @@ export function download(data: any, strFileName?: string, strMimeType?: string)
|
|
35
36
|
var blob;
|
36
37
|
|
37
38
|
var reader;
|
39
|
+
// @ts-ignore
|
38
40
|
myBlob = myBlob.call ? myBlob.bind(self) : Blob;
|
39
41
|
|
42
|
+
// @ts-ignore
|
40
43
|
if (String(this) === "true") {
|
41
44
|
// reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
|
42
45
|
payload = [payload, mimeType];
|
@@ -64,6 +67,7 @@ export function download(data: any, strFileName?: string, strMimeType?: string)
|
|
64
67
|
};
|
65
68
|
ajax.onerror = function (e) {
|
66
69
|
// As a fallback, just open the request in a new tab
|
70
|
+
// @ts-ignore
|
67
71
|
window.open(url, "_blank").focus();
|
68
72
|
};
|
69
73
|
setTimeout(function () {
|
@@ -77,6 +81,7 @@ export function download(data: any, strFileName?: string, strMimeType?: string)
|
|
77
81
|
|
78
82
|
// go ahead and download dataURLs right away
|
79
83
|
if (/^data:[\w+-]+\/[\w+-]+[,;]/.test(payload)) {
|
84
|
+
// @ts-ignore
|
80
85
|
if (payload.length > 1024 * 1024 * 1.999 && myBlob !== toString) {
|
81
86
|
payload = dataUrlToBlob(payload);
|
82
87
|
mimeType = payload.type || defaultMime;
|
@@ -93,13 +98,14 @@ export function download(data: any, strFileName?: string, strMimeType?: string)
|
|
93
98
|
? payload
|
94
99
|
: new myBlob([payload], { type: mimeType });
|
95
100
|
|
96
|
-
function dataUrlToBlob(strUrl) {
|
101
|
+
function dataUrlToBlob(strUrl: string) {
|
97
102
|
var parts = strUrl.split(/[:;,]/);
|
98
103
|
|
99
104
|
var type = parts[1];
|
100
105
|
|
101
106
|
var decoder = parts[2] === "base64" ? atob : decodeURIComponent;
|
102
107
|
|
108
|
+
// @ts-ignore
|
103
109
|
var binData = decoder(parts.pop());
|
104
110
|
|
105
111
|
var mx = binData.length;
|
@@ -113,7 +119,7 @@ export function download(data: any, strFileName?: string, strMimeType?: string)
|
|
113
119
|
return new myBlob([uiArr], { type: type });
|
114
120
|
}
|
115
121
|
|
116
|
-
function saver(url, winMode) {
|
122
|
+
function saver(url: string, winMode: boolean | string) {
|
117
123
|
if ("download" in anchor) {
|
118
124
|
// html5 A[download]
|
119
125
|
anchor.href = url;
|
package/src/helpers/formats.ts
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import { DateTime, IANAZone } from "luxon";
|
2
|
+
import { parse as parseYAML, stringify as stringifyYAML } from "yaml";
|
2
3
|
import { ActionTargetItem, fDateOptions } from "../types";
|
3
4
|
import { isJSON } from "./utils";
|
4
5
|
|
@@ -335,10 +336,56 @@ export function fJSON(string: string | object) {
|
|
335
336
|
}
|
336
337
|
}
|
337
338
|
|
338
|
-
|
339
|
-
|
340
|
-
|
339
|
+
/**
|
340
|
+
* Convert markdown formatted string into a valid JSON object
|
341
|
+
*/
|
342
|
+
export function parseMarkdownJSON(string: string | object): object | null | undefined {
|
343
|
+
if (typeof string === "object") return string as object;
|
344
|
+
|
345
|
+
try {
|
346
|
+
return JSON.parse(parseMarkdownCode(string));
|
347
|
+
} catch (e) {
|
348
|
+
return undefined;
|
341
349
|
}
|
342
|
-
|
343
|
-
|
350
|
+
}
|
351
|
+
|
352
|
+
export function parseMarkdownYAML(string: string): object | null | undefined {
|
353
|
+
try {
|
354
|
+
return parseYAML(parseMarkdownCode(string)) || (string ? undefined : null);
|
355
|
+
} catch (e) {
|
356
|
+
return undefined;
|
357
|
+
}
|
358
|
+
}
|
359
|
+
|
360
|
+
/**
|
361
|
+
* Parse a markdown formatted string and return the code block content
|
362
|
+
*/
|
363
|
+
export function parseMarkdownCode(string: string): string {
|
364
|
+
return string.replace(/^```[a-z0-9]{1,6}\s/, "").replace(/```$/, "");
|
365
|
+
}
|
366
|
+
|
367
|
+
/**
|
368
|
+
* Convert a JSON object or string of code into a markdown formatted JSON string
|
369
|
+
* ie: a valid JSON string with a ```json prefix and ``` postfix
|
370
|
+
*/
|
371
|
+
export function fMarkdownCode(type: string, string: string | object): string {
|
372
|
+
if (typeof string === "object" || isJSON(string)) {
|
373
|
+
switch (type) {
|
374
|
+
case "yaml":
|
375
|
+
string = stringifyYAML(string);
|
376
|
+
break;
|
377
|
+
case "ts":
|
378
|
+
string = "";
|
379
|
+
break;
|
380
|
+
default:
|
381
|
+
string = fJSON(string);
|
382
|
+
}
|
383
|
+
}
|
384
|
+
|
385
|
+
const regex = new RegExp(`\`\`\`${type}`, "g");
|
386
|
+
if (!((string || "") as string).match(regex)) {
|
387
|
+
return `\`\`\`${type}\n${string}\n\`\`\``;
|
388
|
+
}
|
389
|
+
|
390
|
+
return string as string;
|
344
391
|
}
|
@@ -16,14 +16,16 @@ export function useMultiFileUpload(options?: FileUploadOptions) {
|
|
16
16
|
const onFilesSelected = (e: any) => {
|
17
17
|
uploadedFiles.value = [...uploadedFiles.value, ...e.target.files];
|
18
18
|
new FileUpload(e.target.files, options)
|
19
|
-
.
|
20
|
-
|
19
|
+
.prepare()
|
20
|
+
.onProgress(({ file }) => {
|
21
|
+
file && updateFileInList(file);
|
21
22
|
})
|
22
23
|
.onComplete(({ file, uploadedFile }) => {
|
23
24
|
file && updateFileInList(file, uploadedFile);
|
24
25
|
})
|
25
|
-
.onError(({ file
|
26
|
-
|
26
|
+
.onError(({ file, error }) => {
|
27
|
+
console.error("Failed to upload", file, error);
|
28
|
+
FlashMessages.error(`Failed to upload ${file.name}: ${error}`);
|
27
29
|
})
|
28
30
|
.onAllComplete(() => {
|
29
31
|
onCompleteCb.value && onCompleteCb.value({
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import {
|
1
|
+
import { ShallowReactive, shallowReactive } from "vue";
|
2
2
|
import { TypedObject } from "../types";
|
3
3
|
|
4
4
|
const store = new Map<string, any>();
|
@@ -6,27 +6,25 @@ const store = new Map<string, any>();
|
|
6
6
|
/**
|
7
7
|
* Store an object in the object store via type + id
|
8
8
|
* Returns the stored object that should be used instead of the passed object as the returned object is shared across the system
|
9
|
-
*
|
10
|
-
* @param {TypedObject} newObject
|
11
|
-
* @returns {TypedObject}
|
12
9
|
*/
|
13
|
-
export function storeObject<T extends TypedObject>(newObject: T):
|
10
|
+
export function storeObject<T extends TypedObject>(newObject: T): ShallowReactive<T> {
|
14
11
|
const id = newObject.id || newObject.name;
|
15
12
|
const type = newObject.__type;
|
16
|
-
if (!id || !type) return
|
13
|
+
if (!id || !type) return shallowReactive(newObject);
|
14
|
+
|
15
|
+
if (!newObject.__timestamp) {
|
16
|
+
newObject.__timestamp = newObject.updated_at || 0;
|
17
|
+
}
|
17
18
|
|
18
19
|
const objectKey = `${type}:${id}`;
|
19
20
|
|
20
|
-
|
21
|
+
// Retrieve the existing object if it already exists in the store
|
22
|
+
const oldObject = store.get(objectKey);
|
21
23
|
|
22
|
-
//
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
// If the old object is newer, do not store the new object, just return the old
|
27
|
-
if (oldTimestamp && newTimestamp && newTimestamp < oldTimestamp) {
|
28
|
-
return oldObject;
|
29
|
-
}
|
24
|
+
// If an old object exists, and it is newer than the new object, do not store the new object, just return the old
|
25
|
+
// @ts-expect-error __timestamp is guaranteed to be set in this case on both old and new
|
26
|
+
if (oldObject && newObject.__timestamp <= oldObject.__timestamp) {
|
27
|
+
return oldObject;
|
30
28
|
}
|
31
29
|
|
32
30
|
// Recursively store all the children of the object as well
|
@@ -41,12 +39,11 @@ export function storeObject<T extends TypedObject>(newObject: T): UnwrapNestedRe
|
|
41
39
|
|
42
40
|
// Update the old object with the new object properties
|
43
41
|
if (oldObject) {
|
44
|
-
// If the new object is newer, apply all properties from the new object to the old object
|
45
42
|
Object.assign(oldObject, newObject);
|
46
43
|
return oldObject;
|
47
44
|
}
|
48
45
|
|
49
|
-
const reactiveObject =
|
46
|
+
const reactiveObject = shallowReactive(newObject);
|
50
47
|
store.set(objectKey, reactiveObject);
|
51
48
|
return reactiveObject;
|
52
49
|
}
|
package/src/helpers/request.ts
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
import { Ref } from "vue";
|
2
2
|
import { danxOptions } from "../config";
|
3
3
|
import { HttpResponse, RequestApi } from "../types";
|
4
|
+
import { sleep } from "./utils";
|
4
5
|
|
5
6
|
/**
|
6
7
|
* A simple request helper that wraps the fetch API
|
@@ -70,6 +71,17 @@ export const request: RequestApi = {
|
|
70
71
|
|
71
72
|
return result;
|
72
73
|
},
|
74
|
+
|
75
|
+
async poll(url: string, options, interval, fnUntil) {
|
76
|
+
let response;
|
77
|
+
do {
|
78
|
+
response = await request.call(url, options);
|
79
|
+
await sleep(interval);
|
80
|
+
} while (!fnUntil(response));
|
81
|
+
|
82
|
+
return response;
|
83
|
+
},
|
84
|
+
|
73
85
|
async get(url, options) {
|
74
86
|
return await request.call(url, {
|
75
87
|
method: "get",
|
@@ -1,60 +1,68 @@
|
|
1
1
|
import { computed, Ref, ref } from "vue";
|
2
|
-
import {
|
2
|
+
import { FileUploadOptions, UploadedFile } from "../types";
|
3
|
+
import { FileUpload } from "./FileUpload";
|
4
|
+
import { FlashMessages } from "./FlashMessages";
|
3
5
|
|
4
|
-
|
5
|
-
file: UploadedFile;
|
6
|
-
uploadedFile: UploadedFile;
|
7
|
-
}
|
6
|
+
type FileUploadCallback = ((file: UploadedFile | null) => void) | null;
|
8
7
|
|
9
8
|
export function useSingleFileUpload(options: FileUploadOptions | null = null) {
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
9
|
+
const uploadedFile: Ref<UploadedFile | null | undefined> = ref(null);
|
10
|
+
const onCompleteCb: Ref<FileUploadCallback> = ref(null);
|
11
|
+
const onFileChangeCb: Ref<FileUploadCallback> = ref(null);
|
12
|
+
|
13
|
+
const onFileSelected = (e: any) => {
|
14
|
+
uploadedFile.value = null;
|
15
|
+
new FileUpload(e.target?.files[0], options)
|
16
|
+
.prepare()
|
17
|
+
.onProgress(({ file }) => {
|
18
|
+
if (file) {
|
19
|
+
uploadedFile.value = file;
|
20
|
+
onFileChangeCb.value && onFileChangeCb.value(uploadedFile.value);
|
21
|
+
}
|
22
|
+
})
|
23
|
+
.onComplete(({ uploadedFile: completedFile }) => {
|
24
|
+
if (completedFile) {
|
25
|
+
uploadedFile.value = completedFile;
|
26
|
+
onCompleteCb.value && onCompleteCb.value(uploadedFile.value);
|
27
|
+
onFileChangeCb.value && onFileChangeCb.value(uploadedFile.value);
|
28
|
+
}
|
29
|
+
})
|
30
|
+
.onError(({ file, error }) => {
|
31
|
+
console.error("Failed to upload", file, error);
|
32
|
+
FlashMessages.error(`Failed to upload ${file.name}: ${error}`);
|
33
|
+
})
|
34
|
+
.upload();
|
35
|
+
};
|
36
|
+
|
37
|
+
const onDrop = (e: InputEvent) => {
|
38
|
+
onFileSelected({ target: { files: e.dataTransfer?.files } });
|
39
|
+
};
|
40
|
+
|
41
|
+
const isFileUploaded = computed(() => {
|
42
|
+
return uploadedFile.value && uploadedFile.value.url;
|
43
|
+
});
|
44
|
+
|
45
|
+
const onFileChange = (cb: FileUploadCallback) => {
|
46
|
+
onFileChangeCb.value = cb;
|
47
|
+
};
|
48
|
+
|
49
|
+
const onComplete = (cb: FileUploadCallback) => {
|
50
|
+
onCompleteCb.value = cb;
|
51
|
+
};
|
52
|
+
|
53
|
+
const clearUploadedFile = () => {
|
54
|
+
uploadedFile.value = null;
|
55
|
+
onFileChangeCb.value && onFileChangeCb.value(uploadedFile.value);
|
56
|
+
onCompleteCb.value && onCompleteCb.value(uploadedFile.value);
|
57
|
+
};
|
58
|
+
|
59
|
+
return {
|
60
|
+
isFileUploaded,
|
61
|
+
clearUploadedFile,
|
62
|
+
onComplete,
|
63
|
+
onFileChange,
|
64
|
+
onDrop,
|
65
|
+
onFileSelected,
|
66
|
+
uploadedFile
|
67
|
+
};
|
60
68
|
}
|
package/src/helpers/utils.ts
CHANGED
@@ -11,6 +11,15 @@ export function sleep(delay: number) {
|
|
11
11
|
return new Promise((resolve) => setTimeout(resolve, delay));
|
12
12
|
}
|
13
13
|
|
14
|
+
/**
|
15
|
+
* Poll a callback function until the result is true
|
16
|
+
*/
|
17
|
+
export async function pollUntil(callback: () => any, interval = 1000) {
|
18
|
+
while (!(await callback())) {
|
19
|
+
await sleep(interval);
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
14
23
|
/**
|
15
24
|
* Wait for a ref to have a value and then resolve the promise
|
16
25
|
*/
|
@@ -82,12 +91,12 @@ export function incrementName(name: string) {
|
|
82
91
|
* Check if a string is a valid JSON object. If an object is passed, always return true
|
83
92
|
*/
|
84
93
|
export function isJSON(string: string | object) {
|
85
|
-
if (!string) {
|
86
|
-
return false;
|
87
|
-
}
|
88
94
|
if (typeof string === "object") {
|
89
95
|
return true;
|
90
96
|
}
|
97
|
+
if (!string) {
|
98
|
+
return false;
|
99
|
+
}
|
91
100
|
try {
|
92
101
|
JSON.parse(string);
|
93
102
|
return true;
|
package/src/index.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export * from "./vue-plugin";
|
package/src/styles/index.scss
CHANGED