mirror of https://github.com/halo-dev/halo-admin
feat: add AnnotationsForm Component to edit extension annotations (#770)
#### What type of PR is this? /kind feature #### What this PR does / why we need it: 提供一个 Annotations 编辑组件,支持由主题或者插件提供表单定义,也支持使用者自定义 key-value,用于扩展资源字段。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2858 #### Screenshots: <img width="789" alt="image" src="https://user-images.githubusercontent.com/21301288/209517622-80111fd2-8e79-480f-8ca0-9f7073300b2b.png"> #### 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- ``` 3. 后端需要使用 https://github.com/halo-dev/halo/pull/3028 4. 测试为文章设置 Annotations 和自定义的 Annotations。 5. 检查是否可以设置正常。 #### Does this PR introduce a user-facing change? ```release-note 文章设置支持设置元数据。 ```pull/802/head
parent
eccb6e639a
commit
d60931bae5
|
@ -39,8 +39,9 @@
|
|||
"@formkit/inputs": "^1.0.0-beta.12",
|
||||
"@formkit/themes": "^1.0.0-beta.12",
|
||||
"@formkit/utils": "^1.0.0-beta.12",
|
||||
"@formkit/validation": "1.0.0-beta.12",
|
||||
"@formkit/vue": "^1.0.0-beta.12",
|
||||
"@halo-dev/api-client": "^0.0.62",
|
||||
"@halo-dev/api-client": "0.0.64",
|
||||
"@halo-dev/components": "workspace:*",
|
||||
"@halo-dev/console-shared": "workspace:*",
|
||||
"@halo-dev/richtext-editor": "^0.0.0-alpha.17",
|
||||
|
|
|
@ -11,8 +11,9 @@ importers:
|
|||
'@formkit/inputs': ^1.0.0-beta.12
|
||||
'@formkit/themes': ^1.0.0-beta.12
|
||||
'@formkit/utils': ^1.0.0-beta.12
|
||||
'@formkit/validation': 1.0.0-beta.12
|
||||
'@formkit/vue': ^1.0.0-beta.12
|
||||
'@halo-dev/api-client': ^0.0.62
|
||||
'@halo-dev/api-client': 0.0.64
|
||||
'@halo-dev/components': workspace:*
|
||||
'@halo-dev/console-shared': workspace:*
|
||||
'@halo-dev/richtext-editor': ^0.0.0-alpha.17
|
||||
|
@ -105,8 +106,9 @@ importers:
|
|||
'@formkit/inputs': 1.0.0-beta.12-e579559
|
||||
'@formkit/themes': 1.0.0-beta.12-e579559_tailwindcss@3.2.4
|
||||
'@formkit/utils': 1.0.0-beta.12-e579559
|
||||
'@formkit/validation': 1.0.0-beta.12
|
||||
'@formkit/vue': 1.0.0-beta.12-e579559_ior6jr3fpijijuwpr34w2i25va
|
||||
'@halo-dev/api-client': 0.0.62
|
||||
'@halo-dev/api-client': 0.0.64
|
||||
'@halo-dev/components': link:packages/components
|
||||
'@halo-dev/console-shared': link:packages/shared
|
||||
'@halo-dev/richtext-editor': 0.0.0-alpha.17_vue@3.2.45
|
||||
|
@ -1874,6 +1876,12 @@ packages:
|
|||
'@floating-ui/core': 0.3.1
|
||||
dev: false
|
||||
|
||||
/@formkit/core/1.0.0-beta.12:
|
||||
resolution: {integrity: sha512-/Pod7k4N58eDOG+0LE0ccV7BOYD5sEA4RqaRNJn9m6Jg9nF/JATmSNL46kVCH3mV6sSoV045L3Zir8//rya0Lw==}
|
||||
dependencies:
|
||||
'@formkit/utils': 1.0.0-beta.12
|
||||
dev: false
|
||||
|
||||
/@formkit/core/1.0.0-beta.12-e579559:
|
||||
resolution: {integrity: sha512-y90ubMcFr6WtAjZqUOLgA3p4jm024f6R7iDRVKHsmdwSKm1/GZl4D+Yo2k8DL40xM7NbZhIvK40MiEC0pNZ3ig==}
|
||||
dependencies:
|
||||
|
@ -1901,6 +1909,13 @@ packages:
|
|||
'@formkit/core': 1.0.0-beta.12-e579559
|
||||
dev: false
|
||||
|
||||
/@formkit/observer/1.0.0-beta.12:
|
||||
resolution: {integrity: sha512-kXVWUkjbNsymwv50QuI5thjlCknGy8azPlzepTDj3fsuZANuHG8qaj3W5h9mY6cCgvil4d4WeBz5f54iJsaEXA==}
|
||||
dependencies:
|
||||
'@formkit/core': 1.0.0-beta.12
|
||||
'@formkit/utils': 1.0.0-beta.12
|
||||
dev: false
|
||||
|
||||
/@formkit/observer/1.0.0-beta.12-e579559:
|
||||
resolution: {integrity: sha512-6Nki4VmUN3OyU70N6AIttvn/ScZXq5mwv11BIYLgfsfiMI5zoiflmNHrD7yf4KTUVvz16ThJB5nRGIXlnUnH3Q==}
|
||||
dependencies:
|
||||
|
@ -1934,10 +1949,21 @@ packages:
|
|||
tailwindcss: 3.2.4_postcss@8.4.19
|
||||
dev: false
|
||||
|
||||
/@formkit/utils/1.0.0-beta.12:
|
||||
resolution: {integrity: sha512-L2P221teC+58Y1A4TZpJsLt/Y+WErLxbBt4hH6XhokssVX7mYR56YWienY9StN5jmiGVWtcK1+riXCb0n1aMOA==}
|
||||
dev: false
|
||||
|
||||
/@formkit/utils/1.0.0-beta.12-e579559:
|
||||
resolution: {integrity: sha512-4yTz4IRzPX3G08aAO+ZfEuBYfy6OSgk3csx3TBhOcL6NzyIbJkTc2ZsxaUYXgY6FOVclazLGMKM5+jBitMOL5Q==}
|
||||
dev: false
|
||||
|
||||
/@formkit/validation/1.0.0-beta.12:
|
||||
resolution: {integrity: sha512-ZSD5llsfSqXfIYMtSeeDwx7h421JCKobi00RxpGJkuHgpnAi3oxTJB7J6u+3pMrO9BqcidGtk5G/rk9F4fCNEw==}
|
||||
dependencies:
|
||||
'@formkit/core': 1.0.0-beta.12
|
||||
'@formkit/observer': 1.0.0-beta.12
|
||||
dev: false
|
||||
|
||||
/@formkit/validation/1.0.0-beta.12-e579559:
|
||||
resolution: {integrity: sha512-f/S0LefikeBZRxKkjx1fD7RODXMHglg6+B1NUYEKsEKc3ySjH/V9/XroaPszXfkupAnVBSTjZXG7T9hmn39VVA==}
|
||||
dependencies:
|
||||
|
@ -1966,8 +1992,8 @@ packages:
|
|||
- windicss
|
||||
dev: false
|
||||
|
||||
/@halo-dev/api-client/0.0.62:
|
||||
resolution: {integrity: sha512-LVoAH4/+8iHxqHf7X6Ax3wy+IRSB2Tm9IC3zhfGdnbrdgyn/NnnpRIhTCKo6cK0Sr2j/3W4Pvmc0xCWNVzNAyw==}
|
||||
/@halo-dev/api-client/0.0.64:
|
||||
resolution: {integrity: sha512-zzhTdRi4p7nRsWG7u85YCkoHZLHsbpuWeBCwLl5FSXxY8ruKuiOo0FypkS7Ll7505XASk4MJL6rsyeX+l7rLmQ==}
|
||||
dev: false
|
||||
|
||||
/@halo-dev/richtext-editor/0.0.0-alpha.17_vue@3.2.45:
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
<script lang="ts" setup>
|
||||
import {
|
||||
reset,
|
||||
submitForm,
|
||||
type FormKitNode,
|
||||
type FormKitSchemaCondition,
|
||||
type FormKitSchemaNode,
|
||||
} from "@formkit/core";
|
||||
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||
import { apiClient } from "@/utils/api-client";
|
||||
import type { AnnotationSetting } from "@halo-dev/api-client";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import { getValidationMessages } from "@formkit/validation";
|
||||
import { useThemeStore } from "@/stores/theme";
|
||||
|
||||
const themeStore = useThemeStore();
|
||||
|
||||
function keyValidationRule(node: FormKitNode) {
|
||||
return !annotations?.[node.value as string];
|
||||
}
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
group: string;
|
||||
kind: string;
|
||||
value?: {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
}>(),
|
||||
{
|
||||
value: null,
|
||||
}
|
||||
);
|
||||
|
||||
const annotationSettings = ref<AnnotationSetting[]>([] as AnnotationSetting[]);
|
||||
|
||||
const avaliableAnnotationSettings = computed(() => {
|
||||
return annotationSettings.value.filter((setting) => {
|
||||
if (!setting.metadata.labels?.["theme.halo.run/theme-name"]) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
setting.metadata.labels?.["theme.halo.run/theme-name"] ===
|
||||
themeStore.activatedTheme?.metadata.name
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const handleFetchAnnotationSettings = async () => {
|
||||
try {
|
||||
const { data } =
|
||||
await apiClient.extension.annotationSetting.listv1alpha1AnnotationSetting(
|
||||
{
|
||||
labelSelector: [
|
||||
`halo.run/target-ref=${[props.group, props.kind].join("/")}`,
|
||||
],
|
||||
}
|
||||
);
|
||||
annotationSettings.value = data.items;
|
||||
} catch (error) {
|
||||
console.log("Failed to fetch annotation settings", error);
|
||||
}
|
||||
};
|
||||
|
||||
const annotations = ref<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
const customAnnotationsState = ref<{ key: string; value: string }[]>([]);
|
||||
|
||||
const customAnnotations = computed(() => {
|
||||
return customAnnotationsState.value.reduce((acc, cur) => {
|
||||
acc[cur.key] = cur.value;
|
||||
return acc;
|
||||
}, {} as { [key: string]: string });
|
||||
});
|
||||
|
||||
const handleProcessCustomAnnotations = () => {
|
||||
let formSchemas: FormKitSchemaNode[] = [];
|
||||
|
||||
avaliableAnnotationSettings.value.forEach((annotationSetting) => {
|
||||
formSchemas = formSchemas.concat(
|
||||
annotationSetting.spec?.formSchema as FormKitSchemaNode[]
|
||||
);
|
||||
});
|
||||
|
||||
customAnnotationsState.value = Object.entries(props.value || {})
|
||||
.map(([key, value]) => {
|
||||
const fromThemeSpec = formSchemas.some((item) => {
|
||||
if (typeof item === "object" && "$formkit" in item) {
|
||||
return item.name === key;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!fromThemeSpec) {
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter((item) => item) as { key: string; value: string }[];
|
||||
|
||||
annotations.value = Object.entries(props.value || {})
|
||||
.map(([key, value]) => {
|
||||
const fromThemeSpec = formSchemas.some((item) => {
|
||||
if (typeof item === "object" && "$formkit" in item) {
|
||||
return item.name === key;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (fromThemeSpec) {
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
};
|
||||
}
|
||||
})
|
||||
.filter((item) => item)
|
||||
.reduce((acc, cur) => {
|
||||
if (cur) {
|
||||
acc[cur.key] = cur.value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as { [key: string]: string });
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
annotations.value = cloneDeep(props.value) || {};
|
||||
await handleFetchAnnotationSettings();
|
||||
handleProcessCustomAnnotations();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(value) => {
|
||||
reset("specForm");
|
||||
reset("customForm");
|
||||
annotations.value = cloneDeep(props.value) || {};
|
||||
if (value) {
|
||||
handleProcessCustomAnnotations();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// submit
|
||||
|
||||
const specFormInvalid = ref(true);
|
||||
const customFormInvalid = ref(true);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
submitForm("specForm");
|
||||
submitForm("customForm");
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
const onSpecFormSubmitCheck = async (node?: FormKitNode) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const validations = getValidationMessages(node);
|
||||
specFormInvalid.value = validations.size > 0;
|
||||
};
|
||||
|
||||
const onCustomFormSubmitCheck = async (node?: FormKitNode) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const validations = getValidationMessages(node);
|
||||
customFormInvalid.value = validations.size > 0;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
handleSubmit,
|
||||
specFormInvalid,
|
||||
customFormInvalid,
|
||||
annotations,
|
||||
customAnnotations,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-3 divide-y divide-gray-100">
|
||||
<FormKit
|
||||
v-if="annotations && avaliableAnnotationSettings.length > 0"
|
||||
id="specForm"
|
||||
v-model="annotations"
|
||||
type="form"
|
||||
:preserve="true"
|
||||
@submit-invalid="onSpecFormSubmitCheck"
|
||||
@submit="specFormInvalid = false"
|
||||
>
|
||||
<template
|
||||
v-for="(annotationSetting, index) in avaliableAnnotationSettings"
|
||||
>
|
||||
<FormKitSchema
|
||||
v-if="annotationSetting.spec?.formSchema"
|
||||
:key="index"
|
||||
:schema="annotationSetting.spec?.formSchema as (FormKitSchemaCondition| FormKitSchemaNode[])"
|
||||
/>
|
||||
</template>
|
||||
</FormKit>
|
||||
<FormKit
|
||||
v-if="annotations"
|
||||
id="customForm"
|
||||
type="form"
|
||||
:preserve="true"
|
||||
:form-class="`${avaliableAnnotationSettings.length ? 'py-4' : ''}`"
|
||||
@submit-invalid="onCustomFormSubmitCheck"
|
||||
@submit="customFormInvalid = false"
|
||||
>
|
||||
<FormKit v-model="customAnnotationsState" type="repeater" label="自定义">
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Key"
|
||||
name="key"
|
||||
validation="required|keyValidationRule"
|
||||
:validation-rules="{ keyValidationRule }"
|
||||
:validation-messages="{
|
||||
keyValidationRule: '当前 Key 已被占用',
|
||||
}"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
type="text"
|
||||
label="Value"
|
||||
name="value"
|
||||
validation="required"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
</FormKit>
|
||||
</div>
|
||||
</template>
|
|
@ -8,6 +8,7 @@ import { useThemeCustomTemplates } from "@/modules/interface/themes/composables/
|
|||
import { postLabels } from "@/constants/labels";
|
||||
import { randomUUID } from "@/utils/id";
|
||||
import { toDatetimeLocal, toISOString } from "@/utils/date";
|
||||
import AnnotationsForm from "@/components/form/AnnotationsForm.vue";
|
||||
import { submitForm } from "@formkit/core";
|
||||
|
||||
const initialFormState: Post = {
|
||||
|
@ -107,6 +108,21 @@ 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;
|
||||
|
@ -204,6 +220,8 @@ const publishTime = computed(() => {
|
|||
const onPublishTimeChange = (value: string) => {
|
||||
formState.value.spec.publishTime = value ? toISOString(value) : undefined;
|
||||
};
|
||||
|
||||
const annotationsFormRef = ref<InstanceType<typeof AnnotationsForm>>();
|
||||
</script>
|
||||
<template>
|
||||
<VModal
|
||||
|
@ -350,6 +368,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="Post"
|
||||
group="content.halo.run"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<VSpace>
|
||||
<template v-if="publishSupport">
|
||||
|
|
|
@ -31,6 +31,7 @@ import {
|
|||
V1alpha1RoleBindingApi,
|
||||
V1alpha1SettingApi,
|
||||
V1alpha1UserApi,
|
||||
V1alpha1AnnotationSettingApi,
|
||||
} from "@halo-dev/api-client";
|
||||
import type { AxiosError, AxiosInstance } from "axios";
|
||||
import axios from "axios";
|
||||
|
@ -157,6 +158,11 @@ function setupApiClient(axios: AxiosInstance) {
|
|||
axios
|
||||
),
|
||||
},
|
||||
annotationSetting: new V1alpha1AnnotationSettingApi(
|
||||
undefined,
|
||||
baseURL,
|
||||
axios
|
||||
),
|
||||
},
|
||||
// custom endpoints
|
||||
user: new ApiConsoleHaloRunV1alpha1UserApi(undefined, baseURL, axios),
|
||||
|
|
Loading…
Reference in New Issue