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`。仅在多选时有效。
|
||||
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` 解析。
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ function optionsHandler(node: FormKitNode) {
|
|||
|
||||
export const attachmentGroupSelect: FormKitTypeDefinition = {
|
||||
...select,
|
||||
props: ["placeholder"],
|
||||
forceTypeProp: "select",
|
||||
features: [optionsHandler],
|
||||
};
|
||||
|
|
|
@ -19,7 +19,6 @@ function optionsHandler(node: FormKitNode) {
|
|||
|
||||
export const attachmentPolicySelect: FormKitTypeDefinition = {
|
||||
...select,
|
||||
props: ["placeholder"],
|
||||
forceTypeProp: "select",
|
||||
features: [optionsHandler],
|
||||
};
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
|
|
|
@ -53,13 +53,13 @@ function optionsHandler(node: FormKitNode) {
|
|||
search,
|
||||
findOptionsByValues,
|
||||
},
|
||||
searchable: true,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const postSelect: FormKitTypeDefinition = {
|
||||
...select,
|
||||
props: ["placeholder"],
|
||||
forceTypeProp: "select",
|
||||
features: [optionsHandler],
|
||||
};
|
||||
|
|
|
@ -37,7 +37,6 @@ function optionsHandler(node: FormKitNode) {
|
|||
|
||||
export const roleSelect: FormKitTypeDefinition = {
|
||||
...select,
|
||||
props: ["placeholder"],
|
||||
forceTypeProp: "nativeSelect",
|
||||
forceTypeProp: "select",
|
||||
features: [optionsHandler],
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
|
|
@ -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<unknown> = [];
|
||||
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<string, unknown>;
|
||||
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"
|
||||
|
|
|
@ -52,6 +52,7 @@ export const select: FormKitTypeDefinition = {
|
|||
"allowCreate",
|
||||
"remoteOptimize",
|
||||
"searchable",
|
||||
"autoSelect",
|
||||
],
|
||||
|
||||
library: {
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
|
|
|
@ -57,7 +57,6 @@ function optionsHandler(node: FormKitNode) {
|
|||
|
||||
export const userSelect: FormKitTypeDefinition = {
|
||||
...select,
|
||||
props: ["placeholder"],
|
||||
forceTypeProp: "select",
|
||||
features: [optionsHandler],
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue