From 19049c13025e1148678dd2f38e038d395a443dde Mon Sep 17 00:00:00 2001 From: Takagi <1103069291@qq.com> Date: Thu, 29 Aug 2024 12:15:24 +0800 Subject: [PATCH] feat: add default selection option to formkit select component (#6538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /area ui /milestone 2.19.x #### What this PR does / why we need it: 为 formkit select 增加属性 `autoSelect`。 这个属性仅会作用于单选,且目的是为了初始化数据所用。当 value 或者 placeholder 存在时,此属性无效。 会查找第一个 option 不为 disabled 的数据。 #### Does this PR introduce a user-facing change? ```release-note None ``` --- ui/docs/custom-formkit-input/README.md | 25 ++++--- .../formkit/inputs/attachment-group-select.ts | 1 - .../inputs/attachment-policy-select.ts | 1 - ui/src/formkit/inputs/menu-item-select.ts | 2 +- ui/src/formkit/inputs/post-select.ts | 2 +- ui/src/formkit/inputs/role-select.ts | 3 +- .../formkit/inputs/select/SelectContainer.vue | 5 ++ ui/src/formkit/inputs/select/SelectMain.vue | 75 +++++++++++++++---- ui/src/formkit/inputs/select/index.ts | 1 + ui/src/formkit/inputs/singlePage-select.ts | 8 +- ui/src/formkit/inputs/user-select.ts | 1 - 11 files changed, 87 insertions(+), 37 deletions(-) diff --git a/ui/docs/custom-formkit-input/README.md b/ui/docs/custom-formkit-input/README.md index 3cd9a0a89..15b40e508 100644 --- a/ui/docs/custom-formkit-input/README.md +++ b/ui/docs/custom-formkit-input/README.md @@ -71,6 +71,7 @@ 10. `maxCount`:多选时最大可选数量,默认为 `Infinity`。仅在多选时有效。 11. `sortable`:是否支持拖动排序,默认为 `false`。仅在多选时有效。 12. `searchable`:是否支持搜索内容,默认为 `false`。 + 13. `autoSelect`:当 value 不存在时,是否自动选择第一个选项,默认为 `true`。仅在单选时有效。 在 Vue 单组件中使用: @@ -120,16 +121,16 @@ select 是一个选择器类型的输入组件,使用者可以从一批待选 multiple searchable :options="[ - { label: "China", value: "China" }, - { label: "USA", value: "USA" }, - { label: "Japan", value: "Japan" }, - { label: "Korea", value: "Korea" }, - { label: "France", value: "France" }, - { label: "Italy", value: "Italy" }, - { label: "Germany", value: "Germany" }, - { label: "UK", value: "UK" }, - { label: "Canada", value: "Canada" }, - { label: "Australia", value: "Australia" }, + { label: 'China', value: 'China' }, + { label: 'USA', value: 'USA' }, + { label: 'Japan', value: 'Japan' }, + { label: 'Korea', value: 'Korea' }, + { label: 'France', value: 'France' }, + { label: 'Italy', value: 'Italy' }, + { label: 'Germany', value: 'Germany' }, + { label: 'UK', value: 'UK' }, + { label: 'Canada', value: 'Canada' }, + { label: 'Australia', value: 'Australia' }, ]" help="Don’t worry, you can’t get this one wrong." /> @@ -196,6 +197,8 @@ const handleSelectPostAuthorRemote = { clearable: true placeholder: Select a country options: + - label: China + value: cn - label: France value: fr - label: Germany @@ -231,7 +234,7 @@ const handleSelectPostAuthorRemote = { ``` > [!NOTE] -> 当远程数据具有分页时,可能会出现默认选项不在第一页的情况,此时 Select 组件将会发送另一个查询请求,以获取默认选项的数据。此接口会携带如下参数 `labelSelector: ${requestOption.valueField}=in(value1,value2,value3)`。 +> 当远程数据具有分页时,可能会出现默认选项不在第一页的情况,此时 Select 组件将会发送另一个查询请求,以获取默认选项的数据。此接口会携带如下参数 `fieldSelector: ${requestOption.valueField}=(value1,value2,value3)`。 > 其中,value1, value2, value3 为默认选项的值。返回值与查询一致,通过 `requestOption` 解析。 diff --git a/ui/src/formkit/inputs/attachment-group-select.ts b/ui/src/formkit/inputs/attachment-group-select.ts index a450c7dfe..c5ac64719 100644 --- a/ui/src/formkit/inputs/attachment-group-select.ts +++ b/ui/src/formkit/inputs/attachment-group-select.ts @@ -22,7 +22,6 @@ function optionsHandler(node: FormKitNode) { export const attachmentGroupSelect: FormKitTypeDefinition = { ...select, - props: ["placeholder"], 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 aea910b22..b36caa952 100644 --- a/ui/src/formkit/inputs/attachment-policy-select.ts +++ b/ui/src/formkit/inputs/attachment-policy-select.ts @@ -19,7 +19,6 @@ function optionsHandler(node: FormKitNode) { export const attachmentPolicySelect: FormKitTypeDefinition = { ...select, - props: ["placeholder"], 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 c1a882171..4d355e47a 100644 --- a/ui/src/formkit/inputs/menu-item-select.ts +++ b/ui/src/formkit/inputs/menu-item-select.ts @@ -21,7 +21,7 @@ function optionsHandler(node: FormKitNode) { export const menuItemSelect: FormKitTypeDefinition = { ...select, - props: ["placeholder", "menuItems"], + props: ["menuItems", ...(select.props as string[])], forceTypeProp: "select", features: [optionsHandler], }; diff --git a/ui/src/formkit/inputs/post-select.ts b/ui/src/formkit/inputs/post-select.ts index 9a304c651..31be63400 100644 --- a/ui/src/formkit/inputs/post-select.ts +++ b/ui/src/formkit/inputs/post-select.ts @@ -53,13 +53,13 @@ function optionsHandler(node: FormKitNode) { search, findOptionsByValues, }, + searchable: true, }; }); } export const postSelect: FormKitTypeDefinition = { ...select, - props: ["placeholder"], forceTypeProp: "select", features: [optionsHandler], }; diff --git a/ui/src/formkit/inputs/role-select.ts b/ui/src/formkit/inputs/role-select.ts index 8546fe769..ffceebcd7 100644 --- a/ui/src/formkit/inputs/role-select.ts +++ b/ui/src/formkit/inputs/role-select.ts @@ -37,7 +37,6 @@ function optionsHandler(node: FormKitNode) { export const roleSelect: FormKitTypeDefinition = { ...select, - props: ["placeholder"], - forceTypeProp: "nativeSelect", + forceTypeProp: "select", features: [optionsHandler], }; diff --git a/ui/src/formkit/inputs/select/SelectContainer.vue b/ui/src/formkit/inputs/select/SelectContainer.vue index 3ca131dfe..7c6679b20 100644 --- a/ui/src/formkit/inputs/select/SelectContainer.vue +++ b/ui/src/formkit/inputs/select/SelectContainer.vue @@ -78,6 +78,10 @@ const props = defineProps({ type: Boolean, default: false, }, + autoSelect: { + type: Boolean, + default: true, + }, }); const emit = defineEmits<{ @@ -221,6 +225,7 @@ const clearAllSelectedOptions = () => { if (!hasClearable.value) { return; } + selectedOptions.value = []; clearInputValue(); }; diff --git a/ui/src/formkit/inputs/select/SelectMain.vue b/ui/src/formkit/inputs/select/SelectMain.vue index d204fe3f5..15cb446e9 100644 --- a/ui/src/formkit/inputs/select/SelectMain.vue +++ b/ui/src/formkit/inputs/select/SelectMain.vue @@ -77,6 +77,13 @@ export interface SelectProps { * Whether to enable search, default is false. */ searchable?: boolean; + + /** + * Whether to automatically select the first option. default is true. + * + * Only valid when `multiple` is false. + */ + autoSelect?: boolean; } export interface SelectResponse { @@ -214,6 +221,7 @@ const initSelectProps = () => { selectProps.allowCreate = !isFalse(nodeProps.allowCreate); selectProps.clearable = !isFalse(nodeProps.clearable); selectProps.searchable = !isFalse(nodeProps.searchable); + selectProps.autoSelect = !isFalse(nodeProps.autoSelect) || true; if (selectProps.remote) { if (!nodeProps.remoteOption) { throw new Error("remoteOption is required when remote is true."); @@ -341,37 +349,42 @@ const fetchSelectedOptions = async (): Promise< > => { const node = props.context.node; const value = node.value; - if (!value) { - return undefined; - } - const selectedValues: string[] = []; + const selectedValues: Array = []; if (Array.isArray(value)) { selectedValues.push(...value); } else if ( typeof value === "string" || typeof value === "number" || - typeof value === "boolean" + typeof value === "boolean" || + value === void 0 ) { - selectedValues.push(value.toString()); + selectedValues.push(value); } const currentOptions = options.value?.filter((option) => - selectedValues.includes(option.value.toString()) + selectedValues.includes(option.value) ); // Get options that are not yet mapped. - const unmappedSelectValues = selectedValues.filter( - (value) => !currentOptions?.find((option) => option.value === value) - ); + const unmappedSelectValues = selectedValues + .filter( + (value) => !currentOptions?.find((option) => option.value === value) + ) + .filter(Boolean); if (unmappedSelectValues.length === 0) { + if (!currentOptions || currentOptions.length === 0) { + return; + } return currentOptions?.sort((a, b) => selectedValues.indexOf(a.value) > selectedValues.indexOf(b.value) ? 1 : -1 ); } // Map the unresolved options to label and value format. - const mappedSelectOptions = await mapUnresolvedOptions(unmappedSelectValues); + const mappedSelectOptions = await mapUnresolvedOptions( + unmappedSelectValues.map(String) + ); // Merge currentOptions and mappedSelectOptions, then sort them according to selectValues order. return [...(currentOptions || []), ...mappedSelectOptions].sort((a, b) => selectedValues.indexOf(a.value) > selectedValues.indexOf(b.value) ? 1 : -1 @@ -508,16 +521,47 @@ onMounted(async () => { } }); +const getAutoSelectedOption = (): + | { + label: string; + value: string; + } + | undefined => { + if (!options.value || options.value.length === 0) { + return; + } + + // Find the first option that is not disabled. + return options.value.find((option) => { + const attrs = option.attrs as Record; + return isFalse(attrs?.disabled as string | boolean | undefined); + }); +}; + const stopSelectedWatch = watch( () => [options.value, props.context.value], async () => { if (options.value) { const selectedOption = await fetchSelectedOptions(); - selectOptions.value = selectedOption; + if (selectedOption) { + selectOptions.value = selectedOption; + return; + } + const isAutoSelect = + selectProps.autoSelect && + !selectProps.multiple && + !selectProps.placeholder && + !props.context.node.value; + + if (isAutoSelect) { + // Automatically select the first option when the selected value is empty. + const autoSelectedOption = getAutoSelectedOption(); + if (autoSelectedOption) { + selectOptions.value = [autoSelectedOption]; + handleUpdate(selectOptions.value); + } + } } - }, - { - immediate: true, } ); @@ -669,6 +713,7 @@ const handleNextPage = async () => { :remote="isRemote" :clearable="selectProps.clearable" :searchable="selectProps.searchable" + :auto-select="selectProps.autoSelect" @update="handleUpdate" @search="handleSearch" @load-more="handleNextPage" diff --git a/ui/src/formkit/inputs/select/index.ts b/ui/src/formkit/inputs/select/index.ts index 3b2f7553d..8f611ff63 100644 --- a/ui/src/formkit/inputs/select/index.ts +++ b/ui/src/formkit/inputs/select/index.ts @@ -52,6 +52,7 @@ export const select: FormKitTypeDefinition = { "allowCreate", "remoteOptimize", "searchable", + "autoSelect", ], library: { diff --git a/ui/src/formkit/inputs/singlePage-select.ts b/ui/src/formkit/inputs/singlePage-select.ts index 72a23775c..1b122a2b9 100644 --- a/ui/src/formkit/inputs/singlePage-select.ts +++ b/ui/src/formkit/inputs/singlePage-select.ts @@ -1,7 +1,7 @@ import { singlePageLabels } 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"; async function search({ page, size, keyword }) { const { data } = await consoleApiClient.content.singlePage.listSinglePages({ @@ -53,13 +53,13 @@ function optionsHandler(node: FormKitNode) { search, findOptionsByValues, }, + searchable: true, }; }); } export const singlePageSelect: FormKitTypeDefinition = { ...select, - props: ["placeholder"], - forceTypeProp: "nativeSelect", - features: [optionsHandler, selects, defaultIcon("select", "select")], + forceTypeProp: "select", + features: [optionsHandler], }; diff --git a/ui/src/formkit/inputs/user-select.ts b/ui/src/formkit/inputs/user-select.ts index 58e5bac1d..3cc7d2141 100644 --- a/ui/src/formkit/inputs/user-select.ts +++ b/ui/src/formkit/inputs/user-select.ts @@ -57,7 +57,6 @@ function optionsHandler(node: FormKitNode) { export const userSelect: FormKitTypeDefinition = { ...select, - props: ["placeholder"], forceTypeProp: "select", features: [optionsHandler], };