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
@@ -1,72 +1,49 @@
1
1
  <template>
2
- <QInput
3
- class="dx-number-field max-w-full"
4
- :class="{'dx-no-prepend-label': hidePrependLabel, 'dx-prepend-label': !hidePrependLabel}"
2
+ <TextField
3
+ class="dx-number-field"
4
+ v-bind="$props"
5
5
  :model-value="numberVal"
6
- :data-testid="'number-field-' + fieldOptions.id"
7
- :placeholder="fieldOptions.placeholder"
8
- outlined
9
- dense
10
- inputmode="numeric"
11
- :input-class="inputClass"
12
6
  @update:model-value="onInput"
13
- >
14
- <template #prepend>
15
- <FieldLabel
16
- :field="fieldOptions"
17
- :show-name="showName"
18
- />
19
- </template>
20
- </QInput>
7
+ />
21
8
  </template>
22
9
 
23
- <script setup>
10
+ <script setup lang="ts">
24
11
  import { useDebounceFn } from "@vueuse/core";
25
- import { computed, nextTick, ref, watch } from "vue";
12
+ import { nextTick, ref, watch } from "vue";
26
13
  import { fNumber } from "../../../../helpers";
27
- import FieldLabel from "./FieldLabel";
14
+ import { AnyObject, TextFieldProps } from "../../../../types";
15
+ import TextField from "./TextField";
28
16
 
29
17
  const emit = defineEmits(["update:model-value", "update"]);
30
- const props = defineProps({
31
- modelValue: {
32
- type: [String, Number],
33
- default: ""
34
- },
35
- precision: {
36
- type: Number,
37
- default: 2
38
- },
39
- label: {
40
- type: String,
41
- default: undefined
42
- },
43
- field: {
44
- type: Object,
45
- default: null
46
- },
47
- inputClass: {
48
- type: [String, Object],
49
- default: ""
50
- },
51
- delay: {
52
- type: Number,
53
- default: 1000
54
- },
55
- hidePrependLabel: Boolean,
56
- currency: Boolean,
57
- showName: Boolean
18
+
19
+ export interface NumberFieldProps extends TextFieldProps {
20
+ precision?: number;
21
+ delay?: number;
22
+ currency?: boolean;
23
+ min?: number;
24
+ max?: number;
25
+ }
26
+
27
+ const props = withDefaults(defineProps<NumberFieldProps>(), {
28
+ modelValue: "",
29
+ precision: 2,
30
+ label: undefined,
31
+ delay: 1000,
32
+ min: undefined,
33
+ max: undefined
58
34
  });
59
35
 
60
36
  const numberVal = ref(format(props.modelValue));
61
- watch(() => props.modelValue, () => numberVal.value = format(props.modelValue));
62
37
 
63
- const fieldOptions = computed(() => props.field || { label: props.label || "", placeholder: "", id: "" });
38
+ watch(() => props.modelValue, () => numberVal.value = format(props.modelValue));
64
39
 
65
40
  function format(number) {
66
41
  if (!number && number !== 0 && number !== "0") return number;
67
42
 
43
+ if (props.type === "number") return number;
44
+
68
45
  const minimumFractionDigits = Math.min(props.precision, ("" + number).split(".")[1]?.length || 0);
69
- let options = {
46
+ let options: AnyObject = {
70
47
  minimumFractionDigits
71
48
  };
72
49
 
@@ -80,14 +57,15 @@ function format(number) {
80
57
  return fNumber(number, options);
81
58
  }
82
59
 
83
- const onUpdateDebounced = useDebounceFn((val) => emit("update", val), props.delay);
60
+ const onUpdateDebounced = useDebounceFn((val: number | string | undefined) => emit("update", val), props.delay);
84
61
 
85
62
  function onInput(value) {
86
- let number = "";
63
+ let number: number | undefined = undefined;
87
64
 
88
65
  // Prevent invalid characters
89
66
  if (value.match(/[^\d.,$]/)) {
90
67
  const oldVal = numberVal.value;
68
+
91
69
  // XXX: To get QInput to show only the value we want
92
70
  numberVal.value += " ";
93
71
  return nextTick(() => numberVal.value = oldVal);
@@ -95,11 +73,18 @@ function onInput(value) {
95
73
 
96
74
  if (value !== "") {
97
75
  value = value.replace(/[^\d.]/g, "");
98
- number = Number(value);
76
+ number = +value;
77
+
78
+ if (props.min) {
79
+ number = Math.max(number, props.min);
80
+ }
81
+ if (props.max) {
82
+ number = Math.min(number, props.max);
83
+ }
84
+
99
85
  numberVal.value = format(number);
100
86
  }
101
87
 
102
- number = number === "" ? undefined : number;
103
88
  emit("update:model-value", number);
104
89
 
105
90
  // Delay the change event, so we only see the value after the user has finished
@@ -37,8 +37,8 @@
37
37
  <template #selected>
38
38
  <div
39
39
  v-if="$props.multiple"
40
- class="flex gap-y-1 overflow-hidden"
41
- :class="{'flex-nowrap gap-y-0': chipLimit === 1, [selectionClass]: true}"
40
+ class="flex gap-y-1 overflow-hidden dx-selected-label"
41
+ :class="{'flex-nowrap gap-y-0': chipLimit === 1, 'dx-selected-chips': chipOptions.length > 0, [selectionClass]: true}"
42
42
  >
43
43
  <template v-if="chipOptions.length > 0">
44
44
  <QChip
@@ -62,6 +62,7 @@
62
62
  <div
63
63
  v-else
64
64
  :class="selectionClass"
65
+ class="dx-selected-label"
65
66
  >
66
67
  {{ selectedLabel }}
67
68
  </div>
@@ -254,8 +255,8 @@ function onUpdate(value) {
254
255
 
255
256
  value = value === "__null__" ? null : value;
256
257
 
257
- emit("update", value);
258
258
  emit("update:model-value", value);
259
+ emit("update", value);
259
260
  }
260
261
 
261
262
  /** XXX: This tells us when we should apply the filter. QSelect likes to trigger a new filter everytime you open the dropdown
@@ -1,15 +1,21 @@
1
1
  <template>
2
2
  <div>
3
3
  <FieldLabel
4
- :field="field"
4
+ v-if="!prependLabel"
5
5
  :label="label"
6
- :show-name="showName"
6
+ :required="required"
7
+ :required-label="requiredLabel"
7
8
  :class="labelClass"
8
- :value="readonly ? modelValue : ''"
9
9
  />
10
- <template v-if="!readonly">
10
+ <div
11
+ v-if="readonly"
12
+ class="dx-text-field-readonly-value"
13
+ >
14
+ {{ modelValue }}
15
+ </div>
16
+ <template v-else>
11
17
  <QInput
12
- :placeholder="field?.placeholder"
18
+ :placeholder="placeholder || (placeholder === '' ? '' : `Enter ${label}`)"
13
19
  outlined
14
20
  dense
15
21
  :readonly="readonly"
@@ -17,18 +23,31 @@
17
23
  :disable="disabled"
18
24
  :label-slot="!noLabel"
19
25
  :input-class="inputClass"
20
- :class="parentClass"
26
+ :class="{'dx-input-prepend-label': prependLabel}"
21
27
  stack-label
22
28
  :type="type"
23
29
  :model-value="modelValue"
24
- :maxlength="allowOverMax ? undefined : field?.maxLength"
30
+ :maxlength="allowOverMax ? undefined : maxLength"
25
31
  :debounce="debounce"
26
32
  @keydown.enter="$emit('submit')"
27
33
  @update:model-value="$emit('update:model-value', $event)"
28
- />
34
+ >
35
+ <template
36
+ v-if="prependLabel"
37
+ #prepend
38
+ >
39
+ <FieldLabel
40
+ class="dx-prepended-label"
41
+ :label="label"
42
+ :required="required"
43
+ :required-label="requiredLabel"
44
+ :class="labelClass"
45
+ />
46
+ </template>
47
+ </QInput>
29
48
  <MaxLengthCounter
30
- :length="modelValue?.length || 0"
31
- :max-length="field?.maxLength"
49
+ :length="(modelValue + '').length || 0"
50
+ :max-length="maxLength"
32
51
  />
33
52
  </template>
34
53
  </div>
@@ -46,9 +65,9 @@ withDefaults(defineProps<TextFieldProps>(), {
46
65
  type: "text",
47
66
  label: "",
48
67
  labelClass: "",
49
- parentClass: "",
50
68
  inputClass: "",
51
69
  maxLength: null,
52
- debounce: 0
70
+ debounce: 0,
71
+ placeholder: null
53
72
  });
54
73
  </script>
@@ -136,7 +136,7 @@ import { ExclamationCircleIcon as MissingIcon, PencilIcon as EditIcon } from "@h
136
136
  import { computed, ref } from "vue";
137
137
  import { fDateTime, FlashMessages, incrementName, replace } from "../../../helpers";
138
138
  import { TrashIcon as RemoveIcon } from "../../../svg";
139
- import { Form, FormFieldValue } from "../../../types";
139
+ import { AnyObject, Form, FormFieldValue } from "../../../types";
140
140
  import { ConfirmDialog, RenderVnode } from "../../Utility";
141
141
  import {
142
142
  BooleanField,
@@ -151,7 +151,7 @@ import {
151
151
  } from "./Fields";
152
152
 
153
153
  export interface RenderedFormProps {
154
- values?: FormFieldValue[] | object;
154
+ values?: FormFieldValue[] | object | null;
155
155
  form: Form;
156
156
  noLabel?: boolean;
157
157
  showName?: boolean;
@@ -171,7 +171,7 @@ const props = withDefaults(defineProps<RenderedFormProps>(), {
171
171
  emptyValue: undefined,
172
172
  fieldClass: "",
173
173
  savingClass: "text-sm text-slate-500 justify-end mt-4",
174
- savedAt: null
174
+ savedAt: undefined
175
175
  });
176
176
 
177
177
  const emit = defineEmits(["update:values"]);
@@ -226,16 +226,16 @@ const currentVariation = ref(variationNames.value[0] || "");
226
226
  const newVariationName = ref("");
227
227
  const variationToEdit = ref<boolean | string>(false);
228
228
  const variationToDelete = ref("");
229
- const canAddVariation = computed(() => props.canModifyVariations && !props.readonly && !props.disable && variationNames.value.length < props.form.variations);
229
+ const canAddVariation = computed(() => props.canModifyVariations && !props.readonly && !props.disable && variationNames.value.length < (props.form.variations || 0));
230
230
 
231
- function getFieldResponse(name, variation: string = undefined) {
231
+ function getFieldResponse(name: string, variation?: string) {
232
232
  if (!fieldResponses.value) return undefined;
233
233
  return fieldResponses.value.find((fr: FormFieldValue) => fr.variation === (variation !== undefined ? variation : currentVariation.value) && fr.name === name);
234
234
  }
235
- function getFieldValue(name) {
235
+ function getFieldValue(name: string) {
236
236
  return getFieldResponse(name)?.value;
237
237
  }
238
- function onInput(name, value) {
238
+ function onInput(name: string, value: any) {
239
239
  const fieldResponse = getFieldResponse(name);
240
240
  const newFieldResponse = {
241
241
  name,
@@ -291,11 +291,12 @@ function updateValues(values: FormFieldValue[]) {
291
291
  let updatedValues: FormFieldValue[] | object = values;
292
292
 
293
293
  if (!Array.isArray(props.values)) {
294
- updatedValues = values.reduce((acc, v) => {
294
+ updatedValues = values.reduce((acc: AnyObject, v) => {
295
295
  acc[v.name] = v.value;
296
296
  return acc;
297
297
  }, {});
298
298
  }
299
+
299
300
  emit("update:values", updatedValues);
300
301
  }
301
302
 
@@ -311,8 +312,8 @@ function onRemoveVariation(name: string) {
311
312
  variationToDelete.value = "";
312
313
  }
313
314
 
314
- function isVariationFormComplete(variation) {
315
- const requiredGroups = {};
315
+ function isVariationFormComplete(variation: string) {
316
+ const requiredGroups: AnyObject = {};
316
317
  return props.form.fields.filter(r => r.required || r.required_group).every((field) => {
317
318
  const fieldResponse = getFieldResponse(field.name, variation);
318
319
  const hasValue = !!fieldResponse && fieldResponse.value !== null;
@@ -1,3 +1,4 @@
1
1
  export * from "./Fields";
2
2
  export * from "./Utilities";
3
+ export { default as ActionForm } from "./ActionForm.vue";
3
4
  export { default as RenderedForm } from "./RenderedForm.vue";
@@ -68,10 +68,10 @@
68
68
  </template>
69
69
  <script setup lang="ts">
70
70
  import { computed } from "vue";
71
- import { ActionController, ActionOptions, ActionPanel, FilterGroup, TableColumn } from "../../../types";
71
+ import { ActionController, ActionPanel, FilterGroup, ResourceAction, TableColumn } from "../../../types";
72
72
  import { PanelsDrawer } from "../../PanelsDrawer";
73
73
  import { PreviousNextControls } from "../../Utility";
74
- import ActionTable from "../ActionTable";
74
+ import ActionTable from "../ActionTable.vue";
75
75
  import { CollapsableFiltersSidebar } from "../Filters";
76
76
  import { ActionToolbar } from "../Toolbars";
77
77
 
@@ -83,7 +83,7 @@ const props = defineProps<{
83
83
  columns: TableColumn[];
84
84
  filters?: FilterGroup[];
85
85
  panels?: ActionPanel[];
86
- actions?: ActionOptions[];
86
+ actions?: ResourceAction[];
87
87
  exporter?: () => Promise<void>;
88
88
  panelTitleField?: string;
89
89
  tableClass?: string;
@@ -4,8 +4,8 @@
4
4
  :class="{'has-selection': selectedCount, 'is-loading': loading}"
5
5
  >
6
6
  <QTd
7
- :colspan="stickyColspan"
8
- class="dx-table-summary-td transition-all"
7
+ :colspan="colspan"
8
+ class="dx-table-summary-td dx-table-summary-count transition-all"
9
9
  :class="{'has-selection': selectedCount}"
10
10
  >
11
11
  <div class="flex flex-nowrap items-center">
@@ -36,60 +36,71 @@
36
36
  <QTd
37
37
  v-for="column in summaryColumns"
38
38
  :key="column.name"
39
- :align="column.align || 'left'"
39
+ :align="column.align || 'right'"
40
+ :class="column.summaryClass"
41
+ class="dx-table-summary-fd"
40
42
  >
41
- <template v-if="summary">
43
+ <div
44
+ v-if="summary"
45
+ :class="{'dx-summary-column-link': column.onClick}"
46
+ >
42
47
  {{ formatValue(column) }}
43
- </template>
48
+ </div>
44
49
  </QTd>
45
50
  </QTr>
46
51
  </template>
47
- <script setup>
52
+ <script setup lang="ts">
48
53
  import { XCircleIcon as ClearIcon } from "@heroicons/vue/solid";
49
54
  import { QSpinner, QTd, QTr } from "quasar";
50
55
  import { computed } from "vue";
51
56
  import { fNumber } from "../../helpers";
57
+ import { TableColumn } from "../../types";
58
+
59
+ interface TableSummaryRowProps {
60
+ loading: boolean;
61
+ label?: string;
62
+ selectedLabel?: string;
63
+ selectedCount?: number;
64
+ itemCount?: number;
65
+ summary?: Record<string, any> | null;
66
+ columns: TableColumn[];
67
+ stickyColspan?: number;
68
+ }
52
69
 
53
70
  defineEmits(["clear"]);
54
- const props = defineProps({
55
- loading: Boolean,
56
- label: {
57
- type: String,
58
- default: "Rows"
59
- },
60
- selectedLabel: {
61
- type: String,
62
- default: "Selected"
63
- },
64
- selectedCount: {
65
- type: Number,
66
- default: 0
67
- },
68
- itemCount: {
69
- type: Number,
70
- default: 0
71
- },
72
- summary: {
73
- type: Object,
74
- default: null
75
- },
76
- columns: {
77
- type: Array,
78
- required: true
79
- },
80
- stickyColspan: {
81
- type: Number,
82
- default: 2
71
+ const props = withDefaults(defineProps<TableSummaryRowProps>(), {
72
+ label: "Rows",
73
+ selectedLabel: "Selected",
74
+ selectedCount: 0,
75
+ itemCount: 0,
76
+ summary: null,
77
+ stickyColspan: null
78
+ });
79
+
80
+ // Allow the colspan for the first summary column w/ count + label to extend out to the first column with summary data
81
+ // (ie: take up as much room as possible without affecting the summary columns)
82
+ const colspan = computed(() => {
83
+ if (props.stickyColspan) return props.stickyColspan;
84
+
85
+ if (props.summary) {
86
+ for (let i = 0; i < props.columns.length; i++) {
87
+ const fieldName = props.columns[i].field || props.columns[i].name;
88
+ if (props.summary[fieldName]) {
89
+ return i + 1;
90
+ }
91
+ }
83
92
  }
93
+
94
+ return props.columns.length + 1;
84
95
  });
85
96
 
86
97
  const summaryColumns = computed(() => {
87
98
  // The sticky columns are where we display the selection count and should not be included in the summary columns
88
- return props.columns.slice(props.stickyColspan - 1);
99
+ return props.columns.slice(colspan.value - 1);
89
100
  });
90
101
 
91
102
  function formatValue(column) {
92
- const value = props.summary[column.name];
103
+ const value = props.summary && props.summary[column.name];
93
104
  if (value === undefined) return "";
94
105
 
95
106
  if (column.format) {
@@ -30,14 +30,14 @@
30
30
  </div>
31
31
  </template>
32
32
  <script setup lang="ts">
33
- import { ActionOptions, ActionTargetItem, AnyObject } from "../../../types";
33
+ import { ActionTargetItem, AnyObject, ResourceAction } from "../../../types";
34
34
  import { ExportButton, RefreshButton } from "../../Utility";
35
35
  import ActionMenu from "../ActionMenu";
36
36
 
37
37
  defineEmits(["refresh"]);
38
38
  defineProps<{
39
39
  title?: string,
40
- actions?: ActionOptions[],
40
+ actions?: ResourceAction[],
41
41
  actionTarget?: ActionTargetItem[],
42
42
  refreshButton?: boolean,
43
43
  loading?: boolean,
@@ -317,7 +317,8 @@ export function useListControls(name: string, options: ListControlsOptions): Act
317
317
  // (ie: tasks, verifications, creatives, etc.)
318
318
  if (options.routes.details) {
319
319
  watch(() => activeItem.value, async (newItem, oldItem) => {
320
- if (newItem && oldItem?.id !== newItem.id) {
320
+ // Note we want a loose comparison in case it's a string vs int for the ID
321
+ if (newItem?.id && oldItem?.id != newItem.id) {
321
322
  await getActiveItemDetails();
322
323
  }
323
324
  });
@@ -328,7 +329,7 @@ export function useListControls(name: string, options: ListControlsOptions): Act
328
329
  */
329
330
  function activatePanel(item: ActionTargetItem | null, panel: string = "") {
330
331
  // If we're already on the correct item and panel, don't do anything
331
- if (item?.id === activeItem.value?.id && panel === activePanel.value) return;
332
+ if (item?.id == activeItem.value?.id && panel === activePanel.value) return;
332
333
 
333
334
  setActiveItem(item);
334
335
  activePanel.value = panel;
@@ -37,17 +37,37 @@
37
37
  </video>
38
38
  </template>
39
39
  <img
40
- v-else
40
+ v-else-if="getPreviewUrl(file)"
41
41
  :alt="file.filename"
42
42
  :src="getPreviewUrl(file)"
43
43
  >
44
+ <div v-else>
45
+ <h3 class="text-center mb-4">
46
+ No Preview Available
47
+ </h3>
48
+ <a
49
+ :href="file.url"
50
+ target="_blank"
51
+ class="text-base"
52
+ >
53
+ {{ file.url }}
54
+ </a>
55
+ </div>
56
+ </div>
57
+
58
+ <div class="text-base text-center py-5 bg-slate-800 opacity-70 text-slate-300 absolute-top hover:opacity-20 transition-all">
59
+ {{ file.filename || file.name }}
44
60
  </div>
45
61
  </QCarouselSlide>
46
62
  </QCarousel>
47
- <CloseIcon
48
- class="absolute top-4 right-4 cursor-pointer text-white w-8 h-8"
63
+ <a
64
+ class="absolute top-0 right-0 text-white flex items-center justify-center w-16 h-16 hover:bg-slate-600 transition-all"
49
65
  @click="$emit('close')"
50
- />
66
+ >
67
+ <CloseIcon
68
+ class="w-8 h-8"
69
+ />
70
+ </a>
51
71
  </div>
52
72
  </QDialog>
53
73
  </template>
@@ -73,8 +93,13 @@ function isVideo(file) {
73
93
  return file.mime?.startsWith("video");
74
94
  }
75
95
 
96
+ function isImage(file) {
97
+ return file.mime?.startsWith("image");
98
+ }
99
+
76
100
  function getPreviewUrl(file) {
77
- return file.optimized?.url || file.blobUrl || file.url;
101
+ // Use the optimized URL first if available. If not, use the URL directly if its an image, otherwise use the thumb URL
102
+ return file.optimized?.url || (isImage(file) ? (file.blobUrl || file.url) : file.thumb?.url);
78
103
  }
79
104
 
80
105
  function getThumbUrl(file) {