feat: allow switching editors while editing a post (#4180)

#### What type of PR is this?

/area console
/kind feature
/milestone 2.8.x

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

支持在编辑文章时切换编辑器,不再限制仅新建时允许切换。但需要注意的是,目前只支持同类型的编辑器切换(rawType)。

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

Fixes #4176 

#### Special notes for your reviewer:

测试方式:

1. 安装多个编辑器插件,可以在 https://github.com/halo-sigs/awesome-halo 中查找。
2. 测试在新建文章时是否能够正常切换编辑器。
3. 测试在修改文章时能够正常切换同类型的编辑器。

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

```release-note
Console 端编辑文章时允许同类型的编辑器切换。
```
pull/4179/head^2
Ryan Wang 2023-07-18 16:16:02 +08:00 committed by GitHub
parent 5a0e202847
commit e6f31759a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 157 additions and 90 deletions

View File

@ -2,13 +2,15 @@
import { DropdownContextInjectionKey } from "./symbols";
import { inject } from "vue";
withDefaults(
const props = withDefaults(
defineProps<{
selected?: boolean;
disabled?: boolean;
type: "default" | "danger";
}>(),
{
selected: false,
disabled: false,
type: "default",
}
);
@ -20,6 +22,10 @@ const emit = defineEmits<{
const { hide } = inject(DropdownContextInjectionKey) || {};
function onClick(e: MouseEvent) {
if (props.disabled) {
return;
}
hide?.();
emit("click", e);
}
@ -28,7 +34,10 @@ function onClick(e: MouseEvent) {
<template>
<div
class="dropdown-item-wrapper"
:class="[`dropdown-item-wrapper--${type}${selected ? '--selected' : ''}`]"
:class="[
`dropdown-item-wrapper--${type}${selected ? '--selected' : ''}`,
{ 'dropdown-item-wrapper--disabled': disabled },
]"
role="menuitem"
tabindex="-1"
@click="onClick"
@ -62,5 +71,9 @@ function onClick(e: MouseEvent) {
@apply bg-red-50 text-red-700;
}
}
&--disabled {
@apply opacity-70 cursor-not-allowed;
}
}
</style>

View File

@ -13,9 +13,11 @@ import {
withDefaults(
defineProps<{
provider?: EditorProvider;
allowForcedSelect: boolean;
}>(),
{
provider: undefined,
allowForcedSelect: false,
}
);
@ -42,6 +44,11 @@ const { editorProviders } = useEditorExtensionPoints();
v-for="(editorProvider, index) in editorProviders"
:key="index"
:selected="provider?.name === editorProvider.name"
:disabled="
!allowForcedSelect &&
provider?.rawType.toLowerCase() !==
editorProvider.rawType.toLowerCase()
"
@click="emit('select', editorProvider)"
>
<template #prefix-icon>

View File

@ -10,3 +10,9 @@ export enum rbacAnnotations {
DISPLAY_NAME = "rbac.authorization.halo.run/display-name",
DEPENDENCIES = "rbac.authorization.halo.run/dependencies",
}
// content
export enum contentAnnotations {
PREFERRED_EDITOR = "content.halo.run/preferred-editor",
}

View File

@ -36,22 +36,31 @@ import { useLocalStorage } from "@vueuse/core";
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
import { useI18n } from "vue-i18n";
import UrlPreviewModal from "@/components/preview/UrlPreviewModal.vue";
import { contentAnnotations } from "@/constants/annotations";
import { usePageUpdateMutate } from "./composables/use-page-update-mutate";
const router = useRouter();
const { t } = useI18n();
const { mutateAsync: singlePageUpdateMutate } = usePageUpdateMutate();
// Editor providers
const { editorProviders } = useEditorExtensionPoints();
const currentEditorProvider = ref<EditorProvider>();
const storedEditorProviderName = useLocalStorage("editor-provider-name", "");
const handleChangeEditorProvider = (provider: EditorProvider) => {
const handleChangeEditorProvider = async (provider: EditorProvider) => {
currentEditorProvider.value = provider;
storedEditorProviderName.value = provider.name;
formState.value.page.metadata.annotations = {
"content.halo.run/preferred-editor": provider.name,
...formState.value.page.metadata.annotations,
[contentAnnotations.PREFERRED_EDITOR]: provider.name,
};
formState.value.content.rawType = provider.rawType;
if (isUpdateMode.value) {
const { data } = await singlePageUpdateMutate(formState.value.page);
formState.value.page = data;
}
};
// SinglePage form
@ -223,7 +232,7 @@ const handleFetchContent = async () => {
(provider) =>
provider.name ===
formState.value.page.metadata.annotations?.[
"content.halo.run/preferred-editor"
contentAnnotations.PREFERRED_EDITOR
]
);
const provider =
@ -235,16 +244,10 @@ const handleFetchContent = async () => {
currentEditorProvider.value = provider;
formState.value.page.metadata.annotations = {
...formState.value.page.metadata.annotations,
"content.halo.run/preferred-editor": provider.name,
[contentAnnotations.PREFERRED_EDITOR]: provider.name,
};
const { data } =
await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
{
name: formState.value.page.metadata.name,
singlePage: formState.value.page,
}
);
const { data } = await singlePageUpdateMutate(formState.value.page);
formState.value.page = data;
} else {
@ -258,6 +261,9 @@ const handleFetchContent = async () => {
onConfirm: () => {
router.back();
},
onCancel: () => {
router.back();
},
});
}
await nextTick();
@ -315,7 +321,7 @@ onMounted(async () => {
formState.value.content.rawType = provider.rawType;
}
formState.value.page.metadata.annotations = {
"content.halo.run/preferred-editor": provider.name,
[contentAnnotations.PREFERRED_EDITOR]: provider.name,
};
}
@ -366,8 +372,9 @@ const handlePreview = async () => {
<template #actions>
<VSpace>
<EditorProviderSelector
v-if="editorProviders.length > 1 && !isUpdateMode"
v-if="editorProviders.length > 1"
:provider="currentEditorProvider"
:allow-forced-select="!isUpdateMode"
@select="handleChangeEditorProvider"
/>
<VButton

View File

@ -17,8 +17,8 @@ import { toDatetimeLocal, toISOString } from "@/utils/date";
import { submitForm } from "@formkit/core";
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
import useSlugify from "@/composables/use-slugify";
import { useMutation } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n";
import { usePageUpdateMutate } from "../composables/use-page-update-mutate";
const initialFormState: SinglePage = {
spec: {
@ -117,37 +117,7 @@ const handlePublishClick = () => {
// Fix me:
// Force update post settings,
// because currently there may be errors caused by changes in version due to asynchronous processing.
const { mutateAsync: singlePageUpdateMutate } = useMutation({
mutationKey: ["singlePage-update"],
mutationFn: async (page: SinglePage) => {
const { data: latestSinglePage } =
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
name: page.metadata.name,
});
return apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
{
name: page.metadata.name,
singlePage: {
...latestSinglePage,
spec: page.spec,
metadata: {
...latestSinglePage.metadata,
annotations: page.metadata.annotations,
},
},
},
{
mute: true,
}
);
},
retry: 3,
onError: (error) => {
console.error("Failed to update post", error);
Toast.error(t("core.common.toast.server_internal_error"));
},
});
const { mutateAsync: singlePageUpdateMutate } = usePageUpdateMutate();
const handleSave = async () => {
annotationsFormRef.value?.handleSubmit();

View File

@ -0,0 +1,42 @@
import { apiClient } from "@/utils/api-client";
import { useMutation } from "@tanstack/vue-query";
import type { SinglePage } from "@halo-dev/api-client";
import { Toast } from "@halo-dev/components";
import { useI18n } from "vue-i18n";
export function usePageUpdateMutate() {
const { t } = useI18n();
return useMutation({
mutationKey: ["singlePage-update"],
mutationFn: async (page: SinglePage) => {
const { data: latestSinglePage } =
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage(
{
name: page.metadata.name,
}
);
return apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
{
name: page.metadata.name,
singlePage: {
...latestSinglePage,
spec: page.spec,
metadata: {
...latestSinglePage.metadata,
annotations: page.metadata.annotations,
},
},
},
{
mute: true,
}
);
},
retry: 3,
onError: (error) => {
console.error("Failed to update post", error);
Toast.error(t("core.common.toast.server_internal_error"));
},
});
}

View File

@ -36,22 +36,33 @@ import { useLocalStorage } from "@vueuse/core";
import EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
import { useI18n } from "vue-i18n";
import UrlPreviewModal from "@/components/preview/UrlPreviewModal.vue";
import { usePostUpdateMutate } from "./composables/use-post-update-mutate";
import { contentAnnotations } from "@/constants/annotations";
const router = useRouter();
const { t } = useI18n();
const { mutateAsync: postUpdateMutate } = usePostUpdateMutate();
// Editor providers
const { editorProviders } = useEditorExtensionPoints();
const currentEditorProvider = ref<EditorProvider>();
const storedEditorProviderName = useLocalStorage("editor-provider-name", "");
const handleChangeEditorProvider = (provider: EditorProvider) => {
const handleChangeEditorProvider = async (provider: EditorProvider) => {
currentEditorProvider.value = provider;
storedEditorProviderName.value = provider.name;
formState.value.post.metadata.annotations = {
"content.halo.run/preferred-editor": provider.name,
...formState.value.post.metadata.annotations,
[contentAnnotations.PREFERRED_EDITOR]: provider.name,
};
formState.value.content.rawType = provider.rawType;
if (isUpdateMode.value) {
const { data } = await postUpdateMutate(formState.value.post);
formState.value.post = data;
}
};
// Post form
@ -230,7 +241,7 @@ const handleFetchContent = async () => {
(provider) =>
provider.name ===
formState.value.post.metadata.annotations?.[
"content.halo.run/preferred-editor"
contentAnnotations.PREFERRED_EDITOR
]
);
@ -245,14 +256,10 @@ const handleFetchContent = async () => {
formState.value.post.metadata.annotations = {
...formState.value.post.metadata.annotations,
"content.halo.run/preferred-editor": provider.name,
[contentAnnotations.PREFERRED_EDITOR]: provider.name,
};
const { data } =
await apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
name: formState.value.post.metadata.name,
post: formState.value.post,
});
const { data } = await postUpdateMutate(formState.value.post);
formState.value.post = data;
} else {
@ -266,6 +273,9 @@ const handleFetchContent = async () => {
onConfirm: () => {
router.back();
},
onCancel: () => {
router.back();
},
});
}
@ -329,7 +339,7 @@ onMounted(async () => {
}
formState.value.post.metadata.annotations = {
"content.halo.run/preferred-editor": provider.name,
[contentAnnotations.PREFERRED_EDITOR]: provider.name,
};
}
handleResetCache();
@ -379,8 +389,9 @@ const handlePreview = async () => {
<template #actions>
<VSpace>
<EditorProviderSelector
v-if="editorProviders.length > 1 && !isUpdateMode"
v-if="editorProviders.length > 1"
:provider="currentEditorProvider"
:allow-forced-select="!isUpdateMode"
@select="handleChangeEditorProvider"
/>
<VButton

View File

@ -17,8 +17,8 @@ import { toDatetimeLocal, toISOString } from "@/utils/date";
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
import { submitForm } from "@formkit/core";
import useSlugify from "@/composables/use-slugify";
import { useMutation } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n";
import { usePostUpdateMutate } from "../composables/use-post-update-mutate";
const initialFormState: Post = {
spec: {
@ -117,37 +117,7 @@ const handlePublishClick = () => {
// Fix me:
// Force update post settings,
// because currently there may be errors caused by changes in version due to asynchronous processing.
const { mutateAsync: postUpdateMutate } = useMutation({
mutationKey: ["post-update"],
mutationFn: async (post: Post) => {
const { data: latestPost } =
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
name: post.metadata.name,
});
return apiClient.extension.post.updatecontentHaloRunV1alpha1Post(
{
name: post.metadata.name,
post: {
...latestPost,
spec: post.spec,
metadata: {
...latestPost.metadata,
annotations: post.metadata.annotations,
},
},
},
{
mute: true,
}
);
},
retry: 3,
onError: (error) => {
console.error("Failed to update post", error);
Toast.error(t("core.common.toast.server_internal_error"));
},
});
const { mutateAsync: postUpdateMutate } = usePostUpdateMutate();
const handleSave = async () => {
annotationsFormRef.value?.handleSubmit();

View File

@ -0,0 +1,41 @@
import { useMutation } from "@tanstack/vue-query";
import type { Post } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
import { Toast } from "@halo-dev/components";
import { useI18n } from "vue-i18n";
export function usePostUpdateMutate() {
const { t } = useI18n();
return useMutation({
mutationKey: ["post-update"],
mutationFn: async (post: Post) => {
const { data: latestPost } =
await apiClient.extension.post.getcontentHaloRunV1alpha1Post({
name: post.metadata.name,
});
return await apiClient.extension.post.updatecontentHaloRunV1alpha1Post(
{
name: post.metadata.name,
post: {
...latestPost,
spec: post.spec,
metadata: {
...latestPost.metadata,
annotations: post.metadata.annotations,
},
},
},
{
mute: true,
}
);
},
retry: 3,
onError: (error) => {
console.error("Failed to update post", error);
Toast.error(t("core.common.toast.server_internal_error"));
},
});
}