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.
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({