fix: correct attachment selection state after new upload (#7487)

#### What type of PR is this?

/area ui
/kind bug
/milestone 2.21.x

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

Fix correct attachment selection state after new upload.

#### Which issue(s) this PR fixes:

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

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

```release-note
修复当有已选择附件时,上传新附件导致所选附件状态异常的问题。
```
pull/7486/head^2
Ryan Wang 2025-05-30 18:10:28 +08:00 committed by GitHub
parent 9d16388379
commit 7162b8da92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 76 additions and 59 deletions

View File

@ -99,7 +99,7 @@ function handleClearFilters() {
const { const {
attachments, attachments,
selectedAttachment, selectedAttachment,
selectedAttachments, selectedAttachmentNames,
checkedAll, checkedAll,
isLoading, isLoading,
isFetching, isFetching,
@ -128,13 +128,13 @@ const {
size: size, size: size,
}); });
provide<Ref<Set<Attachment>>>("selectedAttachments", selectedAttachments); provide<Ref<Set<string>>>("selectedAttachmentNames", selectedAttachmentNames);
const handleMove = async (group: Group) => { const handleMove = async (group: Group) => {
try { try {
const promises = Array.from(selectedAttachments.value).map((attachment) => { const promises = Array.from(selectedAttachmentNames.value).map((name) => {
return coreApiClient.storage.attachment.patchAttachment({ return coreApiClient.storage.attachment.patchAttachment({
name: attachment.metadata.name, name,
jsonPatchInner: [ jsonPatchInner: [
{ {
op: "add", op: "add",
@ -146,7 +146,7 @@ const handleMove = async (group: Group) => {
}); });
await Promise.all(promises); await Promise.all(promises);
selectedAttachments.value.clear(); selectedAttachmentNames.value.clear();
Toast.success(t("core.attachment.operations.move.toast_success")); Toast.success(t("core.attachment.operations.move.toast_success"));
} catch (e) { } catch (e) {
@ -161,13 +161,13 @@ const handleClickItem = (attachment: Attachment) => {
return; return;
} }
if (selectedAttachments.value.size > 0) { if (selectedAttachmentNames.value.size > 0) {
handleSelect(attachment); handleSelect(attachment);
return; return;
} }
selectedAttachment.value = attachment; selectedAttachment.value = attachment;
selectedAttachments.value.clear(); selectedAttachmentNames.value.clear();
detailVisible.value = true; detailVisible.value = true;
}; };
@ -299,14 +299,14 @@ watch(
</div> </div>
<div class="flex w-full flex-1 items-center sm:w-auto"> <div class="flex w-full flex-1 items-center sm:w-auto">
<SearchInput <SearchInput
v-if="!selectedAttachments.size" v-if="!selectedAttachmentNames.size"
v-model="keyword" v-model="keyword"
/> />
<VSpace v-else> <VSpace v-else>
<VButton type="danger" @click="handleDeleteInBatch"> <VButton type="danger" @click="handleDeleteInBatch">
{{ $t("core.common.buttons.delete") }} {{ $t("core.common.buttons.delete") }}
</VButton> </VButton>
<VButton @click="selectedAttachments.clear()"> <VButton @click="selectedAttachmentNames.clear()">
{{ {{
$t("core.attachment.operations.deselect_items.button") $t("core.attachment.operations.deselect_items.button")
}} }}
@ -560,12 +560,18 @@ watch(
<div <div
v-if="!attachment.metadata.deletionTimestamp" v-if="!attachment.metadata.deletionTimestamp"
v-permission="['system:attachments:manage']" v-permission="['system:attachments:manage']"
:class="{ '!flex': selectedAttachments.has(attachment) }" :class="{
'!flex': selectedAttachmentNames.has(
attachment.metadata.name
),
}"
class="absolute left-0 top-0 hidden h-1/3 w-full cursor-pointer justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex" class="absolute left-0 top-0 hidden h-1/3 w-full cursor-pointer justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex"
> >
<IconCheckboxFill <IconCheckboxFill
:class="{ :class="{
'!text-primary': selectedAttachments.has(attachment), '!text-primary': selectedAttachmentNames.has(
attachment.metadata.name
),
}" }"
class="mr-1 mt-1 h-6 w-6 cursor-pointer text-white transition-all hover:text-primary" class="mr-1 mt-1 h-6 w-6 cursor-pointer text-white transition-all hover:text-primary"
@click.stop="handleSelect(attachment)" @click.stop="handleSelect(attachment)"

View File

@ -44,9 +44,9 @@ const emit = defineEmits<{
(event: "open-detail", attachment: Attachment): void; (event: "open-detail", attachment: Attachment): void;
}>(); }>();
const selectedAttachments = inject<Ref<Set<Attachment>>>( const selectedAttachmentNames = inject<Ref<Set<string>>>(
"selectedAttachments", "selectedAttachmentNames",
ref<Set<Attachment>>(new Set()) ref<Set<string>>(new Set())
); );
const policyDisplayName = computed(() => { const policyDisplayName = computed(() => {
@ -69,7 +69,7 @@ const handleDelete = () => {
name: props.attachment.metadata.name, name: props.attachment.metadata.name,
}); });
selectedAttachments.value.delete(props.attachment); selectedAttachmentNames.value.delete(props.attachment.metadata.name);
Toast.success(t("core.common.toast.delete_success")); Toast.success(t("core.common.toast.delete_success"));
} catch (e) { } catch (e) {
@ -140,7 +140,7 @@ const { operationItems } = useOperationItemExtensionPoint<Attachment>(
#checkbox #checkbox
> >
<input <input
:checked="selectedAttachments.has(attachment)" :checked="selectedAttachmentNames.has(attachment.metadata.name)"
type="checkbox" type="checkbox"
@click="emit('select', attachment)" @click="emit('select', attachment)"
/> />

View File

@ -69,6 +69,7 @@ const {
total, total,
selectedAttachment, selectedAttachment,
selectedAttachments, selectedAttachments,
selectedAttachmentNames,
handleFetchAttachments, handleFetchAttachments,
handleSelect, handleSelect,
handleSelectPrevious, handleSelectPrevious,
@ -105,7 +106,6 @@ function handleClearFilters() {
} }
const uploadVisible = ref(false); const uploadVisible = ref(false);
const detailVisible = ref(false);
watchEffect(() => { watchEffect(() => {
emit("update:selected", Array.from(selectedAttachments.value)); emit("update:selected", Array.from(selectedAttachments.value));
@ -113,7 +113,6 @@ watchEffect(() => {
const handleOpenDetail = (attachment: Attachment) => { const handleOpenDetail = (attachment: Attachment) => {
selectedAttachment.value = attachment; selectedAttachment.value = attachment;
detailVisible.value = true;
}; };
const isDisabled = (attachment: Attachment) => { const isDisabled = (attachment: Attachment) => {
@ -124,7 +123,7 @@ const isDisabled = (attachment: Attachment) => {
if ( if (
props.max !== undefined && props.max !== undefined &&
props.max <= selectedAttachments.value.size && props.max <= selectedAttachmentNames.value.size &&
!isChecked(attachment) !isChecked(attachment)
) { ) {
return true; return true;
@ -139,7 +138,6 @@ function onUploadModalClose() {
} }
function onDetailModalClose() { function onDetailModalClose() {
detailVisible.value = false;
selectedAttachment.value = undefined; selectedAttachment.value = undefined;
} }
@ -359,7 +357,7 @@ const viewType = useLocalStorage("attachment-selector-view-type", "grid");
</p> </p>
<div <div
:class="{ '!flex': selectedAttachments.has(attachment) }" :class="{ '!flex': isChecked(attachment) }"
class="absolute left-0 top-0 hidden h-1/3 w-full justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex" class="absolute left-0 top-0 hidden h-1/3 w-full justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex"
> >
<IconEye <IconEye
@ -368,7 +366,7 @@ const viewType = useLocalStorage("attachment-selector-view-type", "grid");
/> />
<IconCheckboxFill <IconCheckboxFill
:class="{ :class="{
'!text-primary': selectedAttachments.has(attachment), '!text-primary': isChecked(attachment),
}" }"
class="mr-1 mt-1 h-6 w-6 cursor-pointer text-white transition-all hover:text-primary" class="mr-1 mt-1 h-6 w-6 cursor-pointer text-white transition-all hover:text-primary"
/> />
@ -417,14 +415,14 @@ 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-if="detailVisible" v-if="selectedAttachment"
:mount-to-body="true" :mount-to-body="true"
:name="selectedAttachment?.metadata.name" :name="selectedAttachment?.metadata.name"
@close="onDetailModalClose" @close="onDetailModalClose"
> >
<template #actions> <template #actions>
<span <span
v-if="selectedAttachment && selectedAttachments.has(selectedAttachment)" v-if="isChecked(selectedAttachment)"
@click="handleSelect(selectedAttachment)" @click="handleSelect(selectedAttachment)"
> >
<IconCheckboxFill /> <IconCheckboxFill />

View File

@ -2,7 +2,14 @@ import type { Attachment } from "@halo-dev/api-client";
import { consoleApiClient, coreApiClient } from "@halo-dev/api-client"; import { consoleApiClient, coreApiClient } from "@halo-dev/api-client";
import { Dialog, Toast } from "@halo-dev/components"; import { Dialog, Toast } from "@halo-dev/components";
import { useQuery } from "@tanstack/vue-query"; import { useQuery } from "@tanstack/vue-query";
import { nextTick, ref, watch, type Ref } from "vue"; import {
computed,
nextTick,
ref,
watch,
type ComputedRef,
type Ref,
} from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
interface useAttachmentControlReturn { interface useAttachmentControlReturn {
@ -10,7 +17,8 @@ interface useAttachmentControlReturn {
isLoading: Ref<boolean>; isLoading: Ref<boolean>;
isFetching: Ref<boolean>; isFetching: Ref<boolean>;
selectedAttachment: Ref<Attachment | undefined>; selectedAttachment: Ref<Attachment | undefined>;
selectedAttachments: Ref<Set<Attachment>>; selectedAttachments: ComputedRef<Attachment[]>;
selectedAttachmentNames: Ref<Set<string>>;
checkedAll: Ref<boolean>; checkedAll: Ref<boolean>;
total: Ref<number>; total: Ref<number>;
handleFetchAttachments: () => void; handleFetchAttachments: () => void;
@ -39,7 +47,7 @@ export function useAttachmentControl(filterOptions: {
filterOptions; filterOptions;
const selectedAttachment = ref<Attachment>(); const selectedAttachment = ref<Attachment>();
const selectedAttachments = ref<Set<Attachment>>(new Set<Attachment>()); const selectedAttachmentNames = ref<Set<string>>(new Set<string>());
const checkedAll = ref(false); const checkedAll = ref(false);
const total = ref(0); const total = ref(0);
@ -155,15 +163,15 @@ export function useAttachmentControl(filterOptions: {
cancelText: t("core.common.buttons.cancel"), cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => { onConfirm: async () => {
try { try {
const promises = Array.from(selectedAttachments.value).map( const promises = Array.from(selectedAttachmentNames.value).map(
(attachment) => { (name) => {
return coreApiClient.storage.attachment.deleteAttachment({ return coreApiClient.storage.attachment.deleteAttachment({
name: attachment.metadata.name, name,
}); });
} }
); );
await Promise.all(promises); await Promise.all(promises);
selectedAttachments.value.clear(); selectedAttachmentNames.value.clear();
Toast.success(t("core.common.toast.delete_success")); Toast.success(t("core.common.toast.delete_success"));
} catch (e) { } catch (e) {
@ -178,51 +186,55 @@ export function useAttachmentControl(filterOptions: {
const handleCheckAll = (checkAll: boolean) => { const handleCheckAll = (checkAll: boolean) => {
if (checkAll) { if (checkAll) {
data.value?.forEach((attachment) => { data.value?.forEach((attachment) => {
selectedAttachments.value.add(attachment); selectedAttachmentNames.value.add(attachment.metadata.name);
}); });
} else { } else {
selectedAttachments.value.clear(); selectedAttachmentNames.value.clear();
} }
}; };
const handleSelect = async (attachment: Attachment | undefined) => { const handleSelect = async (attachment: Attachment | undefined) => {
if (!attachment) return; if (!attachment) return;
if (selectedAttachments.value.has(attachment)) { if (selectedAttachmentNames.value.has(attachment.metadata.name)) {
selectedAttachments.value.delete(attachment); selectedAttachmentNames.value.delete(attachment.metadata.name);
return; return;
} }
selectedAttachments.value.add(attachment); selectedAttachmentNames.value.add(attachment.metadata.name);
}; };
watch( watch(
() => selectedAttachments.value.size, () => selectedAttachmentNames.value.size,
(newValue) => { (newValue) => {
checkedAll.value = newValue === data.value?.length; checkedAll.value = newValue === data.value?.length;
} }
); );
const isChecked = (attachment: Attachment) => { const isChecked = (attachment: Attachment) => {
return ( return selectedAttachmentNames.value.has(attachment.metadata.name);
attachment.metadata.name === selectedAttachment.value?.metadata.name ||
Array.from(selectedAttachments.value)
.map((item) => item.metadata.name)
.includes(attachment.metadata.name)
);
}; };
const handleReset = () => { const handleReset = () => {
page.value = 1; page.value = 1;
selectedAttachment.value = undefined; selectedAttachment.value = undefined;
selectedAttachments.value.clear(); selectedAttachmentNames.value.clear();
checkedAll.value = false; checkedAll.value = false;
}; };
const selectedAttachments = computed(() => {
return (
data.value?.filter((attachment) =>
selectedAttachmentNames.value.has(attachment.metadata.name)
) || []
);
});
return { return {
attachments: data, attachments: data,
isLoading, isLoading,
isFetching, isFetching,
selectedAttachment, selectedAttachment,
selectedAttachments, selectedAttachments,
selectedAttachmentNames,
checkedAll, checkedAll,
total, total,
handleFetchAttachments: refetch, handleFetchAttachments: refetch,

View File

@ -101,12 +101,18 @@ function onUploadModalClose() {
// Select // Select
const selectedAttachment = ref<Attachment>(); const selectedAttachment = ref<Attachment>();
const selectedAttachments = ref<Set<Attachment>>(new Set<Attachment>()); const selectedAttachmentNames = ref<Set<string>>(new Set<string>());
const selectedAttachments = computed(() => {
return data.value?.items.filter((attachment) =>
selectedAttachmentNames.value.has(attachment.metadata.name)
);
});
watch( watch(
() => selectedAttachments.value, () => selectedAttachments.value,
(newValue) => { (newValue) => {
emit("update:selected", Array.from(newValue)); emit("update:selected", newValue || []);
}, },
{ {
deep: true, deep: true,
@ -114,12 +120,7 @@ watch(
); );
const isChecked = (attachment: Attachment) => { const isChecked = (attachment: Attachment) => {
return ( return selectedAttachmentNames.value.has(attachment.metadata.name);
attachment.metadata.name === selectedAttachment.value?.metadata.name ||
Array.from(selectedAttachments.value)
.map((item) => item.metadata.name)
.includes(attachment.metadata.name)
);
}; };
const isDisabled = (attachment: Attachment) => { const isDisabled = (attachment: Attachment) => {
@ -130,7 +131,7 @@ const isDisabled = (attachment: Attachment) => {
if ( if (
props.max !== undefined && props.max !== undefined &&
props.max <= selectedAttachments.value.size && props.max <= selectedAttachmentNames.value.size &&
!isChecked(attachment) !isChecked(attachment)
) { ) {
return true; return true;
@ -141,11 +142,11 @@ const isDisabled = (attachment: Attachment) => {
const handleSelect = async (attachment: Attachment | undefined) => { const handleSelect = async (attachment: Attachment | undefined) => {
if (!attachment) return; if (!attachment) return;
if (selectedAttachments.value.has(attachment)) { if (selectedAttachmentNames.value.has(attachment.metadata.name)) {
selectedAttachments.value.delete(attachment); selectedAttachmentNames.value.delete(attachment.metadata.name);
return; return;
} }
selectedAttachments.value.add(attachment); selectedAttachmentNames.value.add(attachment.metadata.name);
}; };
// View type // View type
@ -397,7 +398,7 @@ const handleSelectNext = async () => {
</p> </p>
<div <div
:class="{ '!flex': selectedAttachments.has(attachment) }" :class="{ '!flex': isChecked(attachment) }"
class="absolute left-0 top-0 hidden h-1/3 w-full justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex" class="absolute left-0 top-0 hidden h-1/3 w-full justify-end bg-gradient-to-b from-gray-300 to-transparent ease-in-out group-hover:flex"
> >
<IconEye <IconEye
@ -406,7 +407,7 @@ const handleSelectNext = async () => {
/> />
<IconCheckboxFill <IconCheckboxFill
:class="{ :class="{
'!text-primary': selectedAttachments.has(attachment), '!text-primary': isChecked(attachment),
}" }"
class="mr-1 mt-1 h-6 w-6 cursor-pointer text-white transition-all hover:text-primary" class="mr-1 mt-1 h-6 w-6 cursor-pointer text-white transition-all hover:text-primary"
/> />
@ -466,7 +467,7 @@ const handleSelectNext = async () => {
> >
<template #actions> <template #actions>
<span <span
v-if="selectedAttachment && selectedAttachments.has(selectedAttachment)" v-if="isChecked(selectedAttachment)"
@click="handleSelect(selectedAttachment)" @click="handleSelect(selectedAttachment)"
> >
<IconCheckboxFill /> <IconCheckboxFill />