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.
Files changed (57) hide show
  1. package/dist/danx.es.js +6509 -6230
  2. package/dist/danx.es.js.map +1 -1
  3. package/dist/danx.umd.js +7 -7
  4. package/dist/danx.umd.js.map +1 -1
  5. package/dist/style.css +1 -1
  6. package/index.d.ts +7 -0
  7. package/index.ts +1 -0
  8. package/package.json +8 -3
  9. package/src/components/ActionTable/ActionMenu.vue +26 -31
  10. package/src/components/ActionTable/ActionTable.vue +4 -1
  11. package/src/components/ActionTable/Columns/ActionTableColumn.vue +14 -6
  12. package/src/components/ActionTable/Columns/ActionTableHeaderColumn.vue +63 -42
  13. package/src/components/ActionTable/Form/ActionForm.vue +55 -0
  14. package/src/components/ActionTable/Form/Fields/EditOnClickTextField.vue +11 -5
  15. package/src/components/ActionTable/Form/Fields/FieldLabel.vue +18 -15
  16. package/src/components/ActionTable/Form/Fields/FileUploadButton.vue +1 -0
  17. package/src/components/ActionTable/Form/Fields/LabelValueBlock.vue +44 -15
  18. package/src/components/ActionTable/Form/Fields/MultiFileField.vue +1 -1
  19. package/src/components/ActionTable/Form/Fields/MultiKeywordField.vue +12 -13
  20. package/src/components/ActionTable/Form/Fields/NumberField.vue +40 -55
  21. package/src/components/ActionTable/Form/Fields/SelectField.vue +4 -3
  22. package/src/components/ActionTable/Form/Fields/TextField.vue +31 -12
  23. package/src/components/ActionTable/Form/RenderedForm.vue +11 -10
  24. package/src/components/ActionTable/Form/index.ts +1 -0
  25. package/src/components/ActionTable/Layouts/ActionTableLayout.vue +3 -3
  26. package/src/components/ActionTable/TableSummaryRow.vue +48 -37
  27. package/src/components/ActionTable/Toolbars/ActionToolbar.vue +2 -2
  28. package/src/components/ActionTable/listControls.ts +3 -2
  29. package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +30 -5
  30. package/src/components/Utility/Files/FilePreview.vue +72 -12
  31. package/src/components/Utility/Popovers/PopoverMenu.vue +34 -29
  32. package/src/config/index.ts +2 -1
  33. package/src/helpers/FileUpload.ts +59 -8
  34. package/src/helpers/actions.ts +27 -27
  35. package/src/helpers/download.ts +8 -2
  36. package/src/helpers/formats.ts +79 -9
  37. package/src/helpers/multiFileUpload.ts +6 -4
  38. package/src/helpers/objectStore.ts +14 -17
  39. package/src/helpers/request.ts +12 -0
  40. package/src/helpers/singleFileUpload.ts +63 -55
  41. package/src/helpers/utils.ts +11 -0
  42. package/src/index.ts +1 -0
  43. package/src/styles/danx.scss +5 -0
  44. package/src/styles/index.scss +1 -0
  45. package/src/styles/quasar-reset.scss +2 -0
  46. package/src/styles/themes/danx/action-table.scss +24 -13
  47. package/src/styles/themes/danx/forms.scss +1 -19
  48. package/src/types/actions.d.ts +13 -4
  49. package/src/types/controls.d.ts +4 -4
  50. package/src/types/fields.d.ts +10 -9
  51. package/src/types/files.d.ts +10 -5
  52. package/src/types/index.d.ts +0 -1
  53. package/src/types/requests.d.ts +2 -0
  54. package/src/types/tables.d.ts +28 -22
  55. package/src/{vue-plugin.js → vue-plugin.ts} +5 -4
  56. package/tsconfig.json +1 -0
  57. 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="file && file.progress !== undefined"
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="15px"
63
- color="green-600"
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="relatedFiles || [computedImage]"
110
- :default-slide="relatedFiles ? relatedFiles[0].id : (computedImage?.id || '')"
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 v-for="item in items">
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
- {{ item.label }}
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
- {{ item.label }}
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
- items: {
60
- type: Array,
61
- required: true,
62
- validator(items) {
63
- return items.every((item) => item.url || item.action || item.name);
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
- emit("action", item.name || item.action);
83
- emit("action-item", item);
87
+ emit("action", item.name || item.action);
88
+ emit("action-item", item);
84
89
  }
85
90
  </script>
@@ -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
- const uploadedFile = await this.completePresignedUpload(fileUpload);
223
-
228
+ let storedFile = await this.completePresignedUpload(fileUpload);
229
+ storedFile = storeObject(storedFile);
224
230
  // Fire the file complete callbacks
225
- this.fireCompleteCallback(fileUpload, uploadedFile);
231
+ this.fireCompleteCallback(fileUpload, storedFile);
226
232
  this.checkAllComplete();
227
- } catch (error) {
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
  }
@@ -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: ActionOptions | null = null) {
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(action: string | ActionOptions): ActionOptions {
24
- if (typeof action === "string") {
25
- action = actions.find(a => a.name === action) || { name: action };
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(action) && action.__type) return action;
31
+ if (isReactive(actionOptions) && "__type" in actionOptions) return actionOptions as ResourceAction;
30
32
 
31
- action = { ...globalOptions, ...action };
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 (!action.trigger) {
35
- if (action.debounce) {
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
- // Set the initial state for the action
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): ActionOptions[] {
56
+ function getActions(filters?: AnyObject): ResourceAction[] {
56
57
  let filteredActions = [...actions];
57
58
 
58
59
  if (filters) {
59
- for (const filter of Object.keys(filters)) {
60
- const filterValue = filters[filter];
61
- filteredActions = filteredActions.filter((a: AnyObject) => a[filter] === filterValue || (Array.isArray(filterValue) && filterValue.includes(a[filter])));
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: AnyObject) => getAction(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: string | ActionOptions, target: ActionTarget = null, input: any = null) {
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
 
@@ -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;
@@ -29,10 +29,9 @@ export function remoteDateTime(dateTimeString: string) {
29
29
  }
30
30
 
31
31
  /**
32
- * @param {DateTime|String} dateTime
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 = (dateTime ? parseDateTime(dateTime) : DateTime.now()).toFormat(format).toLowerCase();
95
- return formatted === "invalid datetime" ? empty : formatted;
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
- .onProgress(({ file }: { file: UploadedFile }) => {
20
- updateFileInList(file);
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 }: { file: UploadedFile }) => {
26
- FlashMessages.error(`Failed to upload ${file.name}`);
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({