add support for multiple selections
This commit is contained in:
@@ -120,7 +120,10 @@
|
||||
'flex-items-center',
|
||||
'flex-col',
|
||||
selected != null &&
|
||||
item.id == selected.id
|
||||
selected.filter(
|
||||
(selectedItem) =>
|
||||
selectedItem.id == item.id,
|
||||
).length > 0
|
||||
? 'selected-checked'
|
||||
: 'border-white',
|
||||
]"
|
||||
@@ -223,30 +226,44 @@
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="selected != null">
|
||||
<div v-if="lastSelected != null">
|
||||
<div
|
||||
class="flex flex-col text-xs border-b border-gray-3 pb-4">
|
||||
<p class="m-0 text-bold">
|
||||
{{ selected.title }}
|
||||
</p>
|
||||
<p class="m-0">
|
||||
{{ formatDate(selected.created_at) }}
|
||||
</p>
|
||||
<p class="m-0">
|
||||
{{ bytesReadable(selected.size, 0) }}
|
||||
</p>
|
||||
<p
|
||||
v-if="selected.status != 'OK'"
|
||||
class="m-0 italic">
|
||||
{{ selected.status }}
|
||||
</p>
|
||||
class="flex text-xs border-b border-gray-3 pb-4">
|
||||
<img
|
||||
:src="lastSelected.url"
|
||||
class="max-h-20 max-w-20 mr-2" />
|
||||
<div class="flex flex-col">
|
||||
<p class="m-0 text-bold">
|
||||
{{ lastSelected.title }}
|
||||
</p>
|
||||
<p class="m-0">
|
||||
{{
|
||||
formatDate(
|
||||
lastSelected.created_at,
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p class="m-0">
|
||||
{{
|
||||
bytesReadable(
|
||||
lastSelected.size,
|
||||
0,
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p
|
||||
v-if="lastSelected.status != 'OK'"
|
||||
class="m-0 italic">
|
||||
{{ lastSelected.status }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<SMInput
|
||||
class="mb-2"
|
||||
label="Title"
|
||||
:disabled="!allowEditSelected"
|
||||
v-model:modelValue="selected.title"
|
||||
v-model:modelValue="lastSelected.title"
|
||||
@change="handleUpdate"
|
||||
:small="true" />
|
||||
<SMInput
|
||||
@@ -255,7 +272,7 @@
|
||||
textarea
|
||||
:disabled="!allowEditSelected"
|
||||
v-model:modelValue="
|
||||
selected.description
|
||||
lastSelected.description
|
||||
"
|
||||
@change="handleUpdate"
|
||||
:small="true" />
|
||||
@@ -264,39 +281,123 @@
|
||||
</div>
|
||||
</div>
|
||||
</SMTab>
|
||||
<SMTab
|
||||
id="tab-url"
|
||||
label="Insert from URL"
|
||||
:hide="!props.allowUrl"
|
||||
class="flex flex-1 flex-col flex-items-center flex-justify-center">
|
||||
<div>
|
||||
<h2>Insert image from URL</h2>
|
||||
<SMInput
|
||||
class="mb-2"
|
||||
label="Image URL"
|
||||
control="url"
|
||||
:form="form" />
|
||||
<SMInput
|
||||
class="mb-2"
|
||||
label="Title"
|
||||
control="title"
|
||||
:form="form" />
|
||||
<SMInput
|
||||
class="mb-2"
|
||||
label="Description"
|
||||
textarea
|
||||
control="description"
|
||||
:form="form" />
|
||||
</div>
|
||||
</SMTab>
|
||||
</SMTabGroup>
|
||||
<div class="flex flex-justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="mr-4 font-medium px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
@click="handleClickCancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="computedSelectDisabled"
|
||||
:class="[
|
||||
'font-medium',
|
||||
'px-6',
|
||||
'py-1.5',
|
||||
'rounded-md',
|
||||
'hover:shadow-md',
|
||||
'transition',
|
||||
'text-sm',
|
||||
'bg-sky-600',
|
||||
'hover:bg-sky-500',
|
||||
'text-white',
|
||||
'cursor-pointer',
|
||||
[
|
||||
'disabled-bg-gray',
|
||||
'disabled-text-white',
|
||||
'hover-disabled-bg-gray',
|
||||
'disabled-cursor-not-allowed',
|
||||
],
|
||||
]"
|
||||
@click="handleClickSelect">
|
||||
Select
|
||||
</button>
|
||||
<div class="relative h-38 md:h-15">
|
||||
<ul
|
||||
v-if="props.multiple && selected.length > 0"
|
||||
class="absolute top-0 left-0 right-0 md:right-60 overflow-auto flex p-0 gap-2 flex-row">
|
||||
<li
|
||||
v-for="item in selected"
|
||||
:key="item.id"
|
||||
:class="[
|
||||
'flex',
|
||||
'p-1px',
|
||||
'flex-justify-center',
|
||||
'flex-items-center',
|
||||
'flex-col',
|
||||
]"
|
||||
@click="handleClickItem(item.id)">
|
||||
<div
|
||||
:class="[
|
||||
'flex',
|
||||
'flex-items-center',
|
||||
'flex-justify-center',
|
||||
'h-15',
|
||||
'w-20',
|
||||
'bg-contain',
|
||||
'bg-center',
|
||||
'bg-no-repeat',
|
||||
]"
|
||||
:style="{
|
||||
backgroundImage:
|
||||
item.url.length > 0
|
||||
? `url('${mediaGetVariantUrl(
|
||||
item,
|
||||
'small',
|
||||
)}')`
|
||||
: 'none',
|
||||
backgroundColor:
|
||||
item.status === 'OK'
|
||||
? 'initial'
|
||||
: 'rgba(220,220,220,1)',
|
||||
}">
|
||||
<SMLoading
|
||||
v-if="
|
||||
item.status !== 'OK' &&
|
||||
item.status !== 'Failed'
|
||||
"
|
||||
small
|
||||
class="bg-white bg-op-90 w-full h-full" />
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div
|
||||
class="absolute bottom-0 left-0 right-0 md:left-a flex gap-2 flex-col md:flex-row">
|
||||
<button
|
||||
v-if="!formLoading"
|
||||
type="button"
|
||||
class="mr-4 font-medium block w-full md:inline-block md:w-auto px-6 py-1.5 rounded-md hover:shadow-md transition text-sm bg-sky-600 hover:bg-sky-500 text-white cursor-pointer"
|
||||
@click="handleClickCancel">
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-if="!formLoading"
|
||||
type="button"
|
||||
:disabled="computedSelectDisabled"
|
||||
:class="[
|
||||
'font-medium',
|
||||
'block',
|
||||
'md:inline-block',
|
||||
'w-full',
|
||||
'md:w-auto',
|
||||
'px-6',
|
||||
'py-1.5',
|
||||
'rounded-md',
|
||||
'hover:shadow-md',
|
||||
'transition',
|
||||
'text-sm',
|
||||
'bg-sky-600',
|
||||
'hover:bg-sky-500',
|
||||
'text-white',
|
||||
'cursor-pointer',
|
||||
[
|
||||
'disabled-bg-gray',
|
||||
'disabled-text-white',
|
||||
'hover-disabled-bg-gray',
|
||||
'disabled-cursor-not-allowed',
|
||||
],
|
||||
]"
|
||||
@click="handleClickSelect"
|
||||
:loading="true">
|
||||
Select
|
||||
</button>
|
||||
<SMLoading v-if="formLoading" small />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -321,13 +422,13 @@ import {
|
||||
MediaResponse,
|
||||
} from "../../helpers/api.types";
|
||||
import { useApplicationStore } from "../../store/ApplicationStore";
|
||||
import { mediaGetVariantUrl } from "../../helpers/media";
|
||||
import { mediaGetVariantUrl, mimeMatches } from "../../helpers/media";
|
||||
import SMInput from "../SMInput.vue";
|
||||
import SMLoading from "../SMLoading.vue";
|
||||
import SMTabGroup from "../SMTabGroup.vue";
|
||||
import SMTab from "../SMTab.vue";
|
||||
import { Form, FormControl } from "../../helpers/form";
|
||||
import { And, Min, Required } from "../../helpers/validate";
|
||||
import { Form, FormControl, FormObject } from "../../helpers/form";
|
||||
import { And, Required, Url } from "../../helpers/validate";
|
||||
import { convertFileNameToTitle, userHasPermission } from "../../helpers/utils";
|
||||
import { bytesReadable } from "../../helpers/types";
|
||||
import { SMDate } from "../../helpers/datetime";
|
||||
@@ -338,7 +439,7 @@ import { useUserStore } from "../../store/UserStore";
|
||||
const props = defineProps({
|
||||
mime: {
|
||||
type: String,
|
||||
default: "image/",
|
||||
default: "image/*",
|
||||
required: false,
|
||||
},
|
||||
accepts: {
|
||||
@@ -351,6 +452,16 @@ const props = defineProps({
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
allowUrl: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -363,6 +474,14 @@ const refMediaList = ref<HTMLUListElement | null>(null);
|
||||
const userStore = useUserStore();
|
||||
|
||||
const allowUploads = ref(props.allowUpload && userStore.id);
|
||||
const formLoading = ref(false);
|
||||
const form: FormObject = reactive(
|
||||
Form({
|
||||
url: FormControl("", And([Required(), Url()])),
|
||||
title: FormControl(""),
|
||||
description: FormControl(""),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* The selected tab
|
||||
@@ -374,16 +493,6 @@ const selectedTab = ref("tab-browser");
|
||||
*/
|
||||
const max_upload_size = ref(" ");
|
||||
|
||||
/**
|
||||
* Upload form
|
||||
*/
|
||||
let uploadForm = reactive(
|
||||
Form({
|
||||
title: FormControl("", And([Required(), Min(4)])),
|
||||
description: FormControl(""),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Is the media loading/busy
|
||||
*/
|
||||
@@ -407,7 +516,14 @@ const mediaItems: Ref<Media[]> = ref([]);
|
||||
/**
|
||||
* Selected media item id.
|
||||
*/
|
||||
const selected: Ref<Media | null> = ref(null);
|
||||
const selected: Ref<Media[]> = ref([]);
|
||||
const lastSelected = computed(() => {
|
||||
if (selected.value.length > 0) {
|
||||
return selected.value[selected.value.length - 1];
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
/**
|
||||
* How many media items are we showing per page.
|
||||
@@ -469,12 +585,67 @@ const handleClickCancel = () => {
|
||||
* Handle user clicking the select button.
|
||||
*/
|
||||
const handleClickSelect = async () => {
|
||||
forceUpdate();
|
||||
|
||||
if (selectedTab.value == "tab-browser") {
|
||||
if (selected.value != null) {
|
||||
forceUpdate();
|
||||
closeDialog(selected.value);
|
||||
if (selected.value.length > 0) {
|
||||
if (props.multiple) {
|
||||
closeDialog(selected.value);
|
||||
} else {
|
||||
closeDialog(selected.value[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (selectedTab.value == "tab-url") {
|
||||
formLoading.value = true;
|
||||
if (await form.validate()) {
|
||||
const response = await fetch(form.controls.url.value, {
|
||||
method: "HEAD",
|
||||
});
|
||||
|
||||
if (response.status == 404) {
|
||||
form.controls.url.setValidationResult(
|
||||
false,
|
||||
"File not found on server",
|
||||
);
|
||||
} else if (response.status != 200) {
|
||||
form.controls.url.setValidationResult(
|
||||
false,
|
||||
"Error occurred retrieving file from server",
|
||||
);
|
||||
} else {
|
||||
const mime = response.headers
|
||||
.get("Content-Type")
|
||||
.split(";")[0]
|
||||
.trim();
|
||||
if (!mimeMatches(props.mime, mime)) {
|
||||
form.controls.url.setValidationResult(
|
||||
false,
|
||||
"Invalid file type",
|
||||
);
|
||||
} else {
|
||||
closeDialog({
|
||||
id: "",
|
||||
user_id: "",
|
||||
title: form.controls.title.value,
|
||||
name: "",
|
||||
mime_type: mime,
|
||||
permission: "",
|
||||
size: -1,
|
||||
status: "OK",
|
||||
storage: "",
|
||||
url: form.controls.url.value,
|
||||
description: form.controls.description.value,
|
||||
dimensions: "",
|
||||
variants: {},
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -484,9 +655,21 @@ const handleClickSelect = async () => {
|
||||
*/
|
||||
const handleClickItem = (item_id: string): void => {
|
||||
if (isUUID(item_id)) {
|
||||
selected.value = getMediaItem(item_id);
|
||||
if (props.multiple) {
|
||||
if (
|
||||
selected.value.filter((item) => item.id == item_id).length > 0
|
||||
) {
|
||||
selected.value = selected.value.filter(
|
||||
(item) => item.id != item_id,
|
||||
);
|
||||
} else {
|
||||
selected.value.push(getMediaItem(item_id));
|
||||
}
|
||||
} else {
|
||||
selected.value[0] = getMediaItem(item_id);
|
||||
}
|
||||
} else {
|
||||
selected.value = null;
|
||||
// selected.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -495,14 +678,16 @@ const handleClickItem = (item_id: string): void => {
|
||||
* @param item_id The media id.
|
||||
*/
|
||||
const handleDblClickItem = (item_id: string): void => {
|
||||
if (isUUID(item_id)) {
|
||||
const mediaItem = getMediaItem(item_id);
|
||||
if (mediaItem != null) {
|
||||
closeDialog(mediaItem);
|
||||
return;
|
||||
}
|
||||
if (!props.multiple) {
|
||||
if (isUUID(item_id)) {
|
||||
const mediaItem = getMediaItem(item_id);
|
||||
if (mediaItem != null) {
|
||||
closeDialog(mediaItem);
|
||||
return;
|
||||
}
|
||||
|
||||
closeDialog(false);
|
||||
closeDialog(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -519,8 +704,6 @@ const handleClickSelectFile = async () => {
|
||||
* Upload the file to the server.
|
||||
*/
|
||||
const handleChangeSelectFile = async () => {
|
||||
uploadForm._message = "";
|
||||
|
||||
if (refUploadInput.value != null && refUploadInput.value.files != null) {
|
||||
handleFilesUpload(refUploadInput.value.files);
|
||||
showFileBrowserTab();
|
||||
@@ -712,8 +895,7 @@ const handleLoad = async () => {
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
uploadForm._message =
|
||||
error?.data?.message || "An unexpected error occurred";
|
||||
/* empty */
|
||||
})
|
||||
.finally(() => {
|
||||
mediaLoading.value = false;
|
||||
@@ -730,7 +912,7 @@ const eventKeyPress = (event: KeyboardEvent): boolean => {
|
||||
handleClickCancel();
|
||||
return true;
|
||||
} else if (event.key === "Enter") {
|
||||
if (selected.value != null) {
|
||||
if (selected.value.length > 0) {
|
||||
handleClickSelect();
|
||||
}
|
||||
|
||||
@@ -757,7 +939,14 @@ watch(page, () => {
|
||||
*/
|
||||
const computedSelectDisabled = computed(() => {
|
||||
if (selectedTab.value == "tab-browser") {
|
||||
return selected.value == null || selected.value.status !== "OK";
|
||||
return (
|
||||
selected.value.length == 0 ||
|
||||
selected.value.filter((item) => item.status !== "OK").length != 0
|
||||
);
|
||||
} else if (selectedTab.value == "tab-url") {
|
||||
return (
|
||||
!form.controls.url.isValid() || form.controls.url.value.length == 0
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -813,10 +1002,10 @@ const formatDate = (date) => {
|
||||
|
||||
const allowEditSelected = computed(() => {
|
||||
return (
|
||||
selected.value != null &&
|
||||
lastSelected.value != null &&
|
||||
userStore.id &&
|
||||
(userHasPermission("admin/media") ||
|
||||
selected.value.user_id == userStore.id)
|
||||
lastSelected.value.user_id == userStore.id)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -830,11 +1019,11 @@ interface MediaUpdate {
|
||||
const pendingUpdates = ref<MediaUpdate[]>([]);
|
||||
|
||||
const handleUpdate = () => {
|
||||
if (selected.value != null) {
|
||||
if (lastSelected.value != null) {
|
||||
addUpdate(
|
||||
selected.value.id,
|
||||
selected.value.title,
|
||||
selected.value.description,
|
||||
lastSelected.value.id,
|
||||
lastSelected.value.title,
|
||||
lastSelected.value.description,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -895,6 +1084,7 @@ const postUpdate = (data: MediaUpdate): void => {
|
||||
};
|
||||
|
||||
const forceUpdate = () => {
|
||||
formLoading.value = true;
|
||||
pendingUpdates.value.forEach((item, index) => {
|
||||
if (pendingUpdates.value[index].timer != null) {
|
||||
clearTimeout(pendingUpdates.value[index].timer);
|
||||
@@ -904,6 +1094,7 @@ const forceUpdate = () => {
|
||||
});
|
||||
|
||||
pendingUpdates.value = [];
|
||||
formLoading.value = false;
|
||||
};
|
||||
|
||||
// Get max upload size
|
||||
|
||||
Reference in New Issue
Block a user