refactor: operation and entity field extension points (#4530)

#### What type of PR is this?

/area console
/kind improvement
/milestone 2.9.x

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

重构数据列表操作项和显示字段扩展点的类型定义和条件判断:

1. `EntityDropdownItem` 改为 `OperationItem` 以适配不同的场景,最开始仅仅是为了提供给 Entity 组件,但后面发现如主题管理列表并没有使用 Entity 组件,所以定义统一改为 Operation 。
2. 修改 `OperationItem` 和 `EntityFieldItem` 的 `visible` 字段为 `hidden`,方便渲染的时候判断,并方便调用方默认不设置值。

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

```release-note
None
```
pull/4510/head
Ryan Wang 2023-08-31 05:36:12 -05:00 committed by GitHub
parent 8eaedd6ee8
commit 1892dce64b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 128 additions and 133 deletions

View File

@ -48,6 +48,6 @@ export interface EntityFieldItem {
component: Raw<Component>; // 字段组件,可以使用 `@halo-dev/components` 中提供的 `VEntityField`,也可以自定义
props?: Record<string, unknown>; // 组件的 props
permissions?: string[]; // 权限设置
visible?: boolean; // 是否可见
hidden?: boolean; // 是否隐藏
}
```

View File

@ -8,10 +8,10 @@
目前支持扩展的数据列表:
- 文章:`"post:list-item:operation:create"?: (post: Ref<ListedPost>) => | EntityDropdownItem<ListedPost>[] | Promise<EntityDropdownItem<ListedPost>[]>`
- 插件:`"plugin:list-item:operation:create"?: (plugin: Ref<Plugin>) => | EntityDropdownItem<Plugin>[] | Promise<EntityDropdownItem<Plugin>[]>`
- 备份:`"backup:list-item:operation:create"?: (backup: Ref<Backup>) => | EntityDropdownItem<Backup>[] | Promise<EntityDropdownItem<Backup>[]>`
- 主题:`"theme:list-item:operation:create"?: (theme: Ref<Theme>) => | EntityDropdownItem<Theme>[] | Promise<EntityDropdownItem<Theme>[]>`
- 文章:`"post:list-item:operation:create"?: (post: Ref<ListedPost>) => | OperationItem<ListedPost>[] | Promise<OperationItem<ListedPost>[]>`
- 插件:`"plugin:list-item:operation:create"?: (plugin: Ref<Plugin>) => | OperationItem<Plugin>[] | Promise<OperationItem<Plugin>[]>`
- 备份:`"backup:list-item:operation:create"?: (backup: Ref<Backup>) => | OperationItem<Backup>[] | Promise<OperationItem<Backup>[]>`
- 主题:`"theme:list-item:operation:create"?: (theme: Ref<Theme>) => | OperationItem<Theme>[] | Promise<OperationItem<Theme>[]>`
示例:
@ -32,7 +32,6 @@ export default definePlugin({
priority: 21,
component: markRaw(VDropdownItem),
label: "导出为 Markdown 文档",
visible: true,
permissions: [],
action: async (post: ListedPost) => {
const { data } = await axios.get(
@ -54,17 +53,17 @@ export default definePlugin({
});
```
`EntityDropdownItem` 类型:
`OperationItem` 类型:
```ts
export interface EntityDropdownItem<T> {
export interface OperationItem<T> {
priority: number; // 优先级,越小越靠前
component: Raw<Component>; // 菜单项组件,可以使用 `@halo-dev/components` 中提供的 `VDropdownItem`,也可以自定义
props?: Record<string, unknown>; // 组件的 props
action?: (item?: T) => void; // 点击事件
label?: string; // 菜单项名称
visible?: boolean; // 是否可见
hidden?: boolean; // 是否隐藏
permissions?: string[]; // 权限
children?: EntityDropdownItem<T>[]; // 子菜单
children?: OperationItem<T>[]; // 子菜单
}
```

View File

@ -10,3 +10,4 @@ export * from "./states/backup";
export * from "./states/plugin-installation-tabs";
export * from "./states/entity";
export * from "./states/theme-list-tabs";
export * from "./states/operation";

View File

@ -1,21 +1,10 @@
import type { Component, Raw } from "vue";
export interface EntityDropdownItem<T> {
priority: number;
component: Raw<Component>;
props?: Record<string, unknown>;
action?: (item?: T) => void;
label?: string;
visible?: boolean;
permissions?: string[];
children?: EntityDropdownItem<T>[];
}
export interface EntityFieldItem {
priority: number;
position: "start" | "end";
component: Raw<Component>;
props?: Record<string, unknown>;
permissions?: string[];
visible?: boolean;
hidden?: boolean;
}

View File

@ -0,0 +1,12 @@
import type { Component, Raw } from "vue";
export interface OperationItem<T> {
priority: number;
component: Raw<Component>;
props?: Record<string, unknown>;
action?: (item?: T) => void;
label?: string;
hidden?: boolean;
permissions?: string[];
children?: OperationItem<T>[];
}

View File

@ -7,7 +7,8 @@ import type { AnyExtension } from "@tiptap/vue-3";
import type { CommentSubjectRefProvider } from "@/states/comment-subject-ref";
import type { BackupTab } from "@/states/backup";
import type { PluginInstallationTab } from "@/states/plugin-installation-tabs";
import type { EntityDropdownItem, EntityFieldItem } from "@/states/entity";
import type { EntityFieldItem } from "@/states/entity";
import type { OperationItem } from "@/states/operation";
import type { ThemeListTab } from "@/states/theme-list-tabs";
import type { Backup, ListedPost, Plugin, Theme } from "@halo-dev/api-client";
@ -42,17 +43,15 @@ export interface ExtensionPoint {
"post:list-item:operation:create"?: (
post: Ref<ListedPost>
) =>
| EntityDropdownItem<ListedPost>[]
| Promise<EntityDropdownItem<ListedPost>[]>;
) => OperationItem<ListedPost>[] | Promise<OperationItem<ListedPost>[]>;
"plugin:list-item:operation:create"?: (
plugin: Ref<Plugin>
) => EntityDropdownItem<Plugin>[] | Promise<EntityDropdownItem<Plugin>[]>;
) => OperationItem<Plugin>[] | Promise<OperationItem<Plugin>[]>;
"backup:list-item:operation:create"?: (
backup: Ref<Backup>
) => EntityDropdownItem<Backup>[] | Promise<EntityDropdownItem<Backup>[]>;
) => OperationItem<Backup>[] | Promise<OperationItem<Backup>[]>;
"plugin:list-item:field:create"?: (
plugin: Ref<Plugin>
@ -62,7 +61,7 @@ export interface ExtensionPoint {
"theme:list-item:operation:create"?: (
theme: Ref<Theme>
) => EntityDropdownItem<Theme>[] | Promise<EntityDropdownItem<Theme>[]>;
) => OperationItem<Theme>[] | Promise<OperationItem<Theme>[]>;
}
export interface PluginModule {

View File

@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { EntityFieldItem } from "packages/shared/dist";
import { usePermission } from "@/utils/permission";
import type { EntityFieldItem } from "@halo-dev/console-shared";
withDefaults(
defineProps<{
@ -7,6 +8,8 @@ withDefaults(
}>(),
{}
);
const { currentUserHasPermission } = usePermission();
</script>
<template>
@ -17,7 +20,7 @@ withDefaults(
<component
:is="field.component"
v-bind="field.props"
v-if="field.visible"
v-if="!field.hidden && currentUserHasPermission(field.permissions)"
/>
</template>
</template>

View File

@ -1,10 +1,13 @@
<script setup lang="ts" generic="T">
import type { EntityDropdownItem } from "@halo-dev/console-shared";
import type { OperationItem } from "@halo-dev/console-shared";
import { VDropdown } from "@halo-dev/components";
import { usePermission } from "@/utils/permission";
const { currentUserHasPermission } = usePermission();
const props = withDefaults(
defineProps<{
dropdownItems: EntityDropdownItem<T>[];
dropdownItems: OperationItem<T>[];
item?: T;
}>(),
{
@ -12,7 +15,7 @@ const props = withDefaults(
}
);
function action(dropdownItem: EntityDropdownItem<T>) {
function action(dropdownItem: OperationItem<T>) {
if (!dropdownItem.action) {
return;
}
@ -22,11 +25,15 @@ function action(dropdownItem: EntityDropdownItem<T>) {
<template>
<template v-for="(dropdownItem, index) in dropdownItems">
<template v-if="dropdownItem.visible">
<template
v-if="
!dropdownItem.hidden &&
currentUserHasPermission(dropdownItem.permissions)
"
>
<VDropdown
v-if="dropdownItem.children?.length"
:key="`dropdown-children-items-${index}`"
v-permission="dropdownItem.permissions"
:triggers="['click']"
>
<component
@ -40,10 +47,12 @@ function action(dropdownItem: EntityDropdownItem<T>) {
<template v-for="(childItem, childIndex) in dropdownItem.children">
<component
:is="childItem.component"
v-if="childItem.visible"
v-if="
!childItem.hidden &&
currentUserHasPermission(childItem.permissions)
"
v-bind="childItem.props"
:key="`dropdown-child-item-${childIndex}`"
v-permission="childItem.permissions"
@click="action(childItem)"
>
{{ childItem.label }}
@ -56,7 +65,6 @@ function action(dropdownItem: EntityDropdownItem<T>) {
v-else
v-bind="dropdownItem.props"
:key="`dropdown-item-${index}`"
v-permission="dropdownItem.permissions"
@click="action(dropdownItem)"
>
{{ dropdownItem.label }}

View File

@ -1,62 +1,17 @@
import { usePluginModuleStore } from "@/stores/plugin";
import { usePermission } from "@/utils/permission";
import type {
EntityDropdownItem,
EntityFieldItem,
PluginModule,
} from "@halo-dev/console-shared";
import type { EntityFieldItem, PluginModule } from "@halo-dev/console-shared";
import { onMounted, ref, type ComputedRef, type Ref, computed } from "vue";
export function useEntityDropdownItemExtensionPoint<T>(
extensionPointName: string,
entity: Ref<T>,
presets: ComputedRef<EntityDropdownItem<T>[]>
) {
const { pluginModules } = usePluginModuleStore();
const itemsFromPlugins = ref<EntityDropdownItem<T>[]>([]);
onMounted(() => {
pluginModules.forEach((pluginModule: PluginModule) => {
const { extensionPoints } = pluginModule;
if (!extensionPoints?.[extensionPointName]) {
return;
}
const items = extensionPoints[extensionPointName](
entity
) as EntityDropdownItem<T>[];
itemsFromPlugins.value.push(...items);
});
});
const dropdownItems = computed(() => {
return [...presets.value, ...itemsFromPlugins.value].sort((a, b) => {
return a.priority - b.priority;
});
});
return { dropdownItems };
}
export function useEntityFieldItemExtensionPoint<T>(
extensionPointName: string,
entity: Ref<T>,
presets: ComputedRef<EntityFieldItem[]>
) {
const { pluginModules } = usePluginModuleStore();
const { currentUserHasPermission } = usePermission();
const itemsFromPlugins = ref<EntityFieldItem[]>([]);
const allItems = computed(() => {
return [...presets.value, ...itemsFromPlugins.value].map((item) => {
return {
...item,
visible:
item.visible !== false && currentUserHasPermission(item.permissions),
};
});
return [...presets.value, ...itemsFromPlugins.value];
});
onMounted(() => {

View File

@ -0,0 +1,36 @@
import { usePluginModuleStore } from "@/stores/plugin";
import type { OperationItem, PluginModule } from "@halo-dev/console-shared";
import { onMounted, ref, type ComputedRef, type Ref, computed } from "vue";
export function useOperationItemExtensionPoint<T>(
extensionPointName: string,
entity: Ref<T>,
presets: ComputedRef<OperationItem<T>[]>
) {
const { pluginModules } = usePluginModuleStore();
const itemsFromPlugins = ref<OperationItem<T>[]>([]);
onMounted(() => {
pluginModules.forEach((pluginModule: PluginModule) => {
const { extensionPoints } = pluginModule;
if (!extensionPoints?.[extensionPointName]) {
return;
}
const items = extensionPoints[extensionPointName](
entity
) as OperationItem<T>[];
itemsFromPlugins.value.push(...items);
});
});
const operationItems = computed(() => {
return [...presets.value, ...itemsFromPlugins.value].sort((a, b) => {
return a.priority - b.priority;
});
});
return { operationItems };
}

View File

@ -27,9 +27,10 @@ import { ref } from "vue";
import { computed } from "vue";
import { markRaw } from "vue";
import { useRouter } from "vue-router";
import { useEntityDropdownItemExtensionPoint } from "@/composables/use-entity-extension-points";
import { useOperationItemExtensionPoint } from "@/composables/use-operation-extension-points";
import EntityDropdownItems from "@/components/entity/EntityDropdownItems.vue";
import { toRefs } from "vue";
import type { OperationItem } from "packages/shared/dist";
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
@ -122,15 +123,14 @@ const handleDelete = async () => {
});
};
const { dropdownItems } = useEntityDropdownItemExtensionPoint<ListedPost>(
const { operationItems } = useOperationItemExtensionPoint<ListedPost>(
"post:list-item:operation:create",
post,
computed(() => [
computed((): OperationItem<ListedPost>[] => [
{
priority: 10,
component: markRaw(VDropdownItem),
label: t("core.common.buttons.edit"),
visible: true,
permissions: [],
action: () => {
router.push({
@ -143,7 +143,6 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<ListedPost>(
priority: 20,
component: markRaw(VDropdownItem),
label: t("core.common.buttons.setting"),
visible: true,
permissions: [],
action: () => {
emit("open-setting-modal", props.post.post);
@ -152,7 +151,6 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<ListedPost>(
{
priority: 30,
component: markRaw(VDropdownDivider),
visible: true,
},
{
priority: 40,
@ -161,7 +159,6 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<ListedPost>(
type: "danger",
},
label: t("core.common.buttons.delete"),
visible: true,
permissions: [],
action: handleDelete,
},
@ -327,7 +324,7 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<ListedPost>(
v-if="currentUserHasPermission(['system:posts:manage'])"
#dropdownItems
>
<EntityDropdownItems :dropdown-items="dropdownItems" :item="post" />
<EntityDropdownItems :dropdown-items="operationItems" :item="post" />
</template>
</VEntity>
</template>

View File

@ -17,11 +17,12 @@ import { useThemeLifeCycle } from "../composables/use-theme";
import { usePermission } from "@/utils/permission";
import { useI18n } from "vue-i18n";
import { useQueryClient } from "@tanstack/vue-query";
import { useEntityDropdownItemExtensionPoint } from "@/composables/use-entity-extension-points";
import { useOperationItemExtensionPoint } from "@/composables/use-operation-extension-points";
import { markRaw } from "vue";
import { defineComponent } from "vue";
import UninstallOperationItem from "./operation/UninstallOperationItem.vue";
import { computed } from "vue";
import type { OperationItem } from "packages/shared/dist";
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
@ -83,10 +84,10 @@ const handleCreateTheme = async () => {
}
};
const { dropdownItems } = useEntityDropdownItemExtensionPoint<Theme>(
const { operationItems } = useOperationItemExtensionPoint<Theme>(
"theme:list-item:operation:create",
theme,
computed(() => [
computed((): OperationItem<Theme>[] => [
{
priority: 10,
component: markRaw(VButton),
@ -95,9 +96,8 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Theme>(
},
action: () => handleActiveTheme(true),
label: t("core.common.buttons.activate"),
visible:
!isActivated.value &&
currentUserHasPermission(["system:themes:manage"]),
hidden: isActivated.value,
permissions: ["system:themes:manage"],
},
{
priority: 20,
@ -121,7 +121,7 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Theme>(
template: `<VButton size="sm"><IconMore /></VButton>`,
})
),
visible: currentUserHasPermission(["system:themes:manage"]),
permissions: ["system:themes:manage"],
children: [
{
priority: 10,
@ -238,12 +238,13 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Theme>(
</div>
<div>
<VSpace v-if="installed">
<template v-for="(item, index) in dropdownItems" :key="index">
<template v-for="(item, index) in operationItems" :key="index">
<template v-if="!item.children?.length">
<component
:is="item.component"
v-if="item.visible !== false"
v-permission="item.permissions"
v-if="
!item.hidden && currentUserHasPermission(item.permissions)
"
v-bind="item.props"
@click="item.action?.(theme)"
>
@ -252,8 +253,9 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Theme>(
</template>
<template v-else>
<VDropdown
v-if="item.visible !== false"
v-permission="item.permissions"
v-if="
!item.hidden && currentUserHasPermission(item.permissions)
"
>
<component
:is="item.component"
@ -269,8 +271,10 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Theme>(
>
<component
:is="childItem.component"
v-if="childItem.visible !== false"
v-permission="childItem.permissions"
v-if="
!childItem.hidden &&
currentUserHasPermission(childItem.permissions)
"
v-bind="childItem.props"
@click="childItem.action?.(theme)"
>

View File

@ -15,9 +15,10 @@ import { apiClient } from "@/utils/api-client";
import { useQueryClient } from "@tanstack/vue-query";
import prettyBytes from "pretty-bytes";
import { useI18n } from "vue-i18n";
import { useEntityDropdownItemExtensionPoint } from "@/composables/use-entity-extension-points";
import { useOperationItemExtensionPoint } from "@/composables/use-operation-extension-points";
import EntityDropdownItems from "@/components/entity/EntityDropdownItems.vue";
import { toRefs } from "vue";
import type { OperationItem } from "packages/shared/dist";
const queryClient = useQueryClient();
const { t } = useI18n();
@ -100,15 +101,15 @@ function handleDelete() {
});
}
const { dropdownItems } = useEntityDropdownItemExtensionPoint<Backup>(
const { operationItems } = useOperationItemExtensionPoint<Backup>(
"backup:list-item:operation:create",
backup,
computed(() => [
computed((): OperationItem<Backup>[] => [
{
priority: 10,
component: markRaw(VDropdownItem),
label: t("core.common.buttons.download"),
visible: props.backup.status?.phase === "SUCCEEDED",
hidden: props.backup.status?.phase !== "SUCCEEDED",
permissions: [],
action: () => handleDownload(),
},
@ -119,7 +120,6 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Backup>(
type: "danger",
},
label: t("core.common.buttons.delete"),
visible: true,
action: () => handleDelete(),
},
])
@ -187,7 +187,7 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Backup>(
</VEntityField>
</template>
<template #dropdownItems>
<EntityDropdownItems :dropdown-items="dropdownItems" :item="backup" />
<EntityDropdownItems :dropdown-items="operationItems" :item="backup" />
</template>
</VEntity>
</template>

View File

@ -16,10 +16,8 @@ import { apiClient } from "@/utils/api-client";
import { useI18n } from "vue-i18n";
import type { Ref } from "vue";
import { ref } from "vue";
import {
useEntityDropdownItemExtensionPoint,
useEntityFieldItemExtensionPoint,
} from "@/composables/use-entity-extension-points";
import { useEntityFieldItemExtensionPoint } from "@/composables/use-entity-extension-points";
import { useOperationItemExtensionPoint } from "@/composables/use-operation-extension-points";
import { useRouter } from "vue-router";
import EntityDropdownItems from "@/components/entity/EntityDropdownItems.vue";
import EntityFieldItems from "@/components/entity-fields/EntityFieldItems.vue";
@ -28,6 +26,7 @@ import StatusDotField from "@/components/entity-fields/StatusDotField.vue";
import AuthorField from "./entity-fields/AuthorField.vue";
import SwitchField from "./entity-fields/SwitchField.vue";
import { computed } from "vue";
import type { EntityFieldItem, OperationItem } from "packages/shared/dist";
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
@ -76,15 +75,14 @@ const handleResetSettingConfig = async () => {
});
};
const { dropdownItems } = useEntityDropdownItemExtensionPoint<Plugin>(
const { operationItems } = useOperationItemExtensionPoint<Plugin>(
"plugin:list-item:operation:create",
plugin,
computed(() => [
computed((): OperationItem<Plugin>[] => [
{
priority: 10,
component: markRaw(VDropdownItem),
label: t("core.common.buttons.detail"),
visible: true,
permissions: [],
action: () => {
router.push({
@ -97,7 +95,6 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Plugin>(
priority: 20,
component: markRaw(VDropdownItem),
label: t("core.common.buttons.upgrade"),
visible: true,
permissions: [],
action: () => {
emit("open-upgrade-modal", props.plugin);
@ -106,7 +103,6 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Plugin>(
{
priority: 30,
component: markRaw(VDropdownDivider),
visible: true,
},
{
priority: 40,
@ -115,7 +111,6 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Plugin>(
type: "danger",
},
label: t("core.common.buttons.uninstall"),
visible: true,
children: [
{
priority: 10,
@ -124,7 +119,6 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Plugin>(
type: "danger",
},
label: t("core.common.buttons.uninstall"),
visible: true,
action: () => uninstall(),
},
{
@ -134,7 +128,6 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Plugin>(
type: "danger",
},
label: t("core.plugin.operations.uninstall_and_delete_config.button"),
visible: true,
action: () => uninstall(true),
},
],
@ -146,7 +139,6 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Plugin>(
type: "danger",
},
label: t("core.common.buttons.reset"),
visible: true,
action: () => {
handleResetSettingConfig();
},
@ -157,7 +149,7 @@ const { dropdownItems } = useEntityDropdownItemExtensionPoint<Plugin>(
const { startFields, endFields } = useEntityFieldItemExtensionPoint<Plugin>(
"plugin:list-item:field:create",
plugin,
computed(() => [
computed((): EntityFieldItem[] => [
{
position: "start",
priority: 10,
@ -188,7 +180,7 @@ const { startFields, endFields } = useEntityFieldItemExtensionPoint<Plugin>(
state: "error",
animate: true,
},
visible: props.plugin.status?.phase === "FAILED",
hidden: props.plugin.status?.phase !== "FAILED",
},
{
position: "end",
@ -199,7 +191,7 @@ const { startFields, endFields } = useEntityFieldItemExtensionPoint<Plugin>(
state: "warning",
animate: true,
},
visible: !!props.plugin.metadata.deletionTimestamp,
hidden: !props.plugin.metadata.deletionTimestamp,
},
{
position: "end",
@ -208,7 +200,7 @@ const { startFields, endFields } = useEntityFieldItemExtensionPoint<Plugin>(
props: {
plugin: props.plugin,
},
visible: !!props.plugin.spec.author,
hidden: !props.plugin.spec.author,
},
{
position: "end",
@ -225,7 +217,7 @@ const { startFields, endFields } = useEntityFieldItemExtensionPoint<Plugin>(
props: {
description: formatDatetime(props.plugin.metadata.creationTimestamp),
},
visible: !!props.plugin.metadata.creationTimestamp,
hidden: !props.plugin.metadata.creationTimestamp,
},
{
position: "end",
@ -263,7 +255,7 @@ const { startFields, endFields } = useEntityFieldItemExtensionPoint<Plugin>(
v-if="currentUserHasPermission(['system:plugins:manage'])"
#dropdownItems
>
<EntityDropdownItems :dropdown-items="dropdownItems" :item="plugin" />
<EntityDropdownItems :dropdown-items="operationItems" :item="plugin" />
</template>
</VEntity>
</template>