feat: support changing attachment display name (#6504)

#### What type of PR is this?

/area ui
/kind feature
/milestone 2.19.x

#### What this PR does / why we need it:

支持修改附件的显示名称。

Fixes https://github.com/halo-dev/halo/issues/4294

<img width="669" alt="image" src="https://github.com/user-attachments/assets/03571048-dfed-4714-ae86-f527ea6f0b08">

#### Does this PR introduce a user-facing change?

```release-note
支持修改附件的显示名称。
```
pull/6512/head
Ryan Wang 2024-08-25 23:11:11 +08:00 committed by GitHub
parent c92bbd754a
commit 2b84b41987
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 253 additions and 133 deletions

View File

@ -178,7 +178,6 @@ const handleCheckAllChange = (e: Event) => {
const onDetailModalClose = () => { const onDetailModalClose = () => {
selectedAttachment.value = undefined; selectedAttachment.value = undefined;
nameQuery.value = undefined; nameQuery.value = undefined;
nameQueryAttachment.value = undefined;
detailVisible.value = false; detailVisible.value = false;
handleFetchAttachments(); handleFetchAttachments();
}; };
@ -209,16 +208,15 @@ const viewType = useLocalStorage("attachment-view-type", "list");
const routeQueryAction = useRouteQuery<string | undefined>("action"); const routeQueryAction = useRouteQuery<string | undefined>("action");
onMounted(() => { onMounted(() => {
if (!routeQueryAction.value) {
return;
}
if (routeQueryAction.value === "upload") { if (routeQueryAction.value === "upload") {
uploadVisible.value = true; uploadVisible.value = true;
} }
if (nameQuery.value) {
detailVisible.value = true;
}
}); });
const nameQuery = useRouteQuery<string | undefined>("name"); const nameQuery = useRouteQuery<string | undefined>("name");
const nameQueryAttachment = ref<Attachment>();
watch( watch(
() => selectedAttachment.value, () => selectedAttachment.value,
@ -228,25 +226,11 @@ watch(
} }
} }
); );
onMounted(() => {
if (!nameQuery.value) {
return;
}
coreApiClient.storage.attachment
.getAttachment({
name: nameQuery.value,
})
.then((response) => {
nameQueryAttachment.value = response.data;
detailVisible.value = true;
});
});
</script> </script>
<template> <template>
<AttachmentDetailModal <AttachmentDetailModal
v-if="detailVisible" v-if="detailVisible"
:attachment="selectedAttachment || nameQueryAttachment" :name="selectedAttachment?.metadata.name || nameQuery"
@close="onDetailModalClose" @close="onDetailModalClose"
> >
<template #actions> <template #actions>

View File

@ -2,28 +2,29 @@
import LazyImage from "@/components/image/LazyImage.vue"; import LazyImage from "@/components/image/LazyImage.vue";
import { formatDatetime } from "@/utils/date"; import { formatDatetime } from "@/utils/date";
import { isImage } from "@/utils/image"; import { isImage } from "@/utils/image";
import type { Attachment } from "@halo-dev/api-client";
import { coreApiClient } from "@halo-dev/api-client"; import { coreApiClient } from "@halo-dev/api-client";
import { import {
IconRiPencilFill,
VButton, VButton,
VDescription, VDescription,
VDescriptionItem, VDescriptionItem,
VLoading,
VModal, VModal,
VSpace, VSpace,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query"; import { useQuery } from "@tanstack/vue-query";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { computed, ref } from "vue"; import { computed, ref, toRefs } from "vue";
import { useFetchAttachmentGroup } from "../composables/use-attachment-group";
import AttachmentPermalinkList from "./AttachmentPermalinkList.vue"; import AttachmentPermalinkList from "./AttachmentPermalinkList.vue";
import DisplayNameEditForm from "./DisplayNameEditForm.vue";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
attachment: Attachment | undefined; name?: string;
mountToBody?: boolean; mountToBody?: boolean;
}>(), }>(),
{ {
attachment: undefined, name: undefined,
mountToBody: false, mountToBody: false,
} }
); );
@ -32,16 +33,31 @@ const emit = defineEmits<{
(event: "close"): void; (event: "close"): void;
}>(); }>();
const { groups } = useFetchAttachmentGroup(); const { name } = toRefs(props);
const onlyPreview = ref(false); const onlyPreview = ref(false);
const { data: attachment, isLoading } = useQuery({
queryKey: ["core:attachment-by-name", name],
queryFn: async () => {
const { data } = await coreApiClient.storage.attachment.getAttachment({
name: name.value as string,
});
return data;
},
enabled: computed(() => !!name.value),
});
const policyName = computed(() => { const policyName = computed(() => {
return props.attachment?.spec.policyName; return attachment.value?.spec.policyName;
});
const groupName = computed(() => {
return attachment.value?.spec.groupName;
}); });
const { data: policy } = useQuery({ const { data: policy } = useQuery({
queryKey: ["attachment-policy", policyName], queryKey: ["core:attachment-policy-by-name", policyName],
queryFn: async () => { queryFn: async () => {
if (!policyName.value) { if (!policyName.value) {
return; return;
@ -56,10 +72,23 @@ const { data: policy } = useQuery({
enabled: computed(() => !!policyName.value), enabled: computed(() => !!policyName.value),
}); });
const getGroupName = (name: string | undefined) => { const { data: group } = useQuery({
const group = groups.value?.find((group) => group.metadata.name === name); queryKey: ["core:attachment-group-by-name", groupName],
return group?.spec.displayName || name; queryFn: async () => {
}; if (!groupName.value) {
return;
}
const { data } = await coreApiClient.storage.group.getGroup({
name: groupName.value,
});
return data;
},
enabled: computed(() => !!groupName.value),
});
const showDisplayNameForm = ref(false);
</script> </script>
<template> <template>
<VModal <VModal
@ -78,7 +107,9 @@ const getGroupName = (name: string | undefined) => {
<template #actions> <template #actions>
<slot name="actions"></slot> <slot name="actions"></slot>
</template> </template>
<div class="overflow-hidden bg-white"> <div>
<VLoading v-if="isLoading" />
<div v-else class="overflow-hidden bg-white">
<div <div
v-if="onlyPreview && isImage(attachment?.spec.mediaType)" v-if="onlyPreview && isImage(attachment?.spec.mediaType)"
class="flex justify-center p-4" class="flex justify-center p-4"
@ -144,18 +175,35 @@ const getGroupName = (name: string | undefined) => {
<VDescriptionItem <VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.storage_policy')" :label="$t('core.attachment.detail_modal.fields.storage_policy')"
:content="policy?.spec.displayName" :content="policy?.spec.displayName"
></VDescriptionItem> >
</VDescriptionItem>
<VDescriptionItem <VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.group')" :label="$t('core.attachment.detail_modal.fields.group')"
:content=" :content="
getGroupName(attachment?.spec.groupName) || group?.spec.displayName ||
$t('core.attachment.common.text.ungrouped') $t('core.attachment.common.text.ungrouped')
" "
/> />
<VDescriptionItem <VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.display_name')" :label="$t('core.attachment.detail_modal.fields.display_name')"
:content="attachment?.spec.displayName" >
<DisplayNameEditForm
v-if="showDisplayNameForm && attachment"
:attachment="attachment"
@close="showDisplayNameForm = false"
/> />
<div v-else class="flex items-center gap-3">
<span>
{{ attachment?.spec.displayName }}
</span>
<HasPermission :permissions="['system:attachments:manage']">
<IconRiPencilFill
class="cursor-pointer text-sm text-gray-600 hover:text-gray-900"
@click="showDisplayNameForm = true"
/>
</HasPermission>
</div>
</VDescriptionItem>
<VDescriptionItem <VDescriptionItem
:label="$t('core.attachment.detail_modal.fields.media_type')" :label="$t('core.attachment.detail_modal.fields.media_type')"
:content="attachment?.spec.mediaType" :content="attachment?.spec.mediaType"
@ -180,6 +228,8 @@ const getGroupName = (name: string | undefined) => {
</VDescription> </VDescription>
</div> </div>
</div> </div>
</div>
<template #footer> <template #footer>
<VSpace> <VSpace>
<VButton type="default" @click="emit('close')"> <VButton type="default" @click="emit('close')">

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import { setFocus } from "@/formkit/utils/focus";
import { coreApiClient, type Attachment } from "@halo-dev/api-client";
import { Toast, VButton, VSpace } from "@halo-dev/components";
import { useQueryClient } from "@tanstack/vue-query";
import { onMounted, ref } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const queryClient = useQueryClient();
const props = withDefaults(defineProps<{ attachment: Attachment }>(), {});
const emit = defineEmits<{
(event: "close"): void;
}>();
onMounted(() => {
setFocus("displayName");
});
const isSubmitting = ref(false);
async function onSubmit({ displayName }: { displayName: string }) {
try {
isSubmitting.value = true;
await coreApiClient.storage.attachment.patchAttachment({
name: props.attachment.metadata.name,
jsonPatchInner: [
{
op: "add",
path: "/spec/displayName",
value: displayName,
},
],
});
Toast.success(t("core.common.toast.save_success"));
queryClient.invalidateQueries({
queryKey: ["core:attachment-by-name", props.attachment.metadata.name],
});
emit("close");
} catch (error) {
console.error("Failed to update displayName", error);
Toast.error(t("core.common.toast.save_failed_and_retry"));
} finally {
isSubmitting.value = false;
}
}
</script>
<template>
<FormKit
id="attachment-display-name-form"
type="form"
name="attachment-display-name-form"
@submit="onSubmit"
>
<FormKit
id="displayName"
:model-value="attachment.spec.displayName"
type="text"
name="displayName"
validation="required:trim"
:classes="{ outer: '!pb-0' }"
></FormKit>
</FormKit>
<VSpace class="mt-4">
<VButton
type="secondary"
@click="$formkit.submit('attachment-display-name-form')"
>
{{ $t("core.common.buttons.save") }}
</VButton>
<VButton @click="emit('close')">
{{ $t("core.common.buttons.cancel") }}
</VButton>
</VSpace>
</template>

View File

@ -137,6 +137,11 @@ function onUploadModalClose() {
uploadVisible.value = false; uploadVisible.value = false;
} }
function onDetailModalClose() {
detailVisible.value = false;
selectedAttachment.value = undefined;
}
function onGroupSelect(group: Group) { function onGroupSelect(group: Group) {
selectedGroup.value = group.metadata.name; selectedGroup.value = group.metadata.name;
handleReset(); handleReset();
@ -404,10 +409,10 @@ const viewType = useLocalStorage("attachment-selector-view-type", "grid");
</div> </div>
<AttachmentUploadModal v-if="uploadVisible" @close="onUploadModalClose" /> <AttachmentUploadModal v-if="uploadVisible" @close="onUploadModalClose" />
<AttachmentDetailModal <AttachmentDetailModal
v-model:visible="detailVisible" v-if="detailVisible"
:mount-to-body="true" :mount-to-body="true"
:attachment="selectedAttachment" :name="selectedAttachment?.metadata.name"
@close="selectedAttachment = undefined" @close="onDetailModalClose"
> >
<template #actions> <template #actions>
<span <span