feat: make theme list item operations extendable (#4523)

#### What type of PR is this?

/area console
/kind feature
/milestone 2.9.x

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

主题管理列表项的操作按钮支持被插件扩展。

![image](https://github.com/halo-dev/halo/assets/21301288/fd605aa5-91a9-49dc-9f8d-14eeabd4fb93)

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

Fixes #4522 

#### Special notes for your reviewer:

测试已有的操作按钮功能正常即可。

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

```release-note
Console 主题管理列表项的操作按钮支持被插件扩展。
```
pull/4530/head
Ryan Wang 2023-08-31 02:02:12 -05:00 committed by GitHub
parent 58eac2e30b
commit 8eaedd6ee8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 208 additions and 102 deletions

View File

@ -11,6 +11,7 @@
- 文章:`"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>[]>`
示例:

View File

@ -9,7 +9,7 @@ import type { BackupTab } from "@/states/backup";
import type { PluginInstallationTab } from "@/states/plugin-installation-tabs";
import type { EntityDropdownItem, EntityFieldItem } from "@/states/entity";
import type { ThemeListTab } from "@/states/theme-list-tabs";
import type { Backup, ListedPost, Plugin } from "@halo-dev/api-client";
import type { Backup, ListedPost, Plugin, Theme } from "@halo-dev/api-client";
export interface RouteRecordAppend {
parentName: RouteRecordName;
@ -59,6 +59,10 @@ export interface ExtensionPoint {
) => EntityFieldItem[] | Promise<EntityFieldItem[]>;
"theme:list:tabs:create"?: () => ThemeListTab[] | Promise<ThemeListTab[]>;
"theme:list-item:operation:create"?: (
theme: Ref<Theme>
) => EntityDropdownItem<Theme>[] | Promise<EntityDropdownItem<Theme>[]>;
}
export interface PluginModule {

View File

@ -2,7 +2,6 @@
import {
VTag,
VStatusDot,
Dialog,
Toast,
VDropdownItem,
VDropdown,
@ -18,6 +17,11 @@ 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 { markRaw } from "vue";
import { defineComponent } from "vue";
import UninstallOperationItem from "./operation/UninstallOperationItem.vue";
import { computed } from "vue";
const { currentUserHasPermission } = usePermission();
const { t } = useI18n();
@ -52,59 +56,6 @@ const {
handleResetSettingConfig,
} = useThemeLifeCycle(theme);
const handleUninstall = async (theme: Theme, deleteExtensions?: boolean) => {
Dialog.warning({
title: `${
deleteExtensions
? t("core.theme.operations.uninstall_and_delete_config.title")
: t("core.theme.operations.uninstall.title")
}`,
description: t("core.common.dialog.descriptions.cannot_be_recovered"),
confirmText: t("core.common.buttons.confirm"),
cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => {
try {
await apiClient.extension.theme.deletethemeHaloRunV1alpha1Theme({
name: theme.metadata.name,
});
// delete theme setting and configMap
if (deleteExtensions) {
const { settingName, configMapName } = theme.spec;
if (settingName) {
await apiClient.extension.setting.deletev1alpha1Setting(
{
name: settingName,
},
{
mute: true,
}
);
}
if (configMapName) {
await apiClient.extension.configMap.deletev1alpha1ConfigMap(
{
name: configMapName,
},
{
mute: true,
}
);
}
}
Toast.success(t("core.common.toast.uninstall_success"));
} catch (e) {
console.error("Failed to uninstall theme", e);
} finally {
queryClient.invalidateQueries({ queryKey: ["installed-themes"] });
}
},
});
};
// Creating theme
const creating = ref(false);
@ -131,6 +82,79 @@ const handleCreateTheme = async () => {
queryClient.invalidateQueries({ queryKey: ["not-installed-themes"] });
}
};
const { dropdownItems } = useEntityDropdownItemExtensionPoint<Theme>(
"theme:list-item:operation:create",
theme,
computed(() => [
{
priority: 10,
component: markRaw(VButton),
props: {
size: "sm",
},
action: () => handleActiveTheme(true),
label: t("core.common.buttons.activate"),
visible:
!isActivated.value &&
currentUserHasPermission(["system:themes:manage"]),
},
{
priority: 20,
component: markRaw(VButton),
props: {
size: "sm",
},
action: () => {
emit("select", props.theme);
},
label: t("core.common.buttons.select"),
},
{
priority: 30,
component: markRaw(
defineComponent({
components: {
VButton,
IconMore,
},
template: `<VButton size="sm"><IconMore /></VButton>`,
})
),
visible: currentUserHasPermission(["system:themes:manage"]),
children: [
{
priority: 10,
component: markRaw(VDropdownItem),
action: () => {
emit("preview");
},
label: t("core.common.buttons.preview"),
},
{
priority: 20,
component: markRaw(VDropdownDivider),
},
{
priority: 30,
component: markRaw(UninstallOperationItem),
props: {
theme: props.theme,
},
},
{
priority: 40,
component: markRaw(VDropdownItem),
props: {
type: "danger",
},
action: () => handleResetSettingConfig(),
label: t("core.common.buttons.reset"),
},
],
},
])
);
</script>
<template>
@ -214,58 +238,49 @@ const handleCreateTheme = async () => {
</div>
<div>
<VSpace v-if="installed">
<VButton
v-if="
!isActivated &&
currentUserHasPermission(['system:themes:manage'])
"
size="sm"
@click="handleActiveTheme(true)"
>
{{ $t("core.common.buttons.activate") }}
</VButton>
<VButton size="sm" @click="emit('select', theme)">
{{ $t("core.common.buttons.select") }}
</VButton>
<VDropdown
v-if="currentUserHasPermission(['system:themes:manage'])"
>
<VButton size="sm">
<IconMore />
</VButton>
<template #popper>
<VDropdownItem @click="emit('preview')">
{{ $t("core.common.buttons.preview") }}
</VDropdownItem>
<VDropdownDivider />
<VDropdown placement="right" :triggers="['click']">
<VDropdownItem type="danger">
{{ $t("core.common.buttons.uninstall") }}
</VDropdownItem>
<template v-for="(item, index) in dropdownItems" :key="index">
<template v-if="!item.children?.length">
<component
:is="item.component"
v-if="item.visible !== false"
v-permission="item.permissions"
v-bind="item.props"
@click="item.action?.(theme)"
>
{{ item.label }}
</component>
</template>
<template v-else>
<VDropdown
v-if="item.visible !== false"
v-permission="item.permissions"
>
<component
:is="item.component"
v-bind="item.props"
@click="item.action?.(theme)"
>
{{ item.label }}
</component>
<template #popper>
<VDropdownItem
type="danger"
@click="handleUninstall(theme)"
<template
v-for="(childItem, childIndex) in item.children"
:key="`child-${childIndex}`"
>
{{ $t("core.common.buttons.uninstall") }}
</VDropdownItem>
<VDropdownItem
type="danger"
@click="handleUninstall(theme, true)"
>
{{
$t(
"core.theme.operations.uninstall_and_delete_config.button"
)
}}
</VDropdownItem>
<component
:is="childItem.component"
v-if="childItem.visible !== false"
v-permission="childItem.permissions"
v-bind="childItem.props"
@click="childItem.action?.(theme)"
>
{{ childItem.label }}
</component>
</template>
</template>
</VDropdown>
<VDropdownItem type="danger" @click="handleResetSettingConfig">
{{ $t("core.common.buttons.reset") }}
</VDropdownItem>
</template>
</VDropdown>
</template>
</VSpace>
<VButton
v-if="

View File

@ -0,0 +1,86 @@
<script lang="ts" setup>
import { Dialog, Toast, VDropdown, VDropdownItem } from "@halo-dev/components";
import type { Theme } from "@halo-dev/api-client";
import { useI18n } from "vue-i18n";
import { apiClient } from "@/utils/api-client";
import { useQueryClient } from "@tanstack/vue-query";
const { t } = useI18n();
const queryClient = useQueryClient();
const props = withDefaults(
defineProps<{
theme: Theme;
}>(),
{}
);
const handleUninstall = async (deleteExtensions?: boolean) => {
Dialog.warning({
title: `${
deleteExtensions
? t("core.theme.operations.uninstall_and_delete_config.title")
: t("core.theme.operations.uninstall.title")
}`,
description: t("core.common.dialog.descriptions.cannot_be_recovered"),
confirmText: t("core.common.buttons.confirm"),
cancelText: t("core.common.buttons.cancel"),
onConfirm: async () => {
try {
await apiClient.extension.theme.deletethemeHaloRunV1alpha1Theme({
name: props.theme.metadata.name,
});
// delete theme setting and configMap
if (deleteExtensions) {
const { settingName, configMapName } = props.theme.spec;
if (settingName) {
await apiClient.extension.setting.deletev1alpha1Setting(
{
name: settingName,
},
{
mute: true,
}
);
}
if (configMapName) {
await apiClient.extension.configMap.deletev1alpha1ConfigMap(
{
name: configMapName,
},
{
mute: true,
}
);
}
}
Toast.success(t("core.common.toast.uninstall_success"));
} catch (e) {
console.error("Failed to uninstall theme", e);
} finally {
queryClient.invalidateQueries({ queryKey: ["installed-themes"] });
}
},
});
};
</script>
<template>
<VDropdown placement="right" :triggers="['click']">
<VDropdownItem type="danger">
{{ $t("core.common.buttons.uninstall") }}
</VDropdownItem>
<template #popper>
<VDropdownItem type="danger" @click="handleUninstall()">
{{ $t("core.common.buttons.uninstall") }}
</VDropdownItem>
<VDropdownItem type="danger" @click="handleUninstall(true)">
{{ $t("core.theme.operations.uninstall_and_delete_config.button") }}
</VDropdownItem>
</template>
</VDropdown>
</template>