feat: add support for selecting the parent category when creating a new category (#4056)

#### What type of PR is this?

/area console
/kind feature
/milestone 2.7.x

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

支持在创建分类的时候选择上级分类。

<img width="805" alt="image" src="https://github.com/halo-dev/halo/assets/21301288/02912f70-4de4-4b8e-bbbe-f973fbd3e684">

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

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

#### Special notes for your reviewer:

测试方式:

1. 测试新建的时候指定上级分类的功能是否正常。
2. 点击某个分类的操作按钮,选择添加子分类的菜单项,检查在新建分类的表单中上级分类是否选中了此分类。

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

```release-note
Console 端的文章分类支持在新建时指定上级分类。
```
pull/4092/head^2
Ryan Wang 2023-06-19 10:54:11 +08:00 committed by GitHub
parent 4a1fe8dd1e
commit 2fd9cbde33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 93 additions and 18 deletions

View File

@ -276,6 +276,8 @@ core:
delete: delete:
title: Are you sure you want to delete this category? title: Are you sure you want to delete this category?
description: After deleting this category, the association with corresponding articles will be removed. This operation cannot be undone. description: After deleting this category, the association with corresponding articles will be removed. This operation cannot be undone.
add_sub_category:
button: Add sub category
editing_modal: editing_modal:
titles: titles:
update: Update post category update: Update post category
@ -284,6 +286,8 @@ core:
general: General general: General
annotations: Annotations annotations: Annotations
fields: fields:
parent:
label: Parent
display_name: display_name:
label: Display name label: Display name
slug: slug:

View File

@ -276,6 +276,8 @@ core:
delete: delete:
title: 确定要删除该分类吗? title: 确定要删除该分类吗?
description: 删除此分类之后,对应文章的关联将被解除。该操作不可恢复。 description: 删除此分类之后,对应文章的关联将被解除。该操作不可恢复。
add_sub_category:
button: 新增子分类
editing_modal: editing_modal:
titles: titles:
update: 编辑文章分类 update: 编辑文章分类
@ -284,6 +286,8 @@ core:
general: 常规 general: 常规
annotations: 元数据 annotations: 元数据
fields: fields:
parent:
label: 上级分类
display_name: display_name:
label: 名称 label: 名称
slug: slug:

View File

@ -276,6 +276,8 @@ core:
delete: delete:
title: 確定要刪除該分類嗎? title: 確定要刪除該分類嗎?
description: 刪除此分類之後,對應文章的關聯將被解除。該操作不可恢復。 description: 刪除此分類之後,對應文章的關聯將被解除。該操作不可恢復。
add_sub_category:
button: 新增子分類
editing_modal: editing_modal:
titles: titles:
update: 編輯文章分類 update: 編輯文章分類
@ -284,6 +286,8 @@ core:
general: 常規 general: 常規
annotations: 元數據 annotations: 元數據
fields: fields:
parent:
label: 上級分類
display_name: display_name:
label: 名稱 label: 名稱
slug: slug:

View File

@ -33,7 +33,8 @@ import { useDebounceFn } from "@vueuse/core";
import { usePostCategory } from "./composables/use-post-category"; import { usePostCategory } from "./composables/use-post-category";
const editingModal = ref(false); const editingModal = ref(false);
const selectedCategory = ref<Category | null>(null); const selectedCategory = ref<Category>();
const selectedParentCategory = ref<Category>();
const { const {
categories, categories,
@ -68,8 +69,14 @@ const handleOpenEditingModal = (category: CategoryTree) => {
editingModal.value = true; editingModal.value = true;
}; };
const handleOpenCreateByParentModal = (category: CategoryTree) => {
selectedParentCategory.value = convertCategoryTreeToCategory(category);
editingModal.value = true;
};
const onEditingModalClose = () => { const onEditingModalClose = () => {
selectedCategory.value = null; selectedCategory.value = undefined;
selectedParentCategory.value = undefined;
handleFetchCategories(); handleFetchCategories();
}; };
</script> </script>
@ -77,6 +84,7 @@ const onEditingModalClose = () => {
<CategoryEditingModal <CategoryEditingModal
v-model:visible="editingModal" v-model:visible="editingModal"
:category="selectedCategory" :category="selectedCategory"
:parent-category="selectedParentCategory"
@close="onEditingModalClose" @close="onEditingModalClose"
/> />
<VPageHeader :title="$t('core.post_category.title')"> <VPageHeader :title="$t('core.post_category.title')">
@ -147,6 +155,7 @@ const onEditingModalClose = () => {
@change="handleUpdateInBatch" @change="handleUpdateInBatch"
@delete="handleDelete" @delete="handleDelete"
@open-editing="handleOpenEditingModal" @open-editing="handleOpenEditingModal"
@open-create-by-parent="handleOpenCreateByParentModal"
/> />
</Transition> </Transition>
</VCard> </VCard>

View File

@ -28,11 +28,13 @@ import { useI18n } from "vue-i18n";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
visible: boolean; visible: boolean;
category: Category | null; category?: Category;
parentCategory?: Category;
}>(), }>(),
{ {
visible: false, visible: false,
category: null, category: undefined,
parentCategory: undefined,
} }
); );
@ -63,6 +65,7 @@ const initialFormState: Category = {
}; };
const formState = ref<Category>(cloneDeep(initialFormState)); const formState = ref<Category>(cloneDeep(initialFormState));
const selectedParentCategory = ref("");
const saving = ref(false); const saving = ref(false);
const isUpdateMode = computed(() => { const isUpdateMode = computed(() => {
@ -100,9 +103,45 @@ const handleSaveCategory = async () => {
category: formState.value, category: formState.value,
}); });
} else { } else {
await apiClient.extension.category.createcontentHaloRunV1alpha1Category({ // Gets parent category, calculates priority and updates it.
category: formState.value, let parentCategory: Category | undefined = undefined;
if (selectedParentCategory.value) {
const { data } =
await apiClient.extension.category.getcontentHaloRunV1alpha1Category({
name: selectedParentCategory.value,
}); });
parentCategory = data;
}
const priority = parentCategory?.spec.children
? parentCategory.spec.children.length + 1
: 0;
formState.value.spec.priority = priority;
const { data: createdCategory } =
await apiClient.extension.category.createcontentHaloRunV1alpha1Category(
{
category: formState.value,
}
);
if (parentCategory) {
parentCategory.spec.children = Array.from(
new Set([
...(parentCategory.spec.children || []),
createdCategory.metadata.name,
])
);
await apiClient.extension.category.updatecontentHaloRunV1alpha1Category(
{
name: selectedParentCategory.value,
category: parentCategory,
}
);
}
} }
onVisibleChange(false); onVisibleChange(false);
@ -122,6 +161,7 @@ const onVisibleChange = (visible: boolean) => {
}; };
const handleResetForm = () => { const handleResetForm = () => {
selectedParentCategory.value = "";
formState.value = cloneDeep(initialFormState); formState.value = cloneDeep(initialFormState);
reset("category-form"); reset("category-form");
}; };
@ -130,18 +170,15 @@ watch(
() => props.visible, () => props.visible,
(visible) => { (visible) => {
if (visible) { if (visible) {
setFocus("displayNameInput"); if (props.parentCategory) {
} else { selectedParentCategory.value = props.parentCategory.metadata.name;
handleResetForm();
} }
}
);
watch( if (props.category) {
() => props.category, formState.value = cloneDeep(props.category);
(category) => { }
if (category) {
formState.value = cloneDeep(category); setFocus("displayNameInput");
} else { } else {
handleResetForm(); handleResetForm();
} }
@ -189,6 +226,14 @@ const { handleGenerateSlug } = useSlugify(
</div> </div>
</div> </div>
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0"> <div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
<FormKit
v-if="!isUpdateMode"
v-model="selectedParentCategory"
type="categorySelect"
:label="
$t('core.post_category.editing_modal.fields.parent.label')
"
></FormKit>
<FormKit <FormKit
id="displayNameInput" id="displayNameInput"
v-model="formState.spec.displayName" v-model="formState.spec.displayName"

View File

@ -26,6 +26,7 @@ withDefaults(
const emit = defineEmits<{ const emit = defineEmits<{
(event: "change"): void; (event: "change"): void;
(event: "open-editing", category: CategoryTree): void; (event: "open-editing", category: CategoryTree): void;
(event: "open-create-by-parent", category: CategoryTree): void;
(event: "delete", category: CategoryTree): void; (event: "delete", category: CategoryTree): void;
}>(); }>();
@ -39,6 +40,10 @@ function onOpenEditingModal(category: CategoryTree) {
emit("open-editing", category); emit("open-editing", category);
} }
function onOpenCreateByParentModal(category: CategoryTree) {
emit("open-create-by-parent", category);
}
function onDelete(category: CategoryTree) { function onDelete(category: CategoryTree) {
emit("delete", category); emit("delete", category);
} }
@ -117,6 +122,9 @@ function onDelete(category: CategoryTree) {
> >
{{ $t("core.common.buttons.edit") }} {{ $t("core.common.buttons.edit") }}
</VDropdownItem> </VDropdownItem>
<VDropdownItem @click="onOpenCreateByParentModal(category)">
{{ $t("core.post_category.operations.add_sub_category.button") }}
</VDropdownItem>
<VDropdownItem <VDropdownItem
v-permission="['system:posts:manage']" v-permission="['system:posts:manage']"
type="danger" type="danger"
@ -132,6 +140,7 @@ function onDelete(category: CategoryTree) {
@change="onChange" @change="onChange"
@delete="onDelete" @delete="onDelete"
@open-editing="onOpenEditingModal" @open-editing="onOpenEditingModal"
@open-create-by-parent="onOpenCreateByParentModal"
/> />
</li> </li>
</template> </template>