refactor: editor provider switching (#803)

#### What type of PR is this?

/kind improvement

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

重构编辑器的选择方式,改为在编辑页面选择,并支持缓存上一次的选择。

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

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

#### Screenshots:

<img width="1662" alt="image" src="https://user-images.githubusercontent.com/21301288/209756212-b14725da-3d89-4416-8860-e75cdb270b75.png">

#### Special notes for your reviewer:

测试方式:

1. 此改动针对文章和自定义页面的编辑。
2. 测试新建页面选择编辑器是否正常。
3. 测试重新进入新建页面,编辑器是否自动选择为了上一次的编辑器。
4. 测试发布文章或者自定义页面之后,重新编辑时,是否选择了正确的编辑器。

编辑器插件可使用:

1. https://github.com/halo-sigs/plugin-bytemd
2. https://github.com/halo-sigs/plugin-stackedit

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

```release-note
重构 Console 端选择编辑器的方式,支持记住上一次的选择。
```
pull/811/head
Ryan Wang 2022-12-29 21:46:34 +08:00 committed by GitHub
parent 89ee700f9d
commit 565efcb3ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 136 additions and 81 deletions

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,62 @@
<script lang="ts" setup>
import {
useEditorExtensionPoints,
type EditorProvider,
} from "@/composables/use-editor-extension-points";
import { VAvatar, VSpace, IconExchange } from "@halo-dev/components";
withDefaults(
defineProps<{
provider?: EditorProvider;
}>(),
{
provider: undefined,
}
);
const emit = defineEmits<{
(event: "select", provider: EditorProvider): void;
}>();
const { editorProviders } = useEditorExtensionPoints();
</script>
<template>
<FloatingDropdown>
<div
class="group flex w-full cursor-pointer items-center gap-2 rounded p-1 hover:bg-gray-100"
>
<VAvatar v-if="provider?.logo" :src="provider.logo" size="xs"></VAvatar>
<div class="select-none text-sm text-gray-600 group-hover:text-gray-900">
{{ provider?.displayName }}
</div>
<IconExchange class="h-4 w-4 text-gray-600 group-hover:text-gray-900" />
</div>
<template #popper>
<div class="w-48 p-2">
<VSpace class="w-full" direction="column">
<div
v-for="(editorProvider, index) in editorProviders"
:key="index"
v-close-popper
class="group flex w-full cursor-pointer items-center gap-2 rounded p-1 hover:bg-gray-100"
:class="{
'bg-gray-100': editorProvider.name === provider?.name,
}"
@click="emit('select', editorProvider)"
>
<VAvatar :src="editorProvider.logo" size="xs"></VAvatar>
<div
class="text-sm text-gray-600 group-hover:text-gray-900"
:class="{
'text-gray-900': editorProvider.name === provider?.name,
}"
>
{{ editorProvider.displayName }}
</div>
</div>
</VSpace>
</div>
</template>
</FloatingDropdown>
</template>

View File

@ -1,7 +1,13 @@
import { usePluginModuleStore } from "@/stores/plugin";
import type { EditorProvider, PluginModule } from "@halo-dev/console-shared";
import type { EditorProvider as EditorProviderRaw } from "@halo-dev/console-shared";
import type { PluginModule } from "@/stores/plugin";
import { onMounted, ref, type Ref, defineAsyncComponent } from "vue";
import { VLoading } from "@halo-dev/components";
import Logo from "@/assets/logo.png";
export interface EditorProvider extends EditorProviderRaw {
logo?: string;
}
interface useEditorExtensionPointsReturn {
editorProviders: Ref<EditorProvider[]>;
@ -21,6 +27,7 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
delay: 200,
}),
rawType: "HTML",
logo: Logo,
},
]);
@ -35,7 +42,10 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
if (providers) {
providers.forEach((provider) => {
editorProviders.value.push(provider);
editorProviders.value.push({
...provider,
logo: pluginModule.extension.status?.logo,
});
});
}
});

View File

@ -145,7 +145,10 @@ async function loadPluginModules() {
if (pluginModule) {
registerModule(pluginModule, false);
pluginModuleStore.registerPluginModule(pluginModule);
pluginModuleStore.registerPluginModule({
...pluginModule,
extension: plugin,
});
}
} catch (e) {
const message = `${plugin.metadata.name}: 加载插件入口文件失败`;

View File

@ -28,14 +28,29 @@ import cloneDeep from "lodash.clonedeep";
import { useRouter } from "vue-router";
import { randomUUID } from "@/utils/id";
import { useContentCache } from "@/composables/use-content-cache";
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
import type { EditorProvider } from "@halo-dev/console-shared";
import {
useEditorExtensionPoints,
type EditorProvider,
} from "@/composables/use-editor-extension-points";
import { useLocalStorage } from "@vueuse/core";
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
const router = useRouter();
// Editor providers
const { editorProviders } = useEditorExtensionPoints();
const currentEditorProvider = ref<EditorProvider>();
const storedEditorProviderName = useLocalStorage("editor-provider-name", "");
const handleChangeEditorProvider = (provider: EditorProvider) => {
currentEditorProvider.value = provider;
storedEditorProviderName.value = provider.name;
formState.value.page.metadata.annotations = {
"content.halo.run/preferred-editor": provider.name,
};
};
// SinglePage form
const initialFormState: SinglePageRequest = {
page: {
spec: {
@ -238,6 +253,7 @@ const handleFetchContent = async () => {
formState.value.content = Object.assign(formState.value.content, data);
};
// SinglePage settings
const handleOpenSettingModal = async () => {
const { data: latestSinglePage } =
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
@ -267,7 +283,6 @@ const onSettingPublished = (singlePage: SinglePage) => {
handlePublish();
};
const editor = useRouteQuery("editor");
onMounted(async () => {
if (routeQueryName.value) {
const { data: singlePage } =
@ -282,7 +297,7 @@ onMounted(async () => {
// Set default editor
const provider =
editorProviders.value.find(
(provider) => provider.name === editor.value
(provider) => provider.name === storedEditorProviderName.value
) || editorProviders.value[0];
if (provider) {
currentEditorProvider.value = provider;
@ -296,6 +311,7 @@ onMounted(async () => {
handleResetCache();
});
// SinglePage content cache
const { handleSetContentCache, handleResetCache, handleClearCache } =
useContentCache(
"singlePage-content-cache",
@ -320,6 +336,12 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
</template>
<template #actions>
<VSpace>
<EditorProviderSelector
v-if="editorProviders.length > 1 && !isUpdateMode"
:provider="currentEditorProvider"
@select="handleChangeEditorProvider"
/>
<!-- TODO: add preview single page support -->
<VButton
v-if="false"

View File

@ -11,13 +11,10 @@ import {
} from "@halo-dev/components";
import BasicLayout from "@/layouts/BasicLayout.vue";
import { useRoute, useRouter } from "vue-router";
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
const route = useRoute();
const router = useRouter();
const { editorProviders } = useEditorExtensionPoints();
interface PageTab {
id: string;
label: string;
@ -78,38 +75,7 @@ watchEffect(() => {
<VButton :route="{ name: 'DeletedSinglePages' }" size="sm">
回收站
</VButton>
<FloatingDropdown
v-if="editorProviders.length > 1"
v-permission="['system:singlepages:manage']"
>
<VButton type="secondary">
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新建
</VButton>
<template #popper>
<div class="w-48 p-2">
<VSpace class="w-full" direction="column">
<VButton
v-for="(editorProvider, index) in editorProviders"
:key="index"
v-close-popper
block
type="default"
:route="{
name: 'SinglePageEditor',
query: { editor: editorProvider.name },
}"
>
{{ editorProvider.displayName }}
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
<VButton
v-else
v-permission="['system:singlepages:manage']"
:route="{ name: 'SinglePageEditor' }"
type="secondary"

View File

@ -22,20 +22,35 @@ import {
toRef,
type ComputedRef,
} from "vue";
import type { EditorProvider } from "@halo-dev/console-shared";
import cloneDeep from "lodash.clonedeep";
import { apiClient } from "@/utils/api-client";
import { useRouteQuery } from "@vueuse/router";
import { useRouter } from "vue-router";
import { randomUUID } from "@/utils/id";
import { useContentCache } from "@/composables/use-content-cache";
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
import {
useEditorExtensionPoints,
type EditorProvider,
} from "@/composables/use-editor-extension-points";
import { useLocalStorage } from "@vueuse/core";
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
const router = useRouter();
// Editor providers
const { editorProviders } = useEditorExtensionPoints();
const currentEditorProvider = ref<EditorProvider>();
const storedEditorProviderName = useLocalStorage("editor-provider-name", "");
const handleChangeEditorProvider = (provider: EditorProvider) => {
currentEditorProvider.value = provider;
storedEditorProviderName.value = provider.name;
formState.value.post.metadata.annotations = {
"content.halo.run/preferred-editor": provider.name,
};
};
// Post form
const initialFormState: PostRequest = {
post: {
spec: {
@ -254,6 +269,7 @@ const handleOpenSettingModal = async () => {
settingModal.value = true;
};
// Post settings
const onSettingSaved = (post: Post) => {
// Set route query parameter
if (!isUpdateMode.value) {
@ -276,7 +292,6 @@ const onSettingPublished = (post: Post) => {
// Get post data when the route contains the name parameter
const name = useRouteQuery("name");
const editor = useRouteQuery("editor");
onMounted(async () => {
if (name.value) {
// fetch post
@ -292,7 +307,7 @@ onMounted(async () => {
// Set default editor
const provider =
editorProviders.value.find(
(provider) => provider.name === editor.value
(provider) => provider.name === storedEditorProviderName.value
) || editorProviders.value[0];
if (provider) {
@ -307,6 +322,7 @@ onMounted(async () => {
handleResetCache();
});
// Post content cache
const { handleSetContentCache, handleResetCache, handleClearCache } =
useContentCache(
"post-content-cache",
@ -331,6 +347,12 @@ const { handleSetContentCache, handleResetCache, handleClearCache } =
</template>
<template #actions>
<VSpace>
<EditorProviderSelector
v-if="editorProviders.length > 1 && !isUpdateMode"
:provider="currentEditorProvider"
@select="handleChangeEditorProvider"
/>
<!-- TODO: add preview post support -->
<VButton
v-if="false"

View File

@ -45,12 +45,9 @@ import FilterTag from "@/components/filter/FilterTag.vue";
import FilteCleanButton from "@/components/filter/FilterCleanButton.vue";
import { getNode } from "@formkit/core";
import TagDropdownSelector from "@/components/dropdown-selector/TagDropdownSelector.vue";
import { useEditorExtensionPoints } from "@/composables/use-editor-extension-points";
const { currentUserHasPermission } = usePermission();
const { editorProviders } = useEditorExtensionPoints();
const posts = ref<ListedPostList>({
page: 1,
size: 20,
@ -480,39 +477,7 @@ const hasFilters = computed(() => {
<VButton :route="{ name: 'Tags' }" size="sm">标签</VButton>
<VButton :route="{ name: 'DeletedPosts' }" size="sm">回收站</VButton>
<FloatingDropdown
v-if="editorProviders.length > 1"
v-permission="['system:posts:manage']"
>
<VButton type="secondary">
<template #icon>
<IconAddCircle class="h-full w-full" />
</template>
新建
</VButton>
<template #popper>
<div class="w-48 p-2">
<VSpace class="w-full" direction="column">
<VButton
v-for="(editorProvider, index) in editorProviders"
:key="index"
v-close-popper
block
type="default"
:route="{
name: 'PostEditor',
query: { editor: editorProvider.name },
}"
>
{{ editorProvider.displayName }}
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
<VButton
v-else
v-permission="['system:posts:manage']"
:route="{ name: 'PostEditor' }"
type="secondary"

View File

@ -1,5 +1,10 @@
import { defineStore } from "pinia";
import type { PluginModule } from "@halo-dev/console-shared";
import type { PluginModule as PluginModuleRaw } from "@halo-dev/console-shared";
import type { Plugin } from "@halo-dev/api-client";
export interface PluginModule extends PluginModuleRaw {
extension: Plugin;
}
interface PluginStoreState {
pluginModules: PluginModule[];