quasar-ui-danx 0.4.9 → 0.4.12
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 +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({
|