mirror of https://github.com/halo-dev/halo
feat: add default selection option to formkit select component (#6538)
#### 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 ```pull/6548/head
parent
ad267ebed7
commit
19049c1302
|
@ -71,6 +71,7 @@
|
||||||
10. `maxCount`:多选时最大可选数量,默认为 `Infinity`。仅在多选时有效。
|
10. `maxCount`:多选时最大可选数量,默认为 `Infinity`。仅在多选时有效。
|
||||||
11. `sortable`:是否支持拖动排序,默认为 `false`。仅在多选时有效。
|
11. `sortable`:是否支持拖动排序,默认为 `false`。仅在多选时有效。
|
||||||
12. `searchable`:是否支持搜索内容,默认为 `false`。
|
12. `searchable`:是否支持搜索内容,默认为 `false`。
|
||||||
|
13. `autoSelect`:当 value 不存在时,是否自动选择第一个选项,默认为 `true`。仅在单选时有效。
|
||||||
|
|
||||||
在 Vue 单组件中使用:
|
在 Vue 单组件中使用:
|
||||||
|
|
||||||
|
@ -120,16 +121,16 @@ select 是一个选择器类型的输入组件,使用者可以从一批待选
|
||||||
multiple
|
multiple
|
||||||
searchable
|
searchable
|
||||||
:options="[
|
:options="[
|
||||||
{ label: "China", value: "China" },
|
{ label: 'China', value: 'China' },
|
||||||
{ label: "USA", value: "USA" },
|
{ label: 'USA', value: 'USA' },
|
||||||
{ label: "Japan", value: "Japan" },
|
{ label: 'Japan', value: 'Japan' },
|
||||||
{ label: "Korea", value: "Korea" },
|
{ label: 'Korea', value: 'Korea' },
|
||||||
{ label: "France", value: "France" },
|
{ label: 'France', value: 'France' },
|
||||||
{ label: "Italy", value: "Italy" },
|
{ label: 'Italy', value: 'Italy' },
|
||||||
{ label: "Germany", value: "Germany" },
|
{ label: 'Germany', value: 'Germany' },
|
||||||
{ label: "UK", value: "UK" },
|
{ label: 'UK', value: 'UK' },
|
||||||
{ label: "Canada", value: "Canada" },
|
{ label: 'Canada', value: 'Canada' },
|
||||||
{ label: "Australia", value: "Australia" },
|
{ label: 'Australia', value: 'Australia' },
|
||||||
]"
|
]"
|
||||||
help="Don’t worry, you can’t get this one wrong."
|
help="Don’t worry, you can’t get this one wrong."
|
||||||
/>
|
/>
|
||||||
|
@ -196,6 +197,8 @@ const handleSelectPostAuthorRemote = {
|
||||||
clearable: true
|
clearable: true
|
||||||
placeholder: Select a country
|
placeholder: Select a country
|
||||||
options:
|
options:
|
||||||
|
- label: China
|
||||||
|
value: cn
|
||||||
- label: France
|
- label: France
|
||||||
value: fr
|
value: fr
|
||||||
- label: Germany
|
- label: Germany
|
||||||
|
@ -231,7 +234,7 @@ const handleSelectPostAuthorRemote = {
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 当远程数据具有分页时,可能会出现默认选项不在第一页的情况,此时 Select 组件将会发送另一个查询请求,以获取默认选项的数据。此接口会携带如下参数 `labelSelector: ${requestOption.valueField}=in(value1,value2,value3)`。
|
> 当远程数据具有分页时,可能会出现默认选项不在第一页的情况,此时 Select 组件将会发送另一个查询请求,以获取默认选项的数据。此接口会携带如下参数 `fieldSelector: ${requestOption.valueField}=(value1,value2,value3)`。
|
||||||
|
|
||||||
> 其中,value1, value2, value3 为默认选项的值。返回值与查询一致,通过 `requestOption` 解析。
|
> 其中,value1, value2, value3 为默认选项的值。返回值与查询一致,通过 `requestOption` 解析。
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ function optionsHandler(node: FormKitNode) {
|
||||||
|
|
||||||
export const attachmentGroupSelect: FormKitTypeDefinition = {
|
export const attachmentGroupSelect: FormKitTypeDefinition = {
|
||||||
...select,
|
...select,
|
||||||
props: ["placeholder"],
|
|
||||||
forceTypeProp: "select",
|
forceTypeProp: "select",
|
||||||
features: [optionsHandler],
|
features: [optionsHandler],
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,6 @@ function optionsHandler(node: FormKitNode) {
|
||||||
|
|
||||||
export const attachmentPolicySelect: FormKitTypeDefinition = {
|
export const attachmentPolicySelect: FormKitTypeDefinition = {
|
||||||
...select,
|
...select,
|
||||||
props: ["placeholder"],
|
|
||||||
forceTypeProp: "select",
|
forceTypeProp: "select",
|
||||||
features: [optionsHandler],
|
features: [optionsHandler],
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,7 +21,7 @@ function optionsHandler(node: FormKitNode) {
|
||||||
|
|
||||||
export const menuItemSelect: FormKitTypeDefinition = {
|
export const menuItemSelect: FormKitTypeDefinition = {
|
||||||
...select,
|
...select,
|
||||||
props: ["placeholder", "menuItems"],
|
props: ["menuItems", ...(select.props as string[])],
|
||||||
forceTypeProp: "select",
|
forceTypeProp: "select",
|
||||||
features: [optionsHandler],
|
features: [optionsHandler],
|
||||||
};
|
};
|
||||||
|
|
|
@ -53,13 +53,13 @@ function optionsHandler(node: FormKitNode) {
|
||||||
search,
|
search,
|
||||||
findOptionsByValues,
|
findOptionsByValues,
|
||||||
},
|
},
|
||||||
|
searchable: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const postSelect: FormKitTypeDefinition = {
|
export const postSelect: FormKitTypeDefinition = {
|
||||||
...select,
|
...select,
|
||||||
props: ["placeholder"],
|
|
||||||
forceTypeProp: "select",
|
forceTypeProp: "select",
|
||||||
features: [optionsHandler],
|
features: [optionsHandler],
|
||||||
};
|
};
|
||||||
|
|
|
@ -37,7 +37,6 @@ function optionsHandler(node: FormKitNode) {
|
||||||
|
|
||||||
export const roleSelect: FormKitTypeDefinition = {
|
export const roleSelect: FormKitTypeDefinition = {
|
||||||
...select,
|
...select,
|
||||||
props: ["placeholder"],
|
forceTypeProp: "select",
|
||||||
forceTypeProp: "nativeSelect",
|
|
||||||
features: [optionsHandler],
|
features: [optionsHandler],
|
||||||
};
|
};
|
||||||
|
|
|
@ -78,6 +78,10 @@ const props = defineProps({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
autoSelect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -221,6 +225,7 @@ const clearAllSelectedOptions = () => {
|
||||||
if (!hasClearable.value) {
|
if (!hasClearable.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedOptions.value = [];
|
selectedOptions.value = [];
|
||||||
clearInputValue();
|
clearInputValue();
|
||||||
};
|
};
|
||||||
|
|
|
@ -77,6 +77,13 @@ export interface SelectProps {
|
||||||
* Whether to enable search, default is false.
|
* Whether to enable search, default is false.
|
||||||
*/
|
*/
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to automatically select the first option. default is true.
|
||||||
|
*
|
||||||
|
* Only valid when `multiple` is false.
|
||||||
|
*/
|
||||||
|
autoSelect?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectResponse {
|
export interface SelectResponse {
|
||||||
|
@ -214,6 +221,7 @@ const initSelectProps = () => {
|
||||||
selectProps.allowCreate = !isFalse(nodeProps.allowCreate);
|
selectProps.allowCreate = !isFalse(nodeProps.allowCreate);
|
||||||
selectProps.clearable = !isFalse(nodeProps.clearable);
|
selectProps.clearable = !isFalse(nodeProps.clearable);
|
||||||
selectProps.searchable = !isFalse(nodeProps.searchable);
|
selectProps.searchable = !isFalse(nodeProps.searchable);
|
||||||
|
selectProps.autoSelect = !isFalse(nodeProps.autoSelect) || true;
|
||||||
if (selectProps.remote) {
|
if (selectProps.remote) {
|
||||||
if (!nodeProps.remoteOption) {
|
if (!nodeProps.remoteOption) {
|
||||||
throw new Error("remoteOption is required when remote is true.");
|
throw new Error("remoteOption is required when remote is true.");
|
||||||
|
@ -341,37 +349,42 @@ const fetchSelectedOptions = async (): Promise<
|
||||||
> => {
|
> => {
|
||||||
const node = props.context.node;
|
const node = props.context.node;
|
||||||
const value = node.value;
|
const value = node.value;
|
||||||
if (!value) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedValues: string[] = [];
|
const selectedValues: Array<unknown> = [];
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
selectedValues.push(...value);
|
selectedValues.push(...value);
|
||||||
} else if (
|
} else if (
|
||||||
typeof value === "string" ||
|
typeof value === "string" ||
|
||||||
typeof value === "number" ||
|
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) =>
|
const currentOptions = options.value?.filter((option) =>
|
||||||
selectedValues.includes(option.value.toString())
|
selectedValues.includes(option.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get options that are not yet mapped.
|
// Get options that are not yet mapped.
|
||||||
const unmappedSelectValues = selectedValues.filter(
|
const unmappedSelectValues = selectedValues
|
||||||
|
.filter(
|
||||||
(value) => !currentOptions?.find((option) => option.value === value)
|
(value) => !currentOptions?.find((option) => option.value === value)
|
||||||
);
|
)
|
||||||
|
.filter(Boolean);
|
||||||
if (unmappedSelectValues.length === 0) {
|
if (unmappedSelectValues.length === 0) {
|
||||||
|
if (!currentOptions || currentOptions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return currentOptions?.sort((a, b) =>
|
return currentOptions?.sort((a, b) =>
|
||||||
selectedValues.indexOf(a.value) > selectedValues.indexOf(b.value) ? 1 : -1
|
selectedValues.indexOf(a.value) > selectedValues.indexOf(b.value) ? 1 : -1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map the unresolved options to label and value format.
|
// 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.
|
// Merge currentOptions and mappedSelectOptions, then sort them according to selectValues order.
|
||||||
return [...(currentOptions || []), ...mappedSelectOptions].sort((a, b) =>
|
return [...(currentOptions || []), ...mappedSelectOptions].sort((a, b) =>
|
||||||
selectedValues.indexOf(a.value) > selectedValues.indexOf(b.value) ? 1 : -1
|
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<string, unknown>;
|
||||||
|
return isFalse(attrs?.disabled as string | boolean | undefined);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const stopSelectedWatch = watch(
|
const stopSelectedWatch = watch(
|
||||||
() => [options.value, props.context.value],
|
() => [options.value, props.context.value],
|
||||||
async () => {
|
async () => {
|
||||||
if (options.value) {
|
if (options.value) {
|
||||||
const selectedOption = await fetchSelectedOptions();
|
const selectedOption = await fetchSelectedOptions();
|
||||||
|
if (selectedOption) {
|
||||||
selectOptions.value = 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"
|
:remote="isRemote"
|
||||||
:clearable="selectProps.clearable"
|
:clearable="selectProps.clearable"
|
||||||
:searchable="selectProps.searchable"
|
:searchable="selectProps.searchable"
|
||||||
|
:auto-select="selectProps.autoSelect"
|
||||||
@update="handleUpdate"
|
@update="handleUpdate"
|
||||||
@search="handleSearch"
|
@search="handleSearch"
|
||||||
@load-more="handleNextPage"
|
@load-more="handleNextPage"
|
||||||
|
|
|
@ -52,6 +52,7 @@ export const select: FormKitTypeDefinition = {
|
||||||
"allowCreate",
|
"allowCreate",
|
||||||
"remoteOptimize",
|
"remoteOptimize",
|
||||||
"searchable",
|
"searchable",
|
||||||
|
"autoSelect",
|
||||||
],
|
],
|
||||||
|
|
||||||
library: {
|
library: {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { singlePageLabels } from "@/constants/labels";
|
import { singlePageLabels } 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";
|
||||||
|
|
||||||
async function search({ page, size, keyword }) {
|
async function search({ page, size, keyword }) {
|
||||||
const { data } = await consoleApiClient.content.singlePage.listSinglePages({
|
const { data } = await consoleApiClient.content.singlePage.listSinglePages({
|
||||||
|
@ -53,13 +53,13 @@ function optionsHandler(node: FormKitNode) {
|
||||||
search,
|
search,
|
||||||
findOptionsByValues,
|
findOptionsByValues,
|
||||||
},
|
},
|
||||||
|
searchable: true,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const singlePageSelect: FormKitTypeDefinition = {
|
export const singlePageSelect: FormKitTypeDefinition = {
|
||||||
...select,
|
...select,
|
||||||
props: ["placeholder"],
|
forceTypeProp: "select",
|
||||||
forceTypeProp: "nativeSelect",
|
features: [optionsHandler],
|
||||||
features: [optionsHandler, selects, defaultIcon("select", "select")],
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -57,7 +57,6 @@ function optionsHandler(node: FormKitNode) {
|
||||||
|
|
||||||
export const userSelect: FormKitTypeDefinition = {
|
export const userSelect: FormKitTypeDefinition = {
|
||||||
...select,
|
...select,
|
||||||
props: ["placeholder"],
|
|
||||||
forceTypeProp: "select",
|
forceTypeProp: "select",
|
||||||
features: [optionsHandler],
|
features: [optionsHandler],
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue