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

View File

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

View File

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

View File

@ -17,8 +17,8 @@ import { toDatetimeLocal, toISOString } from "@/utils/date";
import { submitForm } from "@formkit/core"; import { submitForm } from "@formkit/core";
import AnnotationsForm from "@/components/form/AnnotationsForm.vue"; import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
import useSlugify from "@/composables/use-slugify"; import useSlugify from "@/composables/use-slugify";
import { useMutation } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { usePageUpdateMutate } from "../composables/use-page-update-mutate";
const initialFormState: SinglePage = { const initialFormState: SinglePage = {
spec: { spec: {
@ -117,37 +117,7 @@ const handlePublishClick = () => {
// Fix me: // Fix me:
// Force update post settings, // Force update post settings,
// because currently there may be errors caused by changes in version due to asynchronous processing. // because currently there may be errors caused by changes in version due to asynchronous processing.
const { mutateAsync: singlePageUpdateMutate } = useMutation({ const { mutateAsync: singlePageUpdateMutate } = usePageUpdateMutate();
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 handleSave = async () => { const handleSave = async () => {
annotationsFormRef.value?.handleSubmit(); 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 EditorProviderSelector from "@/components/dropdown-selector/EditorProviderSelector.vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import UrlPreviewModal from "@/components/preview/UrlPreviewModal.vue"; import UrlPreviewModal from "@/components/preview/UrlPreviewModal.vue";
import { usePostUpdateMutate } from "./composables/use-post-update-mutate";
import { contentAnnotations } from "@/constants/annotations";
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { mutateAsync: postUpdateMutate } = usePostUpdateMutate();
// Editor providers // Editor providers
const { editorProviders } = useEditorExtensionPoints(); const { editorProviders } = useEditorExtensionPoints();
const currentEditorProvider = ref<EditorProvider>(); const currentEditorProvider = ref<EditorProvider>();
const storedEditorProviderName = useLocalStorage("editor-provider-name", ""); const storedEditorProviderName = useLocalStorage("editor-provider-name", "");
const handleChangeEditorProvider = (provider: EditorProvider) => { const handleChangeEditorProvider = async (provider: EditorProvider) => {
currentEditorProvider.value = provider; currentEditorProvider.value = provider;
storedEditorProviderName.value = provider.name; storedEditorProviderName.value = provider.name;
formState.value.post.metadata.annotations = { 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; formState.value.content.rawType = provider.rawType;
if (isUpdateMode.value) {
const { data } = await postUpdateMutate(formState.value.post);
formState.value.post = data;
}
}; };
// Post form // Post form
@ -230,7 +241,7 @@ const handleFetchContent = async () => {
(provider) => (provider) =>
provider.name === provider.name ===
formState.value.post.metadata.annotations?.[ 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 = {
...formState.value.post.metadata.annotations, ...formState.value.post.metadata.annotations,
"content.halo.run/preferred-editor": provider.name, [contentAnnotations.PREFERRED_EDITOR]: provider.name,
}; };
const { data } = const { data } = await postUpdateMutate(formState.value.post);
await apiClient.extension.post.updatecontentHaloRunV1alpha1Post({
name: formState.value.post.metadata.name,
post: formState.value.post,
});
formState.value.post = data; formState.value.post = data;
} else { } else {
@ -266,6 +273,9 @@ const handleFetchContent = async () => {
onConfirm: () => { onConfirm: () => {
router.back(); router.back();
}, },
onCancel: () => {
router.back();
},
}); });
} }
@ -329,7 +339,7 @@ onMounted(async () => {
} }
formState.value.post.metadata.annotations = { formState.value.post.metadata.annotations = {
"content.halo.run/preferred-editor": provider.name, [contentAnnotations.PREFERRED_EDITOR]: provider.name,
}; };
} }
handleResetCache(); handleResetCache();
@ -379,8 +389,9 @@ const handlePreview = async () => {
<template #actions> <template #actions>
<VSpace> <VSpace>
<EditorProviderSelector <EditorProviderSelector
v-if="editorProviders.length > 1 && !isUpdateMode" v-if="editorProviders.length > 1"
:provider="currentEditorProvider" :provider="currentEditorProvider"
:allow-forced-select="!isUpdateMode"
@select="handleChangeEditorProvider" @select="handleChangeEditorProvider"
/> />
<VButton <VButton

View File

@ -17,8 +17,8 @@ import { toDatetimeLocal, toISOString } from "@/utils/date";
import AnnotationsForm from "@/components/form/AnnotationsForm.vue"; import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
import { submitForm } from "@formkit/core"; import { submitForm } from "@formkit/core";
import useSlugify from "@/composables/use-slugify"; import useSlugify from "@/composables/use-slugify";
import { useMutation } from "@tanstack/vue-query";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { usePostUpdateMutate } from "../composables/use-post-update-mutate";
const initialFormState: Post = { const initialFormState: Post = {
spec: { spec: {
@ -117,37 +117,7 @@ const handlePublishClick = () => {
// Fix me: // Fix me:
// Force update post settings, // Force update post settings,
// because currently there may be errors caused by changes in version due to asynchronous processing. // because currently there may be errors caused by changes in version due to asynchronous processing.
const { mutateAsync: postUpdateMutate } = useMutation({ const { mutateAsync: postUpdateMutate } = usePostUpdateMutate();
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 handleSave = async () => { const handleSave = async () => {
annotationsFormRef.value?.handleSubmit(); 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"));
},
});
}