mirror of https://github.com/halo-dev/halo
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
parent
9d16388379
commit
7162b8da92
|
@ -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)"
|
||||
|
|
|
@ -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)"
|
||||
/>
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 />
|
||||
|
|
Loading…
Reference in New Issue