fix: 把remoteSelect和Select组件合并到框架 (#499)
Reviewed-on: daoyoucloud/tachycode#499 Co-authored-by: wjh <wwwjh0710@163.com> Co-committed-by: wjh <wwwjh0710@163.com>
This commit is contained in:
parent
0eeb44c306
commit
9b405b4b9d
@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { RecordProvider, useAPIClient, useApp, useCollectionRecordData } from '../../../';
|
import { RecordProvider, useAPIClient, useApp, useCollectionRecordData } from '../../../';
|
||||||
import { isVariable } from '../../../variables/utils/isVariable';
|
import { isVariable } from '../../../variables/utils/isVariable';
|
||||||
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
|
import { getInnermostKeyAndValue } from '../../common/utils/uitls';
|
||||||
import { RemoteSelectProps } from '../remote-select';
|
import { RemoteSelectProps, RemoteSelect } from '../remote-select';
|
||||||
import useServiceOptions, { useAssociationFieldContext } from './hooks';
|
import useServiceOptions, { useAssociationFieldContext } from './hooks';
|
||||||
|
|
||||||
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
|
export type AssociationSelectProps<P = any> = RemoteSelectProps<P> & {
|
||||||
@ -60,8 +60,6 @@ const InternalAssociationSelect = observer(
|
|||||||
const resource = api.resource(collectionField.target);
|
const resource = api.resource(collectionField.target);
|
||||||
const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter);
|
const linkageFields = filterAnalyses(field.componentProps?.service?.params?.filter);
|
||||||
const recordData = useCollectionRecordData();
|
const recordData = useCollectionRecordData();
|
||||||
const app = useApp();
|
|
||||||
const RemoteSelect = useMemo(() => app.getComponent('RemoteSelect'), [app]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initValue = isVariable(field.value) ? undefined : field.value;
|
const initValue = isVariable(field.value) ? undefined : field.value;
|
||||||
@ -169,7 +167,6 @@ export const AssociationSelectReadPretty = connect(
|
|||||||
(props: any) => {
|
(props: any) => {
|
||||||
const service = useServiceOptions(props);
|
const service = useServiceOptions(props);
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
const RemoteSelect = useMemo(() => app.getComponent('RemoteSelect'), [app]) as any;
|
|
||||||
if (props.fieldNames) {
|
if (props.fieldNames) {
|
||||||
return <RemoteSelect.ReadPretty {...props} service={service}></RemoteSelect.ReadPretty>;
|
return <RemoteSelect.ReadPretty {...props} service={service}></RemoteSelect.ReadPretty>;
|
||||||
}
|
}
|
||||||
|
@ -136,7 +136,7 @@ const InternalRemoteSelect = connect(
|
|||||||
...service,
|
...service,
|
||||||
headers,
|
headers,
|
||||||
params: {
|
params: {
|
||||||
pageSize: 200,
|
pageSize: fieldNames['formula'] ? 9999 : 200,
|
||||||
...service?.params,
|
...service?.params,
|
||||||
filter: service?.params?.filter,
|
filter: service?.params?.filter,
|
||||||
},
|
},
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { CloseCircleFilled, CloseOutlined, LoadingOutlined } from '@ant-design/icons';
|
import { CloseCircleFilled, CloseOutlined } from '@ant-design/icons';
|
||||||
import { connect, mapProps, mapReadPretty, useFieldSchema, useForm } from '@formily/react';
|
import { useFieldSchema, useForm } from '@formily/react';
|
||||||
import { isValid, toArr } from '@formily/shared';
|
import { isValid, toArr } from '@formily/shared';
|
||||||
import { isPlainObject } from '@nocobase/utils/client';
|
import { isPlainObject } from '@nocobase/utils/client';
|
||||||
import type { SelectProps } from 'antd';
|
import type { SelectProps } from 'antd';
|
||||||
import { Select as AntdSelect, Empty, Spin, Tag } from 'antd';
|
import { Select as AntdSelect, Empty, Spin, Tag } from 'antd';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { ReadPretty } from './ReadPretty';
|
import { FieldNames, getCurrentOptions } from './utils';
|
||||||
import { FieldNames, defaultFieldNames, getCurrentOptions } from './utils';
|
|
||||||
import { useAPIClient, useCollection_deprecated, useRequest } from '@nocobase/client';
|
import { useAPIClient, useCollection_deprecated, useRequest } from '@nocobase/client';
|
||||||
import { useAsyncEffect } from 'ahooks';
|
import { useAsyncEffect } from 'ahooks';
|
||||||
|
|
||||||
@ -21,7 +20,7 @@ type Props = SelectProps<any, any> & {
|
|||||||
const isEmptyObject = (val: any) => !isValid(val) || (typeof val === 'object' && Object.keys(val).length === 0);
|
const isEmptyObject = (val: any) => !isValid(val) || (typeof val === 'object' && Object.keys(val).length === 0);
|
||||||
|
|
||||||
const ObjectSelect = (props: Props) => {
|
const ObjectSelect = (props: Props) => {
|
||||||
const { value, options, onChange, fieldNames, mode, loading, rawOptions, defaultValue, ...others } = props;
|
const { value, options, onChange, mode, fieldNames, loading, rawOptions, defaultValue, ...others } = props;
|
||||||
const [defoptions, setDefOptions] = useState();
|
const [defoptions, setDefOptions] = useState();
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const collectionName = fieldSchema['collectionName'];
|
const collectionName = fieldSchema['collectionName'];
|
||||||
@ -45,6 +44,12 @@ const ObjectSelect = (props: Props) => {
|
|||||||
setDefOptions(changOptions);
|
setDefOptions(changOptions);
|
||||||
}
|
}
|
||||||
}, [filterField?.filter, collectionName]);
|
}, [filterField?.filter, collectionName]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (collectionName && fieldSchema['name'].toString().includes('custom') && defaultValue) {
|
||||||
|
const name = fieldSchema['name'].toString().split('.')[1];
|
||||||
|
form.values['custom'][name] = defaultValue?.[fieldNames.value];
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
const toValue = (v: any) => {
|
const toValue = (v: any) => {
|
||||||
if (isEmptyObject(v)) {
|
if (isEmptyObject(v)) {
|
||||||
return;
|
return;
|
||||||
@ -107,7 +112,7 @@ const ObjectSelect = (props: Props) => {
|
|||||||
} else {
|
} else {
|
||||||
onChange?.(current.shift() || null);
|
onChange?.(current.shift() || null);
|
||||||
}
|
}
|
||||||
if (collectionName) {
|
if (collectionName && fieldSchema['name'].toString().includes('custom')) {
|
||||||
const name = fieldSchema['name'].toString().split('.')[1];
|
const name = fieldSchema['name'].toString().split('.')[1];
|
||||||
form.values['custom'][name] = changed?.['value'];
|
form.values['custom'][name] = changed?.['value'];
|
||||||
}
|
}
|
||||||
@ -131,8 +136,6 @@ const ObjectSelect = (props: Props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterOption = (input, option) => (option?.label ?? '').toLowerCase().includes((input || '').toLowerCase());
|
|
||||||
|
|
||||||
const replacePlaceholders = (inputStr, values) => {
|
const replacePlaceholders = (inputStr, values) => {
|
||||||
return inputStr.replace(/{{(.*?)}}/g, function (match, placeholder) {
|
return inputStr.replace(/{{(.*?)}}/g, function (match, placeholder) {
|
||||||
return Object.prototype.hasOwnProperty.call(values, placeholder) ? values[placeholder] : match;
|
return Object.prototype.hasOwnProperty.call(values, placeholder) ? values[placeholder] : match;
|
||||||
@ -200,15 +203,15 @@ const useLabelOptions = (others) => {
|
|||||||
}
|
}
|
||||||
return others;
|
return others;
|
||||||
};
|
};
|
||||||
const InternalSelect = connect(
|
|
||||||
(props: Props) => {
|
const FormulaSelect = (props) => {
|
||||||
const { objectValue, loading, value, rawOptions, defaultValue, ...others } = props;
|
const { objectValue, loading, value, rawOptions, defaultValue, ...others } = props;
|
||||||
let mode: any = props.multiple ? 'multiple' : props.mode;
|
let mode: any = props.multiple ? 'multiple' : props.mode;
|
||||||
if (mode && !['multiple', 'tags'].includes(mode)) {
|
if (mode && !['multiple', 'tags'].includes(mode)) {
|
||||||
mode = undefined;
|
mode = undefined;
|
||||||
}
|
}
|
||||||
const modifiedProps = useLabelOptions(others);
|
const modifiedProps = useLabelOptions(others);
|
||||||
if (objectValue) {
|
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
rawOptions={rawOptions}
|
rawOptions={rawOptions}
|
||||||
@ -219,70 +222,6 @@ const InternalSelect = connect(
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
const toValue = (v) => {
|
|
||||||
if (['tags', 'multiple'].includes(props.mode) || props.multiple) {
|
|
||||||
if (v) {
|
|
||||||
return toArr(v);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<AntdSelect
|
|
||||||
// @ts-ignore
|
|
||||||
role="button"
|
|
||||||
data-testid={`select-${mode || 'single'}`}
|
|
||||||
showSearch
|
|
||||||
filterOption={filterOption}
|
|
||||||
allowClear={{
|
|
||||||
clearIcon: <CloseCircleFilled role="button" aria-label="icon-close-select" />,
|
|
||||||
}}
|
|
||||||
popupMatchSelectWidth={false}
|
|
||||||
notFoundContent={loading ? <Spin /> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />}
|
|
||||||
value={toValue(value)}
|
|
||||||
defaultValue={toValue(defaultValue)}
|
|
||||||
tagRender={(props) => {
|
|
||||||
return (
|
|
||||||
// @ts-ignore
|
|
||||||
<Tag
|
|
||||||
role="button"
|
|
||||||
aria-label={props.label}
|
|
||||||
closeIcon={<CloseOutlined role="button" aria-label="icon-close-tag" />}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{props.label}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
{...others}
|
|
||||||
onChange={(changed) => {
|
|
||||||
props.onChange?.(changed === undefined ? null : changed);
|
|
||||||
}}
|
|
||||||
mode={mode}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
mapProps(
|
|
||||||
{
|
|
||||||
dataSource: 'options',
|
|
||||||
},
|
|
||||||
(props, field) => {
|
|
||||||
return {
|
|
||||||
...props,
|
|
||||||
fieldNames: { ...defaultFieldNames, ...props.fieldNames },
|
|
||||||
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
mapReadPretty(ReadPretty),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const Select = InternalSelect as unknown as typeof InternalSelect & {
|
|
||||||
ReadPretty: typeof ReadPretty;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Select.ReadPretty = ReadPretty;
|
export default FormulaSelect;
|
||||||
|
|
||||||
export default Select;
|
|
@ -7,6 +7,7 @@ import { Select as AntdSelect, Empty, Spin, Tag } from 'antd';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ReadPretty } from './ReadPretty';
|
import { ReadPretty } from './ReadPretty';
|
||||||
import { FieldNames, defaultFieldNames, getCurrentOptions } from './utils';
|
import { FieldNames, defaultFieldNames, getCurrentOptions } from './utils';
|
||||||
|
import FormulaSelect from './FormulaSelect';
|
||||||
|
|
||||||
type Props = SelectProps<any, any> & {
|
type Props = SelectProps<any, any> & {
|
||||||
objectValue?: boolean;
|
objectValue?: boolean;
|
||||||
@ -103,6 +104,9 @@ const InternalSelect = connect(
|
|||||||
if (mode && !['multiple', 'tags'].includes(mode)) {
|
if (mode && !['multiple', 'tags'].includes(mode)) {
|
||||||
mode = undefined;
|
mode = undefined;
|
||||||
}
|
}
|
||||||
|
if ('formula' in others.fieldNames || 'collection' in props) {
|
||||||
|
return <FormulaSelect {...props} />;
|
||||||
|
}
|
||||||
if (objectValue) {
|
if (objectValue) {
|
||||||
return (
|
return (
|
||||||
<ObjectSelect
|
<ObjectSelect
|
||||||
|
@ -41,6 +41,7 @@ export const removeNullCondition = (filter, fieldSchema?) => {
|
|||||||
const flatValue = flat.unflatten(values);
|
const flatValue = flat.unflatten(values);
|
||||||
const flatFieldSchema = flat.unflatten(filterSchemaItem);
|
const flatFieldSchema = flat.unflatten(filterSchemaItem);
|
||||||
flatValue['$and'] = flatValue['$and']?.filter(Boolean);
|
flatValue['$and'] = flatValue['$and']?.filter(Boolean);
|
||||||
|
flatFieldSchema['$and'] = flatFieldSchema['$and']?.filter(Boolean);
|
||||||
return {
|
return {
|
||||||
$and: [flatValue, flatFieldSchema],
|
$and: [flatValue, flatFieldSchema],
|
||||||
};
|
};
|
||||||
|
@ -43,15 +43,7 @@ import {
|
|||||||
GroupBlockConfigure,
|
GroupBlockConfigure,
|
||||||
SignatureInput,
|
SignatureInput,
|
||||||
} from './components';
|
} from './components';
|
||||||
import {
|
import { AutoComplete, DatePicker, GroupBlock, InternalPDFViewer, MenuDesigner } from './schema-components';
|
||||||
AutoComplete,
|
|
||||||
DatePicker,
|
|
||||||
GroupBlock,
|
|
||||||
InternalPDFViewer,
|
|
||||||
MenuDesigner,
|
|
||||||
RemoteSelect,
|
|
||||||
Select,
|
|
||||||
} from './schema-components';
|
|
||||||
import {
|
import {
|
||||||
CreateSubmitActionInitializer,
|
CreateSubmitActionInitializer,
|
||||||
FilterFormItem,
|
FilterFormItem,
|
||||||
@ -186,8 +178,6 @@ export class PluginCoreClient extends Plugin {
|
|||||||
PDFViewerProvider,
|
PDFViewerProvider,
|
||||||
PDFViwer: InternalPDFViewer,
|
PDFViwer: InternalPDFViewer,
|
||||||
PageLayout,
|
PageLayout,
|
||||||
RemoteSelect,
|
|
||||||
Select,
|
|
||||||
SettingBlock: SettingBlockInitializer,
|
SettingBlock: SettingBlockInitializer,
|
||||||
SheetBlock,
|
SheetBlock,
|
||||||
SheetBlockInitializer,
|
SheetBlockInitializer,
|
||||||
|
@ -3,5 +3,3 @@ export * from './blocks/GroupBlock';
|
|||||||
export * from './blocks/PDFViewer';
|
export * from './blocks/PDFViewer';
|
||||||
export * from './date-picker';
|
export * from './date-picker';
|
||||||
export * from './deprecated/ExtendedMenuDesigner';
|
export * from './deprecated/ExtendedMenuDesigner';
|
||||||
export * from './remote-select';
|
|
||||||
export * from './select/Select';
|
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
import { observer, useField, useFieldSchema } from '@formily/react';
|
|
||||||
import React from 'react';
|
|
||||||
import { getValues } from './shared';
|
|
||||||
import { defaultFieldNames, useActionContext, useRecord, useRequest } from '@nocobase/client';
|
|
||||||
import { Select } from '../select';
|
|
||||||
|
|
||||||
export const ReadPretty = observer(
|
|
||||||
(props: any) => {
|
|
||||||
const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
|
|
||||||
const field = useField<any>();
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
const record = useRecord();
|
|
||||||
const { snapshot } = useActionContext();
|
|
||||||
|
|
||||||
const { data } = useRequest<{
|
|
||||||
data: any[];
|
|
||||||
}>(
|
|
||||||
snapshot
|
|
||||||
? async () => ({
|
|
||||||
data: record[fieldSchema.name],
|
|
||||||
})
|
|
||||||
: {
|
|
||||||
action: 'list',
|
|
||||||
...props.service,
|
|
||||||
params: {
|
|
||||||
paginate: false,
|
|
||||||
filter: {
|
|
||||||
[fieldNames.value]: {
|
|
||||||
$in: getValues(field.value, fieldNames),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
refreshDeps: [props.service, field.value],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return <Select.ReadPretty {...props} options={data?.data}></Select.ReadPretty>;
|
|
||||||
},
|
|
||||||
{ displayName: 'ReadPretty' },
|
|
||||||
);
|
|
@ -1,279 +0,0 @@
|
|||||||
import { LoadingOutlined } from '@ant-design/icons';
|
|
||||||
import { connect, mapProps, mapReadPretty, useFieldSchema } from '@formily/react';
|
|
||||||
import { Divider, SelectProps, Tag } from 'antd';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import { uniqBy } from 'lodash';
|
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { ReadPretty } from './ReadPretty';
|
|
||||||
import {
|
|
||||||
defaultFieldNames,
|
|
||||||
mergeFilter,
|
|
||||||
useCollection_deprecated,
|
|
||||||
useCollectionManager_deprecated,
|
|
||||||
useCompile,
|
|
||||||
useRequest,
|
|
||||||
} from '@nocobase/client';
|
|
||||||
import { Select } from '../select';
|
|
||||||
const EMPTY = 'N/A';
|
|
||||||
|
|
||||||
export type ResourceActionOptions<P = any> = {
|
|
||||||
resource?: string;
|
|
||||||
resourceOf?: any;
|
|
||||||
action?: string;
|
|
||||||
params?: P;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
|
|
||||||
objectValue?: boolean;
|
|
||||||
onChange?: (v: any) => void;
|
|
||||||
target: string;
|
|
||||||
wait?: number;
|
|
||||||
manual?: boolean;
|
|
||||||
mapOptions?: (data: any) => RemoteSelectProps['fieldNames'];
|
|
||||||
targetField?: any;
|
|
||||||
service: ResourceActionOptions<P>;
|
|
||||||
CustomDropdownRender?: (v: any) => any;
|
|
||||||
};
|
|
||||||
|
|
||||||
const InternalRemoteSelect = connect(
|
|
||||||
(props: RemoteSelectProps) => {
|
|
||||||
const {
|
|
||||||
fieldNames = {},
|
|
||||||
service = {},
|
|
||||||
wait = 300,
|
|
||||||
value,
|
|
||||||
defaultValue,
|
|
||||||
objectValue,
|
|
||||||
manual = true,
|
|
||||||
mapOptions,
|
|
||||||
targetField: _targetField,
|
|
||||||
CustomDropdownRender,
|
|
||||||
...others
|
|
||||||
} = props;
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const firstRun = useRef(false);
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
const isQuickAdd = fieldSchema['x-component-props']?.addMode === 'quickAdd';
|
|
||||||
const { getField } = useCollection_deprecated();
|
|
||||||
const searchData = useRef(null);
|
|
||||||
const { getCollectionJoinField, getInterface } = useCollectionManager_deprecated();
|
|
||||||
const collectionField = getField(fieldSchema.name) || getCollectionJoinField(fieldSchema.name as string);
|
|
||||||
const targetField =
|
|
||||||
_targetField ||
|
|
||||||
(collectionField?.target &&
|
|
||||||
fieldNames?.label &&
|
|
||||||
getCollectionJoinField(`${collectionField.target}.${fieldNames.label}`));
|
|
||||||
|
|
||||||
const operator = useMemo(() => {
|
|
||||||
if (targetField?.interface) {
|
|
||||||
return getInterface(targetField.interface)?.filterable?.operators?.[0]?.value || '$includes';
|
|
||||||
}
|
|
||||||
return '$includes';
|
|
||||||
}, [targetField]);
|
|
||||||
const compile = useCompile();
|
|
||||||
|
|
||||||
const mapOptionsToTags = useCallback(
|
|
||||||
(options) => {
|
|
||||||
try {
|
|
||||||
return options
|
|
||||||
.filter((v) => ['number', 'string'].includes(typeof v[fieldNames.value]))
|
|
||||||
.map((option) => {
|
|
||||||
let label = compile(option[fieldNames.label]);
|
|
||||||
|
|
||||||
if (targetField?.uiSchema?.enum) {
|
|
||||||
if (Array.isArray(label)) {
|
|
||||||
label = label
|
|
||||||
.map((item, index) => {
|
|
||||||
const option = targetField.uiSchema.enum.find((i) => i.value === item);
|
|
||||||
if (option) {
|
|
||||||
return (
|
|
||||||
<Tag role="button" key={index} color={option.color} style={{ marginRight: 3 }}>
|
|
||||||
{option?.label || item}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Tag role="button" key={item}>
|
|
||||||
{item}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.reverse();
|
|
||||||
} else {
|
|
||||||
const item = targetField.uiSchema.enum.find((i) => i.value === label);
|
|
||||||
if (item) {
|
|
||||||
label = (
|
|
||||||
<Tag role="button" color={item.color}>
|
|
||||||
{item.label}
|
|
||||||
</Tag>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (targetField?.type === 'date') {
|
|
||||||
label = dayjs(label).format('YYYY-MM-DD');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mapOptions) {
|
|
||||||
return mapOptions({
|
|
||||||
[fieldNames.label]: label || EMPTY,
|
|
||||||
[fieldNames.value]: option[fieldNames.value],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...option,
|
|
||||||
[fieldNames.label]: label || EMPTY,
|
|
||||||
[fieldNames.value]: option[fieldNames.value],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[targetField?.uiSchema, fieldNames],
|
|
||||||
);
|
|
||||||
const { data, run, loading } = useRequest(
|
|
||||||
{
|
|
||||||
action: 'list',
|
|
||||||
...service,
|
|
||||||
params: {
|
|
||||||
pageSize: fieldNames['formula'] ? 9999 : 200,
|
|
||||||
...service?.params,
|
|
||||||
filter: service?.params?.filter,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
manual,
|
|
||||||
debounceWait: wait,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const runDep = useMemo(
|
|
||||||
() =>
|
|
||||||
JSON.stringify({
|
|
||||||
service,
|
|
||||||
fieldNames,
|
|
||||||
}),
|
|
||||||
[service, fieldNames],
|
|
||||||
);
|
|
||||||
const CustomRenderCom = useCallback(() => {
|
|
||||||
if (searchData.current && CustomDropdownRender) {
|
|
||||||
return (
|
|
||||||
<CustomDropdownRender
|
|
||||||
search={searchData.current}
|
|
||||||
callBack={() => {
|
|
||||||
searchData.current = null;
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [searchData.current]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Lazy load
|
|
||||||
if (firstRun.current) {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
}, [runDep]);
|
|
||||||
|
|
||||||
const onSearch = async (search) => {
|
|
||||||
run({
|
|
||||||
filter: mergeFilter([
|
|
||||||
search
|
|
||||||
? {
|
|
||||||
[fieldNames.label]: {
|
|
||||||
[operator]: search,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {},
|
|
||||||
service?.params?.filter,
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
searchData.current = search;
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = useMemo(() => {
|
|
||||||
const v = value || defaultValue;
|
|
||||||
if (!data?.data?.length) {
|
|
||||||
return v != null ? (Array.isArray(v) ? v : [v]) : [];
|
|
||||||
}
|
|
||||||
const valueOptions =
|
|
||||||
(v != null && (Array.isArray(v) ? v : [{ ...v, [fieldNames.value]: v[fieldNames.value] || v }])) || [];
|
|
||||||
return uniqBy(data?.data?.concat(valueOptions ?? []), fieldNames.value);
|
|
||||||
}, [value, defaultValue, data?.data, fieldNames.value]);
|
|
||||||
const onDropdownVisibleChange = (visible) => {
|
|
||||||
setOpen(visible);
|
|
||||||
searchData.current = null;
|
|
||||||
if (visible) {
|
|
||||||
run();
|
|
||||||
}
|
|
||||||
firstRun.current = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
open={open}
|
|
||||||
popupMatchSelectWidth={false}
|
|
||||||
autoClearSearchValue
|
|
||||||
filterOption={false}
|
|
||||||
filterSort={null}
|
|
||||||
fieldNames={fieldNames as any}
|
|
||||||
onSearch={onSearch}
|
|
||||||
onDropdownVisibleChange={onDropdownVisibleChange}
|
|
||||||
objectValue={objectValue}
|
|
||||||
value={value}
|
|
||||||
defaultValue={defaultValue}
|
|
||||||
{...others}
|
|
||||||
loading={data! ? loading : true}
|
|
||||||
options={mapOptionsToTags(options)}
|
|
||||||
rawOptions={options}
|
|
||||||
dropdownRender={(menu) => {
|
|
||||||
const isFullMatch = options.some((v) => v[fieldNames.label] === searchData.current);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isQuickAdd ? (
|
|
||||||
<>
|
|
||||||
{!(data?.data.length === 0 && searchData?.current) && menu}
|
|
||||||
{data?.data.length > 0 && searchData?.current && !isFullMatch && <Divider style={{ margin: 0 }} />}
|
|
||||||
{!isFullMatch && <CustomRenderCom />}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
menu
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
mapProps(
|
|
||||||
{
|
|
||||||
dataSource: 'options',
|
|
||||||
},
|
|
||||||
(props, field) => {
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
return {
|
|
||||||
...props,
|
|
||||||
fieldNames: {
|
|
||||||
...defaultFieldNames,
|
|
||||||
...props.fieldNames,
|
|
||||||
...field.componentProps.fieldNames,
|
|
||||||
...fieldSchema['x-component-props']?.fieldNames,
|
|
||||||
},
|
|
||||||
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
),
|
|
||||||
mapReadPretty(ReadPretty),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const RemoteSelect = InternalRemoteSelect as unknown as typeof InternalRemoteSelect & {
|
|
||||||
ReadPretty: typeof ReadPretty;
|
|
||||||
};
|
|
||||||
|
|
||||||
RemoteSelect.ReadPretty = ReadPretty;
|
|
||||||
export default RemoteSelect;
|
|
@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
group:
|
|
||||||
title: Schema Components
|
|
||||||
order: 3
|
|
||||||
---
|
|
||||||
|
|
||||||
# RemoteSelect
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
<code src="./demos/demo1.tsx"></code>
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
基于 Ant Design 的 [Select](https://ant.design/components/select/#API),相关扩展属性有:
|
|
||||||
|
|
||||||
- `objectValue` 值为 object 类型
|
|
||||||
- `fieldNames` 默认值有区别
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export const defaultFieldNames = {
|
|
||||||
label: 'label',
|
|
||||||
value: 'value',
|
|
||||||
color: 'color',
|
|
||||||
options: 'children',
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
@ -1 +0,0 @@
|
|||||||
export * from './RemoteSelect';
|
|
@ -1,7 +0,0 @@
|
|||||||
import { castArray } from 'lodash';
|
|
||||||
|
|
||||||
export const getValues = (values, fieldNames) => {
|
|
||||||
return castArray(values)
|
|
||||||
.filter((item) => item != null)
|
|
||||||
.map((val) => (typeof val === 'object' ? val[fieldNames.value] : val));
|
|
||||||
};
|
|
@ -1,36 +0,0 @@
|
|||||||
import { isArrayField } from '@formily/core';
|
|
||||||
import { observer, useField } from '@formily/react';
|
|
||||||
import { isValid } from '@formily/shared';
|
|
||||||
import { Tag } from 'antd';
|
|
||||||
import React from 'react';
|
|
||||||
import { defaultFieldNames, getCurrentOptions } from './utils';
|
|
||||||
import { EllipsisWithTooltip } from '@nocobase/client';
|
|
||||||
|
|
||||||
export const ReadPretty = observer(
|
|
||||||
(props: any) => {
|
|
||||||
const fieldNames = { ...defaultFieldNames, ...props.fieldNames };
|
|
||||||
const field = useField<any>();
|
|
||||||
|
|
||||||
if (!isValid(props.value)) {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
if (isArrayField(field) && field?.value?.length === 0) {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
const dataSource = field.dataSource || props.options || [];
|
|
||||||
const currentOptions = getCurrentOptions(field.value, dataSource, fieldNames);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<EllipsisWithTooltip ellipsis={props.ellipsis}>
|
|
||||||
{currentOptions.map((option, key) => (
|
|
||||||
<Tag key={key} color={option[fieldNames.color]} icon={option.icon}>
|
|
||||||
{option[fieldNames.label]}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</EllipsisWithTooltip>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ displayName: 'ReadPretty' },
|
|
||||||
);
|
|
@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
group:
|
|
||||||
title: Schema Components
|
|
||||||
order: 3
|
|
||||||
---
|
|
||||||
|
|
||||||
# Select
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
### 单选
|
|
||||||
|
|
||||||
<code src="./demos/demo1.tsx"></code>
|
|
||||||
|
|
||||||
### 多选
|
|
||||||
|
|
||||||
<code src="./demos/demo2.tsx"></code>
|
|
||||||
|
|
||||||
### 值为 Object 类型的 Select
|
|
||||||
|
|
||||||
<code src="./demos/demo3.tsx"></code>
|
|
||||||
|
|
||||||
## API
|
|
||||||
|
|
||||||
基于 Ant Design 的 [Select](https://ant.design/components/select/#API),相关扩展属性有:
|
|
||||||
|
|
||||||
- `objectValue` 值为 object 类型
|
|
||||||
- `fieldNames` 默认值有区别
|
|
||||||
|
|
||||||
```ts
|
|
||||||
export const defaultFieldNames = {
|
|
||||||
label: 'label',
|
|
||||||
value: 'value',
|
|
||||||
color: 'color',
|
|
||||||
options: 'children',
|
|
||||||
};
|
|
||||||
```
|
|
@ -1,2 +0,0 @@
|
|||||||
export * from './Select';
|
|
||||||
export * from './utils';
|
|
@ -1,54 +0,0 @@
|
|||||||
import { isPlainObject } from '@nocobase/utils/client';
|
|
||||||
import { castArray } from 'lodash';
|
|
||||||
|
|
||||||
export interface FieldNames {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
color: string;
|
|
||||||
options: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultFieldNames: FieldNames = {
|
|
||||||
label: 'label',
|
|
||||||
value: 'value',
|
|
||||||
color: 'color',
|
|
||||||
options: 'children',
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Option {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
icon?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flatData(data: any[], fieldNames: FieldNames): any[] {
|
|
||||||
const newArr: any[] = [];
|
|
||||||
if (!Array.isArray(data)) return newArr;
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
const children = data[i][fieldNames.options];
|
|
||||||
if (Array.isArray(children)) {
|
|
||||||
newArr.push(...flatData(children, fieldNames));
|
|
||||||
}
|
|
||||||
newArr.push({ ...data[i] });
|
|
||||||
}
|
|
||||||
return newArr;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCurrentOptions(values: string | string[], dataSource: any[], fieldNames: FieldNames): Option[] {
|
|
||||||
const result = flatData(dataSource, fieldNames);
|
|
||||||
const arrValues = castArray(values)
|
|
||||||
.filter((item) => item != null)
|
|
||||||
.map((val) => (isPlainObject(val) ? val[fieldNames.value] : val)) as string[];
|
|
||||||
|
|
||||||
function findOptions(options: any[]): Option[] {
|
|
||||||
if (!options) return [];
|
|
||||||
const current: Option[] = [];
|
|
||||||
for (const value of arrValues) {
|
|
||||||
const option = options.find((v) => v[fieldNames.value] === value) || { value, label: value };
|
|
||||||
current.push(option);
|
|
||||||
}
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
return findOptions(result);
|
|
||||||
}
|
|
@ -23,6 +23,7 @@ import {
|
|||||||
useFormBlockContext,
|
useFormBlockContext,
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
useCompile,
|
useCompile,
|
||||||
|
Select,
|
||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { ConfigProvider, Radio, Space } from 'antd';
|
import { ConfigProvider, Radio, Space } from 'antd';
|
||||||
import React, { memo, useCallback, useContext, useMemo, Profiler } from 'react';
|
import React, { memo, useCallback, useContext, useMemo, Profiler } from 'react';
|
||||||
@ -116,8 +117,6 @@ const FieldComponentProps: React.FC = observer(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
console.log(scope, 'scope');
|
|
||||||
console.log(components, 'components');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaComponentOptions scope={{ ...scope }} components={{ ArrayItems, FormItem, Space }}>
|
<SchemaComponentOptions scope={{ ...scope }} components={{ ArrayItems, FormItem, Space }}>
|
||||||
@ -180,7 +179,7 @@ export const FilterCustomItemInitializer: React.FC<{
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
components={{ ...components, FieldComponentProps }}
|
components={{ ...components, FieldComponentProps, Select }}
|
||||||
>
|
>
|
||||||
<FormLayout layout={'vertical'}>
|
<FormLayout layout={'vertical'}>
|
||||||
<ConfigProvider locale={locale}>
|
<ConfigProvider locale={locale}>
|
||||||
@ -282,12 +281,12 @@ export const FilterCustomItemInitializer: React.FC<{
|
|||||||
const name = uid();
|
const name = uid();
|
||||||
insert(
|
insert(
|
||||||
gridRowColWrap({
|
gridRowColWrap({
|
||||||
'x-component': component,
|
|
||||||
...defaultSchema,
|
...defaultSchema,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: title,
|
title: title,
|
||||||
name: 'custom.' + name,
|
name: 'custom.' + name,
|
||||||
required: false,
|
required: false,
|
||||||
|
'x-component': component,
|
||||||
'x-designer': 'FilterItemCustomDesigner',
|
'x-designer': 'FilterItemCustomDesigner',
|
||||||
'x-decorator': 'FilterFormItem',
|
'x-decorator': 'FilterFormItem',
|
||||||
'x-decorator-props': collection,
|
'x-decorator-props': collection,
|
||||||
@ -299,6 +298,7 @@ export const FilterCustomItemInitializer: React.FC<{
|
|||||||
},
|
},
|
||||||
associationField,
|
associationField,
|
||||||
collection,
|
collection,
|
||||||
|
objectValue: true,
|
||||||
},
|
},
|
||||||
collectionName: collection,
|
collectionName: collection,
|
||||||
}),
|
}),
|
||||||
|
@ -43,7 +43,9 @@ export const SchemaSettingsRemove: FC<SchemaSettingsRemoveProps> = (props) => {
|
|||||||
}
|
}
|
||||||
await dn.remove(null, options);
|
await dn.remove(null, options);
|
||||||
await confirm?.onOk?.();
|
await confirm?.onOk?.();
|
||||||
|
if (form.values['custom']) {
|
||||||
delete form.values['custom'][fieldName];
|
delete form.values['custom'][fieldName];
|
||||||
|
}
|
||||||
for (const key in form.fields) {
|
for (const key in form.fields) {
|
||||||
if (key.includes(name) && form.fields[key].title === title) {
|
if (key.includes(name) && form.fields[key].title === title) {
|
||||||
delete form.fields[key];
|
delete form.fields[key];
|
||||||
|
Loading…
Reference in New Issue
Block a user