quasar-ui-danx 0.4.9 → 0.4.12
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/danx.es.js +6509 -6230
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +7 -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 +8 -3
- 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/FieldLabel.vue +18 -15
- package/src/components/ActionTable/Form/Fields/FileUploadButton.vue +1 -0
- package/src/components/ActionTable/Form/Fields/LabelValueBlock.vue +44 -15
- package/src/components/ActionTable/Form/Fields/MultiFileField.vue +1 -1
- package/src/components/ActionTable/Form/Fields/MultiKeywordField.vue +12 -13
- package/src/components/ActionTable/Form/Fields/NumberField.vue +40 -55
- package/src/components/ActionTable/Form/Fields/SelectField.vue +4 -3
- package/src/components/ActionTable/Form/Fields/TextField.vue +31 -12
- package/src/components/ActionTable/Form/RenderedForm.vue +11 -10
- 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/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/config/index.ts +2 -1
- package/src/helpers/FileUpload.ts +59 -8
- package/src/helpers/actions.ts +27 -27
- package/src/helpers/download.ts +8 -2
- package/src/helpers/formats.ts +79 -9
- 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 +11 -0
- package/src/index.ts +1 -0
- package/src/styles/danx.scss +5 -0
- package/src/styles/index.scss +1 -0
- package/src/styles/quasar-reset.scss +2 -0
- package/src/styles/themes/danx/action-table.scss +24 -13
- package/src/styles/themes/danx/forms.scss +1 -19
- package/src/types/actions.d.ts +13 -4
- package/src/types/controls.d.ts +4 -4
- package/src/types/fields.d.ts +10 -9
- package/src/types/files.d.ts +10 -5
- 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
@@ -45,6 +45,12 @@
|
|
45
45
|
v-else
|
46
46
|
class="w-24"
|
47
47
|
/>
|
48
|
+
<div
|
49
|
+
v-if="filename"
|
50
|
+
class="text-[.7rem] bg-slate-900 text-slate-300 opacity-80 h-[2.25rem] py-.5 px-1 absolute-bottom"
|
51
|
+
>
|
52
|
+
{{ filename }}
|
53
|
+
</div>
|
48
54
|
</div>
|
49
55
|
</div>
|
50
56
|
<div
|
@@ -54,15 +60,26 @@
|
|
54
60
|
<slot name="action-button" />
|
55
61
|
</div>
|
56
62
|
<div
|
57
|
-
v-if="
|
58
|
-
class="absolute-bottom w-full"
|
63
|
+
v-if="isUploading || transcodingStatus"
|
64
|
+
class="absolute-bottom w-full bg-slate-800"
|
59
65
|
>
|
60
66
|
<QLinearProgress
|
61
|
-
:value="file.progress"
|
62
|
-
size="
|
63
|
-
color="green-
|
67
|
+
:value="isUploading ? file.progress : (transcodingStatus.progress / 100)"
|
68
|
+
size="36px"
|
69
|
+
:color="isUploading ? 'green-800' : 'blue-800'"
|
70
|
+
:animation-speed="transcodingStatus?.estimate_ms || 3000"
|
64
71
|
stripe
|
65
|
-
|
72
|
+
>
|
73
|
+
<div class="absolute-full flex items-center flex-nowrap text-[.7rem] text-slate-200 justify-start px-1">
|
74
|
+
<QSpinnerPie
|
75
|
+
class="mr-2 text-slate-50 ml-1"
|
76
|
+
size="20"
|
77
|
+
/>
|
78
|
+
<div>
|
79
|
+
{{ isUploading ? "Uploading..." : transcodingStatus.message }}
|
80
|
+
</div>
|
81
|
+
</div>
|
82
|
+
</QLinearProgress>
|
66
83
|
</div>
|
67
84
|
</template>
|
68
85
|
<template v-else>
|
@@ -105,9 +122,9 @@
|
|
105
122
|
</div>
|
106
123
|
|
107
124
|
<FullScreenCarouselDialog
|
108
|
-
v-if="showPreview && !disabled"
|
109
|
-
:files="
|
110
|
-
:default-slide="
|
125
|
+
v-if="showPreview && !disabled && previewableFiles"
|
126
|
+
:files="previewableFiles"
|
127
|
+
:default-slide="previewableFiles[0]?.id || ''"
|
111
128
|
@close="showPreview = false"
|
112
129
|
/>
|
113
130
|
</div>
|
@@ -115,12 +132,21 @@
|
|
115
132
|
|
116
133
|
<script setup lang="ts">
|
117
134
|
import { DocumentTextIcon as TextFileIcon, DownloadIcon, PlayIcon } from "@heroicons/vue/outline";
|
118
|
-
import { computed, ComputedRef, ref } from "vue";
|
119
|
-
import { download } from "../../../helpers";
|
135
|
+
import { computed, ComputedRef, onMounted, ref } from "vue";
|
136
|
+
import { download, FileUpload } from "../../../helpers";
|
120
137
|
import { ImageIcon, PdfIcon, TrashIcon as RemoveIcon } from "../../../svg";
|
121
138
|
import { UploadedFile } from "../../../types";
|
122
139
|
import { FullScreenCarouselDialog } from "../Dialogs";
|
123
140
|
|
141
|
+
export interface FileTranscode {
|
142
|
+
status: "Complete" | "Pending" | "In Progress";
|
143
|
+
progress: number;
|
144
|
+
estimate_ms: number;
|
145
|
+
started_at: string;
|
146
|
+
completed_at: string;
|
147
|
+
message?: string;
|
148
|
+
}
|
149
|
+
|
124
150
|
export interface FilePreviewProps {
|
125
151
|
src?: string;
|
126
152
|
file?: UploadedFile;
|
@@ -149,6 +175,7 @@ const props = withDefaults(defineProps<FilePreviewProps>(), {
|
|
149
175
|
square: false
|
150
176
|
});
|
151
177
|
|
178
|
+
|
152
179
|
const showPreview = ref(false);
|
153
180
|
const computedImage: ComputedRef<UploadedFile | null> = computed(() => {
|
154
181
|
if (props.file) {
|
@@ -159,11 +186,19 @@ const computedImage: ComputedRef<UploadedFile | null> = computed(() => {
|
|
159
186
|
url: props.src,
|
160
187
|
type: "image/" + props.src.split(".").pop()?.toLowerCase(),
|
161
188
|
name: "",
|
162
|
-
size: 0
|
189
|
+
size: 0,
|
190
|
+
__type: "BrowserFile"
|
163
191
|
};
|
164
192
|
}
|
165
193
|
return null;
|
166
194
|
});
|
195
|
+
|
196
|
+
const isUploading = computed(() => !props.file || props.file?.progress !== undefined);
|
197
|
+
const previewableFiles: ComputedRef<[UploadedFile | null]> = computed(() => {
|
198
|
+
return props.relatedFiles?.length > 0 ? props.relatedFiles : [computedImage.value];
|
199
|
+
});
|
200
|
+
|
201
|
+
const filename = computed(() => computedImage.value?.name || computedImage.value?.filename || "");
|
167
202
|
const mimeType = computed(
|
168
203
|
() => computedImage.value?.type || computedImage.value?.mime || ""
|
169
204
|
);
|
@@ -179,6 +214,31 @@ const thumbUrl = computed(() => {
|
|
179
214
|
const isPreviewable = computed(() => {
|
180
215
|
return !!thumbUrl.value || isVideo.value || isImage.value;
|
181
216
|
});
|
217
|
+
|
218
|
+
/**
|
219
|
+
* Resolve the active transcoding operation if there is one, otherwise return null
|
220
|
+
*/
|
221
|
+
const transcodingStatus = computed(() => {
|
222
|
+
let status = null;
|
223
|
+
const metaTranscodes: FileTranscode[] = props.file?.meta?.transcodes || [];
|
224
|
+
|
225
|
+
for (let transcodeName of Object.keys(metaTranscodes)) {
|
226
|
+
const transcode = metaTranscodes[transcodeName];
|
227
|
+
if (!transcode?.completed_at) {
|
228
|
+
return { ...transcode, message: `${transcodeName} ${transcode.status}` };
|
229
|
+
}
|
230
|
+
}
|
231
|
+
|
232
|
+
return status;
|
233
|
+
});
|
234
|
+
|
235
|
+
// Check for an active transcode and make sure the file is being polled for updates
|
236
|
+
onMounted(() => {
|
237
|
+
if (transcodingStatus.value) {
|
238
|
+
(new FileUpload([])).waitForTranscode(props.file);
|
239
|
+
}
|
240
|
+
});
|
241
|
+
|
182
242
|
const isConfirmingRemove = ref(false);
|
183
243
|
function onRemove() {
|
184
244
|
if (!isConfirmingRemove.value) {
|
@@ -24,62 +24,67 @@
|
|
24
24
|
auto-close
|
25
25
|
>
|
26
26
|
<QList>
|
27
|
-
<template
|
27
|
+
<template
|
28
|
+
v-for="item in items"
|
29
|
+
:key="item.name"
|
30
|
+
>
|
28
31
|
<a
|
29
32
|
v-if="item.url"
|
30
|
-
:key="item.url"
|
31
33
|
class="q-item"
|
32
34
|
target="_blank"
|
33
35
|
:href="item.url"
|
34
36
|
:class="item.class"
|
35
37
|
>
|
36
|
-
|
38
|
+
<Component
|
39
|
+
:is="item.icon"
|
40
|
+
v-if="item.icon"
|
41
|
+
:class="item.iconClass"
|
42
|
+
class="mr-3 w-4"
|
43
|
+
/> {{ item.label }}
|
37
44
|
</a>
|
38
45
|
<QItem
|
39
46
|
v-else
|
40
|
-
:key="item.name || item.action"
|
41
47
|
clickable
|
42
48
|
:class="item.class"
|
43
49
|
@click="onAction(item)"
|
44
50
|
>
|
45
|
-
|
51
|
+
<Component
|
52
|
+
:is="item.icon"
|
53
|
+
v-if="item.icon"
|
54
|
+
:class="item.iconClass"
|
55
|
+
class="mr-3 w-4"
|
56
|
+
/> {{ item.label }}
|
46
57
|
</QItem>
|
47
58
|
</template>
|
48
59
|
</QList>
|
49
60
|
</QMenu>
|
50
61
|
</a>
|
51
62
|
</template>
|
52
|
-
<script setup>
|
63
|
+
<script setup lang="ts">
|
53
64
|
import { DotsVerticalIcon as MenuIcon } from "@heroicons/vue/outline";
|
54
65
|
import { QSpinner } from "quasar";
|
66
|
+
import { ResourceAction } from "../../../types";
|
55
67
|
import { RenderComponent } from "../Tools";
|
56
68
|
|
69
|
+
export interface PopoverMenuProps {
|
70
|
+
items: ResourceAction;
|
71
|
+
tooltip?: string;
|
72
|
+
disabled?: boolean;
|
73
|
+
loading?: boolean;
|
74
|
+
loadingComponent?: any;
|
75
|
+
}
|
76
|
+
|
57
77
|
const emit = defineEmits(["action", "action-item"]);
|
58
|
-
defineProps({
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
}
|
65
|
-
},
|
66
|
-
tooltip: {
|
67
|
-
type: String,
|
68
|
-
default: null
|
69
|
-
},
|
70
|
-
disabled: Boolean,
|
71
|
-
loading: Boolean,
|
72
|
-
loadingComponent: {
|
73
|
-
type: [Function, Object],
|
74
|
-
default: () => ({
|
75
|
-
is: QSpinner,
|
76
|
-
props: { class: "w-4 h-4" }
|
77
|
-
})
|
78
|
-
}
|
78
|
+
withDefaults(defineProps<PopoverMenuProps>(), {
|
79
|
+
tooltip: null,
|
80
|
+
loadingComponent: () => ({
|
81
|
+
is: QSpinner,
|
82
|
+
props: { class: "w-4 h-4" }
|
83
|
+
})
|
79
84
|
});
|
80
85
|
|
81
86
|
function onAction(item) {
|
82
|
-
|
83
|
-
|
87
|
+
emit("action", item.name || item.action);
|
88
|
+
emit("action-item", item);
|
84
89
|
}
|
85
90
|
</script>
|
package/src/config/index.ts
CHANGED
@@ -11,7 +11,8 @@ export const danxOptions = shallowRef<DanxOptions>({
|
|
11
11
|
fileUpload: {
|
12
12
|
directory: "file-upload",
|
13
13
|
createPresignedUpload: null,
|
14
|
-
completePresignedUpload: null
|
14
|
+
completePresignedUpload: null,
|
15
|
+
refreshFile: null
|
15
16
|
},
|
16
17
|
flashMessages: {
|
17
18
|
default: {},
|
@@ -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/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
@@ -29,10 +29,9 @@ export function remoteDateTime(dateTimeString: string) {
|
|
29
29
|
}
|
30
30
|
|
31
31
|
/**
|
32
|
-
*
|
33
|
-
* @returns {DateTime|*}
|
32
|
+
* Parses a date string into a Luxon DateTime object
|
34
33
|
*/
|
35
|
-
export function parseDateTime(dateTime: string | DateTime) {
|
34
|
+
export function parseDateTime(dateTime: string | DateTime | null): DateTime {
|
36
35
|
if (typeof dateTime === "string") {
|
37
36
|
dateTime = dateTime.replace("T", " ").replace(/\//g, "-");
|
38
37
|
return DateTime.fromSQL(dateTime);
|
@@ -91,8 +90,8 @@ export function fDateTime(
|
|
91
90
|
dateTime: string | DateTime | null = null,
|
92
91
|
{ format = "M/d/yy h:mma", empty = "- -" }: fDateOptions = {}
|
93
92
|
) {
|
94
|
-
const formatted =
|
95
|
-
return
|
93
|
+
const formatted = parseDateTime(dateTime).toFormat(format).toLowerCase();
|
94
|
+
return ["Invalid DateTime", "invalid datetime"].includes(formatted) ? empty : formatted;
|
96
95
|
}
|
97
96
|
|
98
97
|
/**
|
@@ -151,16 +150,87 @@ export function fCurrency(amount: number, options?: object) {
|
|
151
150
|
}).format(amount);
|
152
151
|
}
|
153
152
|
|
153
|
+
/**
|
154
|
+
* Formats an amount into USD currency format without cents
|
155
|
+
*/
|
156
|
+
export function fCurrencyNoCents(amount: number, options?: object) {
|
157
|
+
return fCurrency(amount, {
|
158
|
+
maximumFractionDigits: 0,
|
159
|
+
...options
|
160
|
+
});
|
161
|
+
}
|
162
|
+
|
154
163
|
/**
|
155
164
|
* Formats a number into a human-readable format
|
156
|
-
* @param number
|
157
|
-
* @param options
|
158
|
-
* @returns {string}
|
159
165
|
*/
|
160
|
-
export function fNumber(number: number, options
|
166
|
+
export function fNumber(number: number, options?: object) {
|
161
167
|
return new Intl.NumberFormat("en-US", options).format(number);
|
162
168
|
}
|
163
169
|
|
170
|
+
/**
|
171
|
+
* Formats a currency into a shorthand human-readable format (ie: $1.2M or $5K)
|
172
|
+
*/
|
173
|
+
export function fShortCurrency(value: string | number, options?: { round: boolean }) {
|
174
|
+
return "$" + fShortNumber(value, options);
|
175
|
+
}
|
176
|
+
|
177
|
+
/**
|
178
|
+
* Formats a number into a shorthand human-readable format (ie: 1.2M or 5K)
|
179
|
+
*/
|
180
|
+
export function fShortNumber(value: string | number, options?: { round: boolean }) {
|
181
|
+
const shorts = [
|
182
|
+
{ pow: 3, unit: "K" },
|
183
|
+
{ pow: 6, unit: "M" },
|
184
|
+
{ pow: 9, unit: "B" },
|
185
|
+
{ pow: 12, unit: "T" },
|
186
|
+
{ pow: 15, unit: "Q" }
|
187
|
+
];
|
188
|
+
|
189
|
+
let n = Math.round(+value);
|
190
|
+
|
191
|
+
const short = shorts.find(({ pow }) => Math.pow(10, pow) < n && Math.pow(10, pow + 3) > n) || null;
|
192
|
+
|
193
|
+
if (short) {
|
194
|
+
n = n / Math.pow(10, short.pow);
|
195
|
+
return options?.round
|
196
|
+
? n + short.unit
|
197
|
+
: n.toFixed(n > 100 ? 0 : 1) + short.unit;
|
198
|
+
}
|
199
|
+
|
200
|
+
return n;
|
201
|
+
}
|
202
|
+
|
203
|
+
/**
|
204
|
+
* Formats a number into a human-readable size format (ie: 1.2MB or 5KB)
|
205
|
+
*/
|
206
|
+
export function fShortSize(value: string | number) {
|
207
|
+
const powers = [
|
208
|
+
{ pow: 0, unit: "B" },
|
209
|
+
{ pow: 10, unit: "KB" },
|
210
|
+
{ pow: 20, unit: "MB" },
|
211
|
+
{ pow: 30, unit: "GB" },
|
212
|
+
{ pow: 40, unit: "TB" },
|
213
|
+
{ pow: 50, unit: "PB" },
|
214
|
+
{ pow: 60, unit: "EB" },
|
215
|
+
{ pow: 70, unit: "ZB" },
|
216
|
+
{ pow: 80, unit: "YB" }
|
217
|
+
];
|
218
|
+
|
219
|
+
const n = Math.round(+value);
|
220
|
+
const power = powers.find((p, i) => {
|
221
|
+
const nextPower = powers[i + 1];
|
222
|
+
return !nextPower || n < Math.pow(2, nextPower.pow + 10);
|
223
|
+
}) || powers[powers.length - 1];
|
224
|
+
|
225
|
+
const div = Math.pow(2, power.pow);
|
226
|
+
|
227
|
+
return Math.round(n / div) + " " + power.unit;
|
228
|
+
}
|
229
|
+
|
230
|
+
export function fBoolean(value: boolean) {
|
231
|
+
return value ? "Yes" : "No";
|
232
|
+
}
|
233
|
+
|
164
234
|
/**
|
165
235
|
* Truncates the string by removing chars from the middle of the string
|
166
236
|
* @param str
|
@@ -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({
|