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
@@ -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) {