mirror of https://github.com/halo-dev/halo
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
parent
5a0e202847
commit
e6f31759a0
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"));
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"));
|
||||
},
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue