feat: add annotations form for more extension (#801)

#### What type of PR is this?

/kind feature

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

为更多模型添加 Annotation Form 的支持。此 PR 包括:

1. 自定义页面。
2. 文章分类。
3. 文章标签。
4. 菜单项。
5. 用户。

此 PR 是对 https://github.com/halo-dev/console/pull/770 的补充。

#### Special notes for your reviewer:

测试方式:

1. 将一下内容放到任意一个主题下,后缀为 `yaml`,文件名随意。
    ```yaml
    spec:
      targetRef:
        group: content.halo.run
        kind: Post
      formSchema:
        - $formkit: "text"
          name: "download"
          label: "下载地址"
        - $formkit: "text"
          name: "version"
          label: "版本"
    apiVersion: v1alpha1
    kind: AnnotationSetting
    metadata:
      generateName: annotation-
    ---
    spec:
      targetRef:
        group: content.halo.run
        kind: SinglePage
      formSchema:
        - $formkit: "text"
          name: "download"
          label: "下载地址"
        - $formkit: "text"
          name: "version"
          label: "版本"
    apiVersion: v1alpha1
    kind: AnnotationSetting
    metadata:
      generateName: annotation-
    
    ---
    spec:
      targetRef:
        group: content.halo.run
        kind: Category
      formSchema:
        - $formkit: "text"
          name: "download"
          label: "下载地址"
        - $formkit: "text"
          name: "version"
          label: "版本"
    apiVersion: v1alpha1
    kind: AnnotationSetting
    metadata:
      generateName: annotation-
    ---
    spec:
      targetRef:
        group: content.halo.run
        kind: Tag
      formSchema:
        - $formkit: "text"
          name: "download"
          label: "下载地址"
        - $formkit: "text"
          name: "version"
          label: "版本"
    apiVersion: v1alpha1
    kind: AnnotationSetting
    metadata:
      generateName: annotation-
    ---
    spec:
      targetRef:
        group: ""
        kind: MenuItem
      formSchema:
        - $formkit: "text"
          name: "icon"
          label: "图标"
        - $formkit: "text"
          name: "version"
          label: "版本"
    apiVersion: v1alpha1
    kind: AnnotationSetting
    metadata:
      generateName: annotation-

    ```
3. 后端需要使用 https://github.com/halo-dev/halo/pull/3028
4. 测试上述提到的模型的 Annotations 表单。
5. 检查是否可以设置正常。

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

```release-note
None
```
pull/802/head
Ryan Wang 2022-12-26 23:32:31 +08:00 committed by GitHub
parent d60931bae5
commit 8d3c289559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 425 additions and 169 deletions

View File

@ -148,7 +148,11 @@ const specFormInvalid = ref(true);
const customFormInvalid = ref(true);
const handleSubmit = async () => {
submitForm("specForm");
if (avaliableAnnotationSettings.value.length) {
submitForm("specForm");
} else {
specFormInvalid.value = false;
}
submitForm("customForm");
await nextTick();
};

View File

@ -9,6 +9,7 @@ import { singlePageLabels } from "@/constants/labels";
import { randomUUID } from "@/utils/id";
import { toDatetimeLocal, toISOString } from "@/utils/date";
import { submitForm } from "@formkit/core";
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
const initialFormState: SinglePage = {
spec: {
@ -75,6 +76,8 @@ const onVisibleChange = (visible: boolean) => {
}
};
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
const handleSubmit = () => {
if (submitType.value === "publish") {
handlePublish();
@ -101,6 +104,20 @@ const handlePublishClick = () => {
};
const handleSave = async () => {
annotationsFormRef.value?.handleSubmit();
await nextTick();
const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =
annotationsFormRef.value || {};
if (customFormInvalid || specFormInvalid) {
return;
}
formState.value.metadata.annotations = {
...annotations,
...customAnnotations,
};
if (props.onlyEmit) {
emit("saved", formState.value);
return;
@ -138,6 +155,20 @@ const handleSave = async () => {
};
const handlePublish = async () => {
annotationsFormRef.value?.handleSubmit();
await nextTick();
const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =
annotationsFormRef.value || {};
if (customFormInvalid || specFormInvalid) {
return;
}
formState.value.metadata.annotations = {
...annotations,
...customAnnotations,
};
if (props.onlyEmit) {
emit("published", formState.value);
return;
@ -364,6 +395,27 @@ const onPublishTimeChange = (value: string) => {
</div>
</FormKit>
<div class="py-5">
<div class="border-t border-gray-200"></div>
</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">
<AnnotationsForm
:key="formState.metadata.name"
ref="annotationsFormRef"
:value="formState.metadata.annotations"
kind="SinglePage"
group="content.halo.run"
/>
</div>
</div>
<template #footer>
<VSpace>
<template v-if="publishSupport">

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
// core libs
import { computed, ref, watch } from "vue";
import { computed, nextTick, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client";
// components
@ -15,6 +15,7 @@ import cloneDeep from "lodash.clonedeep";
import { reset } from "@formkit/core";
import { setFocus } from "@/formkit/utils/focus";
import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/use-theme";
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
const props = withDefaults(
defineProps<{
@ -62,7 +63,23 @@ const modalTitle = computed(() => {
return isUpdateMode.value ? "编辑文章分类" : "新增文章分类";
});
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
const handleSaveCategory = async () => {
annotationsFormRef.value?.handleSubmit();
await nextTick();
const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =
annotationsFormRef.value || {};
if (customFormInvalid || specFormInvalid) {
return;
}
formState.value.metadata.annotations = {
...annotations,
...customAnnotations,
};
try {
saving.value = true;
if (isUpdateMode.value) {
@ -126,56 +143,89 @@ const { templates } = useThemeCustomTemplates("category");
<VModal
:title="modalTitle"
:visible="visible"
:width="600"
:width="700"
@update:visible="onVisibleChange"
>
<FormKit
id="category-form"
name="category-form"
type="form"
name="category-form"
:config="{ validationVisibility: 'submit' }"
@submit="handleSaveCategory"
>
<FormKit
id="displayNameInput"
v-model="formState.spec.displayName"
name="displayName"
label="名称"
type="text"
validation="required|length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.slug"
help="通常作为分类访问地址标识"
name="slug"
label="别名"
type="text"
validation="required|length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.template"
:options="templates"
label="自定义模板"
type="select"
name="template"
></FormKit>
<FormKit
v-model="formState.spec.cover"
help="需要主题适配以支持"
name="cover"
label="封面图"
type="attachment"
validation="length:0,1024"
></FormKit>
<FormKit
v-model="formState.spec.description"
name="description"
help="需要主题适配以支持"
label="描述"
type="textarea"
validation="length:0,200"
></FormKit>
<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
id="displayNameInput"
v-model="formState.spec.displayName"
name="displayName"
label="名称"
type="text"
validation="required|length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.slug"
help="通常作为分类访问地址标识"
name="slug"
label="别名"
type="text"
validation="required|length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.template"
:options="templates"
label="自定义模板"
type="select"
name="template"
></FormKit>
<FormKit
v-model="formState.spec.cover"
help="需要主题适配以支持"
name="cover"
label="封面图"
type="attachment"
validation="length:0,1024"
></FormKit>
<FormKit
v-model="formState.spec.description"
name="description"
help="需要主题适配以支持"
label="描述"
type="textarea"
validation="length:0,200"
></FormKit>
</div>
</div>
</div>
</FormKit>
<div class="py-5">
<div class="border-t border-gray-200"></div>
</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">
<AnnotationsForm
:key="formState.metadata.name"
ref="annotationsFormRef"
:value="formState.metadata.annotations"
kind="Category"
group="content.halo.run"
/>
</div>
</div>
<template #footer>
<VSpace>
<SubmitButton

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
// core libs
import { computed, ref, watch } from "vue";
import { computed, nextTick, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client";
// components
@ -21,6 +21,7 @@ import type { Tag } from "@halo-dev/api-client";
import cloneDeep from "lodash.clonedeep";
import { reset } from "@formkit/core";
import { setFocus } from "@/formkit/utils/focus";
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
const props = withDefaults(
defineProps<{
@ -66,7 +67,23 @@ const modalTitle = computed(() => {
return isUpdateMode.value ? "编辑文章标签" : "新增文章标签";
});
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
const handleSaveTag = async () => {
annotationsFormRef.value?.handleSubmit();
await nextTick();
const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =
annotationsFormRef.value || {};
if (customFormInvalid || specFormInvalid) {
return;
}
formState.value.metadata.annotations = {
...annotations,
...customAnnotations,
};
try {
saving.value = true;
if (isUpdateMode.value) {
@ -127,7 +144,7 @@ watch(
<VModal
:title="modalTitle"
:visible="visible"
:width="600"
:width="700"
@update:visible="onVisibleChange"
>
<template #actions>
@ -138,46 +155,80 @@ watch(
<IconArrowRight />
</span>
</template>
<FormKit
id="tag-form"
type="form"
name="tag-form"
:config="{ validationVisibility: 'submit' }"
type="form"
@submit="handleSaveTag"
>
<FormKit
id="displayNameInput"
v-model="formState.spec.displayName"
name="displayName"
label="名称"
type="text"
validation="required|length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.slug"
help="通常作为标签访问地址标识"
label="别名"
name="slug"
type="text"
validation="required|length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.color"
name="color"
help="需要主题适配以支持"
label="颜色"
type="color"
validation="length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.cover"
name="cover"
help="需要主题适配以支持"
label="封面图"
type="attachment"
validation="length:0,1024"
></FormKit>
<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
id="displayNameInput"
v-model="formState.spec.displayName"
name="displayName"
label="名称"
type="text"
validation="required|length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.slug"
help="通常作为标签访问地址标识"
label="别名"
name="slug"
type="text"
validation="required|length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.color"
name="color"
help="需要主题适配以支持"
label="颜色"
type="color"
validation="length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.cover"
name="cover"
help="需要主题适配以支持"
label="封面图"
type="attachment"
validation="length:0,1024"
></FormKit>
</div>
</div>
</div>
</FormKit>
<div class="py-5">
<div class="border-t border-gray-200"></div>
</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">
<AnnotationsForm
:key="formState.metadata.name"
ref="annotationsFormRef"
:value="formState.metadata.annotations"
kind="Tag"
group="content.halo.run"
/>
</div>
</div>
<template #footer>
<VSpace>
<SubmitButton

View File

@ -1,12 +1,13 @@
<script lang="ts" setup>
import { Toast, VButton, VModal, VSpace } from "@halo-dev/components";
import SubmitButton from "@/components/button/SubmitButton.vue";
import { computed, ref, watch } from "vue";
import { computed, nextTick, ref, watch } from "vue";
import type { Menu, MenuItem, Ref } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
import { reset } from "@formkit/core";
import cloneDeep from "lodash.clonedeep";
import { setFocus } from "@/formkit/utils/focus";
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
const props = withDefaults(
defineProps<{
@ -56,7 +57,23 @@ const modalTitle = computed(() => {
return isUpdateMode.value ? "编辑菜单项" : "新增菜单项";
});
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
const handleSaveMenuItem = async () => {
annotationsFormRef.value?.handleSubmit();
await nextTick();
const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =
annotationsFormRef.value || {};
if (customFormInvalid || specFormInvalid) {
return;
}
formState.value.metadata.annotations = {
...annotations,
...customAnnotations,
};
try {
saving.value = true;
@ -243,7 +260,7 @@ const onMenuItemSourceChange = () => {
<template>
<VModal
:visible="visible"
:width="500"
:width="700"
:title="modalTitle"
@update:visible="onVisibleChange"
>
@ -255,54 +272,87 @@ const onMenuItemSourceChange = () => {
:config="{ validationVisibility: 'submit' }"
@submit="handleSaveMenuItem"
>
<FormKit
v-if="!isUpdateMode && menu && visible"
v-model="selectedParentMenuItem"
label="上级菜单项"
placeholder="选择上级菜单项"
type="menuItemSelect"
:menu-items="menu?.spec.menuItems || []"
/>
<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
v-if="!isUpdateMode && menu && visible"
v-model="selectedParentMenuItem"
label="上级菜单项"
placeholder="选择上级菜单项"
type="menuItemSelect"
:menu-items="menu?.spec.menuItems || []"
/>
<FormKit
v-model="selectedRefKind"
:options="menuItemRefsMap"
:disabled="isUpdateMode"
label="类型"
type="select"
@change="onMenuItemSourceChange"
/>
<FormKit
v-model="selectedRefKind"
:options="menuItemRefsMap"
:disabled="isUpdateMode"
label="类型"
type="select"
@change="onMenuItemSourceChange"
/>
<FormKit
v-if="!selectedRefKind"
id="displayNameInput"
v-model="formState.spec.displayName"
label="名称"
type="text"
name="displayName"
validation="required|length:0,100"
/>
<FormKit
v-if="!selectedRefKind"
id="displayNameInput"
v-model="formState.spec.displayName"
label="名称"
type="text"
name="displayName"
validation="required|length:0,100"
/>
<FormKit
v-if="!selectedRefKind"
v-model="formState.spec.href"
label="链接地址"
type="text"
name="href"
validation="required|length:0,1024"
/>
<FormKit
v-if="!selectedRefKind"
v-model="formState.spec.href"
label="链接地址"
type="text"
name="href"
validation="required|length:0,1024"
/>
<FormKit
v-if="selectedRef?.ref"
:id="selectedRef.inputType"
:key="selectedRef.inputType"
v-model="selectedRefName"
:placeholder="`请选择${selectedRef.label}`"
:label="selectedRef.label"
:type="selectedRef.inputType"
validation="required"
/>
<FormKit
v-if="selectedRef?.ref"
:id="selectedRef.inputType"
:key="selectedRef.inputType"
v-model="selectedRefName"
:placeholder="`请选择${selectedRef.label}`"
:label="selectedRef.label"
:type="selectedRef.inputType"
validation="required"
/>
</div>
</div>
</div>
</FormKit>
<div class="py-5">
<div class="border-t border-gray-200"></div>
</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">
<AnnotationsForm
:key="formState.metadata.name"
ref="annotationsFormRef"
:value="formState.metadata.annotations"
kind="MenuItem"
group=""
/>
</div>
</div>
<template #footer>
<VSpace>
<SubmitButton

View File

@ -1,6 +1,6 @@
<script lang="ts" setup>
// core libs
import { computed, ref, watch } from "vue";
import { computed, nextTick, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client";
import type { User } from "@halo-dev/api-client";
@ -14,6 +14,7 @@ import { reset } from "@formkit/core";
// hooks
import { setFocus } from "@/formkit/utils/focus";
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
const props = withDefaults(
defineProps<{
@ -94,7 +95,23 @@ const handleResetForm = () => {
reset("user-form");
};
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
const handleCreateUser = async () => {
annotationsFormRef.value?.handleSubmit();
await nextTick();
const { customAnnotations, annotations, customFormInvalid, specFormInvalid } =
annotationsFormRef.value || {};
if (customFormInvalid || specFormInvalid) {
return;
}
formState.value.metadata.annotations = {
...annotations,
...customAnnotations,
};
try {
saving.value = true;
if (isUpdateMode.value) {
@ -132,52 +149,84 @@ const handleCreateUser = async () => {
type="form"
@submit="handleCreateUser"
>
<FormKit
id="userNameInput"
v-model="formState.metadata.name"
:disabled="isUpdateMode"
label="用户名"
type="text"
name="name"
validation="required|alphanumeric|length:0,50"
></FormKit>
<FormKit
id="displayNameInput"
v-model="formState.spec.displayName"
label="显示名称"
type="text"
name="displayName"
validation="required|length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.email"
label="电子邮箱"
type="email"
name="email"
validation="required|email|length:0,100"
></FormKit>
<FormKit
v-model="formState.spec.phone"
label="手机号"
type="text"
name="phone"
validation="length:0,20"
></FormKit>
<FormKit
v-model="formState.spec.avatar"
label="头像"
type="attachment"
name="avatar"
validation="url|length:0,1024"
></FormKit>
<FormKit
v-model="formState.spec.bio"
label="描述"
type="textarea"
name="bio"
validation="length:0,2048"
></FormKit>
<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
id="userNameInput"
v-model="formState.metadata.name"
:disabled="isUpdateMode"
label="用户名"
type="text"
name="name"
validation="required|alphanumeric|length:0,50"
></FormKit>
<FormKit
id="displayNameInput"
v-model="formState.spec.displayName"
label="显示名称"
type="text"
name="displayName"
validation="required|length:0,50"
></FormKit>
<FormKit
v-model="formState.spec.email"
label="电子邮箱"
type="email"
name="email"
validation="required|email|length:0,100"
></FormKit>
<FormKit
v-model="formState.spec.phone"
label="手机号"
type="text"
name="phone"
validation="length:0,20"
></FormKit>
<FormKit
v-model="formState.spec.avatar"
label="头像"
type="attachment"
name="avatar"
validation="url|length:0,1024"
></FormKit>
<FormKit
v-model="formState.spec.bio"
label="描述"
type="textarea"
name="bio"
validation="length:0,2048"
></FormKit>
</div>
</div>
</div>
</FormKit>
<div class="py-5">
<div class="border-t border-gray-200"></div>
</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">
<AnnotationsForm
:key="formState.metadata.name"
ref="annotationsFormRef"
:value="formState.metadata.annotations"
kind="User"
group=""
/>
</div>
</div>
<template #footer>
<VSpace>
<SubmitButton