quasar-ui-danx 0.4.9 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/danx.es.js +6509 -6230
- package/dist/danx.es.js.map +1 -1
- package/dist/danx.umd.js +7 -7
- package/dist/danx.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/index.d.ts +7 -0
- package/index.ts +1 -0
- package/package.json +8 -3
- package/src/components/ActionTable/ActionMenu.vue +26 -31
- package/src/components/ActionTable/ActionTable.vue +4 -1
- package/src/components/ActionTable/Columns/ActionTableColumn.vue +14 -6
- package/src/components/ActionTable/Columns/ActionTableHeaderColumn.vue +63 -42
- package/src/components/ActionTable/Form/ActionForm.vue +55 -0
- package/src/components/ActionTable/Form/Fields/EditOnClickTextField.vue +11 -5
- package/src/components/ActionTable/Form/Fields/FieldLabel.vue +18 -15
- package/src/components/ActionTable/Form/Fields/FileUploadButton.vue +1 -0
- package/src/components/ActionTable/Form/Fields/LabelValueBlock.vue +44 -15
- package/src/components/ActionTable/Form/Fields/MultiFileField.vue +1 -1
- package/src/components/ActionTable/Form/Fields/MultiKeywordField.vue +12 -13
- package/src/components/ActionTable/Form/Fields/NumberField.vue +40 -55
- package/src/components/ActionTable/Form/Fields/SelectField.vue +4 -3
- package/src/components/ActionTable/Form/Fields/TextField.vue +31 -12
- package/src/components/ActionTable/Form/RenderedForm.vue +11 -10
- package/src/components/ActionTable/Form/index.ts +1 -0
- package/src/components/ActionTable/Layouts/ActionTableLayout.vue +3 -3
- package/src/components/ActionTable/TableSummaryRow.vue +48 -37
- package/src/components/ActionTable/Toolbars/ActionToolbar.vue +2 -2
- package/src/components/ActionTable/listControls.ts +3 -2
- package/src/components/Utility/Dialogs/FullscreenCarouselDialog.vue +30 -5
- package/src/components/Utility/Files/FilePreview.vue +72 -12
- package/src/components/Utility/Popovers/PopoverMenu.vue +34 -29
- package/src/config/index.ts +2 -1
- package/src/helpers/FileUpload.ts +59 -8
- package/src/helpers/actions.ts +27 -27
- package/src/helpers/download.ts +8 -2
- package/src/helpers/formats.ts +79 -9
- package/src/helpers/multiFileUpload.ts +6 -4
- package/src/helpers/objectStore.ts +14 -17
- package/src/helpers/request.ts +12 -0
- package/src/helpers/singleFileUpload.ts +63 -55
- package/src/helpers/utils.ts +11 -0
- package/src/index.ts +1 -0
- package/src/styles/danx.scss +5 -0
- package/src/styles/index.scss +1 -0
- package/src/styles/quasar-reset.scss +2 -0
- package/src/styles/themes/danx/action-table.scss +24 -13
- package/src/styles/themes/danx/forms.scss +1 -19
- package/src/types/actions.d.ts +13 -4
- package/src/types/controls.d.ts +4 -4
- package/src/types/fields.d.ts +10 -9
- package/src/types/files.d.ts +10 -5
- package/src/types/index.d.ts +0 -1
- package/src/types/requests.d.ts +2 -0
- package/src/types/tables.d.ts +28 -22
- package/src/{vue-plugin.js → vue-plugin.ts} +5 -4
- package/tsconfig.json +1 -0
- package/types/index.d.ts +2 -0
@@ -1,72 +1,49 @@
|
|
1
1
|
<template>
|
2
|
-
<
|
3
|
-
class="dx-number-field
|
4
|
-
|
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 {
|
12
|
+
import { nextTick, ref, watch } from "vue";
|
26
13
|
import { fNumber } from "../../../../helpers";
|
27
|
-
import
|
14
|
+
import { AnyObject, TextFieldProps } from "../../../../types";
|
15
|
+
import TextField from "./TextField";
|
28
16
|
|
29
17
|
const emit = defineEmits(["update:model-value", "update"]);
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
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 =
|
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
|
-
|
4
|
+
v-if="!prependLabel"
|
5
5
|
:label="label"
|
6
|
-
:
|
6
|
+
:required="required"
|
7
|
+
:required-label="requiredLabel"
|
7
8
|
:class="labelClass"
|
8
|
-
:value="readonly ? modelValue : ''"
|
9
9
|
/>
|
10
|
-
<
|
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="
|
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="
|
26
|
+
:class="{'dx-input-prepend-label': prependLabel}"
|
21
27
|
stack-label
|
22
28
|
:type="type"
|
23
29
|
:model-value="modelValue"
|
24
|
-
:maxlength="allowOverMax ? undefined :
|
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
|
31
|
-
:max-length="
|
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:
|
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
|
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;
|
@@ -68,10 +68,10 @@
|
|
68
68
|
</template>
|
69
69
|
<script setup lang="ts">
|
70
70
|
import { computed } from "vue";
|
71
|
-
import { ActionController,
|
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?:
|
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="
|
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 || '
|
39
|
+
:align="column.align || 'right'"
|
40
|
+
:class="column.summaryClass"
|
41
|
+
class="dx-table-summary-fd"
|
40
42
|
>
|
41
|
-
<
|
43
|
+
<div
|
44
|
+
v-if="summary"
|
45
|
+
:class="{'dx-summary-column-link': column.onClick}"
|
46
|
+
>
|
42
47
|
{{ formatValue(column) }}
|
43
|
-
</
|
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
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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(
|
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 {
|
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?:
|
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
|
-
|
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
|
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
|
-
<
|
48
|
-
class="absolute top-
|
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
|
-
|
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) {
|