feat: add batch setting for partial post fields (#6142)

#### What type of PR is this?

/kind feature
/area ui
/milestone 2.17.x

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

支持批量为文章设置部分属性。

![image](https://github.com/halo-dev/halo/assets/21301288/cc4aa912-20ba-4b50-869b-705702f56d7d)

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

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

#### Special notes for your reviewer:

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

```release-note
支持批量为文章设置部分属性。
```
pull/6103/head
Ryan Wang 2024-06-26 17:58:50 +08:00 committed by GitHub
parent 5d5df7c7a9
commit 2ae5d222d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 396 additions and 0 deletions

View File

@ -28,6 +28,7 @@ import UserFilterDropdown from "@/components/filter/UserFilterDropdown.vue";
import CategoryFilterDropdown from "@/components/filter/CategoryFilterDropdown.vue";
import TagFilterDropdown from "@/components/filter/TagFilterDropdown.vue";
import PostListItem from "./components/PostListItem.vue";
import PostBatchSettingModal from "./components/PostBatchSettingModal.vue";
const { t } = useI18n();
@ -320,6 +321,23 @@ const handleCancelPublishInBatch = async () => {
});
};
// Batch settings
const batchSettingModalVisible = ref(false);
const batchSettingPosts = ref<ListedPost[]>([]);
function handleOpenBatchSettingModal() {
batchSettingPosts.value = selectedPostNames.value.map((name) => {
return posts.value?.find((post) => post.post.metadata.name === name);
}) as ListedPost[];
batchSettingModalVisible.value = true;
}
function onBatchSettingModalClose() {
batchSettingModalVisible.value = false;
batchSettingPosts.value = [];
}
watch(
() => selectedPostNames.value,
(newValue) => {
@ -342,6 +360,11 @@ watch(
</span>
</template>
</PostSettingModal>
<PostBatchSettingModal
v-if="batchSettingModalVisible"
:posts="batchSettingPosts"
@close="onBatchSettingModalClose"
/>
<VPageHeader :title="$t('core.post.title')">
<template #icon>
<IconBookRead class="mr-2 self-center" />
@ -398,6 +421,9 @@ watch(
<VButton @click="handleCancelPublishInBatch">
{{ $t("core.common.buttons.cancel_publish") }}
</VButton>
<VButton @click="handleOpenBatchSettingModal">
{{ $t("core.post.operations.batch_setting.button") }}
</VButton>
<VButton type="danger" @click="handleDeleteInBatch">
{{ $t("core.common.buttons.delete") }}
</VButton>

View File

@ -0,0 +1,307 @@
<script lang="ts" setup>
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
import {
coreApiClient,
type JsonPatchInner,
type ListedPost,
} from "@halo-dev/api-client";
import { ref } from "vue";
import { useMutation, useQueryClient } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n";
type ArrayPatchOp = "add" | "replace" | "removeAll";
interface FormData {
category: {
enabled: boolean;
names?: string[];
op: ArrayPatchOp;
};
tag: {
enabled: boolean;
names?: string[];
op: ArrayPatchOp;
};
visible: {
enabled: boolean;
value: "PUBLIC" | "PRIVATE";
};
allowComment: {
enabled: boolean;
value: boolean;
};
}
const { t } = useI18n();
const queryClient = useQueryClient();
const props = withDefaults(defineProps<{ posts: ListedPost[] }>(), {});
const emit = defineEmits<{
(event: "close"): void;
}>();
const modal = ref<InstanceType<typeof VModal> | null>(null);
const { mutate, isLoading } = useMutation({
mutationKey: ["batch-update-posts"],
mutationFn: async ({ data }: { data: FormData }) => {
for (const key in props.posts) {
const post = props.posts[key];
const jsonPatchInner: JsonPatchInner[] = [];
if (data.category.enabled) {
jsonPatchInner.push({
op: "add",
path: "/spec/categories",
value: computeArrayPatchValue(
data.category.op,
post.post.spec.categories || [],
data.category.names || []
),
});
}
if (data.tag.enabled) {
jsonPatchInner.push({
op: "add",
path: "/spec/tags",
value: computeArrayPatchValue(
data.tag.op,
post.post.spec.tags || [],
data.tag.names || []
),
});
}
if (data.visible.enabled) {
jsonPatchInner.push({
op: "add",
path: "/spec/visible",
value: data.visible.value,
});
}
if (data.allowComment.enabled) {
jsonPatchInner.push({
op: "add",
path: "/spec/allowComment",
value: data.allowComment.value,
});
}
await coreApiClient.content.post.patchPost({
name: post.post.metadata.name,
jsonPatchInner,
});
}
Toast.success(t("core.common.toast.save_success"));
},
onSuccess() {
queryClient.invalidateQueries({ queryKey: ["posts"] });
modal.value?.close();
},
onError() {
Toast.error(t("core.common.toast.save_failed_and_retry"));
},
});
function computeArrayPatchValue(
op: ArrayPatchOp,
oldValue: string[],
newValue: string[]
) {
if (op === "add") {
return Array.from(new Set([...oldValue, ...newValue]));
} else if (op === "replace") {
return newValue;
} else if (op === "removeAll") {
return [];
}
}
function onSubmit(data: FormData) {
mutate({ data });
}
</script>
<template>
<VModal
ref="modal"
height="calc(100vh - 20px)"
:title="$t('core.post.batch_setting_modal.title')"
:width="700"
@close="emit('close')"
>
<FormKit
id="post-batch-settings-form"
type="form"
name="post-batch-settings-form"
@submit="onSubmit"
>
<FormKit
v-slot="{ value }"
name="category"
type="group"
:label="$t('core.post.batch_setting_modal.fields.category_group')"
>
<FormKit
:value="false"
:label="$t('core.post.batch_setting_modal.fields.common.enabled')"
type="checkbox"
name="enabled"
></FormKit>
<FormKit
v-if="value?.enabled"
type="select"
:options="[
{
value: 'add',
label: $t(
'core.post.batch_setting_modal.fields.common.op.options.add'
),
},
{
value: 'replace',
label: $t(
'core.post.batch_setting_modal.fields.common.op.options.replace'
),
},
{
value: 'removeAll',
label: $t(
'core.post.batch_setting_modal.fields.common.op.options.remove_all'
),
},
]"
:label="$t('core.post.batch_setting_modal.fields.common.op.label')"
name="op"
value="add"
></FormKit>
<FormKit
v-if="value?.enabled && value?.op !== 'removeAll'"
:label="$t('core.post.batch_setting_modal.fields.category_names')"
type="categorySelect"
:multiple="true"
name="names"
validation="required"
></FormKit>
</FormKit>
<FormKit
v-slot="{ value }"
type="group"
name="tag"
:label="$t('core.post.batch_setting_modal.fields.tag_group')"
>
<FormKit
:value="false"
:label="$t('core.post.batch_setting_modal.fields.common.enabled')"
type="checkbox"
name="enabled"
></FormKit>
<FormKit
v-if="value?.enabled"
type="select"
:options="[
{
value: 'add',
label: $t(
'core.post.batch_setting_modal.fields.common.op.options.add'
),
},
{
value: 'replace',
label: $t(
'core.post.batch_setting_modal.fields.common.op.options.replace'
),
},
{
value: 'removeAll',
label: $t(
'core.post.batch_setting_modal.fields.common.op.options.remove_all'
),
},
]"
:label="$t('core.post.batch_setting_modal.fields.common.op.label')"
name="op"
value="add"
></FormKit>
<FormKit
v-if="value?.enabled && value?.op !== 'removeAll'"
:label="$t('core.post.batch_setting_modal.fields.tag_names')"
type="tagSelect"
:multiple="true"
name="names"
validation="required"
></FormKit>
</FormKit>
<FormKit
v-slot="{ value }"
type="group"
name="visible"
:label="$t('core.post.batch_setting_modal.fields.visible_group')"
>
<FormKit
:value="false"
:label="$t('core.post.batch_setting_modal.fields.common.enabled')"
type="checkbox"
name="enabled"
></FormKit>
<FormKit
v-if="value?.enabled"
:options="[
{ label: $t('core.common.select.public'), value: 'PUBLIC' },
{
label: $t('core.common.select.private'),
value: 'PRIVATE',
},
]"
:label="$t('core.post.batch_setting_modal.fields.visible_value')"
name="value"
type="select"
value="PUBLIC"
></FormKit>
</FormKit>
<FormKit
v-slot="{ value }"
type="group"
name="allowComment"
:label="$t('core.post.batch_setting_modal.fields.allow_comment_group')"
>
<FormKit
:value="false"
:label="$t('core.post.batch_setting_modal.fields.common.enabled')"
type="checkbox"
name="enabled"
></FormKit>
<FormKit
v-if="value?.enabled"
:options="[
{ label: $t('core.common.radio.yes'), value: true },
{ label: $t('core.common.radio.no'), value: false },
]"
:label="
$t('core.post.batch_setting_modal.fields.allow_comment_value')
"
name="value"
type="radio"
:value="true"
></FormKit>
</FormKit>
</FormKit>
<template #footer>
<VSpace>
<VButton
type="secondary"
:loading="isLoading"
@click="$formkit.submit('post-batch-settings-form')"
>
{{ $t("core.common.buttons.save") }}
</VButton>
<VButton @click="modal?.close()">
{{ $t("core.common.buttons.cancel") }}
</VButton>
</VSpace>
</template>
</VModal>
</template>

View File

@ -203,6 +203,8 @@ core:
description: >-
Batch cancel publish posts, the selected posts will be set to
unpublished status
batch_setting:
button: Batch settings
filters:
status:
items:
@ -285,6 +287,25 @@ core:
create_time_asc: Earliest Created
display_name_desc: Descending order by tag name
display_name_asc: Ascending order by tag name
batch_setting_modal:
title: Post batch settings
fields:
common:
enabled: Enabled
op:
label: Operate
options:
add: Add
replace: Replace
remove_all: Remove all
category_group: Category
category_names: Select categories
tag_group: Tag
tag_names: Select tags
visible_group: Visible
visible_value: "Select visible option "
allow_comment_group: " Allow comment"
allow_comment_value: Choose whether to allow comments
deleted_post:
title: Deleted Posts
empty:

View File

@ -191,6 +191,8 @@ core:
cancel_publish_in_batch:
title: 取消发布文章
description: 批量取消发布文章,所选文章会被设置为未发布状态
batch_setting:
button: 批量设置
filters:
status:
items:
@ -273,6 +275,25 @@ core:
create_time_asc: 较早创建
display_name_desc: 标签名降序
display_name_asc: 标签名升序
batch_setting_modal:
title: 文章批量设置
fields:
common:
enabled: 启用
op:
label: 设置方式
options:
add: 追加
replace: 替换
remove_all: 移除全部
category_group: 分类
category_names: 选择分类
tag_group: 标签
tag_names: 选择标签
visible_group: 可见性
visible_value: 选择可见性
allow_comment_group: 允许评论
allow_comment_value: 选择是否允许评论
deleted_post:
title: 文章回收站
empty:

View File

@ -191,6 +191,8 @@ core:
cancel_publish_in_batch:
title: 取消發佈文章
description: 批量取消發佈文章,所選文章會被設置為未發布狀態
batch_setting:
button: 批量設置
filters:
status:
items:
@ -265,6 +267,25 @@ core:
label: 自定義模板
cover:
label: 封面圖
batch_setting_modal:
title: 文章批量設置
fields:
common:
enabled: 啟用
op:
label: 設置方式
options:
add: 追加
replace: 替换
remove_all: 移除全部
category_group: 分类
category_names: 選擇分類
tag_group: 標籤
tag_names: 選擇標籤
visible_group: 可見性
visible_value: 選擇可見性
allow_comment_group: 允許評論
allow_comment_value: 選擇是否允許評論
deleted_post:
title: 文章回收站
empty: