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