pref: implement business selector using new selector component (#6525)

#### What type of PR is this?

/kind improvement
/area ui
/milestone 2.19.x

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

使用 #6473 中重构的 Formkit Select 组件来实现用户、文章、页面等各种业务搜索组件。 

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

#### How to test it?

测试各类搜索组件是否正常可用。
测试从旧版本升级后,原有数据是否可以正常显示。

#### Does this PR introduce a user-facing change?
```release-note
使用重构的 Formkit Select 组件来实现业务选择器。
```
pull/6536/head v2.19.0-rc.2
Takagi 2024-08-27 20:39:21 +08:00 committed by GitHub
parent a5c6d6672f
commit 281567877a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 225 additions and 85 deletions

View File

@ -1,6 +1,6 @@
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
import { defaultIcon, select, selects } from "@formkit/inputs";
import { coreApiClient } from "@halo-dev/api-client"; import { coreApiClient } from "@halo-dev/api-client";
import { select } from "./select";
function optionsHandler(node: FormKitNode) { function optionsHandler(node: FormKitNode) {
node.on("created", async () => { node.on("created", async () => {
@ -9,18 +9,20 @@ function optionsHandler(node: FormKitNode) {
sort: ["metadata.creationTimestamp,desc"], sort: ["metadata.creationTimestamp,desc"],
}); });
node.props.options = data.items.map((group) => { if (node.context) {
node.context.attrs.options = data.items.map((group) => {
return { return {
value: group.metadata.name, value: group.metadata.name,
label: group.spec.displayName, label: group.spec.displayName,
}; };
}); });
}
}); });
} }
export const attachmentGroupSelect: FormKitTypeDefinition = { export const attachmentGroupSelect: FormKitTypeDefinition = {
...select, ...select,
props: ["placeholder"], props: ["placeholder"],
forceTypeProp: "nativeSelect", forceTypeProp: "select",
features: [optionsHandler, selects, defaultIcon("select", "select")], features: [optionsHandler],
}; };

View File

@ -1,23 +1,25 @@
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
import { defaultIcon, select, selects } from "@formkit/inputs";
import { coreApiClient } from "@halo-dev/api-client"; import { coreApiClient } from "@halo-dev/api-client";
import { select } from "./select";
function optionsHandler(node: FormKitNode) { function optionsHandler(node: FormKitNode) {
node.on("created", async () => { node.on("created", async () => {
const { data } = await coreApiClient.storage.policy.listPolicy(); const { data } = await coreApiClient.storage.policy.listPolicy();
node.props.options = data.items.map((policy) => { if (node.context) {
node.context.attrs.options = data.items.map((policy) => {
return { return {
value: policy.metadata.name, value: policy.metadata.name,
label: policy.spec.displayName, label: policy.spec.displayName,
}; };
}); });
}
}); });
} }
export const attachmentPolicySelect: FormKitTypeDefinition = { export const attachmentPolicySelect: FormKitTypeDefinition = {
...select, ...select,
props: ["placeholder"], props: ["placeholder"],
forceTypeProp: "nativeSelect", forceTypeProp: "select",
features: [optionsHandler, selects, defaultIcon("select", "select")], features: [optionsHandler],
}; };

View File

@ -1,6 +1,6 @@
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
import { defaultIcon, select, selects } from "@formkit/inputs";
import { coreApiClient } from "@halo-dev/api-client"; import { coreApiClient } from "@halo-dev/api-client";
import { select } from "./select";
function optionsHandler(node: FormKitNode) { function optionsHandler(node: FormKitNode) {
node.on("created", async () => { node.on("created", async () => {
@ -8,18 +8,20 @@ function optionsHandler(node: FormKitNode) {
fieldSelector: [`name=(${node.props.menuItems.join(",")})`], fieldSelector: [`name=(${node.props.menuItems.join(",")})`],
}); });
node.props.options = data.items.map((menuItem) => { if (node.context) {
node.context.attrs.options = data.items.map((menuItem) => {
return { return {
value: menuItem.metadata.name, value: menuItem.metadata.name,
label: menuItem.status?.displayName, label: menuItem.status?.displayName,
}; };
}); });
}
}); });
} }
export const menuItemSelect: FormKitTypeDefinition = { export const menuItemSelect: FormKitTypeDefinition = {
...select, ...select,
props: ["placeholder", "menuItems"], props: ["placeholder", "menuItems"],
forceTypeProp: "nativeSelect", forceTypeProp: "select",
features: [optionsHandler, selects, defaultIcon("select", "select")], features: [optionsHandler],
}; };

View File

@ -1,29 +1,65 @@
import { postLabels } from "@/constants/labels"; import { postLabels } from "@/constants/labels";
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
import { defaultIcon, select, selects } from "@formkit/inputs";
import { consoleApiClient } from "@halo-dev/api-client"; import { consoleApiClient } from "@halo-dev/api-client";
import { select } from "./select";
function optionsHandler(node: FormKitNode) { async function search({ page, size, keyword }) {
node.on("created", async () => {
const { data } = await consoleApiClient.content.post.listPosts({ const { data } = await consoleApiClient.content.post.listPosts({
page,
size,
keyword,
labelSelector: [ labelSelector: [
`${postLabels.DELETED}=false`, `${postLabels.DELETED}=false`,
`${postLabels.PUBLISHED}=true`, `${postLabels.PUBLISHED}=true`,
], ],
}); });
node.props.options = data.items.map((post) => { return {
options: data.items.map((post) => {
return {
value: post.post.metadata.name,
label: post.post.spec.title,
};
}),
total: data.total,
size: data.size,
page: data.page,
};
}
async function findOptionsByValues(values: string[]) {
if (values.length === 0) {
return [];
}
const { data } = await consoleApiClient.content.post.listPosts({
fieldSelector: [`metadata.name=(${values.join(",")})`],
});
return data.items.map((post) => {
return { return {
value: post.post.metadata.name, value: post.post.metadata.name,
label: post.post.spec.title, label: post.post.spec.title,
}; };
}); });
}
function optionsHandler(node: FormKitNode) {
node.on("created", async () => {
node.props = {
...node.props,
remote: true,
remoteOption: {
search,
findOptionsByValues,
},
};
}); });
} }
export const postSelect: FormKitTypeDefinition = { export const postSelect: FormKitTypeDefinition = {
...select, ...select,
props: ["placeholder"], props: ["placeholder"],
forceTypeProp: "nativeSelect", forceTypeProp: "select",
features: [optionsHandler, selects, defaultIcon("select", "select")], features: [optionsHandler],
}; };

View File

@ -2,8 +2,8 @@ import { rbacAnnotations } from "@/constants/annotations";
import { roleLabels } from "@/constants/labels"; import { roleLabels } from "@/constants/labels";
import { i18n } from "@/locales"; import { i18n } from "@/locales";
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
import { defaultIcon, select, selects } from "@formkit/inputs";
import { coreApiClient } from "@halo-dev/api-client"; import { coreApiClient } from "@halo-dev/api-client";
import { select } from "./select";
function optionsHandler(node: FormKitNode) { function optionsHandler(node: FormKitNode) {
node.on("created", async () => { node.on("created", async () => {
@ -13,7 +13,7 @@ function optionsHandler(node: FormKitNode) {
labelSelector: [`!${roleLabels.TEMPLATE}`], labelSelector: [`!${roleLabels.TEMPLATE}`],
}); });
node.props.options = [ const options = [
{ {
label: i18n.global.t( label: i18n.global.t(
"core.user.grant_permission_modal.fields.role.placeholder" "core.user.grant_permission_modal.fields.role.placeholder"
@ -29,6 +29,9 @@ function optionsHandler(node: FormKitNode) {
}; };
}), }),
]; ];
if (node.context) {
node.context.attrs.options = options;
}
}); });
} }
@ -36,5 +39,5 @@ export const roleSelect: FormKitTypeDefinition = {
...select, ...select,
props: ["placeholder"], props: ["placeholder"],
forceTypeProp: "nativeSelect", forceTypeProp: "nativeSelect",
features: [optionsHandler, selects, defaultIcon("select", "select")], features: [optionsHandler],
}; };

View File

@ -13,6 +13,7 @@ import SelectContainer from "./SelectContainer.vue";
import { axiosInstance } from "@halo-dev/api-client"; import { axiosInstance } from "@halo-dev/api-client";
import { get, has, type PropertyPath } from "lodash-es"; import { get, has, type PropertyPath } from "lodash-es";
import { useDebounceFn } from "@vueuse/core"; import { useDebounceFn } from "@vueuse/core";
import { useFuse } from "@vueuse/integrations/useFuse";
import type { AxiosRequestConfig } from "axios"; import type { AxiosRequestConfig } from "axios";
export interface SelectProps { export interface SelectProps {
@ -184,7 +185,6 @@ const selectProps: SelectProps = shallowReactive({
placeholder: "", placeholder: "",
}); });
const hasSelected = ref(false);
const isRemote = computed(() => !!selectProps.action || !!selectProps.remote); const isRemote = computed(() => !!selectProps.action || !!selectProps.remote);
const hasMoreOptions = computed( const hasMoreOptions = computed(
() => options.value && options.value.length < total.value () => options.value && options.value.length < total.value
@ -400,7 +400,7 @@ const mapUnresolvedOptions = async (
value: string; value: string;
}> }>
> => { > => {
if (!selectProps.action || !selectProps.remote) { if (!isRemote.value) {
if (selectProps.allowCreate) { if (selectProps.allowCreate) {
// TODO: Add mapped values to options // TODO: Add mapped values to options
return unmappedSelectValues.map((value) => ({ label: value, value })); return unmappedSelectValues.map((value) => ({ label: value, value }));
@ -413,10 +413,17 @@ const mapUnresolvedOptions = async (
} }
// Asynchronous request for options, fetch label and value via API. // Asynchronous request for options, fetch label and value via API.
let mappedOptions: Array<{ let mappedOptions:
| Array<{
label: string; label: string;
value: string; value: string;
}> = []; }>
| undefined = undefined;
if (noNeedFetchOptions.value) {
mappedOptions = cacheAllOptions.value?.filter((option) =>
unmappedSelectValues.includes(option.value)
);
} else {
if (selectProps.action) { if (selectProps.action) {
mappedOptions = await fetchRemoteMappedOptions(unmappedSelectValues); mappedOptions = await fetchRemoteMappedOptions(unmappedSelectValues);
} else if (selectProps.remote) { } else if (selectProps.remote) {
@ -425,6 +432,11 @@ const mapUnresolvedOptions = async (
unmappedSelectValues unmappedSelectValues
); );
} }
}
if (!mappedOptions) {
return unmappedSelectValues.map((value) => ({ label: value, value }));
}
// Get values that are still unresolved. // Get values that are still unresolved.
const unmappedValues = unmappedSelectValues.filter( const unmappedValues = unmappedSelectValues.filter(
(value) => !mappedOptions.find((option) => option.value === value) (value) => !mappedOptions.find((option) => option.value === value)
@ -496,11 +508,12 @@ onMounted(async () => {
} }
}); });
watch( const stopSelectedWatch = watch(
() => [options.value, props.context.value], () => [options.value, props.context.value],
async () => { async () => {
if (!hasSelected.value && options.value) { if (options.value) {
selectOptions.value = await fetchSelectedOptions(); const selectedOption = await fetchSelectedOptions();
selectOptions.value = selectedOption;
} }
}, },
{ {
@ -521,7 +534,7 @@ watch(
const handleUpdate = (value: Array<{ label: string; value: string }>) => { const handleUpdate = (value: Array<{ label: string; value: string }>) => {
const values = value.map((item) => item.value); const values = value.map((item) => item.value);
hasSelected.value = true; stopSelectedWatch();
selectOptions.value = value; selectOptions.value = value;
if (selectProps.multiple) { if (selectProps.multiple) {
props.context.node.input(values); props.context.node.input(values);
@ -544,14 +557,23 @@ const fetchOptions = async (
} }
// If the total number of options is less than the page size, no more requests are made. // If the total number of options is less than the page size, no more requests are made.
if (noNeedFetchOptions.value) { if (noNeedFetchOptions.value) {
const filterOptions = cacheAllOptions.value?.filter((option) => const { results } = useFuse<{
option.label.includes(tempKeyword) label: string;
); value: string;
}>(tempKeyword, cacheAllOptions.value || [], {
fuseOptions: {
keys: ["label", "value"],
threshold: 0,
ignoreLocation: true,
},
matchAllWhenSearchEmpty: true,
});
const filterOptions = results.value?.map((fuseItem) => fuseItem.item) || [];
return { return {
options: filterOptions || [], options: filterOptions || [],
page: page.value, page: page.value,
size: size.value, size: size.value,
total: filterOptions?.length || 0, total: filterOptions.length || 0,
}; };
} }
isLoading.value = true; isLoading.value = true;

View File

@ -3,21 +3,57 @@ import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
import { defaultIcon, select, selects } from "@formkit/inputs"; import { defaultIcon, select, selects } from "@formkit/inputs";
import { consoleApiClient } from "@halo-dev/api-client"; import { consoleApiClient } from "@halo-dev/api-client";
function optionsHandler(node: FormKitNode) { async function search({ page, size, keyword }) {
node.on("created", async () => {
const { data } = await consoleApiClient.content.singlePage.listSinglePages({ const { data } = await consoleApiClient.content.singlePage.listSinglePages({
page,
size,
keyword,
labelSelector: [ labelSelector: [
`${singlePageLabels.DELETED}=false`, `${singlePageLabels.DELETED}=false`,
`${singlePageLabels.PUBLISHED}=true`, `${singlePageLabels.PUBLISHED}=true`,
], ],
}); });
node.props.options = data.items.map((singlePage) => { return {
options: data.items.map((singlePage) => {
return {
value: singlePage.page.metadata.name,
label: singlePage.page.spec.title,
};
}),
total: data.total,
size: data.size,
page: data.page,
};
}
async function findOptionsByValues(values: string[]) {
if (values.length === 0) {
return [];
}
const { data } = await consoleApiClient.content.singlePage.listSinglePages({
fieldSelector: [`metadata.name=(${values.join(",")})`],
});
return data.items.map((singlePage) => {
return { return {
value: singlePage.page.metadata.name, value: singlePage.page.metadata.name,
label: singlePage.page.spec.title, label: singlePage.page.spec.title,
}; };
}); });
}
function optionsHandler(node: FormKitNode) {
node.on("created", async () => {
node.props = {
...node.props,
remote: true,
remoteOption: {
search,
findOptionsByValues,
},
};
}); });
} }

View File

@ -2,25 +2,62 @@
// We will provide searchable user selection components in the future. // We will provide searchable user selection components in the future.
import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core";
import { defaultIcon, select, selects } from "@formkit/inputs"; import { consoleApiClient } from "@halo-dev/api-client";
import { coreApiClient } from "@halo-dev/api-client"; import { select } from "./select";
const search = async ({ page, size, keyword }) => {
const { data } = await consoleApiClient.user.listUsers({
page,
size,
keyword,
});
return {
options: data.items?.map((user) => {
return {
value: user.user.metadata.name,
label: user.user.spec.displayName,
};
}),
total: data.total,
size: data.size,
page: data.page,
};
};
const findOptionsByValues = async (values: string[]) => {
if (values.length === 0) {
return [];
}
const { data } = await consoleApiClient.user.listUsers({
fieldSelector: [`metadata.name=(${values.join(",")})`],
});
return data.items?.map((user) => {
return {
value: user.user.metadata.name,
label: user.user.spec.displayName,
};
});
};
function optionsHandler(node: FormKitNode) { function optionsHandler(node: FormKitNode) {
node.on("created", async () => { node.on("created", async () => {
const { data } = await coreApiClient.user.listUser(); node.props = {
...node.props,
node.props.options = data.items.map((user) => { remote: true,
return { remoteOption: {
value: user.metadata.name, search,
label: user.spec.displayName, findOptionsByValues,
},
searchable: true,
}; };
}); });
});
} }
export const userSelect: FormKitTypeDefinition = { export const userSelect: FormKitTypeDefinition = {
...select, ...select,
props: ["placeholder"], props: ["placeholder"],
forceTypeProp: "nativeSelect", forceTypeProp: "select",
features: [optionsHandler, selects, defaultIcon("select", "select")], features: [optionsHandler],
}; };