diff --git a/ui/console-src/composables/use-slugify.ts b/ui/console-src/composables/use-slugify.ts index d77a4b8d3..613f76240 100644 --- a/ui/console-src/composables/use-slugify.ts +++ b/ui/console-src/composables/use-slugify.ts @@ -32,15 +32,6 @@ export default function useSlugify( auto: Ref, formType: FormType ) { - watch( - () => source.value, - () => { - if (auto.value) { - handleGenerateSlug(false, formType); - } - } - ); - const handleGenerateSlug = (forceUpdate = false, formType: FormType) => { const globalInfoStore = useGlobalInfoStore(); const mode = globalInfoStore.globalInfo?.postSlugGenerationStrategy; @@ -60,6 +51,18 @@ export default function useSlugify( target.value = Strategy[mode](source.value); }; + watch( + () => source.value, + () => { + if (auto.value) { + handleGenerateSlug(false, formType); + } + }, + { + immediate: true, + } + ); + return { handleGenerateSlug, }; diff --git a/ui/console-src/modules/contents/pages/SinglePageEditor.vue b/ui/console-src/modules/contents/pages/SinglePageEditor.vue index 86aebacae..c98cd0e53 100644 --- a/ui/console-src/modules/contents/pages/SinglePageEditor.vue +++ b/ui/console-src/modules/contents/pages/SinglePageEditor.vue @@ -21,10 +21,10 @@ import { ref, toRef, type ComputedRef, + watch, } from "vue"; import { apiClient } from "@/utils/api-client"; import { useRouteQuery } from "@vueuse/router"; -import { cloneDeep } from "lodash-es"; import { useRouter } from "vue-router"; import { randomUUID } from "@/utils/id"; import { useContentCache } from "@/composables/use-content-cache"; @@ -69,7 +69,7 @@ const handleChangeEditorProvider = async (provider: EditorProvider) => { }; // SinglePage form -const initialFormState: SinglePageRequest = { +const formState = ref({ page: { spec: { title: "", @@ -101,13 +101,19 @@ const initialFormState: SinglePageRequest = { content: "", rawType: "HTML", }, -}; - -const formState = ref(cloneDeep(initialFormState)); +}); const saving = ref(false); const publishing = ref(false); const settingModal = ref(false); +const isTitleChanged = ref(false); +watch( + () => formState.value.page.spec.title, + (newValue, oldValue) => { + isTitleChanged.value = newValue !== oldValue; + } +); + const isUpdateMode = computed(() => { return !!formState.value.page.metadata.creationTimestamp; }); @@ -143,15 +149,23 @@ const handleSave = async (options?: { mute?: boolean }) => { } if (isUpdateMode.value) { + if (isTitleChanged.value) { + formState.value.page = ( + await singlePageUpdateMutate(formState.value.page) + ).data; + } + const { data } = await apiClient.singlePage.updateSinglePageContent({ name: formState.value.page.metadata.name, content: formState.value.content, }); formState.value.page = data; + isTitleChanged.value = false; } else { // Clear new page content cache handleClearCache(); + const { data } = await apiClient.singlePage.draftSinglePage({ singlePageRequest: formState.value, }); @@ -184,6 +198,12 @@ const handlePublish = async () => { const { name: singlePageName } = formState.value.page.metadata; const { permalink } = formState.value.page.status || {}; + if (isTitleChanged.value) { + formState.value.page = ( + await singlePageUpdateMutate(formState.value.page) + ).data; + } + await apiClient.singlePage.updateSinglePageContent({ name: singlePageName, content: formState.value.content, @@ -224,6 +244,7 @@ const handlePublishClick = () => { if (isUpdateMode.value) { handlePublish(); } else { + // Set editor title to page settingModal.value = true; } }; @@ -477,6 +498,7 @@ async function handleUploadImage(file: File, options?: AxiosRequestConfig) { v-if="currentEditorProvider" v-model:raw="formState.content.raw" v-model:content="formState.content.content" + v-model:title="formState.page.spec.title" :upload-image="handleUploadImage" class="h-full" @update="handleSetContentCache" diff --git a/ui/console-src/modules/contents/pages/components/SinglePageSettingModal.vue b/ui/console-src/modules/contents/pages/components/SinglePageSettingModal.vue index 0065ccfef..bc9921b5b 100644 --- a/ui/console-src/modules/contents/pages/components/SinglePageSettingModal.vue +++ b/ui/console-src/modules/contents/pages/components/SinglePageSettingModal.vue @@ -255,6 +255,9 @@ watch( formState.value = toRaw(value); publishTime.value = toDatetimeLocal(formState.value.spec.publishTime); } + }, + { + immediate: true, } ); diff --git a/ui/console-src/modules/contents/posts/PostEditor.vue b/ui/console-src/modules/contents/posts/PostEditor.vue index 2d669cd0d..898af701b 100644 --- a/ui/console-src/modules/contents/posts/PostEditor.vue +++ b/ui/console-src/modules/contents/posts/PostEditor.vue @@ -21,8 +21,8 @@ import { ref, toRef, type ComputedRef, + watch, } from "vue"; -import { cloneDeep } from "lodash-es"; import { apiClient } from "@/utils/api-client"; import { useRouteQuery } from "@vueuse/router"; import { useRouter } from "vue-router"; @@ -80,7 +80,7 @@ interface PostRequestWithContent extends PostRequest { } // Post form -const initialFormState: PostRequestWithContent = { +const formState = ref({ post: { spec: { title: "", @@ -114,13 +114,19 @@ const initialFormState: PostRequestWithContent = { content: "", rawType: "HTML", }, -}; - -const formState = ref(cloneDeep(initialFormState)); +}); const settingModal = ref(false); const saving = ref(false); const publishing = ref(false); +const isTitleChanged = ref(false); +watch( + () => formState.value.post.spec.title, + (newValue, oldValue) => { + isTitleChanged.value = newValue !== oldValue; + } +); + const isUpdateMode = computed(() => { return !!formState.value.post.metadata.creationTimestamp; }); @@ -155,15 +161,25 @@ const handleSave = async (options?: { mute?: boolean }) => { } if (isUpdateMode.value) { + // Save post title + if (isTitleChanged.value) { + formState.value.post = ( + await postUpdateMutate(formState.value.post) + ).data; + } + const { data } = await apiClient.post.updatePostContent({ name: formState.value.post.metadata.name, content: formState.value.content, }); formState.value.post = data; + + isTitleChanged.value = false; } else { // Clear new post content cache handleClearCache(); + const { data } = await apiClient.post.draftPost({ postRequest: formState.value, }); @@ -195,6 +211,12 @@ const handlePublish = async () => { const { name: postName } = formState.value.post.metadata; const { permalink } = formState.value.post.status || {}; + if (isTitleChanged.value) { + formState.value.post = ( + await postUpdateMutate(formState.value.post) + ).data; + } + await apiClient.post.updatePostContent({ name: postName, content: formState.value.content, @@ -240,6 +262,7 @@ const handlePublishClick = () => { if (isUpdateMode.value) { handlePublish(); } else { + // Set editor title to post settingModal.value = true; } }; @@ -503,6 +526,7 @@ async function handleUploadImage(file: File, options?: AxiosRequestConfig) { v-if="currentEditorProvider" v-model:raw="formState.content.raw" v-model:content="formState.content.content" + v-model:title="formState.post.spec.title" :upload-image="handleUploadImage" class="h-full" @update="handleSetContentCache" diff --git a/ui/console-src/modules/contents/posts/components/PostSettingModal.vue b/ui/console-src/modules/contents/posts/components/PostSettingModal.vue index 82c8d6c83..1f9678235 100644 --- a/ui/console-src/modules/contents/posts/components/PostSettingModal.vue +++ b/ui/console-src/modules/contents/posts/components/PostSettingModal.vue @@ -216,6 +216,9 @@ watch( formState.value = toRaw(value); publishTime.value = toDatetimeLocal(formState.value.spec.publishTime); } + }, + { + immediate: true, } ); diff --git a/ui/packages/editor/src/components/Editor.vue b/ui/packages/editor/src/components/Editor.vue index 70245db0f..9fffff68c 100644 --- a/ui/packages/editor/src/components/Editor.vue +++ b/ui/packages/editor/src/components/Editor.vue @@ -37,14 +37,20 @@ watch(
- +
+
+ +
+ + +
diff --git a/ui/packages/editor/src/extensions/search-and-replace/SearchAndReplacePlugin.ts b/ui/packages/editor/src/extensions/search-and-replace/SearchAndReplacePlugin.ts index d1a8e914e..92130421b 100644 --- a/ui/packages/editor/src/extensions/search-and-replace/SearchAndReplacePlugin.ts +++ b/ui/packages/editor/src/extensions/search-and-replace/SearchAndReplacePlugin.ts @@ -30,15 +30,24 @@ export class SearchAndReplacePluginView { public containerElement: HTMLElement; + public init: boolean; + constructor({ view, editor, element }: SearchAndReplacePluginViewProps) { this.editor = editor; this.view = view; this.containerElement = element; - const { element: editorElement } = this.editor.options; - editorElement.insertAdjacentElement("afterbegin", this.containerElement); + this.init = false; } update() { + const { parentElement: editorParentElement } = this.editor.options.element; + if (!this.init && editorParentElement) { + editorParentElement.insertAdjacentElement( + "afterbegin", + this.containerElement + ); + this.init = true; + } return false; } diff --git a/ui/packages/editor/src/styles/base.scss b/ui/packages/editor/src/styles/base.scss index a8a7dc86e..4f53933e4 100644 --- a/ui/packages/editor/src/styles/base.scss +++ b/ui/packages/editor/src/styles/base.scss @@ -10,6 +10,11 @@ height: 48px; } + .editor-header-extra { + width: 100%; + padding: $editorVerticalPadding 20px; + } + .editor-content { width: 100%; position: relative; @@ -78,7 +83,8 @@ } @media screen { - .ProseMirror { + .ProseMirror, + .editor-header-extra { @media (min-width: 640px) { padding: $editorVerticalPadding min($editorHorizontalPadding, 10%) !important; } diff --git a/ui/src/components/editor/DefaultEditor.vue b/ui/src/components/editor/DefaultEditor.vue index b097e1d98..9982d4072 100644 --- a/ui/src/components/editor/DefaultEditor.vue +++ b/ui/src/components/editor/DefaultEditor.vue @@ -98,12 +98,14 @@ import { onBeforeUnmount } from "vue"; import { usePermission } from "@/utils/permission"; import type { AxiosRequestConfig } from "axios"; import { getContents } from "./utils/attachment"; +import { nextTick } from "vue"; const { t } = useI18n(); const { currentUserHasPermission } = usePermission(); const props = withDefaults( defineProps<{ + title?: string; raw?: string; content: string; uploadImage?: ( @@ -112,6 +114,7 @@ const props = withDefaults( ) => Promise; }>(), { + title: "", raw: "", content: "", uploadImage: undefined, @@ -119,6 +122,7 @@ const props = withDefaults( ); const emit = defineEmits<{ + (event: "update:title", value: string): void; (event: "update:raw", value: string): void; (event: "update:content", value: string): void; (event: "update", value: string): void; @@ -404,7 +408,6 @@ onMounted(() => { UiExtensionUpload, ExtensionSearchAndReplace, ], - autofocus: "start", parseOptions: { preserveWhitespace: true, }, @@ -441,6 +444,26 @@ const currentLocale = i18n.global.locale.value as | "en" | "zh" | "en-US"; + +function onTitleInput(event: Event) { + emit("update:title", (event.target as HTMLInputElement).value); +} + +// Set focus +const editorTitleRef = ref(); +onMounted(() => { + // if name is empty, it means the editor is in the creation mode + const urlParams = new URLSearchParams(window.location.search); + const name = urlParams.get("name"); + + if (!name) { + nextTick(() => { + editorTitleRef.value.focus(); + }); + } else { + editor.value?.commands.focus(); + } +});