mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 06:15:11 +00:00
refactor: support attachmentURL (#5313)
* refactor: support attachmentURL * refactor: support attachment field * fix: bug * refactor: attachment url field * fix: bug * fix: bug * fix: merge bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: bug * fix: kanban appends * fix: action export * fix: action import * fix: test * fix: bug * fix(client): fix file type check logic * fix(client): fix image previewer by file type * fix(client): fix null file type caused error in matching * fix(client): fix thumbnail data * refactor: datetime * test: fix test * fix(client): fix preview based on file type when url contains search part * refactor: remote select * fix: test * fix: bug --------- Co-authored-by: mytharcher <mytharcher@gmail.com>
This commit is contained in:
parent
664eb3df24
commit
15e05d5a2c
@ -49,6 +49,7 @@
|
|||||||
"markdown-it-highlightjs": "3.3.1",
|
"markdown-it-highlightjs": "3.3.1",
|
||||||
"mathjs": "^10.6.0",
|
"mathjs": "^10.6.0",
|
||||||
"mermaid": "9.4.3",
|
"mermaid": "9.4.3",
|
||||||
|
"mime": "^4.0.4",
|
||||||
"mime-match": "^1.0.2",
|
"mime-match": "^1.0.2",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-drag-listview": "^0.1.9",
|
"react-drag-listview": "^0.1.9",
|
||||||
|
@ -15,7 +15,7 @@ export class DatetimeFieldInterface extends CollectionFieldInterface {
|
|||||||
type = 'object';
|
type = 'object';
|
||||||
group = 'datetime';
|
group = 'datetime';
|
||||||
order = 1;
|
order = 1;
|
||||||
title = '{{t("Datetime(with time zone)")}}';
|
title = '{{t("Datetime (with time zone)")}}';
|
||||||
sortable = true;
|
sortable = true;
|
||||||
default = {
|
default = {
|
||||||
type: 'date',
|
type: 'date',
|
||||||
|
@ -15,7 +15,7 @@ export class DatetimeNoTzFieldInterface extends CollectionFieldInterface {
|
|||||||
type = 'object';
|
type = 'object';
|
||||||
group = 'datetime';
|
group = 'datetime';
|
||||||
order = 2;
|
order = 2;
|
||||||
title = '{{t("Datetime(without time zone)")}}';
|
title = '{{t("Datetime (without time zone)")}}';
|
||||||
sortable = true;
|
sortable = true;
|
||||||
default = {
|
default = {
|
||||||
type: 'datetimeNoTz',
|
type: 'datetimeNoTz',
|
||||||
|
@ -166,9 +166,9 @@ describe('transformToFilter', () => {
|
|||||||
const getCollectionJoinField = vi.fn((name: string) => {
|
const getCollectionJoinField = vi.fn((name: string) => {
|
||||||
if (name === `${collectionName}.field1`) return {};
|
if (name === `${collectionName}.field1`) return {};
|
||||||
if (name === `${collectionName}.field2`) return {};
|
if (name === `${collectionName}.field2`) return {};
|
||||||
if (name === `${collectionName}.field3`) return { target: 'targetCollection', targetKey: 'id' };
|
if (name === `${collectionName}.field3`) return { target: 'targetCollection', targetKey: 'id', type: 'belongsTo' };
|
||||||
if (name === `${collectionName}.chinaRegion`)
|
if (name === `${collectionName}.chinaRegion`)
|
||||||
return { target: 'chinaRegions', targetKey: 'code', interface: 'chinaRegion' };
|
return { target: 'chinaRegions', targetKey: 'code', interface: 'chinaRegion', type: 'belongsToMany' };
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -139,7 +139,10 @@ export const transformToFilter = (
|
|||||||
let value = _.get(values, key);
|
let value = _.get(values, key);
|
||||||
const collectionField = getCollectionJoinField(`${collectionName}.${key}`);
|
const collectionField = getCollectionJoinField(`${collectionName}.${key}`);
|
||||||
|
|
||||||
if (collectionField?.target) {
|
if (
|
||||||
|
collectionField?.target &&
|
||||||
|
['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(collectionField.type)
|
||||||
|
) {
|
||||||
value = getValuesByPath(value, collectionField.targetKey || 'id');
|
value = getValuesByPath(value, collectionField.targetKey || 'id');
|
||||||
key = `${key}.${collectionField.targetKey || 'id'}`;
|
key = `${key}.${collectionField.targetKey || 'id'}`;
|
||||||
|
|
||||||
|
@ -983,8 +983,8 @@
|
|||||||
"Automatically update timestamp on update": "当记录更新时自动设置字段值为当前时间",
|
"Automatically update timestamp on update": "当记录更新时自动设置字段值为当前时间",
|
||||||
"Default value to current server time": "设置字段默认值为当前服务端时间",
|
"Default value to current server time": "设置字段默认值为当前服务端时间",
|
||||||
"Automatically update timestamp to the current server time on update": "当记录更新时自动设置字段值为当前服务端时间",
|
"Automatically update timestamp to the current server time on update": "当记录更新时自动设置字段值为当前服务端时间",
|
||||||
"Datetime(with time zone)": "日期时间(含时区)",
|
"Datetime (with time zone)": "日期时间(含时区)",
|
||||||
"Datetime(without time zone)": "日期时间(不含时区)",
|
"Datetime (without time zone)": "日期时间(不含时区)",
|
||||||
"DateOnly": "仅日期",
|
"DateOnly": "仅日期",
|
||||||
"Enable secondary confirmation": "启用二次确认",
|
"Enable secondary confirmation": "启用二次确认",
|
||||||
"Content": "内容",
|
"Content": "内容",
|
||||||
|
@ -82,14 +82,13 @@ const useTableSelectorProps = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function FileSelector(props) {
|
function FileSelector(props) {
|
||||||
const { disabled, multiple, value, onChange, action, onSelect, quickUpload, selectFile } = props;
|
const { disabled, multiple, value, onChange, action, onSelect, quickUpload, selectFile, ...other } = props;
|
||||||
const { wrapSSR, hashId, componentCls: prefixCls } = useStyles();
|
const { wrapSSR, hashId, componentCls: prefixCls } = useStyles();
|
||||||
const { useFileCollectionStorageRules } = useExpressionScope();
|
const { useFileCollectionStorageRules } = useExpressionScope();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const rules = useFileCollectionStorageRules();
|
const rules = useFileCollectionStorageRules();
|
||||||
// 兼容旧版本
|
// 兼容旧版本
|
||||||
const showSelectButton = selectFile === undefined && quickUpload === undefined;
|
const showSelectButton = selectFile === undefined && quickUpload === undefined;
|
||||||
|
|
||||||
return wrapSSR(
|
return wrapSSR(
|
||||||
<div className={cls(`${prefixCls}-wrapper`, `${prefixCls}-picture-card-wrapper`, 'nb-upload', hashId)}>
|
<div className={cls(`${prefixCls}-wrapper`, `${prefixCls}-picture-card-wrapper`, 'nb-upload', hashId)}>
|
||||||
<div className={cls(`${prefixCls}-list`, `${prefixCls}-list-picture-card`)}>
|
<div className={cls(`${prefixCls}-list`, `${prefixCls}-list-picture-card`)}>
|
||||||
@ -122,6 +121,8 @@ function FileSelector(props) {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
action={action}
|
action={action}
|
||||||
rules={rules}
|
rules={rules}
|
||||||
|
disabled={disabled}
|
||||||
|
{...other}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{selectFile && (multiple || !value) ? (
|
{selectFile && (multiple || !value) ? (
|
||||||
@ -216,6 +217,7 @@ const InternalFileManager = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div style={{ width: '100%', overflow: 'auto' }}>
|
<div style={{ width: '100%', overflow: 'auto' }}>
|
||||||
<FileSelector
|
<FileSelector
|
||||||
|
{...others}
|
||||||
value={multiple ? options : options?.[0]}
|
value={multiple ? options : options?.[0]}
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
quickUpload={fieldSchema['x-component-props']?.quickUpload !== false}
|
quickUpload={fieldSchema['x-component-props']?.quickUpload !== false}
|
||||||
@ -268,4 +270,4 @@ const FileManageReadPretty = connect((props) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export { FileManageReadPretty, InternalFileManager };
|
export { FileManageReadPretty, InternalFileManager, FileSelector };
|
||||||
|
@ -14,7 +14,7 @@ import { InternalPicker } from './InternalPicker';
|
|||||||
import { Nester } from './Nester';
|
import { Nester } from './Nester';
|
||||||
import { ReadPretty } from './ReadPretty';
|
import { ReadPretty } from './ReadPretty';
|
||||||
import { SubTable } from './SubTable';
|
import { SubTable } from './SubTable';
|
||||||
|
import { FileSelector } from './FileManager';
|
||||||
export {
|
export {
|
||||||
AssociationFieldMode,
|
AssociationFieldMode,
|
||||||
AssociationFieldModeProvider,
|
AssociationFieldModeProvider,
|
||||||
@ -29,3 +29,4 @@ AssociationField.Selector = Action.Container;
|
|||||||
AssociationField.Viewer = Action.Container;
|
AssociationField.Viewer = Action.Container;
|
||||||
AssociationField.InternalSelect = InternalPicker;
|
AssociationField.InternalSelect = InternalPicker;
|
||||||
AssociationField.ReadPretty = ReadPretty;
|
AssociationField.ReadPretty = ReadPretty;
|
||||||
|
AssociationField.FileSelector = FileSelector;
|
||||||
|
@ -21,6 +21,7 @@ import { FieldNames, Select, SelectProps, defaultFieldNames } from '../select';
|
|||||||
import { ReadPretty } from './ReadPretty';
|
import { ReadPretty } from './ReadPretty';
|
||||||
import { useDataSourceHeaders } from '../../../data-source/utils';
|
import { useDataSourceHeaders } from '../../../data-source/utils';
|
||||||
import { useDataSourceKey } from '../../../data-source/data-source/DataSourceProvider';
|
import { useDataSourceKey } from '../../../data-source/data-source/DataSourceProvider';
|
||||||
|
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
||||||
const EMPTY = 'N/A';
|
const EMPTY = 'N/A';
|
||||||
|
|
||||||
export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
|
export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
|
||||||
@ -44,250 +45,254 @@ export type RemoteSelectProps<P = any> = SelectProps<P, any> & {
|
|||||||
dataSource?: string;
|
dataSource?: string;
|
||||||
CustomDropdownRender?: (v: any) => any;
|
CustomDropdownRender?: (v: any) => any;
|
||||||
optionFilter?: (option: any) => boolean;
|
optionFilter?: (option: any) => boolean;
|
||||||
|
toOptionsItem?: (data) => any;
|
||||||
};
|
};
|
||||||
|
|
||||||
const InternalRemoteSelect = connect(
|
const InternalRemoteSelect = withDynamicSchemaProps(
|
||||||
(props: RemoteSelectProps) => {
|
connect(
|
||||||
const {
|
(props: RemoteSelectProps) => {
|
||||||
fieldNames = {} as FieldNames,
|
const {
|
||||||
service = {},
|
fieldNames = {} as FieldNames,
|
||||||
wait = 300,
|
service = {},
|
||||||
value,
|
wait = 300,
|
||||||
defaultValue,
|
value,
|
||||||
objectValue,
|
defaultValue,
|
||||||
manual = true,
|
objectValue,
|
||||||
mapOptions,
|
manual = true,
|
||||||
targetField: _targetField,
|
mapOptions,
|
||||||
CustomDropdownRender,
|
targetField: _targetField,
|
||||||
optionFilter,
|
CustomDropdownRender,
|
||||||
dataSource: propsDataSource,
|
optionFilter,
|
||||||
...others
|
dataSource: propsDataSource,
|
||||||
} = props;
|
toOptionsItem = (value) => value,
|
||||||
const dataSource = useDataSourceKey();
|
...others
|
||||||
const headers = useDataSourceHeaders(propsDataSource || dataSource);
|
} = props;
|
||||||
const [open, setOpen] = useState(false);
|
const dataSource = useDataSourceKey();
|
||||||
const firstRun = useRef(false);
|
const headers = useDataSourceHeaders(propsDataSource || dataSource);
|
||||||
const fieldSchema = useFieldSchema();
|
const [open, setOpen] = useState(false);
|
||||||
const isQuickAdd = fieldSchema['x-component-props']?.addMode === 'quickAdd';
|
const firstRun = useRef(false);
|
||||||
const { getField } = useCollection_deprecated();
|
const fieldSchema = useFieldSchema();
|
||||||
const searchData = useRef(null);
|
const isQuickAdd = fieldSchema['x-component-props']?.addMode === 'quickAdd';
|
||||||
const { getCollectionJoinField, getInterface } = useCollectionManager_deprecated();
|
const { getField } = useCollection_deprecated();
|
||||||
const colletionFieldName = fieldSchema['x-collection-field'] || fieldSchema.name;
|
const searchData = useRef(null);
|
||||||
const collectionField = getField(colletionFieldName) || getCollectionJoinField(colletionFieldName);
|
const { getCollectionJoinField, getInterface } = useCollectionManager_deprecated();
|
||||||
const targetField =
|
const colletionFieldName = fieldSchema['x-collection-field'] || fieldSchema.name;
|
||||||
_targetField ||
|
const collectionField = getField(colletionFieldName) || getCollectionJoinField(colletionFieldName);
|
||||||
(collectionField?.target &&
|
const targetField =
|
||||||
fieldNames?.label &&
|
_targetField ||
|
||||||
getCollectionJoinField(`${collectionField.target}.${fieldNames.label}`));
|
(collectionField?.target &&
|
||||||
|
fieldNames?.label &&
|
||||||
|
getCollectionJoinField(`${collectionField.target}.${fieldNames.label}`));
|
||||||
|
|
||||||
const operator = useMemo(() => {
|
const operator = useMemo(() => {
|
||||||
if (targetField?.interface) {
|
if (targetField?.interface) {
|
||||||
return getInterface(targetField.interface)?.filterable?.operators[0].value || '$includes';
|
return getInterface(targetField.interface)?.filterable?.operators[0].value || '$includes';
|
||||||
}
|
}
|
||||||
return '$includes';
|
return '$includes';
|
||||||
}, [targetField]);
|
}, [targetField]);
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
|
||||||
const mapOptionsToTags = useCallback(
|
const mapOptionsToTags = useCallback(
|
||||||
(options) => {
|
(options) => {
|
||||||
try {
|
try {
|
||||||
return options
|
return options
|
||||||
.filter((v) => {
|
.filter((v) => {
|
||||||
return ['number', 'string'].includes(typeof v[fieldNames.value]) || !v[fieldNames.value];
|
return ['number', 'string'].includes(typeof v[fieldNames.value]) || !v[fieldNames.value];
|
||||||
})
|
})
|
||||||
.map((option) => {
|
.map((option) => {
|
||||||
let label = compile(option[fieldNames.label]);
|
let label = compile(option[fieldNames.label]);
|
||||||
|
|
||||||
if (targetField?.uiSchema?.enum) {
|
if (targetField?.uiSchema?.enum) {
|
||||||
if (Array.isArray(label)) {
|
if (Array.isArray(label)) {
|
||||||
label = label
|
label = label
|
||||||
.map((item, index) => {
|
.map((item, index) => {
|
||||||
const option = targetField.uiSchema.enum.find((i) => i.value === item);
|
const option = targetField.uiSchema.enum.find((i) => i.value === item);
|
||||||
if (option) {
|
if (option) {
|
||||||
return (
|
return (
|
||||||
<Tag role="button" key={index} color={option.color} style={{ marginRight: 3 }}>
|
<Tag role="button" key={index} color={option.color} style={{ marginRight: 3 }}>
|
||||||
{option?.label || item}
|
{option?.label || item}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Tag role="button" key={item}>
|
<Tag role="button" key={item}>
|
||||||
{item}
|
{item}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.reverse();
|
.reverse();
|
||||||
} else {
|
} else {
|
||||||
const item = targetField.uiSchema.enum.find((i) => i.value === label);
|
const item = targetField.uiSchema.enum.find((i) => i.value === label);
|
||||||
if (item) {
|
if (item) {
|
||||||
label = (
|
label = (
|
||||||
<Tag role="button" color={item.color}>
|
<Tag role="button" color={item.color}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if (targetField?.type === 'date') {
|
||||||
if (targetField?.type === 'date') {
|
label = dayjs(label).format('YYYY-MM-DD');
|
||||||
label = dayjs(label).format('YYYY-MM-DD');
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (mapOptions) {
|
if (mapOptions) {
|
||||||
return mapOptions({
|
return mapOptions({
|
||||||
|
[fieldNames.label]: label || EMPTY,
|
||||||
|
[fieldNames.value]: option[fieldNames.value],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...option,
|
||||||
[fieldNames.label]: label || EMPTY,
|
[fieldNames.label]: label || EMPTY,
|
||||||
[fieldNames.value]: option[fieldNames.value],
|
[fieldNames.value]: option[fieldNames.value],
|
||||||
});
|
};
|
||||||
}
|
})
|
||||||
return {
|
.filter(Boolean);
|
||||||
...option,
|
} catch (err) {
|
||||||
[fieldNames.label]: label || EMPTY,
|
console.error(err);
|
||||||
[fieldNames.value]: option[fieldNames.value],
|
return options;
|
||||||
};
|
}
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
return options;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[targetField?.uiSchema, fieldNames],
|
|
||||||
);
|
|
||||||
const { data, run, loading } = useRequest(
|
|
||||||
{
|
|
||||||
action: 'list',
|
|
||||||
...service,
|
|
||||||
headers,
|
|
||||||
params: {
|
|
||||||
pageSize: 200,
|
|
||||||
...service?.params,
|
|
||||||
filter: service?.params?.filter,
|
|
||||||
},
|
},
|
||||||
},
|
[targetField?.uiSchema, fieldNames],
|
||||||
{
|
);
|
||||||
manual,
|
const { data, run, loading } = useRequest(
|
||||||
debounceWait: wait,
|
{
|
||||||
},
|
action: 'list',
|
||||||
);
|
...service,
|
||||||
const runDep = useMemo(
|
headers,
|
||||||
() =>
|
params: {
|
||||||
JSON.stringify({
|
pageSize: 200,
|
||||||
service,
|
...service?.params,
|
||||||
fieldNames,
|
filter: service?.params?.filter,
|
||||||
}),
|
},
|
||||||
[service, fieldNames],
|
},
|
||||||
);
|
{
|
||||||
const CustomRenderCom = useCallback(() => {
|
manual,
|
||||||
if (searchData.current && CustomDropdownRender) {
|
debounceWait: wait,
|
||||||
return (
|
},
|
||||||
<CustomDropdownRender
|
);
|
||||||
search={searchData.current}
|
const runDep = useMemo(
|
||||||
callBack={() => {
|
() =>
|
||||||
searchData.current = null;
|
JSON.stringify({
|
||||||
setOpen(false);
|
service,
|
||||||
}}
|
fieldNames,
|
||||||
/>
|
}),
|
||||||
);
|
[service, fieldNames],
|
||||||
}
|
);
|
||||||
return null;
|
const CustomRenderCom = useCallback(() => {
|
||||||
}, [searchData.current]);
|
if (searchData.current && CustomDropdownRender) {
|
||||||
|
|
||||||
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 }])) || [];
|
|
||||||
const filtered = typeof optionFilter === 'function' ? data.data.filter(optionFilter) : data.data;
|
|
||||||
return uniqBy(filtered.concat(valueOptions ?? []), fieldNames.value);
|
|
||||||
}, [value, defaultValue, data?.data, fieldNames.value, optionFilter]);
|
|
||||||
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 (
|
return (
|
||||||
<>
|
<CustomDropdownRender
|
||||||
{isQuickAdd ? (
|
search={searchData.current}
|
||||||
<>
|
callBack={() => {
|
||||||
{!(data?.data.length === 0 && searchData?.current) && menu}
|
searchData.current = null;
|
||||||
{data?.data.length > 0 && searchData?.current && !isFullMatch && <Divider style={{ margin: 0 }} />}
|
setOpen(false);
|
||||||
{!isFullMatch && <CustomRenderCom />}
|
}}
|
||||||
</>
|
/>
|
||||||
) : (
|
|
||||||
menu
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}}
|
}
|
||||||
/>
|
return null;
|
||||||
);
|
}, [searchData.current]);
|
||||||
},
|
|
||||||
mapProps(
|
useEffect(() => {
|
||||||
{
|
// Lazy load
|
||||||
dataSource: 'options',
|
if (firstRun.current) {
|
||||||
},
|
run();
|
||||||
(props, field) => {
|
}
|
||||||
const fieldSchema = useFieldSchema();
|
}, [runDep]);
|
||||||
return {
|
|
||||||
...props,
|
const onSearch = async (search) => {
|
||||||
fieldNames: {
|
run({
|
||||||
...defaultFieldNames,
|
filter: mergeFilter([
|
||||||
...props.fieldNames,
|
search
|
||||||
...field.componentProps.fieldNames,
|
? {
|
||||||
...fieldSchema['x-component-props']?.fieldNames,
|
[fieldNames.label]: {
|
||||||
},
|
[operator]: search,
|
||||||
suffixIcon: field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffixIcon,
|
},
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
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 }])) || [];
|
||||||
|
const filtered = typeof optionFilter === 'function' ? data.data.filter(optionFilter) : data.data;
|
||||||
|
return uniqBy(filtered.concat(valueOptions ?? []), fieldNames.value);
|
||||||
|
}, [value, defaultValue, data?.data, fieldNames.value, optionFilter]);
|
||||||
|
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={toOptionsItem(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),
|
||||||
),
|
),
|
||||||
mapReadPretty(ReadPretty),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const RemoteSelect = InternalRemoteSelect as unknown as typeof InternalRemoteSelect & {
|
export const RemoteSelect = InternalRemoteSelect as unknown as typeof InternalRemoteSelect & {
|
||||||
|
@ -18,7 +18,6 @@ import filesize from 'filesize';
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import LightBox from 'react-image-lightbox';
|
import LightBox from 'react-image-lightbox';
|
||||||
import match from 'mime-match';
|
|
||||||
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
|
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
|
||||||
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
|
||||||
import { useProps } from '../../hooks/useProps';
|
import { useProps } from '../../hooks/useProps';
|
||||||
@ -26,9 +25,10 @@ import {
|
|||||||
FILE_SIZE_LIMIT_DEFAULT,
|
FILE_SIZE_LIMIT_DEFAULT,
|
||||||
attachmentFileTypes,
|
attachmentFileTypes,
|
||||||
getThumbnailPlaceholderURL,
|
getThumbnailPlaceholderURL,
|
||||||
|
matchMimetype,
|
||||||
normalizeFile,
|
normalizeFile,
|
||||||
toFileList,
|
toFileList,
|
||||||
toValueItem,
|
toValueItem as toValueItemDefault,
|
||||||
useBeforeUpload,
|
useBeforeUpload,
|
||||||
useUploadProps,
|
useUploadProps,
|
||||||
} from './shared';
|
} from './shared';
|
||||||
@ -37,7 +37,7 @@ import type { ComposedUpload, DraggerProps, DraggerV2Props, UploadProps } from '
|
|||||||
|
|
||||||
attachmentFileTypes.add({
|
attachmentFileTypes.add({
|
||||||
match(file) {
|
match(file) {
|
||||||
return match(file.mimetype || file.type, 'image/*');
|
return matchMimetype(file, 'image/*');
|
||||||
},
|
},
|
||||||
getThumbnailURL(file) {
|
getThumbnailURL(file) {
|
||||||
return file.url ? `${file.url}${file.thumbnailRule || ''}` : URL.createObjectURL(file.originFileObj);
|
return file.url ? `${file.url}${file.thumbnailRule || ''}` : URL.createObjectURL(file.originFileObj);
|
||||||
@ -136,7 +136,7 @@ function IframePreviewer({ index, list, onSwitchIndex }) {
|
|||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{iframePreviewSupportedTypes.some((type) => match(file.mimetype || file.extname, type)) ? (
|
{iframePreviewSupportedTypes.some((type) => matchMimetype(file, type)) ? (
|
||||||
<iframe
|
<iframe
|
||||||
src={file.url}
|
src={file.url}
|
||||||
style={{
|
style={{
|
||||||
@ -174,7 +174,6 @@ function ReadPretty({ value, onChange, disabled, multiple, size }: UploadProps)
|
|||||||
const useUploadStyleVal = (useUploadStyle as any).default ? (useUploadStyle as any).default : useUploadStyle;
|
const useUploadStyleVal = (useUploadStyle as any).default ? (useUploadStyle as any).default : useUploadStyle;
|
||||||
// 加载 antd 的样式
|
// 加载 antd 的样式
|
||||||
useUploadStyleVal(prefixCls);
|
useUploadStyleVal(prefixCls);
|
||||||
|
|
||||||
return wrapSSR(
|
return wrapSSR(
|
||||||
<div
|
<div
|
||||||
className={cls(
|
className={cls(
|
||||||
@ -245,12 +244,12 @@ function AttachmentListItem(props) {
|
|||||||
{file.status === 'uploading' ? t('Uploading') : file.title}
|
{file.status === 'uploading' ? t('Uploading') : file.title}
|
||||||
</span>,
|
</span>,
|
||||||
];
|
];
|
||||||
const wrappedItem = file.id ? (
|
const wrappedItem = file.url ? (
|
||||||
<a target="_blank" rel="noopener noreferrer" href={file.url} onClick={handleClick}>
|
<a target="_blank" rel="noopener noreferrer" href={file.url} onClick={handleClick}>
|
||||||
{item}
|
{item}
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<span className={`${prefixCls}-span`}>{item}</span>
|
<span className={`${prefixCls}-span`}>{item}3</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
@ -264,7 +263,7 @@ function AttachmentListItem(props) {
|
|||||||
<div className={`${prefixCls}-list-item-info`}>{wrappedItem}</div>
|
<div className={`${prefixCls}-list-item-info`}>{wrappedItem}</div>
|
||||||
<span className={`${prefixCls}-list-item-actions`}>
|
<span className={`${prefixCls}-list-item-actions`}>
|
||||||
<Space size={3}>
|
<Space size={3}>
|
||||||
{!readPretty && file.id && (
|
{!readPretty && file.url && (
|
||||||
<Button size={'small'} type={'text'} icon={<DownloadOutlined />} onClick={onDownload} />
|
<Button size={'small'} type={'text'} icon={<DownloadOutlined />} onClick={onDownload} />
|
||||||
)}
|
)}
|
||||||
{!readPretty && !disabled && file.status !== 'uploading' && (
|
{!readPretty && !disabled && file.status !== 'uploading' && (
|
||||||
@ -299,7 +298,6 @@ function Previewer({ index, onSwitchIndex, list }) {
|
|||||||
}
|
}
|
||||||
const file = list[index];
|
const file = list[index];
|
||||||
const { Previewer: Component = IframePreviewer } = attachmentFileTypes.getTypeByFile(file) ?? {};
|
const { Previewer: Component = IframePreviewer } = attachmentFileTypes.getTypeByFile(file) ?? {};
|
||||||
|
|
||||||
return <Component index={index} list={list} onSwitchIndex={onSwitchIndex} />;
|
return <Component index={index} list={list} onSwitchIndex={onSwitchIndex} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -331,12 +329,11 @@ export function AttachmentList(props) {
|
|||||||
},
|
},
|
||||||
[multiple, onChange, value],
|
[multiple, onChange, value],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{fileList.map((file, index) => (
|
{fileList.map((file, index) => (
|
||||||
<AttachmentListItem
|
<AttachmentListItem
|
||||||
key={file.id}
|
key={file.id || file.url}
|
||||||
file={file}
|
file={file}
|
||||||
index={index}
|
index={index}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -351,7 +348,7 @@ export function AttachmentList(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Uploader({ rules, ...props }: UploadProps) {
|
export function Uploader({ rules, ...props }: UploadProps) {
|
||||||
const { disabled, multiple, value, onChange } = props;
|
const { disabled, multiple, value, onChange, toValueItem = toValueItemDefault } = props;
|
||||||
const [pendingList, setPendingList] = useState<any[]>([]);
|
const [pendingList, setPendingList] = useState<any[]>([]);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { componentCls: prefixCls } = useStyles();
|
const { componentCls: prefixCls } = useStyles();
|
||||||
@ -378,7 +375,7 @@ export function Uploader({ rules, ...props }: UploadProps) {
|
|||||||
if (multiple) {
|
if (multiple) {
|
||||||
const uploadedList = info.fileList.filter((file) => file.status === 'done');
|
const uploadedList = info.fileList.filter((file) => file.status === 'done');
|
||||||
if (uploadedList.length) {
|
if (uploadedList.length) {
|
||||||
const valueList = [...(value ?? []), ...uploadedList.map(toValueItem)];
|
const valueList = [...(value ?? []), ...uploadedList.map((v) => toValueItem(v.response?.data))];
|
||||||
onChange?.(valueList);
|
onChange?.(valueList);
|
||||||
}
|
}
|
||||||
setPendingList(info.fileList.filter((file) => file.status !== 'done').map(normalizeFile));
|
setPendingList(info.fileList.filter((file) => file.status !== 'done').map(normalizeFile));
|
||||||
@ -386,7 +383,7 @@ export function Uploader({ rules, ...props }: UploadProps) {
|
|||||||
// NOTE: 用 fileList 里的才有附加的验证状态信息,file 没有(不清楚为何)
|
// NOTE: 用 fileList 里的才有附加的验证状态信息,file 没有(不清楚为何)
|
||||||
const file = info.fileList.find((f) => f.uid === info.file.uid);
|
const file = info.fileList.find((f) => f.uid === info.file.uid);
|
||||||
if (file.status === 'done') {
|
if (file.status === 'done') {
|
||||||
onChange?.(toValueItem(file));
|
onChange?.(toValueItem(file.response?.data));
|
||||||
setPendingList([]);
|
setPendingList([]);
|
||||||
} else {
|
} else {
|
||||||
setPendingList([normalizeFile(file)]);
|
setPendingList([normalizeFile(file)]);
|
||||||
@ -408,7 +405,6 @@ export function Uploader({ rules, ...props }: UploadProps) {
|
|||||||
const sizeHint = useSizeHint(size);
|
const sizeHint = useSizeHint(size);
|
||||||
const selectable =
|
const selectable =
|
||||||
!disabled && (multiple || ((!value || (Array.isArray(value) && !value.length)) && !pendingList.length));
|
!disabled && (multiple || ((!value || (Array.isArray(value) && !value.length)) && !pendingList.length));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{pendingList.map((file, index) => (
|
{pendingList.map((file, index) => (
|
||||||
@ -451,7 +447,6 @@ export function Uploader({ rules, ...props }: UploadProps) {
|
|||||||
|
|
||||||
function Attachment(props: UploadProps) {
|
function Attachment(props: UploadProps) {
|
||||||
const { wrapSSR, hashId, componentCls: prefixCls } = useStyles();
|
const { wrapSSR, hashId, componentCls: prefixCls } = useStyles();
|
||||||
|
|
||||||
return wrapSSR(
|
return wrapSSR(
|
||||||
<div className={cls(`${prefixCls}-wrapper`, `${prefixCls}-picture-card-wrapper`, 'nb-upload', hashId)}>
|
<div className={cls(`${prefixCls}-wrapper`, `${prefixCls}-picture-card-wrapper`, 'nb-upload', hashId)}>
|
||||||
<div className={cls(`${prefixCls}-list`, `${prefixCls}-list-picture-card`)}>
|
<div className={cls(`${prefixCls}-list`, `${prefixCls}-list-picture-card`)}>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
import { isArr, isValid, toArr as toArray } from '@formily/shared';
|
import { isArr, isValid, toArr as toArray } from '@formily/shared';
|
||||||
import { UploadFile } from 'antd/es/upload/interface';
|
import { UploadFile } from 'antd/es/upload/interface';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import mime from 'mime';
|
||||||
import match from 'mime-match';
|
import match from 'mime-match';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useAPIClient } from '../../../api-client';
|
import { useAPIClient } from '../../../api-client';
|
||||||
@ -58,6 +59,17 @@ export class AttachmentFileTypes {
|
|||||||
*/
|
*/
|
||||||
export const attachmentFileTypes = new AttachmentFileTypes();
|
export const attachmentFileTypes = new AttachmentFileTypes();
|
||||||
|
|
||||||
|
export function matchMimetype(file: FileModel, type: string) {
|
||||||
|
if (file.mimetype) {
|
||||||
|
return match(file.mimetype, type);
|
||||||
|
}
|
||||||
|
if (file.url) {
|
||||||
|
const [fileUrl] = file.url.split('?');
|
||||||
|
return match(mime.getType(fileUrl) || '', type);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const toArr = (value) => {
|
const toArr = (value) => {
|
||||||
if (!isValid(value)) {
|
if (!isValid(value)) {
|
||||||
return [];
|
return [];
|
||||||
@ -81,6 +93,9 @@ const testOpts = (ext: RegExp, options: { exclude?: string[]; include?: string[]
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function getThumbnailPlaceholderURL(file, options: any = {}) {
|
export function getThumbnailPlaceholderURL(file, options: any = {}) {
|
||||||
|
if (file.url) {
|
||||||
|
return file.url;
|
||||||
|
}
|
||||||
for (let i = 0; i < UPLOAD_PLACEHOLDER.length; i++) {
|
for (let i = 0; i < UPLOAD_PLACEHOLDER.length; i++) {
|
||||||
// console.log(UPLOAD_PLACEHOLDER[i].ext, testOpts(UPLOAD_PLACEHOLDER[i].ext, options));
|
// console.log(UPLOAD_PLACEHOLDER[i].ext, testOpts(UPLOAD_PLACEHOLDER[i].ext, options));
|
||||||
if (UPLOAD_PLACEHOLDER[i].ext.test(file.extname || file.filename || file.url || file.name)) {
|
if (UPLOAD_PLACEHOLDER[i].ext.test(file.extname || file.filename || file.url || file.name)) {
|
||||||
@ -166,11 +181,16 @@ export function useUploadProps<T extends IUploadProps = UploadProps>(props: T) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toValueItem(file) {
|
export function toValueItem(data) {
|
||||||
return file.response?.data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const toItem = (file) => {
|
export const toItem = (file) => {
|
||||||
|
if (typeof file === 'string') {
|
||||||
|
return {
|
||||||
|
url: file,
|
||||||
|
};
|
||||||
|
}
|
||||||
if (file?.response?.data) {
|
if (file?.response?.data) {
|
||||||
file = {
|
file = {
|
||||||
uid: file.uid,
|
uid: file.uid,
|
||||||
|
@ -19,6 +19,7 @@ export type UploadProps = Omit<AntdUploadProps, 'onChange'> & {
|
|||||||
value?: any;
|
value?: any;
|
||||||
size?: string;
|
size?: string;
|
||||||
rules?: PropsRules;
|
rules?: PropsRules;
|
||||||
|
toValueItem?: function;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DraggerProps = Omit<AntdDraggerProps, 'onChange'> & {
|
export type DraggerProps = Omit<AntdDraggerProps, 'onChange'> & {
|
||||||
|
@ -32,7 +32,7 @@ export const useFieldModeOptions = (props?) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!['o2o', 'oho', 'obo', 'o2m', 'linkTo', 'm2o', 'm2m', 'updatedBy', 'createdBy', 'mbm'].includes(
|
!['o2o', 'oho', 'obo', 'o2m', 'linkTo', 'm2o', 'm2m', 'updatedBy', 'createdBy', 'mbm', 'attachmentURL'].includes(
|
||||||
collectionField.interface,
|
collectionField.interface,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -510,6 +510,7 @@ export const useAssociatedFormItemInitializerFields = (options?: any) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { readPretty = form.readPretty, block = 'Form' } = options || {};
|
const { readPretty = form.readPretty, block = 'Form' } = options || {};
|
||||||
const interfaces = block === 'Form' ? ['m2o'] : ['o2o', 'oho', 'obo', 'm2o'];
|
const interfaces = block === 'Form' ? ['m2o'] : ['o2o', 'oho', 'obo', 'm2o'];
|
||||||
|
|
||||||
const groups = fields
|
const groups = fields
|
||||||
?.filter((field) => {
|
?.filter((field) => {
|
||||||
return interfaces.includes(field.interface);
|
return interfaces.includes(field.interface);
|
||||||
@ -592,8 +593,9 @@ const associationFieldToMenu = (
|
|||||||
export const useFilterAssociatedFormItemInitializerFields = () => {
|
export const useFilterAssociatedFormItemInitializerFields = () => {
|
||||||
const { name, fields } = useCollection_deprecated();
|
const { name, fields } = useCollection_deprecated();
|
||||||
const { getCollectionFields } = useCollectionManager_deprecated();
|
const { getCollectionFields } = useCollectionManager_deprecated();
|
||||||
|
const interfaces = ['o2o', 'oho', 'obo', 'm2o', 'm2m'];
|
||||||
return fields
|
return fields
|
||||||
?.filter((field) => field.target && field.uiSchema)
|
?.filter((field) => field.target && field.uiSchema && interfaces.includes(field.interface))
|
||||||
.map((field) => associationFieldToMenu(field, field.name, name, getCollectionFields, []))
|
.map((field) => associationFieldToMenu(field, field.name, name, getCollectionFields, []))
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
};
|
};
|
||||||
|
@ -28,7 +28,7 @@ export const useFields = (collectionName: string) => {
|
|||||||
return option;
|
return option;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.target) {
|
if (field.target && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(field.type)) {
|
||||||
const targetFields = getCollectionFields(field.target);
|
const targetFields = getCollectionFields(field.target);
|
||||||
const options = getOptions(targetFields, depth + 1).filter(Boolean);
|
const options = getOptions(targetFields, depth + 1).filter(Boolean);
|
||||||
option['children'] = option['children'] || [];
|
option['children'] = option['children'] || [];
|
||||||
|
@ -39,7 +39,7 @@ export const useFields = (collectionName: string) => {
|
|||||||
return option;
|
return option;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.target) {
|
if (field.target && ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'belongsToArray'].includes(field.type)) {
|
||||||
const targetFields = getCollectionFields(field.target);
|
const targetFields = getCollectionFields(field.target);
|
||||||
const options = getOptions(targetFields, depth + 1).filter(Boolean);
|
const options = getOptions(targetFields, depth + 1).filter(Boolean);
|
||||||
option['children'] = option['children'] || [];
|
option['children'] = option['children'] || [];
|
||||||
|
@ -75,8 +75,8 @@ test.describe('configure fields', () => {
|
|||||||
await addField('Attachment');
|
await addField('Attachment');
|
||||||
|
|
||||||
// 添加 date & time 字段
|
// 添加 date & time 字段
|
||||||
await addField('Datetime(with time zone)');
|
await addField('Datetime (with time zone)');
|
||||||
await addField('Datetime(without time zone)');
|
await addField('Datetime (without time zone)');
|
||||||
await addField('DateOnly');
|
await addField('DateOnly');
|
||||||
await addField('Unix Timestamp');
|
await addField('Unix Timestamp');
|
||||||
await addField('Time');
|
await addField('Time');
|
||||||
|
@ -430,8 +430,8 @@ export type FieldInterface =
|
|||||||
| 'Markdown'
|
| 'Markdown'
|
||||||
| 'Rich Text'
|
| 'Rich Text'
|
||||||
| 'Attachment'
|
| 'Attachment'
|
||||||
| 'Datetime(with time zone)'
|
| 'Datetime (with time zone)'
|
||||||
| 'Datetime(without time zone)'
|
| 'Datetime (without time zone)'
|
||||||
| 'Date'
|
| 'Date'
|
||||||
| 'Time'
|
| 'Time'
|
||||||
| 'Unix Timestamp'
|
| 'Unix Timestamp'
|
||||||
|
@ -8,14 +8,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ArrayField } from '@formily/core';
|
import { ArrayField } from '@formily/core';
|
||||||
import { Schema, useField, useFieldSchema } from '@formily/react';
|
import { useField } from '@formily/react';
|
||||||
import { Spin } from 'antd';
|
import { Spin } from 'antd';
|
||||||
import uniq from 'lodash/uniq';
|
|
||||||
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
import React, { createContext, useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
useACLRoleContext,
|
useACLRoleContext,
|
||||||
useCollection_deprecated,
|
useCollection_deprecated,
|
||||||
useCollectionManager_deprecated,
|
|
||||||
FixedBlockWrapper,
|
FixedBlockWrapper,
|
||||||
BlockProvider,
|
BlockProvider,
|
||||||
useBlockRequestContext,
|
useBlockRequestContext,
|
||||||
@ -70,60 +68,8 @@ const InternalKanbanBlockProvider = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const recursiveProperties = (schema: Schema, component = 'CollectionField', associationFields, appends: any = []) => {
|
|
||||||
schema.mapProperties((s: any) => {
|
|
||||||
const name = s.name.toString();
|
|
||||||
if (s['x-component'] === component && !appends.includes(name)) {
|
|
||||||
// 关联字段和关联的关联字段
|
|
||||||
const [firstName] = name.split('.');
|
|
||||||
if (associationFields.has(name)) {
|
|
||||||
appends.push(name);
|
|
||||||
} else if (associationFields.has(firstName) && !appends.includes(firstName)) {
|
|
||||||
appends.push(firstName);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
recursiveProperties(s, component, associationFields, appends);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const useAssociationNames = (collection) => {
|
|
||||||
const { getCollectionFields } = useCollectionManager_deprecated(collection.dataSource);
|
|
||||||
const collectionFields = getCollectionFields(collection);
|
|
||||||
const associationFields = new Set();
|
|
||||||
for (const collectionField of collectionFields) {
|
|
||||||
if (collectionField.target) {
|
|
||||||
associationFields.add(collectionField.name);
|
|
||||||
const fields = getCollectionFields(collectionField.target);
|
|
||||||
for (const field of fields) {
|
|
||||||
if (field.target) {
|
|
||||||
associationFields.add(`${collectionField.name}.${field.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const fieldSchema = useFieldSchema();
|
|
||||||
const kanbanSchema = fieldSchema.reduceProperties((buf, schema) => {
|
|
||||||
if (schema['x-component'].startsWith('Kanban')) {
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
return buf;
|
|
||||||
}, new Schema({}));
|
|
||||||
const gridSchema: any = kanbanSchema?.properties?.card?.properties?.grid;
|
|
||||||
const appends = [];
|
|
||||||
if (gridSchema) {
|
|
||||||
recursiveProperties(gridSchema, 'CollectionField', associationFields, appends);
|
|
||||||
}
|
|
||||||
|
|
||||||
return uniq(appends);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const KanbanBlockProvider = (props) => {
|
export const KanbanBlockProvider = (props) => {
|
||||||
const params = { ...props.params };
|
const params = { ...props.params };
|
||||||
const appends = useAssociationNames(props.association || props.collection);
|
|
||||||
if (!Object.keys(params).includes('appends')) {
|
|
||||||
params['appends'] = appends;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<BlockProvider name="kanban" {...props} params={params}>
|
<BlockProvider name="kanban" {...props} params={params}>
|
||||||
<InternalKanbanBlockProvider {...props} params={params} />
|
<InternalKanbanBlockProvider {...props} params={params} />
|
||||||
|
@ -565,7 +565,7 @@ test.describe('field data entry', () => {
|
|||||||
await page
|
await page
|
||||||
.locator(`button[aria-label^="schema-initializer-Grid-workflowManual:customForm:configureFields-${randomValue}"]`)
|
.locator(`button[aria-label^="schema-initializer-Grid-workflowManual:customForm:configureFields-${randomValue}"]`)
|
||||||
.hover();
|
.hover();
|
||||||
await page.getByRole('menuitem', { name: 'Datetime(with time zone)' }).click();
|
await page.getByRole('menuitem', { name: 'Datetime (with time zone)' }).click();
|
||||||
await page
|
await page
|
||||||
.getByLabel(`block-item-Input-${randomValue}-Field display name`)
|
.getByLabel(`block-item-Input-${randomValue}-Field display name`)
|
||||||
.getByRole('textbox')
|
.getByRole('textbox')
|
||||||
|
Loading…
Reference in New Issue
Block a user