mirror of https://github.com/halo-dev/halo
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
parent
a5c6d6672f
commit
281567877a
|
@ -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],
|
||||
};
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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],
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue