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 { 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],
};

View File

@ -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],
};

View File

@ -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],
};

View File

@ -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],
};

View File

@ -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],
};

View File

@ -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
@ -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,17 +413,29 @@ const mapUnresolvedOptions = async (
}
// Asynchronous request for options, fetch label and value via API.
let mappedOptions: Array<{
label: string;
value: string;
}> = [];
if (selectProps.action) {
mappedOptions = await fetchRemoteMappedOptions(unmappedSelectValues);
} else if (selectProps.remote) {
const remoteOption = selectProps.remoteOption as SelectRemoteOption;
mappedOptions = await remoteOption.findOptionsByValues(
unmappedSelectValues
let mappedOptions:
| Array<{
label: string;
value: string;
}>
| undefined = undefined;
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) {
return unmappedSelectValues.map((value) => ({ label: value, value }));
}
// Get values that are still unresolved.
const unmappedValues = unmappedSelectValues.filter(
@ -496,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;
}
},
{
@ -521,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);
@ -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 (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;

View File

@ -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,
},
};
});
}

View File

@ -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";
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) {
node.on("created", async () => {
const { data } = await coreApiClient.user.listUser();
node.props.options = data.items.map((user) => {
return {
value: user.metadata.name,
label: user.spec.displayName,
};
});
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],
};