refactor: add validation for post and singlePage settings form (#791)

#### What type of PR is this?

/kind improvement

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

重构文章和自定义页面的设置表单,支持提交时验证表单。

> 因为之前的多选项卡设计导致无法同时验证所有表单,所以这个 PR 重构了表单的布局。

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

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

#### Screenshots:

![2022-12-21 17 23 22](https://user-images.githubusercontent.com/21301288/208870059-5039a565-def2-4622-9a78-de30dceb4d65.gif)

#### Special notes for your reviewer:

测试方式:

1. 测试在内容编辑页面和列表打开文章和自定义页面的设置表单。
2. 检查表单验证是否有效。

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


```release-note
重构 Console 端文章和自定义页面的设置表单布局,支持提交时验证表单。
```
pull/797/head^2
Ryan Wang 2022-12-24 12:14:30 +08:00 committed by GitHub
parent 9c29538c7a
commit f7ece9135a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 363 additions and 280 deletions

View File

@ -1,13 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
Toast, import { computed, nextTick, ref, watchEffect } from "vue";
VButton,
VModal,
VSpace,
VTabItem,
VTabs,
} from "@halo-dev/components";
import { computed, ref, watchEffect } from "vue";
import type { SinglePage } from "@halo-dev/api-client"; import type { SinglePage } from "@halo-dev/api-client";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
@ -15,6 +8,7 @@ import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/
import { singlePageLabels } from "@/constants/labels"; import { singlePageLabels } from "@/constants/labels";
import { randomUUID } from "@/utils/id"; import { randomUUID } from "@/utils/id";
import { toDatetimeLocal, toISOString } from "@/utils/date"; import { toDatetimeLocal, toISOString } from "@/utils/date";
import { submitForm } from "@formkit/core";
const initialFormState: SinglePage = { const initialFormState: SinglePage = {
spec: { spec: {
@ -64,11 +58,11 @@ const emit = defineEmits<{
(event: "published", singlePage: SinglePage): void; (event: "published", singlePage: SinglePage): void;
}>(); }>();
const activeTab = ref("general");
const formState = ref<SinglePage>(cloneDeep(initialFormState)); const formState = ref<SinglePage>(cloneDeep(initialFormState));
const saving = ref(false); const saving = ref(false);
const publishing = ref(false); const publishing = ref(false);
const publishCanceling = ref(false); const publishCanceling = ref(false);
const submitType = ref<"publish" | "save">();
const isUpdateMode = computed(() => { const isUpdateMode = computed(() => {
return !!formState.value.metadata.creationTimestamp; return !!formState.value.metadata.creationTimestamp;
@ -77,13 +71,35 @@ const isUpdateMode = computed(() => {
const onVisibleChange = (visible: boolean) => { const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible); emit("update:visible", visible);
if (!visible) { if (!visible) {
setTimeout(() => {
activeTab.value = "general";
}, 200);
emit("close"); emit("close");
} }
}; };
const handleSubmit = () => {
if (submitType.value === "publish") {
handlePublish();
}
if (submitType.value === "save") {
handleSave();
}
};
const handleSaveClick = () => {
submitType.value = "save";
nextTick(() => {
submitForm("singlePage-setting-form");
});
};
const handlePublishClick = () => {
submitType.value = "publish";
nextTick(() => {
submitForm("singlePage-setting-form");
});
};
const handleSave = async () => { const handleSave = async () => {
if (props.onlyEmit) { if (props.onlyEmit) {
emit("saved", formState.value); emit("saved", formState.value);
@ -121,50 +137,71 @@ const handleSave = async () => {
} }
}; };
const handleSwitchPublish = async (publish: boolean) => { const handlePublish = async () => {
if (props.onlyEmit) { if (props.onlyEmit) {
emit("published", formState.value); emit("published", formState.value);
return; return;
} }
try { try {
if (publish) {
publishing.value = true; publishing.value = true;
} else {
publishCanceling.value = true;
}
if (publish) { const singlePageToUpdate = cloneDeep(formState.value);
formState.value.spec.releaseSnapshot = formState.value.spec.headSnapshot;
} singlePageToUpdate.spec.releaseSnapshot =
singlePageToUpdate.spec.headSnapshot;
singlePageToUpdate.spec.publish = true;
const { data } = const { data } =
await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage( await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
{ {
name: formState.value.metadata.name, name: formState.value.metadata.name,
singlePage: { singlePage: singlePageToUpdate,
...formState.value,
spec: {
...formState.value.spec,
publish: publish,
},
},
} }
); );
formState.value = data; formState.value = data;
if (publish) {
emit("published", data); emit("published", data);
}
onVisibleChange(false); onVisibleChange(false);
Toast.success(`${publish ? "发布" : "取消发布"}成功`); Toast.success("发布成功");
} catch (error) { } catch (error) {
console.error("Failed to publish single page", error); console.error("Failed to publish single page", error);
} finally { } finally {
publishing.value = false; publishing.value = false;
}
};
const handleUnpublish = async () => {
try {
publishCanceling.value = true;
const { data: singlePage } =
await apiClient.extension.singlePage.getcontentHaloRunV1alpha1SinglePage({
name: formState.value.metadata.name,
});
const singlePageToUpdate = cloneDeep(singlePage);
singlePageToUpdate.spec.publish = false;
const { data } =
await apiClient.extension.singlePage.updatecontentHaloRunV1alpha1SinglePage(
{
name: formState.value.metadata.name,
singlePage: singlePageToUpdate,
}
);
formState.value = data;
onVisibleChange(false);
Toast.success("取消发布成功");
} catch (error) {
console.error("Failed to unpublish single page", error);
} finally {
publishCanceling.value = false; publishCanceling.value = false;
} }
}; };
@ -204,15 +241,23 @@ const onPublishTimeChange = (value: string) => {
<slot name="actions"></slot> <slot name="actions"></slot>
</template> </template>
<VTabs v-model:active-id="activeTab" type="outline">
<VTabItem id="general" label="常规">
<FormKit <FormKit
id="basic" id="singlePage-setting-form"
name="basic"
:actions="false"
:preserve="true"
type="form" type="form"
name="singlePage-setting-form"
:config="{ validationVisibility: 'submit' }"
@submit="handleSubmit"
> >
<div>
<div class="md:grid md:grid-cols-4 md:gap-6">
<div class="md:col-span-1">
<div class="sticky top-0">
<span class="text-base font-medium text-gray-900">
常规设置
</span>
</div>
</div>
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
<FormKit <FormKit
v-model="formState.spec.title" v-model="formState.spec.title"
label="标题" label="标题"
@ -247,16 +292,22 @@ const onPublishTimeChange = (value: string) => {
validation="length:0,1024" validation="length:0,1024"
:rows="5" :rows="5"
></FormKit> ></FormKit>
</FormKit> </div>
</VTabItem> </div>
<VTabItem id="advanced" label="高级">
<FormKit <div class="py-5">
id="advanced" <div class="border-t border-gray-200"></div>
name="advanced" </div>
:actions="false"
:preserve="true" <div class="md:grid md:grid-cols-4 md:gap-6">
type="form" <div class="md:col-span-1">
> <div class="sticky top-0">
<span class="text-base font-medium text-gray-900">
高级设置
</span>
</div>
</div>
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
<FormKit <FormKit
v-model="formState.spec.allowComment" v-model="formState.spec.allowComment"
:options="[ :options="[
@ -308,10 +359,10 @@ const onPublishTimeChange = (value: string) => {
name="cover" name="cover"
validation="length:0,1024" validation="length:0,1024"
></FormKit> ></FormKit>
</div>
</div>
</div>
</FormKit> </FormKit>
<!--TODO: add SEO/Metas/Inject Code form-->
</VTabItem>
</VTabs>
<template #footer> <template #footer>
<VSpace> <VSpace>
@ -322,7 +373,7 @@ const onPublishTimeChange = (value: string) => {
" "
:loading="publishing" :loading="publishing"
type="secondary" type="secondary"
@click="handleSwitchPublish(true)" @click="handlePublishClick()"
> >
发布 发布
</VButton> </VButton>
@ -330,17 +381,15 @@ const onPublishTimeChange = (value: string) => {
v-else v-else
:loading="publishCanceling" :loading="publishCanceling"
type="danger" type="danger"
@click="handleSwitchPublish(false)" @click="handleUnpublish()"
> >
取消发布 取消发布
</VButton> </VButton>
</template> </template>
<VButton :loading="saving" type="secondary" @click="handleSave"> <VButton :loading="saving" type="secondary" @click="handleSaveClick">
保存 保存
</VButton> </VButton>
<VButton size="sm" type="default" @click="onVisibleChange(false)"> <VButton type="default" @click="onVisibleChange(false)"> </VButton>
关闭
</VButton>
</VSpace> </VSpace>
</template> </template>
</VModal> </VModal>

View File

@ -1,13 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
Toast, import { computed, nextTick, ref, watchEffect } from "vue";
VButton,
VModal,
VSpace,
VTabItem,
VTabs,
} from "@halo-dev/components";
import { computed, ref, watchEffect } from "vue";
import type { Post } from "@halo-dev/api-client"; import type { Post } from "@halo-dev/api-client";
import cloneDeep from "lodash.clonedeep"; import cloneDeep from "lodash.clonedeep";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
@ -15,6 +8,7 @@ import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/
import { postLabels } from "@/constants/labels"; import { postLabels } from "@/constants/labels";
import { randomUUID } from "@/utils/id"; import { randomUUID } from "@/utils/id";
import { toDatetimeLocal, toISOString } from "@/utils/date"; import { toDatetimeLocal, toISOString } from "@/utils/date";
import { submitForm } from "@formkit/core";
const initialFormState: Post = { const initialFormState: Post = {
spec: { spec: {
@ -66,11 +60,11 @@ const emit = defineEmits<{
(event: "published", post: Post): void; (event: "published", post: Post): void;
}>(); }>();
const activeTab = ref("general");
const formState = ref<Post>(cloneDeep(initialFormState)); const formState = ref<Post>(cloneDeep(initialFormState));
const saving = ref(false); const saving = ref(false);
const publishing = ref(false); const publishing = ref(false);
const publishCanceling = ref(false); const publishCanceling = ref(false);
const submitType = ref<"publish" | "save">();
const isUpdateMode = computed(() => { const isUpdateMode = computed(() => {
return !!formState.value.metadata.creationTimestamp; return !!formState.value.metadata.creationTimestamp;
@ -79,13 +73,39 @@ const isUpdateMode = computed(() => {
const handleVisibleChange = (visible: boolean) => { const handleVisibleChange = (visible: boolean) => {
emit("update:visible", visible); emit("update:visible", visible);
if (!visible) { if (!visible) {
setTimeout(() => {
activeTab.value = "general";
}, 200);
emit("close"); emit("close");
} }
}; };
const handleSubmit = () => {
if (submitType.value === "publish") {
handlePublish();
}
if (submitType.value === "save") {
handleSave();
}
};
const handleSaveClick = () => {
submitType.value = "save";
nextTick(() => {
submitForm("post-setting-form");
});
};
const handlePublishClick = () => {
if (!props.onlyEmit) {
handlePublish();
}
submitType.value = "publish";
nextTick(() => {
submitForm("post-setting-form");
});
};
const handleSave = async () => { const handleSave = async () => {
if (props.onlyEmit) { if (props.onlyEmit) {
emit("saved", formState.value); emit("saved", formState.value);
@ -197,15 +217,23 @@ const onPublishTimeChange = (value: string) => {
<slot name="actions"></slot> <slot name="actions"></slot>
</template> </template>
<VTabs v-model:active-id="activeTab" type="outline">
<VTabItem id="general" label="常规">
<FormKit <FormKit
id="basic" id="post-setting-form"
name="basic"
:actions="false"
:preserve="true"
type="form" type="form"
name="post-setting-form"
:config="{ validationVisibility: 'submit' }"
@submit="handleSubmit"
> >
<div>
<div class="md:grid md:grid-cols-4 md:gap-6">
<div class="md:col-span-1">
<div class="sticky top-0">
<span class="text-base font-medium text-gray-900">
常规设置
</span>
</div>
</div>
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
<FormKit <FormKit
v-model="formState.spec.title" v-model="formState.spec.title"
label="标题" label="标题"
@ -252,16 +280,22 @@ const onPublishTimeChange = (value: string) => {
:rows="5" :rows="5"
validation="length:0,1024" validation="length:0,1024"
></FormKit> ></FormKit>
</FormKit> </div>
</VTabItem> </div>
<VTabItem id="advanced" label="高级">
<FormKit <div class="py-5">
id="advanced" <div class="border-t border-gray-200"></div>
name="advanced" </div>
:actions="false"
:preserve="true" <div class="md:grid md:grid-cols-4 md:gap-6">
type="form" <div class="md:col-span-1">
> <div class="sticky top-0">
<span class="text-base font-medium text-gray-900">
高级设置
</span>
</div>
</div>
<div class="mt-5 divide-y divide-gray-100 md:col-span-3 md:mt-0">
<FormKit <FormKit
v-model="formState.spec.allowComment" v-model="formState.spec.allowComment"
:options="[ :options="[
@ -311,10 +345,10 @@ const onPublishTimeChange = (value: string) => {
type="attachment" type="attachment"
validation="length:0,1024" validation="length:0,1024"
></FormKit> ></FormKit>
</div>
</div>
</div>
</FormKit> </FormKit>
<!--TODO: add SEO/Metas/Inject Code form-->
</VTabItem>
</VTabs>
<template #footer> <template #footer>
<VSpace> <VSpace>
@ -323,7 +357,7 @@ const onPublishTimeChange = (value: string) => {
v-if="formState.metadata.labels?.[postLabels.PUBLISHED] !== 'true'" v-if="formState.metadata.labels?.[postLabels.PUBLISHED] !== 'true'"
:loading="publishing" :loading="publishing"
type="secondary" type="secondary"
@click="handlePublish()" @click="handlePublishClick()"
> >
发布 发布
</VButton> </VButton>
@ -336,7 +370,7 @@ const onPublishTimeChange = (value: string) => {
取消发布 取消发布
</VButton> </VButton>
</template> </template>
<VButton :loading="saving" type="secondary" @click="handleSave"> <VButton :loading="saving" type="secondary" @click="handleSaveClick()">
保存 保存
</VButton> </VButton>
<VButton type="default" @click="handleVisibleChange(false)"> <VButton type="default" @click="handleVisibleChange(false)">