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 {
attachments,
selectedAttachment,
selectedAttachments,
selectedAttachmentNames,
checkedAll,
isLoading,
isFetching,
@ -128,13 +128,13 @@ const {
size: size,
});
provide<Ref<Set<Attachment>>>("selectedAttachments", selectedAttachments);
provide<Ref<Set<string>>>("selectedAttachmentNames", selectedAttachmentNames);
const handleMove = async (group: Group) => {
try {
const promises = Array.from(selectedAttachments.value).map((attachment) => {
const promises = Array.from(selectedAttachmentNames.value).map((name) => {
return coreApiClient.storage.attachment.patchAttachment({
name: attachment.metadata.name,
name,
jsonPatchInner: [
{
op: "add",
@ -146,7 +146,7 @@ const handleMove = async (group: Group) => {
});
await Promise.all(promises);
selectedAttachments.value.clear();
selectedAttachmentNames.value.clear();
Toast.success(t("core.attachment.operations.move.toast_success"));
} catch (e) {
@ -161,13 +161,13 @@ const handleClickItem = (attachment: Attachment) => {
return;
}
if (selectedAttachments.value.size > 0) {
if (selectedAttachmentNames.value.size > 0) {
handleSelect(attachment);
return;
}
selectedAttachment.value = attachment;
selectedAttachments.value.clear();
selectedAttachmentNames.value.clear();
detailVisible.value = true;
};
@ -299,14 +299,14 @@ watch(
</div>
<div class="flex w-full flex-1 items-center sm:w-auto">
<SearchInput
v-if="!selectedAttachments.size"
v-if="!selectedAttachmentNames.size"
v-model="keyword"
/>
<VSpace v-else>
<VButton type="danger" @click="handleDeleteInBatch">
{{ $t("core.common.buttons.delete") }}
</VButton>
<VButton @click="selectedAttachments.clear()">
<VButton @click="selectedAttachmentNames.clear()">
{{
$t("core.attachment.operations.deselect_items.button")
}}
@ -560,12 +560,18 @@ watch(
<div
v-if="!attachment.metadata.deletionTimestamp"
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"
>
<IconCheckboxFill
: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"
@click.stop="handleSelect(attachment)"

View File

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

View File

@ -69,6 +69,7 @@ const {
total,
selectedAttachment,
selectedAttachments,
selectedAttachmentNames,
handleFetchAttachments,
handleSelect,
handleSelectPrevious,
@ -105,7 +106,6 @@ function handleClearFilters() {
}
const uploadVisible = ref(false);
const detailVisible = ref(false);
watchEffect(() => {
emit("update:selected", Array.from(selectedAttachments.value));
@ -113,7 +113,6 @@ watchEffect(() => {
const handleOpenDetail = (attachment: Attachment) => {
selectedAttachment.value = attachment;
detailVisible.value = true;
};
const isDisabled = (attachment: Attachment) => {
@ -124,7 +123,7 @@ const isDisabled = (attachment: Attachment) => {
if (
props.max !== undefined &&
props.max <= selectedAttachments.value.size &&
props.max <= selectedAttachmentNames.value.size &&
!isChecked(attachment)
) {
return true;
@ -139,7 +138,6 @@ function onUploadModalClose() {
}
function onDetailModalClose() {
detailVisible.value = false;
selectedAttachment.value = undefined;
}
@ -359,7 +357,7 @@ const viewType = useLocalStorage("attachment-selector-view-type", "grid");
</p>
<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"
>
<IconEye
@ -368,7 +366,7 @@ const viewType = useLocalStorage("attachment-selector-view-type", "grid");
/>
<IconCheckboxFill
: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"
/>
@ -417,14 +415,14 @@ const viewType = useLocalStorage("attachment-selector-view-type", "grid");
</div>
<AttachmentUploadModal v-if="uploadVisible" @close="onUploadModalClose" />
<AttachmentDetailModal
v-if="detailVisible"
v-if="selectedAttachment"
:mount-to-body="true"
:name="selectedAttachment?.metadata.name"
@close="onDetailModalClose"
>
<template #actions>
<span
v-if="selectedAttachment && selectedAttachments.has(selectedAttachment)"
v-if="isChecked(selectedAttachment)"
@click="handleSelect(selectedAttachment)"
>
<IconCheckboxFill />

View File

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

View File

@ -101,12 +101,18 @@ function onUploadModalClose() {
// Select
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(
() => selectedAttachments.value,
(newValue) => {
emit("update:selected", Array.from(newValue));
emit("update:selected", newValue || []);
},
{
deep: true,
@ -114,12 +120,7 @@ watch(
);
const isChecked = (attachment: Attachment) => {
return (
attachment.metadata.name === selectedAttachment.value?.metadata.name ||
Array.from(selectedAttachments.value)
.map((item) => item.metadata.name)
.includes(attachment.metadata.name)
);
return selectedAttachmentNames.value.has(attachment.metadata.name);
};
const isDisabled = (attachment: Attachment) => {
@ -130,7 +131,7 @@ const isDisabled = (attachment: Attachment) => {
if (
props.max !== undefined &&
props.max <= selectedAttachments.value.size &&
props.max <= selectedAttachmentNames.value.size &&
!isChecked(attachment)
) {
return true;
@ -141,11 +142,11 @@ const isDisabled = (attachment: Attachment) => {
const handleSelect = async (attachment: Attachment | undefined) => {
if (!attachment) return;
if (selectedAttachments.value.has(attachment)) {
selectedAttachments.value.delete(attachment);
if (selectedAttachmentNames.value.has(attachment.metadata.name)) {
selectedAttachmentNames.value.delete(attachment.metadata.name);
return;
}
selectedAttachments.value.add(attachment);
selectedAttachmentNames.value.add(attachment.metadata.name);
};
// View type
@ -397,7 +398,7 @@ const handleSelectNext = async () => {
</p>
<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"
>
<IconEye
@ -406,7 +407,7 @@ const handleSelectNext = async () => {
/>
<IconCheckboxFill
: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"
/>
@ -466,7 +467,7 @@ const handleSelectNext = async () => {
>
<template #actions>
<span
v-if="selectedAttachment && selectedAttachments.has(selectedAttachment)"
v-if="isChecked(selectedAttachment)"
@click="handleSelect(selectedAttachment)"
>
<IconCheckboxFill />