feat: menu items support setting ref relations (#604)

<!--  Thanks for sending a pull request!  Here are some tips for you:
1. 如果这是你的第一次,请阅读我们的贡献指南:<https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>。
1. If this is your first time, please read our contributor guidelines: <https://github.com/halo-dev/halo/blob/master/CONTRIBUTING.md>.
2. 请根据你解决问题的类型为 Pull Request 添加合适的标签。
2. Please label this pull request according to what type of issue you are addressing, especially if this is a release targeted pull request.
3. 请确保你已经添加并运行了适当的测试。
3. Ensure you have added or ran the appropriate tests for your PR.
-->

#### What type of PR is this?

/kind feature
/milestone 2.0

<!--
添加其中一个类别:
Add one of the following kinds:

/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind optimization

适当添加其中一个或多个类别(可选):
Optionally add one or more of the following kinds if applicable:

/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

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

菜单项支持设置与文章、分类、标签的关联关系。关联之后,当被关联对象有所改变时,同步菜单项更新。适配:https://github.com/halo-dev/halo/pull/2380

> 自定义页面(pageRef)会等到 https://github.com/halo-dev/halo/pull/2381 合并之后再做适配。

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

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

<!--
PR 合并时自动关闭 issue。
Automatically closes linked issue when PR is merged.

用法:`Fixes #<issue 号>`,或者 `Fixes (粘贴 issue 完整链接)`
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->

#### Screenshots:

<img width="1389" alt="image" src="https://user-images.githubusercontent.com/21301288/188453129-26711a32-707f-4c45-a137-fa386beff6a3.png">
<img width="1389" alt="image" src="https://user-images.githubusercontent.com/21301288/188453143-4c32ae32-3910-49a1-9a1f-1ae51f596c99.png">


<!--
如果此 PR 有 UI 的改动,最好截图说明这个 PR 的改动。
If there are UI changes to this PR, it is best to take a screenshot to illustrate the changes to this PR.

eg.

Before:

![screenshot-before](https://user-images.githubusercontent.com/screenshot.png)

After:

![screenshot-after](https://user-images.githubusercontent.com/screenshot.png)
-->

#### Special notes for your reviewer:

/hold until https://github.com/halo-dev/halo/pull/2380 merge

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

<!--
如果当前 Pull Request 的修改不会造成用户侧的任何变更,在 `release-note` 代码块儿中填写 `NONE`。
否则请填写用户侧能够理解的 Release Note。如果当前 Pull Request 包含破坏性更新(Break Change),
Release Note 需要以 `action required` 开头。
If no, just write "NONE" in the release-note block below.
If yes, a release note is required:
Enter your extended release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".
-->

```release-note
None
```
pull/607/head
Ryan Wang 2022-09-06 15:24:11 +08:00 committed by GitHub
parent f2a1f3a303
commit a57e832816
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 268 additions and 41 deletions

View File

@ -33,7 +33,7 @@
"@formkit/themes": "1.0.0-beta.10",
"@formkit/vue": "1.0.0-beta.10",
"@halo-dev/admin-shared": "workspace:*",
"@halo-dev/api-client": "^0.0.14",
"@halo-dev/api-client": "^0.0.15",
"@halo-dev/components": "workspace:*",
"@halo-dev/richtext-editor": "^0.0.0-alpha.5",
"@tiptap/extension-character-count": "2.0.0-beta.31",

View File

@ -38,7 +38,7 @@
"homepage": "https://github.com/halo-dev/halo-admin/tree/next/shared/components#readme",
"license": "MIT",
"dependencies": {
"@halo-dev/api-client": "^0.0.14",
"@halo-dev/api-client": "^0.0.15",
"@halo-dev/components": "workspace:*",
"axios": "^0.27.2",
"lodash.merge": "^4.6.2"

View File

@ -13,7 +13,7 @@ importers:
'@formkit/themes': 1.0.0-beta.10
'@formkit/vue': 1.0.0-beta.10
'@halo-dev/admin-shared': workspace:*
'@halo-dev/api-client': ^0.0.14
'@halo-dev/api-client': ^0.0.15
'@halo-dev/components': workspace:*
'@halo-dev/richtext-editor': ^0.0.0-alpha.5
'@iconify-json/vscode-icons': ^1.1.11
@ -92,7 +92,7 @@ importers:
'@formkit/themes': 1.0.0-beta.10_tailwindcss@3.1.8
'@formkit/vue': 1.0.0-beta.10_wwmyxdjqen5bmh3tr2meig5lki
'@halo-dev/admin-shared': link:packages/shared
'@halo-dev/api-client': 0.0.14
'@halo-dev/api-client': 0.0.15
'@halo-dev/components': link:packages/components
'@halo-dev/richtext-editor': 0.0.0-alpha.5_vue@3.2.37
'@tiptap/extension-character-count': 2.0.0-beta.31
@ -194,14 +194,14 @@ importers:
packages/shared:
specifiers:
'@halo-dev/api-client': ^0.0.14
'@halo-dev/api-client': ^0.0.15
'@halo-dev/components': workspace:*
'@types/lodash.merge': ^4.6.7
axios: ^0.27.2
lodash.merge: ^4.6.2
vite-plugin-dts: ^1.4.1
dependencies:
'@halo-dev/api-client': 0.0.14
'@halo-dev/api-client': 0.0.15
'@halo-dev/components': link:../components
axios: 0.27.2
lodash.merge: 4.6.2
@ -2129,8 +2129,8 @@ packages:
- windicss
dev: false
/@halo-dev/api-client/0.0.14:
resolution: {integrity: sha512-Qh0/l2f5e8lxBgAU2brN28F3CzZTlxGUGY0puUGbuDRYqEENbQ5pGTHG3CzuiG4it4Pn16xSTCXICO6M9m9X4A==}
/@halo-dev/api-client/0.0.15:
resolution: {integrity: sha512-RCQXU2s5IJJ3pUORg0n2b/CYbDox/Jm4aXB2J9ZbKod3vdS3HM6X33iVXc/pyuxRjucijly3WyLkiTZ0bkwLbg==}
dev: false
/@halo-dev/richtext-editor/0.0.0-alpha.5_vue@3.2.37:

View File

@ -54,8 +54,8 @@ const checkedAll = ref(false);
const selectedPostNames = ref<string[]>([]);
const { users } = useUserFetch();
const { categories } = usePostCategory();
const { tags } = usePostTag();
const { categories } = usePostCategory({ fetchOnMounted: true });
const { tags } = usePostTag({ fetchOnMounted: true });
const dialog = useDialog();
const handleFetchPosts = async () => {

View File

@ -40,7 +40,7 @@ const {
loading,
handleFetchCategories,
handleDelete,
} = usePostCategory();
} = usePostCategory({ fetchOnMounted: true });
const handleUpdateInBatch = useDebounceFn(async () => {
const categoriesTreeToUpdate = resetCategoriesTreePriority(

View File

@ -14,7 +14,11 @@ interface usePostCategoryReturn {
handleDelete: (category: CategoryTree) => void;
}
export function usePostCategory(): usePostCategoryReturn {
export function usePostCategory(options?: {
fetchOnMounted: boolean;
}): usePostCategoryReturn {
const { fetchOnMounted } = options || {};
const categories = ref<Category[]>([] as Category[]);
const categoriesTree = ref<CategoryTree[]>([] as CategoryTree[]);
const loading = ref(false);
@ -59,7 +63,9 @@ export function usePostCategory(): usePostCategoryReturn {
});
};
onMounted(handleFetchCategories);
onMounted(() => {
fetchOnMounted && handleFetchCategories();
});
return {
categories,

View File

@ -67,7 +67,7 @@ const saving = ref(false);
const publishing = ref(false);
const publishCanceling = ref(false);
const { categories } = usePostCategory();
const { categories } = usePostCategory({ fetchOnMounted: true });
const categoriesMap = computed(() => {
return categories.value.map((category) => {
return {
@ -77,7 +77,7 @@ const categoriesMap = computed(() => {
});
});
const { tags } = usePostTag();
const { tags } = usePostTag({ fetchOnMounted: true });
const tagsMap = computed(() => {
return tags.value.map((tag) => {
return {

View File

@ -40,7 +40,9 @@ const viewTypes = [
const viewType = ref("list");
const { tags, loading, handleFetchTags, handleDelete } = usePostTag();
const { tags, loading, handleFetchTags, handleDelete } = usePostTag({
fetchOnMounted: true,
});
const editingModal = ref(false);
const selectedTag = ref<Tag | null>(null);

View File

@ -11,7 +11,11 @@ interface usePostTagReturn {
handleDelete: (tag: Tag) => void;
}
export function usePostTag(): usePostTagReturn {
export function usePostTag(options?: {
fetchOnMounted: boolean;
}): usePostTagReturn {
const { fetchOnMounted } = options || {};
const tags = ref<Tag[]>([] as Tag[]);
const loading = ref(false);
@ -53,7 +57,9 @@ export function usePostTag(): usePostTagReturn {
});
};
onMounted(handleFetchTags);
onMounted(() => {
fetchOnMounted && handleFetchTags();
});
return {
tags,

View File

@ -1,12 +1,14 @@
<script lang="ts" setup>
import { VButton, VModal, VSpace } from "@halo-dev/components";
import { computed, ref, watch, watchEffect } from "vue";
import type { MenuItem } from "@halo-dev/api-client";
import type { MenuItem, Post } from "@halo-dev/api-client";
import { v4 as uuid } from "uuid";
import { apiClient } from "@halo-dev/admin-shared";
import { reset, submitForm } from "@formkit/core";
import cloneDeep from "lodash.clonedeep";
import { useMagicKeys } from "@vueuse/core";
import { usePostCategory } from "@/modules/contents/posts/categories/composables/use-post-category";
import { usePostTag } from "@/modules/contents/posts/tags/composables/use-post-tag";
const props = withDefaults(
defineProps<{
@ -50,6 +52,21 @@ const handleSaveMenuItem = async () => {
try {
saving.value = true;
const menuItemSource = menuItemSources.find(
(source) => source.value === selectedMenuItemSource.value
);
if (menuItemSource) {
const { ref } = menuItemSource;
if (ref) {
formState.value.spec[ref] = {
version: "content.halo.run/v1alpha1",
kind: menuItemSource.kind,
name: selectedRef.value as string,
};
}
}
if (isUpdateMode.value) {
const { data } =
await apiClient.extension.menuItem.updatev1alpha1MenuItem({
@ -80,29 +97,164 @@ const onVisibleChange = (visible: boolean) => {
}
};
watch(props, (newVal) => {
if (newVal.visible && props.menuItem) {
formState.value = cloneDeep(props.menuItem);
return;
}
const handleResetForm = () => {
formState.value = cloneDeep(initialFormState);
formState.value.metadata.name = uuid();
reset("menuitem-form");
});
};
const { Command_Enter } = useMagicKeys();
watchEffect(() => {
let keyboardWatcher;
const { Command_Enter } = useMagicKeys();
if (props.visible) {
keyboardWatcher = watch(Command_Enter, (v) => {
if (v) {
if (Command_Enter.value && props.visible) {
submitForm("menuitem-form");
}
});
} else {
keyboardWatcher?.unwatch();
}
});
watch(
() => props.visible,
(visible) => {
if (!visible) {
handleResetForm();
}
}
);
watch(
() => props.menuItem,
(menuItem) => {
if (menuItem) {
formState.value = cloneDeep(menuItem);
// Set Ref related
const { postRef, categoryRef, tagRef } = formState.value.spec;
if (postRef) {
selectedMenuItemSource.value = "post";
selectedRef.value = postRef.name;
}
if (categoryRef) {
selectedMenuItemSource.value = "category";
selectedRef.value = categoryRef.name;
}
if (tagRef) {
selectedMenuItemSource.value = "tag";
selectedRef.value = tagRef.name;
}
} else {
handleResetForm();
}
}
);
// MenuItem Ref
interface MenuItemSource {
label: string;
value: string;
ref?: "postRef" | "categoryRef" | "tagRef";
kind?: "Post" | "Category" | "Tag";
}
const menuItemSources: MenuItemSource[] = [
{
label: "自定义链接",
value: "custom",
},
{
label: "文章",
value: "post",
ref: "postRef",
kind: "Post",
},
{
label: "分类",
value: "category",
ref: "categoryRef",
kind: "Post",
},
{
label: "标签",
value: "tag",
ref: "tagRef",
kind: "Tag",
},
];
const selectedMenuItemSource = ref<string>(menuItemSources[0].value);
const { categories, handleFetchCategories } = usePostCategory();
const { tags, handleFetchTags } = usePostTag();
const posts = ref<Post[]>([] as Post[]);
const postMap = computed(() => {
return [
{ label: "请选择文章", value: undefined },
...posts.value.map((post) => {
return {
label: post.spec.title,
value: post.metadata.name,
};
}),
];
});
const categoryMap = computed(() => {
return [
{
label: "请选择分类",
value: undefined,
},
...categories.value.map((category) => {
return {
label: category.spec.displayName,
value: category.metadata.name,
};
}),
];
});
const tagMap = computed(() => {
return [
{
label: "请选择标签",
value: undefined,
},
...tags.value.map((tag) => {
return {
label: tag.spec.displayName,
value: tag.metadata.name,
};
}),
];
});
const selectedRef = ref<string>("");
const handleFetchPosts = async () => {
const { data } =
await apiClient.extension.post.listcontentHaloRunV1alpha1Post({
page: 0,
size: 0,
});
posts.value = data.items;
};
const onMenuItemSourceChange = () => {
selectedRef.value = "";
};
watch(
() => props.visible,
(newValue) => {
if (newValue) {
handleFetchCategories();
handleFetchTags();
handleFetchPosts();
}
}
);
</script>
<template>
<VModal
@ -113,17 +265,56 @@ watchEffect(() => {
>
<FormKit id="menuitem-form" type="form" @submit="handleSaveMenuItem">
<FormKit
v-model="selectedMenuItemSource"
:options="menuItemSources"
:disabled="isUpdateMode"
label="类型"
type="select"
@change="onMenuItemSourceChange"
>
</FormKit>
<FormKit
v-if="selectedMenuItemSource === 'custom'"
v-model="formState.spec.displayName"
label="名称"
type="text"
validation="required"
></FormKit>
<FormKit
v-if="selectedMenuItemSource === 'custom'"
v-model="formState.spec.href"
label="链接地址"
type="text"
validation="required"
></FormKit>
<FormKit
v-if="selectedMenuItemSource === 'post'"
v-model="selectedRef"
label="文章"
type="select"
:options="postMap"
validation="required"
></FormKit>
<FormKit
v-if="selectedMenuItemSource === 'tag'"
v-model="selectedRef"
label="标签"
type="select"
:options="tagMap"
validation="required"
></FormKit>
<FormKit
v-if="selectedMenuItemSource === 'category'"
v-model="selectedRef"
label="分类"
type="select"
:options="categoryMap"
validation="required"
></FormKit>
</FormKit>
<template #footer>
<VSpace>

View File

@ -1,5 +1,11 @@
<script lang="ts" setup>
import { IconList, IconSettings, VButton, VSpace } from "@halo-dev/components";
import {
IconList,
IconSettings,
VButton,
VSpace,
VTag,
} from "@halo-dev/components";
import Draggable from "vuedraggable";
import { ref } from "vue";
import type { MenuTreeItem } from "@/modules/interface/menus/utils";
@ -32,6 +38,19 @@ function onOpenEditingModal(menuItem: MenuTreeItem) {
function onDelete(menuItem: MenuTreeItem) {
emit("delete", menuItem);
}
function getMenuItemRefDisplayName(menuItem: MenuTreeItem) {
if (menuItem.spec.postRef) {
return "文章";
}
if (menuItem.spec.categoryRef) {
return "分类";
}
if (menuItem.spec.tagRef) {
return "标签";
}
return undefined;
}
</script>
<template>
<draggable
@ -52,26 +71,29 @@ function onDelete(menuItem: MenuTreeItem) {
class="group relative block cursor-pointer px-4 py-3 transition-all hover:bg-gray-50"
>
<div
class="drag-element absolute inset-y-0 left-0 flex hidden w-3.5 cursor-move items-center bg-gray-100 transition-all hover:bg-gray-200 group-hover:flex"
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>
<div class="relative flex flex-row items-center">
<div class="flex-1">
<div class="flex flex-row items-center">
<div class="flex flex-row items-center gap-2">
<span class="truncate text-sm font-medium text-gray-900">
{{ menuItem.spec.displayName }}
{{ menuItem.status.displayName }}
</span>
<VTag v-if="getMenuItemRefDisplayName(menuItem)">
{{ getMenuItemRefDisplayName(menuItem) }}
</VTag>
</div>
<div class="mt-1 flex">
<VSpace align="start" direction="column" spacing="xs">
<a
:href="menuItem.spec.href"
:href="menuItem.status.href"
class="text-xs text-gray-500 hover:text-gray-900"
target="_blank"
>
{{ menuItem.spec.href }}
{{ menuItem.status.href }}
</a>
</VSpace>
</div>