mirror of https://github.com/halo-dev/halo
refactor: improve drag-and-drop sorting feature for categories (#7424)
Signed-off-by: Ryan Wang <i@ryanc.cc>pull/7433/head
parent
18105cbe44
commit
6cc7cf6d22
|
@ -1,9 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
// core libs
|
||||
import { coreApiClient } from "@halo-dev/api-client";
|
||||
import { ref } from "vue";
|
||||
|
||||
// components
|
||||
import {
|
||||
IconAddCircle,
|
||||
IconBookRead,
|
||||
|
@ -14,16 +10,13 @@ import {
|
|||
VPageHeader,
|
||||
VSpace,
|
||||
} from "@halo-dev/components";
|
||||
import { Draggable } from "@he-tree/vue";
|
||||
import "@he-tree/vue/style/default.css";
|
||||
import { ref } from "vue";
|
||||
import CategoryEditingModal from "./components/CategoryEditingModal.vue";
|
||||
import CategoryListItem from "./components/CategoryListItem.vue";
|
||||
|
||||
import { convertTreeToCategories, resetCategoriesTreePriority } from "./utils";
|
||||
|
||||
// libs
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
|
||||
// hooks
|
||||
import { usePostCategory } from "./composables/use-post-category";
|
||||
import { convertTreeToCategories, resetCategoriesTreePriority } from "./utils";
|
||||
|
||||
const creationModal = ref(false);
|
||||
|
||||
|
@ -31,8 +24,9 @@ const { categories, categoriesTree, isLoading, handleFetchCategories } =
|
|||
usePostCategory();
|
||||
|
||||
const batchUpdating = ref(false);
|
||||
const isDragging = ref(false);
|
||||
|
||||
const handleUpdateInBatch = useDebounceFn(async () => {
|
||||
async function handleUpdateInBatch() {
|
||||
const categoriesTreeToUpdate = resetCategoriesTreePriority(
|
||||
categoriesTree.value
|
||||
);
|
||||
|
@ -62,8 +56,9 @@ const handleUpdateInBatch = useDebounceFn(async () => {
|
|||
} finally {
|
||||
await handleFetchCategories();
|
||||
batchUpdating.value = false;
|
||||
isDragging.value = false;
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<CategoryEditingModal v-if="creationModal" @close="creationModal = false" />
|
||||
|
@ -130,14 +125,33 @@ const handleUpdateInBatch = useDebounceFn(async () => {
|
|||
</VEmpty>
|
||||
</Transition>
|
||||
<Transition v-else appear name="fade">
|
||||
<CategoryListItem
|
||||
<Draggable
|
||||
v-model="categoriesTree"
|
||||
:class="{
|
||||
'cursor-progress opacity-60': batchUpdating,
|
||||
}"
|
||||
@change="handleUpdateInBatch"
|
||||
trigger-class="drag-element"
|
||||
:indent="40"
|
||||
@after-drop="handleUpdateInBatch"
|
||||
@before-drag-start="isDragging = true"
|
||||
>
|
||||
<template #default="{ node, stat }">
|
||||
<CategoryListItem
|
||||
:category-tree-node="node"
|
||||
:is-child-level="stat.level > 1"
|
||||
:is-dragging="isDragging"
|
||||
/>
|
||||
</template>
|
||||
</Draggable>
|
||||
</Transition>
|
||||
</VCard>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
.vtlist-inner {
|
||||
@apply divide-y divide-gray-100;
|
||||
}
|
||||
.he-tree-drag-placeholder {
|
||||
height: 60px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
<script lang="ts" setup>
|
||||
// core libs
|
||||
import { coreApiClient } from "@halo-dev/api-client";
|
||||
import { computed, nextTick, onMounted, ref } from "vue";
|
||||
|
||||
// components
|
||||
import SubmitButton from "@/components/button/SubmitButton.vue";
|
||||
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
|
@ -12,6 +7,7 @@ import useSlugify from "@console/composables/use-slugify";
|
|||
import { useThemeCustomTemplates } from "@console/modules/interface/themes/composables/use-theme";
|
||||
import { reset, submitForm } from "@formkit/core";
|
||||
import type { Category } from "@halo-dev/api-client";
|
||||
import { coreApiClient } from "@halo-dev/api-client";
|
||||
import {
|
||||
IconRefreshLine,
|
||||
Toast,
|
||||
|
@ -21,6 +17,7 @@ import {
|
|||
} from "@halo-dev/components";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { computed, nextTick, onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = withDefaults(
|
||||
|
@ -186,7 +183,13 @@ const { handleGenerateSlug } = useSlugify(
|
|||
);
|
||||
</script>
|
||||
<template>
|
||||
<VModal ref="modal" :title="modalTitle" :width="700" @close="emit('close')">
|
||||
<VModal
|
||||
ref="modal"
|
||||
mount-to-body
|
||||
:title="modalTitle"
|
||||
:width="700"
|
||||
@close="emit('close')"
|
||||
>
|
||||
<FormKit
|
||||
id="category-form"
|
||||
type="form"
|
||||
|
|
|
@ -1,46 +1,63 @@
|
|||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import { formatDatetime } from "@/utils/date";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import type { Category } from "@halo-dev/api-client";
|
||||
import { coreApiClient } from "@halo-dev/api-client";
|
||||
import { coreApiClient, type Category } from "@halo-dev/api-client";
|
||||
import {
|
||||
Dialog,
|
||||
IconEyeOff,
|
||||
IconList,
|
||||
IconMore,
|
||||
Toast,
|
||||
VDropdown,
|
||||
VDropdownItem,
|
||||
VEntity,
|
||||
VEntityField,
|
||||
VStatusDot,
|
||||
} from "@halo-dev/components";
|
||||
import "@he-tree/vue/style/default.css";
|
||||
import { useQueryClient } from "@tanstack/vue-query";
|
||||
import type { PropType } from "vue";
|
||||
import { ref } from "vue";
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import GridiconsLinkBreak from "~icons/gridicons/link-break";
|
||||
import { convertCategoryTreeToCategory, type CategoryTree } from "../utils";
|
||||
import { convertCategoryTreeToCategory, type CategoryTreeNode } from "../utils";
|
||||
import CategoryEditingModal from "./CategoryEditingModal.vue";
|
||||
|
||||
const { currentUserHasPermission } = usePermission();
|
||||
|
||||
withDefaults(defineProps<{ isChildLevel?: boolean }>(), {});
|
||||
|
||||
const categories = defineModel({
|
||||
type: Array as PropType<CategoryTree[]>,
|
||||
default: [],
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "change"): void;
|
||||
}>();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
function onChange() {
|
||||
emit("change");
|
||||
}
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
isChildLevel?: boolean;
|
||||
isDragging?: boolean;
|
||||
categoryTreeNode: CategoryTreeNode;
|
||||
}>(),
|
||||
{
|
||||
isChildLevel: false,
|
||||
isDragging: false,
|
||||
}
|
||||
);
|
||||
|
||||
const handleDelete = async () => {
|
||||
Dialog.warning({
|
||||
title: t("core.post_category.operations.delete.title"),
|
||||
description: t("core.post_category.operations.delete.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await coreApiClient.content.category.deleteCategory({
|
||||
name: props.categoryTreeNode.metadata.name,
|
||||
});
|
||||
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["post-categories"] });
|
||||
} catch (e) {
|
||||
console.error("Failed to delete category", e);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Editing category
|
||||
const editingModal = ref(false);
|
||||
|
@ -53,49 +70,101 @@ function onEditingModalClose() {
|
|||
editingModal.value = false;
|
||||
}
|
||||
|
||||
const handleOpenEditingModal = (category: CategoryTree) => {
|
||||
selectedCategory.value = convertCategoryTreeToCategory(category);
|
||||
const handleOpenEditingModal = () => {
|
||||
selectedCategory.value = convertCategoryTreeToCategory(
|
||||
props.categoryTreeNode
|
||||
);
|
||||
editingModal.value = true;
|
||||
};
|
||||
|
||||
const handleOpenCreateByParentModal = (category: CategoryTree) => {
|
||||
selectedParentCategory.value = convertCategoryTreeToCategory(category);
|
||||
const handleOpenCreateByParentModal = () => {
|
||||
selectedParentCategory.value = convertCategoryTreeToCategory(
|
||||
props.categoryTreeNode
|
||||
);
|
||||
editingModal.value = true;
|
||||
};
|
||||
|
||||
const handleDelete = async (category: CategoryTree) => {
|
||||
Dialog.warning({
|
||||
title: t("core.post_category.operations.delete.title"),
|
||||
description: t("core.post_category.operations.delete.description"),
|
||||
confirmType: "danger",
|
||||
confirmText: t("core.common.buttons.confirm"),
|
||||
cancelText: t("core.common.buttons.cancel"),
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await coreApiClient.content.category.deleteCategory({
|
||||
name: category.metadata.name,
|
||||
});
|
||||
|
||||
Toast.success(t("core.common.toast.delete_success"));
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ["post-categories"] });
|
||||
} catch (e) {
|
||||
console.error("Failed to delete tag", e);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<VueDraggable
|
||||
v-model="categories"
|
||||
class="box-border h-full w-full divide-y divide-gray-100"
|
||||
ghost-class="opacity-50"
|
||||
group="category-item"
|
||||
handle=".drag-element"
|
||||
tag="ul"
|
||||
@sort="onChange"
|
||||
<div
|
||||
class="px-4 py-3 hover:bg-gray-50 w-full group items-center flex justify-between relative"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
v-permission="['system:posts:manage']"
|
||||
class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
|
||||
:class="{
|
||||
'!hidden': isDragging,
|
||||
}"
|
||||
>
|
||||
<IconList class="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<div class="gap-1 flex flex-col">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<span class="truncate text-sm font-medium text-gray-900">
|
||||
{{ categoryTreeNode.spec.displayName }}
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
v-if="categoryTreeNode.status?.permalink"
|
||||
:href="categoryTreeNode.status?.permalink"
|
||||
:title="categoryTreeNode.status?.permalink"
|
||||
target="_blank"
|
||||
class="truncate text-xs text-gray-500 group-hover:text-gray-900"
|
||||
>
|
||||
{{ categoryTreeNode.status.permalink }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-6">
|
||||
<VStatusDot
|
||||
v-if="categoryTreeNode.metadata.deletionTimestamp"
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
<IconEyeOff
|
||||
v-if="categoryTreeNode.spec.hideFromList"
|
||||
v-tooltip="$t('core.post_category.list.fields.hide_from_list')"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
<GridiconsLinkBreak
|
||||
v-if="categoryTreeNode.spec.preventParentPostCascadeQuery"
|
||||
v-tooltip="
|
||||
$t('core.post_category.list.fields.prevent_parent_post_cascade_query')
|
||||
"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
<span class="truncate text-xs text-gray-500">
|
||||
{{
|
||||
$t("core.common.fields.post_count", {
|
||||
count: categoryTreeNode.status?.postCount || 0,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span class="truncate text-xs tabular-nums text-gray-500">
|
||||
{{ formatDatetime(categoryTreeNode.metadata.creationTimestamp) }}
|
||||
</span>
|
||||
<VDropdown v-if="currentUserHasPermission(['system:posts:manage'])">
|
||||
<div
|
||||
class="cursor-pointer rounded p-1 transition-all hover:text-blue-600 group-hover:bg-gray-200/60"
|
||||
@click.stop
|
||||
>
|
||||
<IconMore />
|
||||
</div>
|
||||
<template #popper>
|
||||
<VDropdownItem @click="handleOpenEditingModal">
|
||||
{{ $t("core.common.buttons.edit") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem @click="handleOpenCreateByParentModal">
|
||||
{{ $t("core.post_category.operations.add_sub_category.button") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem type="danger" @click="handleDelete">
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VDropdownItem>
|
||||
</template>
|
||||
</VDropdown>
|
||||
</div>
|
||||
|
||||
<CategoryEditingModal
|
||||
v-if="editingModal"
|
||||
:is-child-level-category="isChildLevel"
|
||||
|
@ -103,104 +172,5 @@ const handleDelete = async (category: CategoryTree) => {
|
|||
:parent-category="selectedParentCategory"
|
||||
@close="onEditingModalClose"
|
||||
/>
|
||||
<li v-for="category in categories" :key="category.metadata.name">
|
||||
<VEntity>
|
||||
<template #prepend>
|
||||
<div
|
||||
v-permission="['system:posts:manage']"
|
||||
class="drag-element absolute inset-y-0 left-0 hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
|
||||
>
|
||||
<IconList class="h-3.5 w-3.5" />
|
||||
</div>
|
||||
</template>
|
||||
<template #start>
|
||||
<VEntityField :title="category.spec.displayName">
|
||||
<template #description>
|
||||
<a
|
||||
v-if="category.status?.permalink"
|
||||
:href="category.status.permalink"
|
||||
:title="category.status.permalink"
|
||||
target="_blank"
|
||||
class="truncate text-xs text-gray-500 group-hover:text-gray-900"
|
||||
>
|
||||
{{ category.status.permalink }}
|
||||
</a>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template #end>
|
||||
<VEntityField v-if="category.metadata.deletionTimestamp">
|
||||
<template #description>
|
||||
<VStatusDot
|
||||
v-tooltip="$t('core.common.status.deleting')"
|
||||
state="warning"
|
||||
animate
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField v-if="category.spec.hideFromList">
|
||||
<template #description>
|
||||
<IconEyeOff
|
||||
v-tooltip="$t('core.post_category.list.fields.hide_from_list')"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField v-if="category.spec.preventParentPostCascadeQuery">
|
||||
<template #description>
|
||||
<GridiconsLinkBreak
|
||||
v-tooltip="
|
||||
$t(
|
||||
'core.post_category.list.fields.prevent_parent_post_cascade_query'
|
||||
)
|
||||
"
|
||||
class="cursor-pointer text-sm transition-all hover:text-blue-600"
|
||||
/>
|
||||
</template>
|
||||
</VEntityField>
|
||||
<VEntityField
|
||||
:description="
|
||||
$t('core.common.fields.post_count', {
|
||||
count: category.status?.postCount || 0,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<VEntityField>
|
||||
<template #description>
|
||||
<span class="truncate text-xs tabular-nums text-gray-500">
|
||||
{{ formatDatetime(category.metadata.creationTimestamp) }}
|
||||
</span>
|
||||
</template>
|
||||
</VEntityField>
|
||||
</template>
|
||||
<template
|
||||
v-if="currentUserHasPermission(['system:posts:manage'])"
|
||||
#dropdownItems
|
||||
>
|
||||
<VDropdownItem
|
||||
v-permission="['system:posts:manage']"
|
||||
@click="handleOpenEditingModal(category)"
|
||||
>
|
||||
{{ $t("core.common.buttons.edit") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem @click="handleOpenCreateByParentModal(category)">
|
||||
{{ $t("core.post_category.operations.add_sub_category.button") }}
|
||||
</VDropdownItem>
|
||||
<VDropdownItem
|
||||
v-permission="['system:posts:manage']"
|
||||
type="danger"
|
||||
@click="handleDelete(category)"
|
||||
>
|
||||
{{ $t("core.common.buttons.delete") }}
|
||||
</VDropdownItem>
|
||||
</template>
|
||||
</VEntity>
|
||||
<CategoryListItem
|
||||
v-model="category.spec.children"
|
||||
is-child-level
|
||||
class="pl-10 transition-all duration-300"
|
||||
@change="onChange"
|
||||
/>
|
||||
</li>
|
||||
</VueDraggable>
|
||||
</template>
|
||||
|
|
|
@ -1,20 +1,10 @@
|
|||
import type { Category } from "@halo-dev/api-client";
|
||||
import { coreApiClient } from "@halo-dev/api-client";
|
||||
import { useQuery } from "@tanstack/vue-query";
|
||||
import type { Ref } from "vue";
|
||||
import { ref } from "vue";
|
||||
import type { CategoryTree } from "../utils";
|
||||
import { buildCategoriesTree } from "../utils";
|
||||
import { buildCategoriesTree, type CategoryTreeNode } from "../utils";
|
||||
|
||||
interface usePostCategoryReturn {
|
||||
categories: Ref<Category[] | undefined>;
|
||||
categoriesTree: Ref<CategoryTree[]>;
|
||||
isLoading: Ref<boolean>;
|
||||
handleFetchCategories: () => void;
|
||||
}
|
||||
|
||||
export function usePostCategory(): usePostCategoryReturn {
|
||||
const categoriesTree = ref<CategoryTree[]>([] as CategoryTree[]);
|
||||
export function usePostCategory() {
|
||||
const categoriesTree = ref<CategoryTreeNode[]>([] as CategoryTreeNode[]);
|
||||
|
||||
const {
|
||||
data: categories,
|
||||
|
|
|
@ -0,0 +1,335 @@
|
|||
import type { Category } from "@halo-dev/api-client";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildCategoriesTree,
|
||||
convertCategoryTreeToCategory,
|
||||
convertTreeToCategories,
|
||||
getCategoryPath,
|
||||
resetCategoriesTreePriority,
|
||||
sortCategoriesTree,
|
||||
type CategoryTreeNode,
|
||||
} from "../index";
|
||||
|
||||
function createMockCategory(
|
||||
name: string,
|
||||
priority = 0,
|
||||
children: string[] = []
|
||||
): Category {
|
||||
return {
|
||||
metadata: {
|
||||
name,
|
||||
annotations: {},
|
||||
labels: {},
|
||||
version: 1,
|
||||
creationTimestamp: new Date().toISOString(),
|
||||
},
|
||||
apiVersion: "content.halo.run/v1alpha1",
|
||||
kind: "Category",
|
||||
spec: {
|
||||
displayName: `Category ${name}`,
|
||||
slug: name,
|
||||
description: `Description for ${name}`,
|
||||
cover: "",
|
||||
template: "",
|
||||
priority: priority,
|
||||
children: children,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createMockCategoryTreeNode(
|
||||
name: string,
|
||||
priority = 0,
|
||||
children: CategoryTreeNode[] = []
|
||||
): CategoryTreeNode {
|
||||
return {
|
||||
metadata: {
|
||||
name,
|
||||
annotations: {},
|
||||
labels: {},
|
||||
version: 1,
|
||||
creationTimestamp: new Date().toISOString(),
|
||||
},
|
||||
apiVersion: "content.halo.run/v1alpha1",
|
||||
kind: "Category",
|
||||
spec: {
|
||||
displayName: `Category ${name}`,
|
||||
slug: name,
|
||||
description: `Description for ${name}`,
|
||||
cover: "",
|
||||
template: "",
|
||||
priority: priority,
|
||||
children: children.map((child) => child.metadata.name),
|
||||
},
|
||||
children: children,
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildCategoriesTree", () => {
|
||||
it("should convert flat category array to tree structure", () => {
|
||||
// Prepare test data
|
||||
const categories: Category[] = [
|
||||
createMockCategory("parent1", 0, ["child1", "child2"]),
|
||||
createMockCategory("child1", 0),
|
||||
createMockCategory("child2", 1),
|
||||
createMockCategory("parent2", 1, ["child3"]),
|
||||
createMockCategory("child3", 0),
|
||||
];
|
||||
|
||||
// Execute test
|
||||
const result = buildCategoriesTree(categories);
|
||||
|
||||
// Verify results
|
||||
expect(result.length).toBe(2); // Should have two root nodes
|
||||
expect(result[0].metadata.name).toBe("parent1"); // First root node should be parent1
|
||||
expect(result[1].metadata.name).toBe("parent2"); // Second root node should be parent2
|
||||
expect(result[0].children.length).toBe(2); // parent1 should have two children
|
||||
expect(result[1].children.length).toBe(1); // parent2 should have one child
|
||||
expect(result[0].children[0].metadata.name).toBe("child1");
|
||||
expect(result[0].children[1].metadata.name).toBe("child2");
|
||||
expect(result[1].children[0].metadata.name).toBe("child3");
|
||||
});
|
||||
|
||||
it("should handle empty array input", () => {
|
||||
const result = buildCategoriesTree([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle categories without parent-child relationships", () => {
|
||||
const categories: Category[] = [
|
||||
createMockCategory("category1", 0),
|
||||
createMockCategory("category2", 1),
|
||||
createMockCategory("category3", 2),
|
||||
];
|
||||
|
||||
const result = buildCategoriesTree(categories);
|
||||
|
||||
expect(result.length).toBe(3);
|
||||
expect(result.every((node) => node.children.length === 0)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle multi-level nested category structure", () => {
|
||||
const categories: Category[] = [
|
||||
createMockCategory("root", 0, ["level1"]),
|
||||
createMockCategory("level1", 0, ["level2"]),
|
||||
createMockCategory("level2", 0, ["level3"]),
|
||||
createMockCategory("level3", 0),
|
||||
];
|
||||
|
||||
const result = buildCategoriesTree(categories);
|
||||
|
||||
expect(result.length).toBe(1);
|
||||
expect(result[0].metadata.name).toBe("root");
|
||||
expect(result[0].children[0].metadata.name).toBe("level1");
|
||||
expect(result[0].children[0].children[0].metadata.name).toBe("level2");
|
||||
expect(result[0].children[0].children[0].children[0].metadata.name).toBe(
|
||||
"level3"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sortCategoriesTree", () => {
|
||||
it("should sort category tree by priority", () => {
|
||||
const categoriesTree: CategoryTreeNode[] = [
|
||||
createMockCategoryTreeNode("node3", 2),
|
||||
createMockCategoryTreeNode("node1", 0),
|
||||
createMockCategoryTreeNode("node2", 1),
|
||||
];
|
||||
|
||||
const result = sortCategoriesTree(categoriesTree);
|
||||
|
||||
expect(result[0].metadata.name).toBe("node1");
|
||||
expect(result[1].metadata.name).toBe("node2");
|
||||
expect(result[2].metadata.name).toBe("node3");
|
||||
});
|
||||
|
||||
it("should recursively sort child nodes", () => {
|
||||
const categoriesTree: CategoryTreeNode[] = [
|
||||
createMockCategoryTreeNode("parent1", 0, [
|
||||
createMockCategoryTreeNode("child3", 2),
|
||||
createMockCategoryTreeNode("child1", 0),
|
||||
createMockCategoryTreeNode("child2", 1),
|
||||
]),
|
||||
];
|
||||
|
||||
const result = sortCategoriesTree(categoriesTree);
|
||||
|
||||
expect(result[0].children[0].metadata.name).toBe("child1");
|
||||
expect(result[0].children[1].metadata.name).toBe("child2");
|
||||
expect(result[0].children[2].metadata.name).toBe("child3");
|
||||
});
|
||||
|
||||
it("should handle empty array input", () => {
|
||||
const result = sortCategoriesTree([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetCategoriesTreePriority", () => {
|
||||
it("should reset priority values of all nodes in the tree", () => {
|
||||
const categoriesTree: CategoryTreeNode[] = [
|
||||
createMockCategoryTreeNode("node1", 5),
|
||||
createMockCategoryTreeNode("node2", 10),
|
||||
createMockCategoryTreeNode("node3", 15),
|
||||
];
|
||||
|
||||
const result = resetCategoriesTreePriority(categoriesTree);
|
||||
|
||||
expect(result[0].spec.priority).toBe(0);
|
||||
expect(result[1].spec.priority).toBe(1);
|
||||
expect(result[2].spec.priority).toBe(2);
|
||||
});
|
||||
|
||||
it("should recursively reset child node priorities", () => {
|
||||
const categoriesTree: CategoryTreeNode[] = [
|
||||
createMockCategoryTreeNode("parent", 5, [
|
||||
createMockCategoryTreeNode("child1", 10),
|
||||
createMockCategoryTreeNode("child2", 15),
|
||||
]),
|
||||
];
|
||||
|
||||
const result = resetCategoriesTreePriority(categoriesTree);
|
||||
|
||||
expect(result[0].spec.priority).toBe(0);
|
||||
expect(result[0].children[0].spec.priority).toBe(0);
|
||||
expect(result[0].children[1].spec.priority).toBe(1);
|
||||
});
|
||||
|
||||
it("should handle empty array input", () => {
|
||||
const result = resetCategoriesTreePriority([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertTreeToCategories", () => {
|
||||
it("should convert tree structure back to flat category array", () => {
|
||||
const child1 = createMockCategoryTreeNode("child1", 0);
|
||||
const child2 = createMockCategoryTreeNode("child2", 1);
|
||||
const parent = createMockCategoryTreeNode("parent", 0, [child1, child2]);
|
||||
const categoriesTree: CategoryTreeNode[] = [parent];
|
||||
|
||||
const result = convertTreeToCategories(categoriesTree);
|
||||
|
||||
expect(result.length).toBe(3);
|
||||
|
||||
// Verify parent node
|
||||
const parentCategory = result.find((c) => c.metadata.name === "parent");
|
||||
expect(parentCategory).toBeDefined();
|
||||
expect(parentCategory?.spec.children).toContain("child1");
|
||||
expect(parentCategory?.spec.children).toContain("child2");
|
||||
|
||||
// Verify child nodes
|
||||
const child1Category = result.find((c) => c.metadata.name === "child1");
|
||||
const child2Category = result.find((c) => c.metadata.name === "child2");
|
||||
expect(child1Category).toBeDefined();
|
||||
expect(child2Category).toBeDefined();
|
||||
expect(child1Category?.spec.children).toEqual([]);
|
||||
expect(child2Category?.spec.children).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle multi-level nested structure", () => {
|
||||
const level3 = createMockCategoryTreeNode("level3", 0);
|
||||
const level2 = createMockCategoryTreeNode("level2", 0, [level3]);
|
||||
const level1 = createMockCategoryTreeNode("level1", 0, [level2]);
|
||||
const root = createMockCategoryTreeNode("root", 0, [level1]);
|
||||
const categoriesTree: CategoryTreeNode[] = [root];
|
||||
|
||||
const result = convertTreeToCategories(categoriesTree);
|
||||
|
||||
expect(result.length).toBe(4);
|
||||
|
||||
const rootCategory = result.find((c) => c.metadata.name === "root");
|
||||
const level1Category = result.find((c) => c.metadata.name === "level1");
|
||||
const level2Category = result.find((c) => c.metadata.name === "level2");
|
||||
const level3Category = result.find((c) => c.metadata.name === "level3");
|
||||
|
||||
expect(rootCategory?.spec.children).toContain("level1");
|
||||
expect(level1Category?.spec.children).toContain("level2");
|
||||
expect(level2Category?.spec.children).toContain("level3");
|
||||
expect(level3Category?.spec.children).toEqual([]);
|
||||
});
|
||||
|
||||
it("should handle empty array input", () => {
|
||||
const result = convertTreeToCategories([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("convertCategoryTreeToCategory", () => {
|
||||
it("should convert a single tree node to a category object", () => {
|
||||
const child1 = createMockCategoryTreeNode("child1", 0);
|
||||
const child2 = createMockCategoryTreeNode("child2", 1);
|
||||
const parent = createMockCategoryTreeNode("parent", 0, [child1, child2]);
|
||||
|
||||
const result = convertCategoryTreeToCategory(parent);
|
||||
|
||||
expect(result.metadata.name).toBe("parent");
|
||||
expect(result.spec.children).toContain("child1");
|
||||
expect(result.spec.children).toContain("child2");
|
||||
expect(result.spec.children?.length).toBe(2);
|
||||
// eslint-disable-next-line
|
||||
expect((result as any).children).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle nodes without children", () => {
|
||||
const node = createMockCategoryTreeNode("node", 0);
|
||||
|
||||
const result = convertCategoryTreeToCategory(node);
|
||||
|
||||
expect(result.metadata.name).toBe("node");
|
||||
expect(result.spec.children).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCategoryPath", () => {
|
||||
it("should return path from root to specified category", () => {
|
||||
const level3 = createMockCategoryTreeNode("level3", 0);
|
||||
const level2 = createMockCategoryTreeNode("level2", 0, [level3]);
|
||||
const level1 = createMockCategoryTreeNode("level1", 0, [level2]);
|
||||
const root = createMockCategoryTreeNode("root", 0, [level1]);
|
||||
const categoriesTree: CategoryTreeNode[] = [root];
|
||||
|
||||
const result = getCategoryPath(categoriesTree, "level3");
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.length).toBe(4);
|
||||
expect(result?.[0].metadata.name).toBe("root");
|
||||
expect(result?.[1].metadata.name).toBe("level1");
|
||||
expect(result?.[2].metadata.name).toBe("level2");
|
||||
expect(result?.[3].metadata.name).toBe("level3");
|
||||
});
|
||||
|
||||
it("should handle case when category is not found", () => {
|
||||
const categoriesTree: CategoryTreeNode[] = [
|
||||
createMockCategoryTreeNode("node1", 0),
|
||||
createMockCategoryTreeNode("node2", 1),
|
||||
];
|
||||
|
||||
const result = getCategoryPath(categoriesTree, "nonexistent");
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle multiple branches", () => {
|
||||
const child1 = createMockCategoryTreeNode("child1", 0);
|
||||
const child2 = createMockCategoryTreeNode("child2", 1);
|
||||
const target = createMockCategoryTreeNode("target", 0);
|
||||
const branch1 = createMockCategoryTreeNode("branch1", 0, [child1, child2]);
|
||||
const branch2 = createMockCategoryTreeNode("branch2", 1, [target]);
|
||||
const root = createMockCategoryTreeNode("root", 0, [branch1, branch2]);
|
||||
const categoriesTree: CategoryTreeNode[] = [root];
|
||||
|
||||
const result = getCategoryPath(categoriesTree, "target");
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.length).toBe(3);
|
||||
expect(result?.[0].metadata.name).toBe("root");
|
||||
expect(result?.[1].metadata.name).toBe("branch2");
|
||||
expect(result?.[2].metadata.name).toBe("target");
|
||||
});
|
||||
|
||||
it("should handle empty array input", () => {
|
||||
const result = getCategoryPath([], "any");
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -1,38 +1,41 @@
|
|||
import type { Category, CategorySpec } from "@halo-dev/api-client";
|
||||
import type { Category } from "@halo-dev/api-client";
|
||||
import { cloneDeep } from "lodash-es";
|
||||
|
||||
export interface CategoryTreeSpec extends Omit<CategorySpec, "children"> {
|
||||
children: CategoryTree[];
|
||||
export interface CategoryTreeNode extends Category {
|
||||
children: CategoryTreeNode[];
|
||||
}
|
||||
|
||||
export interface CategoryTree extends Omit<Category, "spec"> {
|
||||
spec: CategoryTreeSpec;
|
||||
}
|
||||
|
||||
export function buildCategoriesTree(categories: Category[]): CategoryTree[] {
|
||||
export function buildCategoriesTree(
|
||||
categories: Category[]
|
||||
): CategoryTreeNode[] {
|
||||
const categoriesToUpdate = cloneDeep(categories);
|
||||
|
||||
const categoriesMap = {};
|
||||
const parentMap = {};
|
||||
const categoriesMap: Record<string, CategoryTreeNode> = {};
|
||||
const parentMap: Record<string, string> = {};
|
||||
|
||||
categoriesToUpdate.forEach((category) => {
|
||||
categoriesMap[category.metadata.name] = category;
|
||||
// @ts-ignore
|
||||
categoriesMap[category.metadata.name] = {
|
||||
...category,
|
||||
children: [],
|
||||
} as CategoryTreeNode;
|
||||
|
||||
if (category.spec.children) {
|
||||
category.spec.children.forEach((child) => {
|
||||
parentMap[child] = category.metadata.name;
|
||||
});
|
||||
// @ts-ignore
|
||||
category.spec.children = [];
|
||||
}
|
||||
});
|
||||
|
||||
categoriesToUpdate.forEach((category) => {
|
||||
const parentName = parentMap[category.metadata.name];
|
||||
if (parentName && categoriesMap[parentName]) {
|
||||
categoriesMap[parentName].spec.children.push(category);
|
||||
categoriesMap[parentName].children.push(
|
||||
categoriesMap[category.metadata.name]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const categoriesTree = categoriesToUpdate.filter(
|
||||
const categoriesTree = Object.values(categoriesMap).filter(
|
||||
(node) => parentMap[node.metadata.name] === undefined
|
||||
);
|
||||
|
||||
|
@ -40,8 +43,8 @@ export function buildCategoriesTree(categories: Category[]): CategoryTree[] {
|
|||
}
|
||||
|
||||
export function sortCategoriesTree(
|
||||
categoriesTree: CategoryTree[] | Category[]
|
||||
): CategoryTree[] {
|
||||
categoriesTree: CategoryTreeNode[]
|
||||
): CategoryTreeNode[] {
|
||||
return categoriesTree
|
||||
.sort((a, b) => {
|
||||
if (a.spec.priority < b.spec.priority) {
|
||||
|
@ -53,13 +56,10 @@ export function sortCategoriesTree(
|
|||
return 0;
|
||||
})
|
||||
.map((category) => {
|
||||
if (category.spec.children.length) {
|
||||
if (category.children && category.children.length) {
|
||||
return {
|
||||
...category,
|
||||
spec: {
|
||||
...category.spec,
|
||||
children: sortCategoriesTree(category.spec.children),
|
||||
},
|
||||
children: sortCategoriesTree(category.children),
|
||||
};
|
||||
}
|
||||
return category;
|
||||
|
@ -67,54 +67,62 @@ export function sortCategoriesTree(
|
|||
}
|
||||
|
||||
export function resetCategoriesTreePriority(
|
||||
categoriesTree: CategoryTree[]
|
||||
): CategoryTree[] {
|
||||
categoriesTree: CategoryTreeNode[]
|
||||
): CategoryTreeNode[] {
|
||||
for (let i = 0; i < categoriesTree.length; i++) {
|
||||
categoriesTree[i].spec.priority = i;
|
||||
if (categoriesTree[i].spec.children) {
|
||||
resetCategoriesTreePriority(categoriesTree[i].spec.children);
|
||||
if (categoriesTree[i].children && categoriesTree[i].children.length) {
|
||||
resetCategoriesTreePriority(categoriesTree[i].children);
|
||||
}
|
||||
}
|
||||
return categoriesTree;
|
||||
}
|
||||
|
||||
export function convertTreeToCategories(categoriesTree: CategoryTree[]) {
|
||||
export function convertTreeToCategories(categoriesTree: CategoryTreeNode[]) {
|
||||
const categories: Category[] = [];
|
||||
const categoriesMap = new Map<string, Category>();
|
||||
const convertCategory = (node: CategoryTree | undefined) => {
|
||||
|
||||
const convertCategory = (node: CategoryTreeNode | undefined) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const children = node.spec.children || [];
|
||||
|
||||
const children = node.children || [];
|
||||
|
||||
categoriesMap.set(node.metadata.name, {
|
||||
...node,
|
||||
spec: {
|
||||
...node.spec,
|
||||
// @ts-ignore
|
||||
children: children.map((child) => child.metadata.name),
|
||||
},
|
||||
});
|
||||
|
||||
children.forEach((child) => {
|
||||
convertCategory(child);
|
||||
});
|
||||
};
|
||||
|
||||
categoriesTree.forEach((node) => {
|
||||
convertCategory(node);
|
||||
});
|
||||
|
||||
categoriesMap.forEach((node) => {
|
||||
categories.push(node);
|
||||
});
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
export function convertCategoryTreeToCategory(
|
||||
categoryTree: CategoryTree
|
||||
categoryTree: CategoryTreeNode
|
||||
): Category {
|
||||
const childNames = categoryTree.spec.children.map(
|
||||
(child) => child.metadata.name
|
||||
);
|
||||
const childNames = categoryTree.children.map((child) => child.metadata.name);
|
||||
|
||||
// eslint-disable-next-line
|
||||
const { children: _, ...categoryWithoutChildren } = categoryTree;
|
||||
|
||||
return {
|
||||
...categoryTree,
|
||||
...categoryWithoutChildren,
|
||||
spec: {
|
||||
...categoryTree.spec,
|
||||
children: childNames,
|
||||
|
@ -123,18 +131,18 @@ export function convertCategoryTreeToCategory(
|
|||
}
|
||||
|
||||
export const getCategoryPath = (
|
||||
categories: CategoryTree[],
|
||||
categories: CategoryTreeNode[],
|
||||
name: string,
|
||||
path: CategoryTree[] = []
|
||||
): CategoryTree[] | undefined => {
|
||||
path: CategoryTreeNode[] = []
|
||||
): CategoryTreeNode[] | undefined => {
|
||||
for (const category of categories) {
|
||||
if (category.metadata && category.metadata.name === name) {
|
||||
return path.concat([category]);
|
||||
}
|
||||
|
||||
if (category.spec && category.spec.children) {
|
||||
if (category.children && category.children.length) {
|
||||
const found = getCategoryPath(
|
||||
category.spec.children,
|
||||
category.children,
|
||||
name,
|
||||
path.concat([category])
|
||||
);
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
import HasPermission from "@/components/permission/HasPermission.vue";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
import { usePostCategory } from "@console/modules/contents/posts/categories/composables/use-post-category";
|
||||
import type { CategoryTree } from "@console/modules/contents/posts/categories/utils";
|
||||
import { convertTreeToCategories } from "@console/modules/contents/posts/categories/utils";
|
||||
import {
|
||||
convertTreeToCategories,
|
||||
type CategoryTreeNode,
|
||||
} from "@console/modules/contents/posts/categories/utils";
|
||||
import type { FormKitFrameworkContext } from "@formkit/core";
|
||||
import type { Category } from "@halo-dev/api-client";
|
||||
import { coreApiClient } from "@halo-dev/api-client";
|
||||
|
@ -38,11 +40,11 @@ const multiple = computed(() => {
|
|||
|
||||
const { categories, categoriesTree, handleFetchCategories } = usePostCategory();
|
||||
|
||||
provide<Ref<CategoryTree[]>>("categoriesTree", categoriesTree);
|
||||
provide<Ref<CategoryTreeNode[]>>("categoriesTree", categoriesTree);
|
||||
|
||||
const selectedCategory = ref<Category | CategoryTree>();
|
||||
const selectedCategory = ref<Category | CategoryTreeNode>();
|
||||
|
||||
provide<Ref<Category | CategoryTree | undefined>>(
|
||||
provide<Ref<Category | CategoryTreeNode | undefined>>(
|
||||
"selectedCategory",
|
||||
selectedCategory
|
||||
);
|
||||
|
@ -109,19 +111,19 @@ const selectedCategories = computed(() => {
|
|||
return [category].filter(Boolean) as Category[];
|
||||
});
|
||||
|
||||
const isSelected = (category: CategoryTree | Category) => {
|
||||
const isSelected = (category: CategoryTreeNode | Category) => {
|
||||
if (multiple.value) {
|
||||
return (props.context._value || []).includes(category.metadata.name);
|
||||
}
|
||||
return props.context._value === category.metadata.name;
|
||||
};
|
||||
|
||||
provide<(category: CategoryTree | Category) => boolean>(
|
||||
provide<(category: CategoryTreeNode | Category) => boolean>(
|
||||
"isSelected",
|
||||
isSelected
|
||||
);
|
||||
|
||||
const handleSelect = (category: CategoryTree | Category) => {
|
||||
const handleSelect = (category: CategoryTreeNode | Category) => {
|
||||
if (multiple.value) {
|
||||
const currentValue = props.context._value || [];
|
||||
if (currentValue.includes(category.metadata.name)) {
|
||||
|
|
|
@ -1,27 +1,28 @@
|
|||
<script lang="ts" setup>
|
||||
import type { CategoryTree } from "@console/modules/contents/posts/categories/utils";
|
||||
import type { CategoryTreeNode } from "@console/modules/contents/posts/categories/utils";
|
||||
import type { Category } from "@halo-dev/api-client";
|
||||
import { IconCheckboxCircle } from "@halo-dev/components";
|
||||
import { inject, ref, type Ref } from "vue";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
category: CategoryTree;
|
||||
category: CategoryTreeNode;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const isSelected = inject<(category: CategoryTree) => boolean>("isSelected");
|
||||
const selectedCategory = inject<Ref<Category | CategoryTree | undefined>>(
|
||||
const isSelected =
|
||||
inject<(category: CategoryTreeNode) => boolean>("isSelected");
|
||||
const selectedCategory = inject<Ref<Category | CategoryTreeNode | undefined>>(
|
||||
"selectedCategory",
|
||||
ref(undefined)
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "select", category: CategoryTree): void;
|
||||
(event: "select", category: CategoryTreeNode): void;
|
||||
}>();
|
||||
|
||||
const onSelect = (childCategory: CategoryTree) => {
|
||||
const onSelect = (childCategory: CategoryTreeNode) => {
|
||||
emit("select", childCategory);
|
||||
};
|
||||
</script>
|
||||
|
@ -54,11 +55,11 @@ const onSelect = (childCategory: CategoryTree) => {
|
|||
</div>
|
||||
|
||||
<ul
|
||||
v-if="category.spec.children.length > 0"
|
||||
v-if="category.children.length > 0"
|
||||
class="my-2.5 ml-2.5 border-l pl-1.5"
|
||||
>
|
||||
<CategoryListItem
|
||||
v-for="(childCategory, index) in category.spec.children"
|
||||
v-for="(childCategory, index) in category.children"
|
||||
:key="index"
|
||||
:category="childCategory"
|
||||
@select="onSelect"
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<script lang="ts" setup>
|
||||
import type { CategoryTree } from "@console/modules/contents/posts/categories/utils";
|
||||
import { getCategoryPath } from "@console/modules/contents/posts/categories/utils";
|
||||
import {
|
||||
getCategoryPath,
|
||||
type CategoryTreeNode,
|
||||
} from "@console/modules/contents/posts/categories/utils";
|
||||
import type { Category } from "@halo-dev/api-client";
|
||||
import { IconClose } from "@halo-dev/components";
|
||||
import { computed, inject, ref, type Ref } from "vue";
|
||||
|
@ -13,10 +15,13 @@ const props = withDefaults(
|
|||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "select", category: CategoryTree | Category): void;
|
||||
(event: "select", category: CategoryTreeNode | Category): void;
|
||||
}>();
|
||||
|
||||
const categoriesTree = inject<Ref<CategoryTree[]>>("categoriesTree", ref([]));
|
||||
const categoriesTree = inject<Ref<CategoryTreeNode[]>>(
|
||||
"categoriesTree",
|
||||
ref([])
|
||||
);
|
||||
|
||||
const label = computed(() => {
|
||||
const categories = getCategoryPath(
|
||||
|
@ -24,7 +29,7 @@ const label = computed(() => {
|
|||
props.category.metadata.name
|
||||
);
|
||||
return categories
|
||||
?.map((category: CategoryTree) => category.spec.displayName)
|
||||
?.map((category: CategoryTreeNode) => category.spec.displayName)
|
||||
.join(" / ");
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
getCategoryPath,
|
||||
type CategoryTree,
|
||||
type CategoryTreeNode,
|
||||
} from "@console/modules/contents/posts/categories/utils";
|
||||
import type { Category } from "@halo-dev/api-client";
|
||||
import { IconCheckboxCircle } from "@halo-dev/components";
|
||||
|
@ -14,17 +14,20 @@ const props = withDefaults(
|
|||
{}
|
||||
);
|
||||
|
||||
const categoriesTree = inject<Ref<CategoryTree[]>>("categoriesTree", ref([]));
|
||||
const selectedCategory = inject<Ref<Category | CategoryTree | undefined>>(
|
||||
const categoriesTree = inject<Ref<CategoryTreeNode[]>>(
|
||||
"categoriesTree",
|
||||
ref([])
|
||||
);
|
||||
const selectedCategory = inject<Ref<Category | CategoryTreeNode | undefined>>(
|
||||
"selectedCategory",
|
||||
ref(undefined)
|
||||
);
|
||||
|
||||
const isSelected =
|
||||
inject<(category: Category | CategoryTree) => boolean>("isSelected");
|
||||
inject<(category: Category | CategoryTreeNode) => boolean>("isSelected");
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "select", category: CategoryTree | Category): void;
|
||||
(event: "select", category: CategoryTreeNode | Category): void;
|
||||
}>();
|
||||
|
||||
const label = computed(() => {
|
||||
|
@ -33,7 +36,7 @@ const label = computed(() => {
|
|||
props.category.metadata.name
|
||||
);
|
||||
return categories
|
||||
?.map((category: CategoryTree) => category.spec.displayName)
|
||||
?.map((category: CategoryTreeNode) => category.spec.displayName)
|
||||
.join(" / ");
|
||||
});
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue