mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
566 lines
16 KiB
TypeScript
566 lines
16 KiB
TypeScript
import React, { useEffect } from 'react';
|
||
import {
|
||
connect,
|
||
mapReadPretty,
|
||
mapProps,
|
||
useField,
|
||
observer,
|
||
RecursionField,
|
||
useFieldSchema,
|
||
Schema,
|
||
} from '@formily/react';
|
||
import { Drawer, Select as AntdSelect, Tag } from 'antd';
|
||
import { PreviewText } from '@formily/antd';
|
||
import { LoadingOutlined } from '@ant-design/icons';
|
||
import { SelectProps } from 'antd/lib/select';
|
||
import { isArr, isValid, toArr } from '@formily/shared';
|
||
import { useState } from 'react';
|
||
import { useDesignable } from '../';
|
||
import { createContext } from 'react';
|
||
import { useContext } from 'react';
|
||
import { get, isEmpty } from 'lodash';
|
||
import { Field, isArrayField, isField } from '@formily/core';
|
||
import { Action } from '../action';
|
||
import { BlockSchemaContext, VisibleContext } from '../../context';
|
||
import { SchemaRenderer } from '../../components/schema-renderer';
|
||
import { uid } from '@formily/shared';
|
||
import { CollectionFieldContext } from '../table';
|
||
import {
|
||
CollectionProvider,
|
||
useCollectionContext,
|
||
useResourceRequest,
|
||
} from '../../constate';
|
||
import { Resource } from '../../resource';
|
||
import { useRequest } from 'ahooks';
|
||
import constate from 'constate';
|
||
|
||
import './index.less';
|
||
import { useCompile } from '../../hooks/useCompile';
|
||
|
||
export const Select: any = connect(
|
||
(props) => {
|
||
const { options = [], ...others } = props;
|
||
const compile = useCompile();
|
||
return (
|
||
<AntdSelect {...others}>
|
||
{options.map((option: any, key: any) => {
|
||
if (option.children) {
|
||
return (
|
||
<AntdSelect.OptGroup key={key} label={option.label}>
|
||
{option.children.map((child: any, childKey: any) => (
|
||
<AntdSelect.Option key={`${key}-${childKey}`} {...child}>
|
||
{compile(child.label)}
|
||
</AntdSelect.Option>
|
||
))}
|
||
</AntdSelect.OptGroup>
|
||
);
|
||
} else {
|
||
return (
|
||
<AntdSelect.Option key={key} {...option}>
|
||
{compile(option.label)}
|
||
</AntdSelect.Option>
|
||
);
|
||
}
|
||
})}
|
||
</AntdSelect>
|
||
);
|
||
},
|
||
mapProps(
|
||
{
|
||
dataSource: 'options',
|
||
loading: true,
|
||
},
|
||
(props, field) => {
|
||
return {
|
||
...props,
|
||
suffixIcon:
|
||
field?.['loading'] || field?.['validating'] ? (
|
||
<LoadingOutlined />
|
||
) : (
|
||
props.suffixIcon
|
||
),
|
||
};
|
||
},
|
||
),
|
||
mapReadPretty((props) => {
|
||
// console.log('mapReadPretty', props.value)
|
||
// return <div></div>;
|
||
if (!isValid(props.value)) {
|
||
return <div></div>;
|
||
}
|
||
const compile = useCompile();
|
||
const field = useField<any>();
|
||
if (isArrayField(field) && field?.value?.length === 0) {
|
||
return <div></div>;
|
||
}
|
||
const dataSource = field.dataSource || [];
|
||
console.log('field.value', field.value, dataSource);
|
||
const values = toArr(field.value);
|
||
const findOptions = (options: any[]) => {
|
||
let current = [];
|
||
for (const option of options) {
|
||
if (values.includes(option.value)) {
|
||
current.push(option);
|
||
}
|
||
if (option.children) {
|
||
const children = findOptions(option.children);
|
||
current.push(...children);
|
||
}
|
||
}
|
||
return current;
|
||
};
|
||
const options = findOptions(dataSource);
|
||
return (
|
||
<div>
|
||
{options.map((option, key) => (
|
||
<Tag key={key} color={option.color}>
|
||
{compile(option.label)}
|
||
</Tag>
|
||
))}
|
||
</div>
|
||
);
|
||
}),
|
||
);
|
||
|
||
Select.Object = connect(
|
||
(props) => {
|
||
const field = useField();
|
||
const {
|
||
value,
|
||
onChange,
|
||
fieldNames = {
|
||
label: 'label',
|
||
value: 'value',
|
||
},
|
||
...others
|
||
} = props;
|
||
const options = field?.['dataSource'] || props.options || [];
|
||
let optionValue = undefined;
|
||
if (isArr(value)) {
|
||
optionValue = value.map((val) => {
|
||
return {
|
||
label: val[fieldNames.label],
|
||
value: val[fieldNames.value],
|
||
};
|
||
});
|
||
} else if (value) {
|
||
optionValue = {
|
||
label: value[fieldNames.label],
|
||
value: value[fieldNames.value],
|
||
};
|
||
}
|
||
return (
|
||
<AntdSelect
|
||
allowClear
|
||
labelInValue
|
||
{...others}
|
||
value={optionValue}
|
||
onChange={(selectValue: any) => {
|
||
if (!isValid(selectValue)) {
|
||
onChange(null);
|
||
return;
|
||
}
|
||
if (isArr(selectValue)) {
|
||
const selectValues = selectValue.map((s) => s.value);
|
||
const values = {};
|
||
if (isArr(value)) {
|
||
value.forEach((option) => {
|
||
const val = option[fieldNames.value];
|
||
if (selectValues.includes(val)) {
|
||
values[val] = option;
|
||
}
|
||
});
|
||
}
|
||
options.forEach((option) => {
|
||
const val = option[fieldNames.value];
|
||
if (selectValues.includes(val)) {
|
||
values[val] = option;
|
||
}
|
||
});
|
||
console.log({ selectValue, values });
|
||
onChange(Object.values(values));
|
||
} else {
|
||
// 这里不能用 undefined,需要用 null
|
||
onChange(
|
||
options.find(
|
||
(option) => option[fieldNames.value] === selectValue.value,
|
||
),
|
||
);
|
||
}
|
||
}}
|
||
options={options.map((option) => {
|
||
return {
|
||
label: option[fieldNames.label],
|
||
value: option[fieldNames.value],
|
||
};
|
||
})}
|
||
/>
|
||
);
|
||
},
|
||
mapProps(
|
||
{
|
||
dataSource: 'options',
|
||
loading: true,
|
||
},
|
||
(props, field) => {
|
||
return {
|
||
...props,
|
||
options: field?.['dataSource'],
|
||
suffixIcon:
|
||
field?.['loading'] || field?.['validating'] ? (
|
||
<LoadingOutlined />
|
||
) : (
|
||
props.suffixIcon
|
||
),
|
||
};
|
||
},
|
||
),
|
||
mapReadPretty(
|
||
observer((props: any) => {
|
||
const {
|
||
value,
|
||
fieldNames = { label: 'label', color: 'color' },
|
||
...others
|
||
} = props;
|
||
if (!value) {
|
||
return null;
|
||
}
|
||
if (isEmpty(value)) {
|
||
return null;
|
||
}
|
||
const values = toArr(value);
|
||
return (
|
||
<div>
|
||
{values.map((val) =>
|
||
fieldNames.color ? (
|
||
<Tag color={val[fieldNames.color]}>{val[fieldNames.label]}</Tag>
|
||
) : (
|
||
<Tag>{val[fieldNames.label]}</Tag>
|
||
),
|
||
)}
|
||
</div>
|
||
);
|
||
}),
|
||
),
|
||
);
|
||
|
||
const OptionTagContext = createContext(null);
|
||
|
||
const SelectedRowsContext = createContext<any>({});
|
||
|
||
Select.useOkAction = () => {
|
||
const { props } = useContext(SelectContext);
|
||
const { selectedRows } = useContext(SelectedRowsContext);
|
||
return {
|
||
async run() {
|
||
props.onChange(selectedRows);
|
||
console.log('selectedRows', selectedRows);
|
||
},
|
||
};
|
||
};
|
||
|
||
Select.useRowSelection = () => {
|
||
const { props } = useContext(SelectContext);
|
||
return {
|
||
type: props.multiple ? 'checkbox' : 'radio',
|
||
};
|
||
};
|
||
|
||
Select.useSelect = () => {
|
||
const { setSelectedRows } = useContext(SelectedRowsContext);
|
||
return (keys, rows) => {
|
||
setSelectedRows && setSelectedRows(rows);
|
||
console.log('Select.onSelect', keys, rows);
|
||
};
|
||
};
|
||
|
||
export const useSelectedRowKeys = () => {
|
||
const { rowKey, selectedRows } = useContext(SelectedRowsContext);
|
||
const [selectedRowKeys, setSelectedRowKeys] = useState<any>(
|
||
selectedRows.map((row) => row[rowKey]),
|
||
);
|
||
useEffect(() => {
|
||
setSelectedRowKeys(selectedRows.map((row) => row[rowKey]));
|
||
}, [selectedRows]);
|
||
console.log('useSelectedRowKeys', selectedRows);
|
||
return { selectedRowKeys, setSelectedRowKeys };
|
||
};
|
||
|
||
Select.useSelectedRowKeys = useSelectedRowKeys;
|
||
|
||
const SelectContext = createContext(null);
|
||
|
||
Select.Drawer = connect(
|
||
(props) => {
|
||
const field = useField<Field>();
|
||
const {
|
||
onChange,
|
||
// fieldNames = {
|
||
// label: 'id',
|
||
// value: 'id',
|
||
// },
|
||
...others
|
||
} = props;
|
||
let value = props.value;
|
||
const [visible, setVisible] = useState(false);
|
||
const { schema } = useDesignable();
|
||
const fieldNames = {
|
||
label: 'id',
|
||
value: 'id',
|
||
...(get(schema['x-component-props'], 'fieldNames') || {}),
|
||
};
|
||
const options = field?.['dataSource'] || props.options || [];
|
||
if (props.multiple) {
|
||
Object.assign(others, {
|
||
mode: 'multiple',
|
||
});
|
||
}
|
||
let optionValue = undefined;
|
||
if (props.multiple) {
|
||
value = toArr(value);
|
||
}
|
||
if (isArr(value)) {
|
||
optionValue = value.map((val) => {
|
||
return {
|
||
label: val[fieldNames.label],
|
||
value: val[fieldNames.value],
|
||
};
|
||
});
|
||
} else if (value) {
|
||
optionValue = {
|
||
label: value[fieldNames.label],
|
||
value: value[fieldNames.value],
|
||
};
|
||
}
|
||
const [selectedRows, setSelectedRows] = useState(toArr(field.value));
|
||
console.log('fieldNames', { fieldNames, field, value });
|
||
console.log('useSelectedRowKeys.toArr', toArr(field.value));
|
||
useEffect(() => {
|
||
setSelectedRows(toArr(field.value));
|
||
}, [field.value]);
|
||
const onFieldChange = (selectValue) => {
|
||
if (!isValid(selectValue)) {
|
||
onChange(null);
|
||
return;
|
||
}
|
||
if (isArr(selectValue)) {
|
||
const values = {};
|
||
if (isArr(value)) {
|
||
value.forEach((option) => {
|
||
const val = option[fieldNames.value];
|
||
if (selectValue.includes(val)) {
|
||
values[val] = option;
|
||
}
|
||
});
|
||
}
|
||
options.forEach((option) => {
|
||
const val = option[fieldNames.value];
|
||
if (selectValue.includes(val)) {
|
||
values[val] = option;
|
||
}
|
||
});
|
||
onChange(Object.values(values));
|
||
} else {
|
||
// 这里不能用 undefined,需要用 null
|
||
onChange(
|
||
options.find((option) => option[fieldNames.value] === selectValue),
|
||
);
|
||
}
|
||
// setSelectedRows(toArr(field.value));
|
||
};
|
||
// const selectedKeys = toArr(optionValue).map((item) => item.value);
|
||
const collectionField = useContext(CollectionFieldContext);
|
||
// console.log({ optionValue, value, schema, collectionField });
|
||
return (
|
||
<SelectContext.Provider value={{ field, schema, props }}>
|
||
<VisibleContext.Provider value={[visible, setVisible]}>
|
||
<AntdSelect
|
||
{...others}
|
||
labelInValue
|
||
open={false}
|
||
value={optionValue}
|
||
onClick={() => {
|
||
setVisible(true);
|
||
}}
|
||
onChange={(selectValue: any) => {
|
||
if (!selectValue) {
|
||
onChange(null);
|
||
return;
|
||
}
|
||
if (isArr(selectValue)) {
|
||
const selectValues = selectValue.map((s) => s.value);
|
||
onFieldChange(selectValues);
|
||
} else {
|
||
onFieldChange(selectValue.value);
|
||
}
|
||
}}
|
||
></AntdSelect>
|
||
<SelectedRowsContext.Provider
|
||
value={{ rowKey: collectionField?.targetKey || 'id', selectedRows, setSelectedRows }}
|
||
>
|
||
<CollectionProvider collectionName={collectionField?.target}>
|
||
<RecursionField
|
||
onlyRenderProperties
|
||
schema={schema}
|
||
filterProperties={(s) => {
|
||
if (s['x-component'] === 'Select.Options.Drawer') {
|
||
const prop = Object.values(s.properties).shift();
|
||
if (prop) {
|
||
prop['x-component-props']['rowKey'] = collectionField?.targetKey || 'id';
|
||
}
|
||
}
|
||
return s['x-component'] === 'Select.Options.Drawer';
|
||
}}
|
||
/>
|
||
</CollectionProvider>
|
||
</SelectedRowsContext.Provider>
|
||
</VisibleContext.Provider>
|
||
</SelectContext.Provider>
|
||
);
|
||
},
|
||
mapProps(
|
||
{
|
||
dataSource: 'options',
|
||
loading: true,
|
||
},
|
||
(props, field) => {
|
||
return {
|
||
...props,
|
||
options: field?.['dataSource'],
|
||
suffixIcon:
|
||
field?.['loading'] || field?.['validating'] ? (
|
||
<LoadingOutlined />
|
||
) : (
|
||
props.suffixIcon
|
||
),
|
||
};
|
||
},
|
||
),
|
||
mapReadPretty(
|
||
observer((props: any) => {
|
||
const collectionField = useContext(CollectionFieldContext);
|
||
const field = useField<Formily.Core.Models.Field>();
|
||
const { ...others } = props;
|
||
const value = field.value || field.initialValue;
|
||
const { schema } = useDesignable();
|
||
const fieldNames = {
|
||
label: 'id',
|
||
value: 'id',
|
||
...(get(schema['x-component-props'], 'fieldNames') || {}),
|
||
};
|
||
console.log('fieldNames', { fieldNames, field, value });
|
||
if (!value) {
|
||
return null;
|
||
}
|
||
const values = toArr(value);
|
||
const s = schema.reduceProperties((buf, current) => {
|
||
if (current['x-component'] === 'Select.OptionTag') {
|
||
return current;
|
||
}
|
||
return buf;
|
||
}, null);
|
||
return (
|
||
<div>
|
||
<BlockSchemaContext.Provider value={schema}>
|
||
<CollectionProvider collectionName={collectionField?.target}>
|
||
{values.map((data, index) => {
|
||
return (
|
||
<OptionTagContext.Provider
|
||
value={{ index, data, fieldNames }}
|
||
>
|
||
{s ? (
|
||
<RecursionField name={s.name} schema={s} />
|
||
) : (
|
||
data[fieldNames.label]
|
||
)}
|
||
</OptionTagContext.Provider>
|
||
);
|
||
})}
|
||
</CollectionProvider>
|
||
</BlockSchemaContext.Provider>
|
||
</div>
|
||
);
|
||
}),
|
||
),
|
||
);
|
||
|
||
Select.Drawer.useResource = ({ onSuccess }) => {
|
||
const { collection } = useCollectionContext();
|
||
const ctx = useContext(OptionTagContext);
|
||
const resource = useResourceRequest({
|
||
resourceName: collection?.name,
|
||
resourceKey: ctx.data.id,
|
||
});
|
||
console.log('OptionTagContext', ctx.data.id);
|
||
const { schema } = useDesignable();
|
||
const fieldFields = (schema: Schema) => {
|
||
const names = [];
|
||
schema.reduceProperties((buf, current) => {
|
||
if (current['x-component'] === 'Form.Field') {
|
||
const fieldName = current['x-component-props']?.['fieldName'];
|
||
if (fieldName) {
|
||
buf.push(fieldName);
|
||
}
|
||
} else {
|
||
const fieldNames = fieldFields(current);
|
||
buf.push(...fieldNames);
|
||
}
|
||
return buf;
|
||
}, names);
|
||
return names;
|
||
};
|
||
const [visible] = useContext(VisibleContext);
|
||
const service = useRequest(
|
||
(params?: any) => {
|
||
console.log('Table.useResource', params);
|
||
return resource.get({ ...params, appends: fieldFields(schema) });
|
||
},
|
||
{
|
||
formatResult: (result) => result?.data,
|
||
onSuccess,
|
||
manual: true,
|
||
},
|
||
);
|
||
useEffect(() => {
|
||
if (visible) {
|
||
service.run();
|
||
}
|
||
}, [visible]);
|
||
return { resource, service, initialValues: service.data, ...service };
|
||
};
|
||
|
||
Select.Options = observer((props) => {
|
||
return <>{props.children}</>;
|
||
});
|
||
|
||
Select.Options.Drawer = Action.Drawer;
|
||
|
||
export function useSelect() {
|
||
const { onChange } = useContext(SelectContext);
|
||
return {
|
||
async run() {
|
||
onChange && onChange([]);
|
||
},
|
||
};
|
||
}
|
||
|
||
export function useOptionTagValues() {
|
||
const { data } = useContext(OptionTagContext);
|
||
return data;
|
||
}
|
||
|
||
Select.OptionTag = observer((props) => {
|
||
const [visible, setVisible] = useState(false);
|
||
const { data, fieldNames } = useContext(OptionTagContext);
|
||
return (
|
||
<VisibleContext.Provider value={[visible, setVisible]}>
|
||
<a className="option-tag" onClick={() => setVisible(true)}>
|
||
{data[fieldNames.label]}
|
||
</a>
|
||
{props.children}
|
||
</VisibleContext.Provider>
|
||
);
|
||
});
|
||
|
||
export default Select;
|