From 84a0e9cc767404d0435a201f611e73ecb6171c22 Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Mon, 26 Aug 2024 18:34:19 +0800 Subject: [PATCH 1/2] pref: implement business selector using new selector component --- .../formkit/inputs/attachment-group-select.ts | 20 +++--- .../inputs/attachment-policy-select.ts | 20 +++--- ui/src/formkit/inputs/menu-item-select.ts | 20 +++--- ui/src/formkit/inputs/post-select.ts | 62 +++++++++++++++---- ui/src/formkit/inputs/role-select.ts | 9 ++- ui/src/formkit/inputs/select/SelectMain.vue | 16 +++-- ui/src/formkit/inputs/singlePage-select.ts | 56 ++++++++++++++--- ui/src/formkit/inputs/user-select.ts | 61 ++++++++++++++---- 8 files changed, 194 insertions(+), 70 deletions(-) diff --git a/ui/src/formkit/inputs/attachment-group-select.ts b/ui/src/formkit/inputs/attachment-group-select.ts index 59c5d29e24..a450c7dfea 100644 --- a/ui/src/formkit/inputs/attachment-group-select.ts +++ b/ui/src/formkit/inputs/attachment-group-select.ts @@ -1,6 +1,6 @@ import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; -import { defaultIcon, select, selects } from "@formkit/inputs"; import { coreApiClient } from "@halo-dev/api-client"; +import { select } from "./select"; function optionsHandler(node: FormKitNode) { node.on("created", async () => { @@ -9,18 +9,20 @@ function optionsHandler(node: FormKitNode) { sort: ["metadata.creationTimestamp,desc"], }); - node.props.options = data.items.map((group) => { - return { - value: group.metadata.name, - label: group.spec.displayName, - }; - }); + if (node.context) { + node.context.attrs.options = data.items.map((group) => { + return { + value: group.metadata.name, + label: group.spec.displayName, + }; + }); + } }); } export const attachmentGroupSelect: FormKitTypeDefinition = { ...select, props: ["placeholder"], - forceTypeProp: "nativeSelect", - features: [optionsHandler, selects, defaultIcon("select", "select")], + forceTypeProp: "select", + features: [optionsHandler], }; diff --git a/ui/src/formkit/inputs/attachment-policy-select.ts b/ui/src/formkit/inputs/attachment-policy-select.ts index fb85f3411b..aea910b22e 100644 --- a/ui/src/formkit/inputs/attachment-policy-select.ts +++ b/ui/src/formkit/inputs/attachment-policy-select.ts @@ -1,23 +1,25 @@ import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; -import { defaultIcon, select, selects } from "@formkit/inputs"; import { coreApiClient } from "@halo-dev/api-client"; +import { select } from "./select"; function optionsHandler(node: FormKitNode) { node.on("created", async () => { const { data } = await coreApiClient.storage.policy.listPolicy(); - node.props.options = data.items.map((policy) => { - return { - value: policy.metadata.name, - label: policy.spec.displayName, - }; - }); + if (node.context) { + node.context.attrs.options = data.items.map((policy) => { + return { + value: policy.metadata.name, + label: policy.spec.displayName, + }; + }); + } }); } export const attachmentPolicySelect: FormKitTypeDefinition = { ...select, props: ["placeholder"], - forceTypeProp: "nativeSelect", - features: [optionsHandler, selects, defaultIcon("select", "select")], + forceTypeProp: "select", + features: [optionsHandler], }; diff --git a/ui/src/formkit/inputs/menu-item-select.ts b/ui/src/formkit/inputs/menu-item-select.ts index 1f089f271e..c1a882171b 100644 --- a/ui/src/formkit/inputs/menu-item-select.ts +++ b/ui/src/formkit/inputs/menu-item-select.ts @@ -1,6 +1,6 @@ import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; -import { defaultIcon, select, selects } from "@formkit/inputs"; import { coreApiClient } from "@halo-dev/api-client"; +import { select } from "./select"; function optionsHandler(node: FormKitNode) { node.on("created", async () => { @@ -8,18 +8,20 @@ function optionsHandler(node: FormKitNode) { fieldSelector: [`name=(${node.props.menuItems.join(",")})`], }); - node.props.options = data.items.map((menuItem) => { - return { - value: menuItem.metadata.name, - label: menuItem.status?.displayName, - }; - }); + if (node.context) { + node.context.attrs.options = data.items.map((menuItem) => { + return { + value: menuItem.metadata.name, + label: menuItem.status?.displayName, + }; + }); + } }); } export const menuItemSelect: FormKitTypeDefinition = { ...select, props: ["placeholder", "menuItems"], - forceTypeProp: "nativeSelect", - features: [optionsHandler, selects, defaultIcon("select", "select")], + forceTypeProp: "select", + features: [optionsHandler], }; diff --git a/ui/src/formkit/inputs/post-select.ts b/ui/src/formkit/inputs/post-select.ts index 65ca8d77e4..9a304c651e 100644 --- a/ui/src/formkit/inputs/post-select.ts +++ b/ui/src/formkit/inputs/post-select.ts @@ -1,29 +1,65 @@ import { postLabels } from "@/constants/labels"; import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; -import { defaultIcon, select, selects } from "@formkit/inputs"; import { consoleApiClient } from "@halo-dev/api-client"; +import { select } from "./select"; -function optionsHandler(node: FormKitNode) { - node.on("created", async () => { - const { data } = await consoleApiClient.content.post.listPosts({ - labelSelector: [ - `${postLabels.DELETED}=false`, - `${postLabels.PUBLISHED}=true`, - ], - }); +async function search({ page, size, keyword }) { + const { data } = await consoleApiClient.content.post.listPosts({ + page, + size, + keyword, + labelSelector: [ + `${postLabels.DELETED}=false`, + `${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 { + value: post.post.metadata.name, + 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 = { ...select, props: ["placeholder"], - forceTypeProp: "nativeSelect", - features: [optionsHandler, selects, defaultIcon("select", "select")], + forceTypeProp: "select", + features: [optionsHandler], }; diff --git a/ui/src/formkit/inputs/role-select.ts b/ui/src/formkit/inputs/role-select.ts index 717a80e26c..8546fe7691 100644 --- a/ui/src/formkit/inputs/role-select.ts +++ b/ui/src/formkit/inputs/role-select.ts @@ -2,8 +2,8 @@ import { rbacAnnotations } from "@/constants/annotations"; import { roleLabels } from "@/constants/labels"; import { i18n } from "@/locales"; import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; -import { defaultIcon, select, selects } from "@formkit/inputs"; import { coreApiClient } from "@halo-dev/api-client"; +import { select } from "./select"; function optionsHandler(node: FormKitNode) { node.on("created", async () => { @@ -13,7 +13,7 @@ function optionsHandler(node: FormKitNode) { labelSelector: [`!${roleLabels.TEMPLATE}`], }); - node.props.options = [ + const options = [ { label: i18n.global.t( "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, props: ["placeholder"], forceTypeProp: "nativeSelect", - features: [optionsHandler, selects, defaultIcon("select", "select")], + features: [optionsHandler], }; diff --git a/ui/src/formkit/inputs/select/SelectMain.vue b/ui/src/formkit/inputs/select/SelectMain.vue index 99948f8f22..ca56276007 100644 --- a/ui/src/formkit/inputs/select/SelectMain.vue +++ b/ui/src/formkit/inputs/select/SelectMain.vue @@ -400,7 +400,7 @@ const mapUnresolvedOptions = async ( value: string; }> > => { - if (!selectProps.action || !selectProps.remote) { + if (!isRemote.value) { if (selectProps.allowCreate) { // TODO: Add mapped values to options return unmappedSelectValues.map((value) => ({ label: value, value })); @@ -413,10 +413,12 @@ const mapUnresolvedOptions = async ( } // Asynchronous request for options, fetch label and value via API. - let mappedOptions: Array<{ - label: string; - value: string; - }> = []; + let mappedOptions: + | Array<{ + label: string; + value: string; + }> + | undefined = undefined; if (selectProps.action) { mappedOptions = await fetchRemoteMappedOptions(unmappedSelectValues); } else if (selectProps.remote) { @@ -425,6 +427,10 @@ const mapUnresolvedOptions = async ( unmappedSelectValues ); } + + if (!mappedOptions) { + return unmappedSelectValues.map((value) => ({ label: value, value })); + } // Get values that are still unresolved. const unmappedValues = unmappedSelectValues.filter( (value) => !mappedOptions.find((option) => option.value === value) diff --git a/ui/src/formkit/inputs/singlePage-select.ts b/ui/src/formkit/inputs/singlePage-select.ts index 2073589ef9..72a23775c9 100644 --- a/ui/src/formkit/inputs/singlePage-select.ts +++ b/ui/src/formkit/inputs/singlePage-select.ts @@ -3,21 +3,57 @@ import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; import { defaultIcon, select, selects } from "@formkit/inputs"; import { consoleApiClient } from "@halo-dev/api-client"; -function optionsHandler(node: FormKitNode) { - node.on("created", async () => { - const { data } = await consoleApiClient.content.singlePage.listSinglePages({ - labelSelector: [ - `${singlePageLabels.DELETED}=false`, - `${singlePageLabels.PUBLISHED}=true`, - ], - }); +async function search({ page, size, keyword }) { + const { data } = await consoleApiClient.content.singlePage.listSinglePages({ + page, + size, + keyword, + labelSelector: [ + `${singlePageLabels.DELETED}=false`, + `${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 { + value: singlePage.page.metadata.name, + label: singlePage.page.spec.title, + }; + }); +} + +function optionsHandler(node: FormKitNode) { + node.on("created", async () => { + node.props = { + ...node.props, + remote: true, + remoteOption: { + search, + findOptionsByValues, + }, + }; }); } diff --git a/ui/src/formkit/inputs/user-select.ts b/ui/src/formkit/inputs/user-select.ts index 83a2d19b84..58e5bac1de 100644 --- a/ui/src/formkit/inputs/user-select.ts +++ b/ui/src/formkit/inputs/user-select.ts @@ -2,25 +2,62 @@ // We will provide searchable user selection components in the future. import type { FormKitNode, FormKitTypeDefinition } from "@formkit/core"; -import { defaultIcon, select, selects } from "@formkit/inputs"; -import { coreApiClient } from "@halo-dev/api-client"; +import { consoleApiClient } from "@halo-dev/api-client"; +import { select } from "./select"; -function optionsHandler(node: FormKitNode) { - node.on("created", async () => { - const { data } = await coreApiClient.user.listUser(); - - node.props.options = data.items.map((user) => { +const search = async ({ page, size, keyword }) => { + const { data } = await consoleApiClient.user.listUsers({ + page, + size, + keyword, + }); + return { + options: data.items?.map((user) => { return { - value: user.metadata.name, - label: user.spec.displayName, + 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) { + node.on("created", async () => { + node.props = { + ...node.props, + remote: true, + remoteOption: { + search, + findOptionsByValues, + }, + searchable: true, + }; }); } export const userSelect: FormKitTypeDefinition = { ...select, props: ["placeholder"], - forceTypeProp: "nativeSelect", - features: [optionsHandler, selects, defaultIcon("select", "select")], + forceTypeProp: "select", + features: [optionsHandler], }; From b1cc75a7bab50a3b1f91e873104efb6eea35c313 Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Tue, 27 Aug 2024 15:07:02 +0800 Subject: [PATCH 2/2] optimize content matching issues in remote search --- ui/src/formkit/inputs/select/SelectMain.vue | 46 ++++++++++++++------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/ui/src/formkit/inputs/select/SelectMain.vue b/ui/src/formkit/inputs/select/SelectMain.vue index ca56276007..dd07170cfa 100644 --- a/ui/src/formkit/inputs/select/SelectMain.vue +++ b/ui/src/formkit/inputs/select/SelectMain.vue @@ -13,6 +13,7 @@ import SelectContainer from "./SelectContainer.vue"; import { axiosInstance } from "@halo-dev/api-client"; import { get, has, type PropertyPath } from "lodash-es"; import { useDebounceFn } from "@vueuse/core"; +import { useFuse } from "@vueuse/integrations/useFuse"; import type { AxiosRequestConfig } from "axios"; export interface SelectProps { @@ -184,7 +185,6 @@ const selectProps: SelectProps = shallowReactive({ placeholder: "", }); -const hasSelected = ref(false); const isRemote = computed(() => !!selectProps.action || !!selectProps.remote); const hasMoreOptions = computed( () => options.value && options.value.length < total.value @@ -419,13 +419,19 @@ const mapUnresolvedOptions = async ( value: string; }> | undefined = undefined; - if (selectProps.action) { - mappedOptions = await fetchRemoteMappedOptions(unmappedSelectValues); - } else if (selectProps.remote) { - const remoteOption = selectProps.remoteOption as SelectRemoteOption; - mappedOptions = await remoteOption.findOptionsByValues( - unmappedSelectValues + if (noNeedFetchOptions.value) { + mappedOptions = cacheAllOptions.value?.filter((option) => + unmappedSelectValues.includes(option.value) ); + } else { + if (selectProps.action) { + mappedOptions = await fetchRemoteMappedOptions(unmappedSelectValues); + } else if (selectProps.remote) { + const remoteOption = selectProps.remoteOption as SelectRemoteOption; + mappedOptions = await remoteOption.findOptionsByValues( + unmappedSelectValues + ); + } } if (!mappedOptions) { @@ -502,11 +508,12 @@ onMounted(async () => { } }); -watch( +const stopSelectedWatch = watch( () => [options.value, props.context.value], async () => { - if (!hasSelected.value && options.value) { - selectOptions.value = await fetchSelectedOptions(); + if (options.value) { + const selectedOption = await fetchSelectedOptions(); + selectOptions.value = selectedOption; } }, { @@ -527,7 +534,7 @@ watch( const handleUpdate = (value: Array<{ label: string; value: string }>) => { const values = value.map((item) => item.value); - hasSelected.value = true; + stopSelectedWatch(); selectOptions.value = value; if (selectProps.multiple) { props.context.node.input(values); @@ -550,14 +557,23 @@ const fetchOptions = async ( } // If the total number of options is less than the page size, no more requests are made. if (noNeedFetchOptions.value) { - const filterOptions = cacheAllOptions.value?.filter((option) => - option.label.includes(tempKeyword) - ); + const { results } = useFuse<{ + 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 { options: filterOptions || [], page: page.value, size: size.value, - total: filterOptions?.length || 0, + total: filterOptions.length || 0, }; } isLoading.value = true;