feat: optimize file collection (#1666)
* feat(Upload.DraggerV2): add loading * feat: auto close on uploading over * feat: auto selected when upload is complete * feat: filter selected options * feat: add quick-upload and select-file * fix: should not appear on readPretty mode * fix: radio * fix: compatible with older version * feat: improving the interaction of radio selections * fix: show two butons by default
This commit is contained in:
parent
f35da4a2a0
commit
2050eb5317
@ -45,12 +45,14 @@ export const linkTo: IField = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
availableTypes:['belongsToMany'],
|
availableTypes: ['belongsToMany'],
|
||||||
schemaInitialize(schema: ISchema, { readPretty, block, targetCollection }) {
|
schemaInitialize(schema: ISchema, { readPretty, block, targetCollection }) {
|
||||||
if (targetCollection?.template === 'file') {
|
if (targetCollection?.template === 'file') {
|
||||||
const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' };
|
const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' };
|
||||||
fieldNames.label = 'preview';
|
fieldNames.label = 'preview';
|
||||||
schema['x-component-props']['fieldNames'] = fieldNames;
|
schema['x-component-props']['fieldNames'] = fieldNames;
|
||||||
|
schema['x-component-props'].quickUpload = true;
|
||||||
|
schema['x-component-props'].selectFile = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block === 'Form') {
|
if (block === 'Form') {
|
||||||
|
@ -57,6 +57,8 @@ export const m2m: IField = {
|
|||||||
const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' };
|
const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' };
|
||||||
fieldNames.label = 'preview';
|
fieldNames.label = 'preview';
|
||||||
schema['x-component-props']['fieldNames'] = fieldNames;
|
schema['x-component-props']['fieldNames'] = fieldNames;
|
||||||
|
schema['x-component-props'].quickUpload = true;
|
||||||
|
schema['x-component-props'].selectFile = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block === 'Form') {
|
if (block === 'Form') {
|
||||||
|
@ -56,6 +56,8 @@ export const m2o: IField = {
|
|||||||
const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' };
|
const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' };
|
||||||
fieldNames.label = 'preview';
|
fieldNames.label = 'preview';
|
||||||
schema['x-component-props']['fieldNames'] = fieldNames;
|
schema['x-component-props']['fieldNames'] = fieldNames;
|
||||||
|
schema['x-component-props'].quickUpload = true;
|
||||||
|
schema['x-component-props'].selectFile = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (block === 'Form') {
|
if (block === 'Form') {
|
||||||
|
@ -51,13 +51,7 @@ export const o2m: IField = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
availableTypes: ['hasMany'],
|
availableTypes: ['hasMany'],
|
||||||
schemaInitialize(schema: ISchema, { field, block, readPretty, targetCollection }) {
|
schemaInitialize(schema: ISchema, { field, block, readPretty }) {
|
||||||
if (targetCollection?.template === 'file') {
|
|
||||||
const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' };
|
|
||||||
fieldNames.label = 'preview';
|
|
||||||
schema['x-component-props']['fieldNames'] = fieldNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (block === 'Form') {
|
if (block === 'Form') {
|
||||||
if (schema['x-component'] === 'TableField') {
|
if (schema['x-component'] === 'TableField') {
|
||||||
const association = `${field.collectionName}.${field.name}`;
|
const association = `${field.collectionName}.${field.name}`;
|
||||||
|
@ -118,7 +118,7 @@ export const o2o: IField = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
availableTypes: ['hasOne'],
|
availableTypes: ['hasOne'],
|
||||||
schemaInitialize(schema: ISchema, { field, block, readPretty, action, targetCollection }) {
|
schemaInitialize(schema: ISchema, { field, block, readPretty, action }) {
|
||||||
internalSchameInitialize(schema, { field, block, readPretty, action });
|
internalSchameInitialize(schema, { field, block, readPretty, action });
|
||||||
if (['Table', 'Kanban'].includes(block)) {
|
if (['Table', 'Kanban'].includes(block)) {
|
||||||
schema['x-component-props'] = schema['x-component-props'] || {};
|
schema['x-component-props'] = schema['x-component-props'] || {};
|
||||||
@ -127,12 +127,6 @@ export const o2o: IField = {
|
|||||||
// 预览文件时需要的参数
|
// 预览文件时需要的参数
|
||||||
schema['x-component-props']['size'] = 'small';
|
schema['x-component-props']['size'] = 'small';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetCollection?.template === 'file') {
|
|
||||||
const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' };
|
|
||||||
fieldNames.label = 'preview';
|
|
||||||
schema['x-component-props']['fieldNames'] = fieldNames;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
'uiSchema.title': {
|
'uiSchema.title': {
|
||||||
@ -302,7 +296,7 @@ export const oho: IField = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
schemaInitialize(schema: ISchema, { field, block, readPretty, action, targetCollection }) {
|
schemaInitialize(schema: ISchema, { field, block, readPretty, action }) {
|
||||||
internalSchameInitialize(schema, { field, block, readPretty, action });
|
internalSchameInitialize(schema, { field, block, readPretty, action });
|
||||||
if (['Table', 'Kanban'].includes(block)) {
|
if (['Table', 'Kanban'].includes(block)) {
|
||||||
schema['x-component-props'] = schema['x-component-props'] || {};
|
schema['x-component-props'] = schema['x-component-props'] || {};
|
||||||
@ -311,12 +305,6 @@ export const oho: IField = {
|
|||||||
// 预览文件时需要的参数
|
// 预览文件时需要的参数
|
||||||
schema['x-component-props']['size'] = 'small';
|
schema['x-component-props']['size'] = 'small';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetCollection?.template === 'file') {
|
|
||||||
const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' };
|
|
||||||
fieldNames.label = 'preview';
|
|
||||||
schema['x-component-props']['fieldNames'] = fieldNames;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
'uiSchema.title': {
|
'uiSchema.title': {
|
||||||
@ -491,6 +479,8 @@ export const obo: IField = {
|
|||||||
const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' };
|
const fieldNames = schema['x-component-props']['fieldNames'] || { label: 'preview', value: 'id' };
|
||||||
fieldNames.label = 'preview';
|
fieldNames.label = 'preview';
|
||||||
schema['x-component-props']['fieldNames'] = fieldNames;
|
schema['x-component-props']['fieldNames'] = fieldNames;
|
||||||
|
schema['x-component-props'].quickUpload = true;
|
||||||
|
schema['x-component-props'].selectFile = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -8,7 +8,7 @@ import React, { useContext, useEffect } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider';
|
import { ACLCollectionFieldProvider } from '../../../acl/ACLProvider';
|
||||||
import { BlockRequestContext, useFilterByTk, useFormBlockContext } from '../../../block-provider';
|
import { BlockRequestContext, useFilterByTk, useFormBlockContext } from '../../../block-provider';
|
||||||
import { useCollection, useCollectionManager } from '../../../collection-manager';
|
import { Collection, useCollection, useCollectionManager } from '../../../collection-manager';
|
||||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||||
import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks';
|
import { useCompile, useDesignable, useFieldComponentOptions } from '../../hooks';
|
||||||
import { BlockItem } from '../block-item';
|
import { BlockItem } from '../block-item';
|
||||||
@ -81,6 +81,7 @@ FormItem.Designer = () => {
|
|||||||
const { dn, refresh, insertAdjacent } = useDesignable();
|
const { dn, refresh, insertAdjacent } = useDesignable();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
const collectionField = getField(fieldSchema['name']) || getCollectionJoinField(fieldSchema['x-collection-field']);
|
||||||
|
const targetCollection = getCollection(collectionField.target);
|
||||||
const interfaceConfig = getInterface(collectionField?.interface);
|
const interfaceConfig = getInterface(collectionField?.interface);
|
||||||
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
|
const validateSchema = interfaceConfig?.['validateSchema']?.(fieldSchema);
|
||||||
const originalTitle = collectionField?.uiSchema?.title;
|
const originalTitle = collectionField?.uiSchema?.title;
|
||||||
@ -229,6 +230,46 @@ FormItem.Designer = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{!form?.readPretty && isFileCollection(targetCollection) ? (
|
||||||
|
<SchemaSettings.SwitchItem
|
||||||
|
key="quick-upload"
|
||||||
|
title={t('Quick upload')}
|
||||||
|
checked={fieldSchema['x-component-props']?.quickUpload as boolean}
|
||||||
|
onChange={(value) => {
|
||||||
|
const schema = {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
};
|
||||||
|
field.componentProps.quickUpload = value;
|
||||||
|
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
|
fieldSchema['x-component-props'].quickUpload = value;
|
||||||
|
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{!form?.readPretty && isFileCollection(targetCollection) ? (
|
||||||
|
<SchemaSettings.SwitchItem
|
||||||
|
key="select-file"
|
||||||
|
title={t('Select file')}
|
||||||
|
checked={fieldSchema['x-component-props']?.selectFile as boolean}
|
||||||
|
onChange={(value) => {
|
||||||
|
const schema = {
|
||||||
|
['x-uid']: fieldSchema['x-uid'],
|
||||||
|
};
|
||||||
|
field.componentProps.selectFile = value;
|
||||||
|
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
|
||||||
|
fieldSchema['x-component-props'].selectFile = value;
|
||||||
|
schema['x-component-props'] = fieldSchema['x-component-props'];
|
||||||
|
dn.emit('patch', {
|
||||||
|
schema,
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{form && !form?.readPretty && validateSchema && (
|
{form && !form?.readPretty && validateSchema && (
|
||||||
<SchemaSettings.ModalItem
|
<SchemaSettings.ModalItem
|
||||||
title={t('Set validation rules')}
|
title={t('Set validation rules')}
|
||||||
@ -415,7 +456,7 @@ FormItem.Designer = () => {
|
|||||||
block: 'Form',
|
block: 'Form',
|
||||||
readPretty: field.readPretty,
|
readPretty: field.readPretty,
|
||||||
action: tk ? 'get' : null,
|
action: tk ? 'get' : null,
|
||||||
targetCollection: getCollection(collectionField.target),
|
targetCollection,
|
||||||
});
|
});
|
||||||
|
|
||||||
insertAdjacent('beforeBegin', divWrap(schema), {
|
insertAdjacent('beforeBegin', divWrap(schema), {
|
||||||
@ -547,4 +588,8 @@ FormItem.Designer = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isFileCollection(collection: Collection) {
|
||||||
|
return collection?.template === 'file';
|
||||||
|
}
|
||||||
|
|
||||||
FormItem.FilterFormDesigner = FilterFormDesigner;
|
FormItem.FilterFormDesigner = FilterFormDesigner;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DeleteOutlined, DownloadOutlined, PlusOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, DownloadOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import { connect, mapReadPretty } from '@formily/react';
|
import { connect, mapReadPretty } from '@formily/react';
|
||||||
import { Upload as AntdUpload, Button, Space } from 'antd';
|
import { Upload as AntdUpload, Button, Progress, Space } from 'antd';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { saveAs } from 'file-saver';
|
import { saveAs } from 'file-saver';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
@ -8,25 +8,34 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import Lightbox from 'react-image-lightbox';
|
import Lightbox from 'react-image-lightbox';
|
||||||
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 { ReadPretty } from '../upload/ReadPretty';
|
import { ReadPretty } from '../upload/ReadPretty';
|
||||||
import { isImage, toArr, toFileList } from '../upload/shared';
|
import { isImage, toFileList, useUploadProps } from '../upload/shared';
|
||||||
import '../upload/style.less';
|
import '../upload/style.less';
|
||||||
|
import { UploadProps } from '../upload/type';
|
||||||
|
|
||||||
interface TPreview {
|
type Props = UploadProps & {
|
||||||
(props: any): JSX.Element;
|
/** 是否显示 Upload 按钮 */
|
||||||
Selector: React.FC<any>;
|
quickUpload: boolean;
|
||||||
}
|
/** 是否显示 Select 按钮 */
|
||||||
|
selectFile: boolean;
|
||||||
|
onRemove?: (file) => void;
|
||||||
|
onSelect?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
export const Preview = connect((props) => {
|
export const Preview = connect((props) => {
|
||||||
return <ReadPretty.File {...props} />;
|
return <ReadPretty.File {...props} />;
|
||||||
}, mapReadPretty(ReadPretty.File));
|
}, mapReadPretty(ReadPretty.File));
|
||||||
|
|
||||||
export const FileSelector = (props: any) => {
|
export const FileSelector = (props: Props) => {
|
||||||
const { disabled, multiple, value, onRemove, onSelect } = props;
|
const { disabled, multiple, value, quickUpload, selectFile, onRemove, onSelect } = props;
|
||||||
|
const uploadProps = useUploadProps({ ...props });
|
||||||
const [fileList, setFileList] = useState([]);
|
const [fileList, setFileList] = useState([]);
|
||||||
const [photoIndex, setPhotoIndex] = useState(0);
|
const [photoIndex, setPhotoIndex] = useState(0);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
// 兼容旧版本
|
||||||
|
const showSelectButton = selectFile === undefined && quickUpload === undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFileList(toFileList(value));
|
setFileList(toFileList(value));
|
||||||
}, [value]);
|
}, [value]);
|
||||||
@ -42,11 +51,13 @@ export const FileSelector = (props: any) => {
|
|||||||
onSelect?.();
|
onSelect?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const list = fileList.length ? (multiple ? fileList : [fileList[fileList.length - 1]]) : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={cls('ant-upload-picture-card-wrapper nb-upload')}>
|
<div className={cls('ant-upload-picture-card-wrapper nb-upload')}>
|
||||||
<div className={'ant-upload-list ant-upload-list-picture-card'}>
|
<div className={'ant-upload-list ant-upload-list-picture-card'}>
|
||||||
{fileList.map((file) => {
|
{list.map((file) => {
|
||||||
const handleClick = (e) => {
|
const handleClick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -59,7 +70,7 @@ export const FileSelector = (props: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className={'ant-upload-list-picture-card-container'}>
|
<div key={file.id} className={'ant-upload-list-picture-card-container'}>
|
||||||
<div className="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture-card">
|
<div className="ant-upload-list-item ant-upload-list-item-done ant-upload-list-item-list-type-picture-card">
|
||||||
<div className={'ant-upload-list-item-info'}>
|
<div className={'ant-upload-list-item-info'}>
|
||||||
<span className="ant-upload-span">
|
<span className="ant-upload-span">
|
||||||
@ -108,21 +119,25 @@ export const FileSelector = (props: any) => {
|
|||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</span>
|
</span>
|
||||||
|
{file.status === 'uploading' && (
|
||||||
|
<div className={'ant-upload-list-item-progress'}>
|
||||||
|
<Progress strokeWidth={2} type={'line'} showInfo={false} percent={file.percent} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{!disabled && (multiple || toArr(value).length < 1) && (
|
<>
|
||||||
<div className={'ant-upload-list-picture-card-container'}>
|
{showSelectButton ? (
|
||||||
<AntdUpload
|
<div className={'ant-upload-list-picture-card-container'}>
|
||||||
disabled={disabled}
|
<AntdUpload
|
||||||
multiple={multiple}
|
disabled={disabled}
|
||||||
listType={'picture-card'}
|
multiple={multiple}
|
||||||
fileList={fileList}
|
listType={'picture-card'}
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
onRemove={handleRemove}
|
onRemove={handleRemove}
|
||||||
>
|
>
|
||||||
{!disabled && (multiple || toArr(value).length < 1) && (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -137,10 +152,68 @@ export const FileSelector = (props: any) => {
|
|||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
{t('Select')}
|
{t('Select')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</AntdUpload>
|
||||||
</AntdUpload>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
)}
|
{quickUpload ? (
|
||||||
|
<div className={'ant-upload-list-picture-card-container'}>
|
||||||
|
<AntdUpload
|
||||||
|
{...uploadProps}
|
||||||
|
disabled={disabled}
|
||||||
|
multiple={multiple}
|
||||||
|
listType={'picture-card'}
|
||||||
|
fileList={fileList}
|
||||||
|
showUploadList={false}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
onChange={(info) => {
|
||||||
|
// 如果不在这里 setFileList 的话,会导致 onChange 只会执行一次
|
||||||
|
setFileList([...info.fileList]);
|
||||||
|
uploadProps.onChange?.(info);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
{t('Upload')}
|
||||||
|
</div>
|
||||||
|
</AntdUpload>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{selectFile ? (
|
||||||
|
<div className={'ant-upload-list-picture-card-container'}>
|
||||||
|
<AntdUpload
|
||||||
|
disabled={disabled}
|
||||||
|
multiple={multiple}
|
||||||
|
listType={'picture-card'}
|
||||||
|
showUploadList={false}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
onClick={handleSelect}
|
||||||
|
>
|
||||||
|
<PlusOutlined />
|
||||||
|
{t('Select')}
|
||||||
|
</div>
|
||||||
|
</AntdUpload>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* 预览图片的弹框 */}
|
{/* 预览图片的弹框 */}
|
||||||
|
@ -12,7 +12,8 @@ import { FileSelector } from '../preview';
|
|||||||
import { useFieldNames } from './useFieldNames';
|
import { useFieldNames } from './useFieldNames';
|
||||||
import { getLabelFormatValue, useLabelUiSchema } from './util';
|
import { getLabelFormatValue, useLabelUiSchema } from './util';
|
||||||
|
|
||||||
const RecordPickerContext = createContext(null);
|
export const RecordPickerContext = createContext(null);
|
||||||
|
|
||||||
function flatData(data) {
|
function flatData(data) {
|
||||||
let newArr = [];
|
let newArr = [];
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
@ -27,28 +28,40 @@ function flatData(data) {
|
|||||||
|
|
||||||
const useTableSelectorProps = () => {
|
const useTableSelectorProps = () => {
|
||||||
const field = useField<ArrayField>();
|
const field = useField<ArrayField>();
|
||||||
const { multiple, value, setSelectedRows, selectedRows: rcSelectRows } = useContext(RecordPickerContext);
|
const {
|
||||||
const { onRowSelectionChange, rowKey, ...others } = useTsp();
|
multiple,
|
||||||
|
options = [],
|
||||||
|
setSelectedRows,
|
||||||
|
selectedRows: rcSelectRows = [],
|
||||||
|
onChange,
|
||||||
|
} = useContext(RecordPickerContext);
|
||||||
|
const { onRowSelectionChange, rowKey = 'id', ...others } = useTsp();
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...others,
|
...others,
|
||||||
rowKey,
|
rowKey,
|
||||||
rowSelection: {
|
rowSelection: {
|
||||||
type: multiple ? 'checkbox' : 'radio',
|
type: multiple ? 'checkbox' : 'radio',
|
||||||
// defaultSelectedRowKeys: rcSelectRows?.map((item) => item[rowKey||'id']),
|
selectedRowKeys: rcSelectRows
|
||||||
selectedRowKeys: rcSelectRows?.map((item) => item[rowKey || 'id']),
|
.filter((item) => options.every((row) => row[rowKey] !== item[rowKey]))
|
||||||
|
.map((item) => item[rowKey]),
|
||||||
},
|
},
|
||||||
|
dataSource: field.value?.filter((item) => options.every((row) => row[rowKey] !== item[rowKey])),
|
||||||
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
onRowSelectionChange(selectedRowKeys, selectedRows) {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
const scopeRows = flatData(field.value) || [];
|
const scopeRows = flatData(field.value) || [];
|
||||||
const allSelectedRows = rcSelectRows || [];
|
const allSelectedRows = rcSelectRows || [];
|
||||||
const otherRows = differenceBy(allSelectedRows, scopeRows, rowKey || 'id');
|
const otherRows = differenceBy(allSelectedRows, scopeRows, rowKey);
|
||||||
const unionSelectedRows = unionBy(otherRows, selectedRows, rowKey || 'id');
|
const unionSelectedRows = unionBy(otherRows, selectedRows, rowKey);
|
||||||
const unionSelectedRowKeys = unionSelectedRows.map((item) => item[rowKey || 'id']);
|
const unionSelectedRowKeys = unionSelectedRows.map((item) => item[rowKey]);
|
||||||
setSelectedRows?.(unionSelectedRows);
|
setSelectedRows?.(unionSelectedRows);
|
||||||
onRowSelectionChange?.(unionSelectedRowKeys, unionSelectedRows);
|
onRowSelectionChange?.(unionSelectedRowKeys, unionSelectedRows);
|
||||||
} else {
|
} else {
|
||||||
setSelectedRows?.(selectedRows);
|
setSelectedRows?.(selectedRows);
|
||||||
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
onRowSelectionChange?.(selectedRowKeys, selectedRows);
|
||||||
|
onChange(selectedRows?.[0] || null);
|
||||||
|
setVisible(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -56,11 +69,11 @@ const useTableSelectorProps = () => {
|
|||||||
|
|
||||||
const usePickActionProps = () => {
|
const usePickActionProps = () => {
|
||||||
const { setVisible } = useActionContext();
|
const { setVisible } = useActionContext();
|
||||||
const { multiple, selectedRows, onChange } = useContext(RecordPickerContext);
|
const { multiple, selectedRows, onChange, options } = useContext(RecordPickerContext);
|
||||||
return {
|
return {
|
||||||
onClick() {
|
onClick() {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
onChange(selectedRows);
|
onChange([...options, ...selectedRows]);
|
||||||
} else {
|
} else {
|
||||||
onChange(selectedRows?.[0] || null);
|
onChange(selectedRows?.[0] || null);
|
||||||
}
|
}
|
||||||
@ -79,8 +92,20 @@ const useAssociation = (props) => {
|
|||||||
return getField(fieldSchema.name);
|
return getField(fieldSchema.name);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InputRecordPicker: React.FC<any> = (props) => {
|
interface IRecordPickerProps {
|
||||||
const { value, multiple, onChange, ...others } = props;
|
multiple?: boolean;
|
||||||
|
association?: string;
|
||||||
|
value?: any;
|
||||||
|
/** 是否显示 Upload 按钮(仅适用于文件场景下) */
|
||||||
|
quickUpload?: boolean;
|
||||||
|
/** 是否显示 Select 按钮(仅适用于文件场景下) */
|
||||||
|
selectFile?: boolean;
|
||||||
|
onChange?: (value: any) => void;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputRecordPicker: React.FC<any> = (props: IRecordPickerProps) => {
|
||||||
|
const { value, multiple, onChange, quickUpload, selectFile, ...others } = props;
|
||||||
const fieldNames = useFieldNames(props);
|
const fieldNames = useFieldNames(props);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
@ -101,7 +126,6 @@ export const InputRecordPicker: React.FC<any> = (props) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
setOptions(opts);
|
setOptions(opts);
|
||||||
setSelectedRows(opts);
|
|
||||||
}
|
}
|
||||||
}, [value, fieldNames?.label]);
|
}, [value, fieldNames?.label]);
|
||||||
|
|
||||||
@ -113,13 +137,12 @@ export const InputRecordPicker: React.FC<any> = (props) => {
|
|||||||
|
|
||||||
const handleSelect = () => {
|
const handleSelect = () => {
|
||||||
setVisible(true);
|
setVisible(true);
|
||||||
setSelectedRows(options);
|
setSelectedRows([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = (file) => {
|
const handleRemove = (file) => {
|
||||||
const newOptions = options.filter((option) => option.id !== file.id);
|
const newOptions = options.filter((option) => option.id !== file.id);
|
||||||
setOptions(newOptions);
|
setOptions(newOptions);
|
||||||
setSelectedRows(newOptions);
|
|
||||||
if (newOptions.length === 0) {
|
if (newOptions.length === 0) {
|
||||||
return onChange(null);
|
return onChange(null);
|
||||||
}
|
}
|
||||||
@ -129,7 +152,25 @@ export const InputRecordPicker: React.FC<any> = (props) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{showFilePicker ? (
|
{showFilePicker ? (
|
||||||
<FileSelector value={options} multiple onSelect={handleSelect} onRemove={handleRemove} />
|
<FileSelector
|
||||||
|
value={options}
|
||||||
|
multiple={multiple}
|
||||||
|
quickUpload={quickUpload}
|
||||||
|
selectFile={selectFile}
|
||||||
|
action={`${collectionField.target}:create`}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
onRemove={handleRemove}
|
||||||
|
onChange={(changed) => {
|
||||||
|
if (changed.every((file) => file.status !== 'uploading')) {
|
||||||
|
changed = changed.filter((file) => file.status === 'done').map((file) => file.response.data);
|
||||||
|
if (multiple) {
|
||||||
|
onChange([...options, ...changed]);
|
||||||
|
} else {
|
||||||
|
onChange(changed[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Select
|
<Select
|
||||||
{...others}
|
{...others}
|
||||||
@ -157,7 +198,17 @@ export const InputRecordPicker: React.FC<any> = (props) => {
|
|||||||
open={false}
|
open={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{Drawer({ multiple, onChange, selectedRows, setSelectedRows, collectionField, visible, setVisible, fieldSchema })}
|
{Drawer({
|
||||||
|
multiple,
|
||||||
|
onChange,
|
||||||
|
selectedRows,
|
||||||
|
setSelectedRows,
|
||||||
|
collectionField,
|
||||||
|
visible,
|
||||||
|
setVisible,
|
||||||
|
fieldSchema,
|
||||||
|
options,
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -171,9 +222,20 @@ const Drawer: React.FunctionComponent<{
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
fieldSchema;
|
fieldSchema;
|
||||||
}> = ({ multiple, onChange, selectedRows, setSelectedRows, collectionField, visible, setVisible, fieldSchema }) => {
|
options: any[];
|
||||||
|
}> = ({
|
||||||
|
multiple,
|
||||||
|
onChange,
|
||||||
|
selectedRows,
|
||||||
|
setSelectedRows,
|
||||||
|
collectionField,
|
||||||
|
visible,
|
||||||
|
setVisible,
|
||||||
|
fieldSchema,
|
||||||
|
options,
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<RecordPickerContext.Provider value={{ multiple, onChange, selectedRows, setSelectedRows }}>
|
<RecordPickerContext.Provider value={{ multiple, onChange, selectedRows, setSelectedRows, options }}>
|
||||||
<CollectionProvider allowNull name={collectionField?.target}>
|
<CollectionProvider allowNull name={collectionField?.target}>
|
||||||
<ActionContext.Provider value={{ openMode: 'drawer', visible, setVisible }}>
|
<ActionContext.Provider value={{ openMode: 'drawer', visible, setVisible }}>
|
||||||
<FormProvider>
|
<FormProvider>
|
||||||
|
@ -15,7 +15,7 @@ import {
|
|||||||
RecordProvider,
|
RecordProvider,
|
||||||
useSchemaInitializer,
|
useSchemaInitializer,
|
||||||
useTableBlockContext,
|
useTableBlockContext,
|
||||||
useTableSelectorContext
|
useTableSelectorContext,
|
||||||
} from '../../../';
|
} from '../../../';
|
||||||
import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
|
import { useACLFieldWhitelist } from '../../../acl/ACLProvider';
|
||||||
import { extractIndex, getIdsWithChildren, isCollectionFieldComponent, isColumnComponent } from './utils';
|
import { extractIndex, getIdsWithChildren, isCollectionFieldComponent, isColumnComponent } from './utils';
|
||||||
@ -473,6 +473,7 @@ export const Table: any = observer((props: any) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
rowKey={rowKey ?? defaultRowKey}
|
rowKey={rowKey ?? defaultRowKey}
|
||||||
|
dataSource={field?.value?.slice?.()}
|
||||||
{...others}
|
{...others}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
pagination={paginationProps}
|
pagination={paginationProps}
|
||||||
@ -492,7 +493,6 @@ export const Table: any = observer((props: any) => {
|
|||||||
},
|
},
|
||||||
expandedRowKeys: expandedKeys,
|
expandedRowKeys: expandedKeys,
|
||||||
}}
|
}}
|
||||||
dataSource={field?.value?.slice?.()}
|
|
||||||
/>
|
/>
|
||||||
</SortableWrapper>
|
</SortableWrapper>
|
||||||
{field.errors.length > 0 && (
|
{field.errors.length > 0 && (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { DeleteOutlined, DownloadOutlined, InboxOutlined, PlusOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, DownloadOutlined, InboxOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons';
|
||||||
import { usePrefixCls } from '@formily/antd/lib/__builtins__';
|
import { usePrefixCls } from '@formily/antd/lib/__builtins__';
|
||||||
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
||||||
import { Upload as AntdUpload, Button, Progress, Space } from 'antd';
|
import { Upload as AntdUpload, Button, Progress, Space } from 'antd';
|
||||||
@ -24,7 +24,7 @@ export const Upload: ComposedUpload = connect(
|
|||||||
);
|
);
|
||||||
|
|
||||||
Upload.Attachment = connect((props: UploadProps) => {
|
Upload.Attachment = connect((props: UploadProps) => {
|
||||||
const { disabled, multiple, value, onChange, onRemove } = props;
|
const { disabled, multiple, value, onChange } = props;
|
||||||
const [fileList, setFileList] = useState([]);
|
const [fileList, setFileList] = useState([]);
|
||||||
const [sync, setSync] = useState(true);
|
const [sync, setSync] = useState(true);
|
||||||
const images = fileList;
|
const images = fileList;
|
||||||
@ -216,12 +216,24 @@ Upload.DraggerV2 = connect(
|
|||||||
const defaultSubTitle = t('Support for a single or bulk upload, file size should not exceed') + ` 10MB`;
|
const defaultSubTitle = t('Support for a single or bulk upload, file size should not exceed') + ` 10MB`;
|
||||||
const { title = defaultTitle, subTitle = defaultSubTitle, useProps } = props;
|
const { title = defaultTitle, subTitle = defaultSubTitle, useProps } = props;
|
||||||
const extraProps: Record<string, any> = useProps?.() || {};
|
const extraProps: Record<string, any> = useProps?.() || {};
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleChange = (fileList = []) => {
|
||||||
|
const { onChange } = extraProps;
|
||||||
|
onChange?.(fileList);
|
||||||
|
|
||||||
|
if (fileList.some((file) => file.status === 'uploading')) {
|
||||||
|
setLoading(true);
|
||||||
|
} else {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={usePrefixCls('upload-dragger')}>
|
<div className={usePrefixCls('upload-dragger')}>
|
||||||
<AntdUpload.Dragger {...useUploadProps({ ...props, ...extraProps })}>
|
<AntdUpload.Dragger {...useUploadProps({ ...props, ...extraProps, onChange: handleChange })}>
|
||||||
<p className="ant-upload-drag-icon">
|
<p className="ant-upload-drag-icon">
|
||||||
<InboxOutlined />
|
{loading ? <LoadingOutlined style={{ fontSize: 36 }} spin /> : <InboxOutlined />}
|
||||||
</p>
|
</p>
|
||||||
<p className="ant-upload-text">{title}</p>
|
<p className="ant-upload-text">{title}</p>
|
||||||
<p className="ant-upload-hint">{subTitle}</p>
|
<p className="ant-upload-hint">{subTitle}</p>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Field } from '@formily/core';
|
import { Field } from '@formily/core';
|
||||||
import { useField, useForm } from '@formily/react';
|
import { useField } from '@formily/react';
|
||||||
import { reaction } from '@formily/reactive';
|
import { reaction } from '@formily/reactive';
|
||||||
import { isArr, isValid, toArr as toArray } from '@formily/shared';
|
import { isArr, isValid, toArr as toArray } from '@formily/shared';
|
||||||
import { UploadChangeParam } from 'antd/lib/upload';
|
import { UploadChangeParam } from 'antd/lib/upload';
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useBlockRequestContext } from '@nocobase/client';
|
import { RecordPickerContext, useActionContext, useBlockRequestContext } from '@nocobase/client';
|
||||||
import { notification } from 'antd';
|
import { notification } from 'antd';
|
||||||
|
import { useContext } from 'react';
|
||||||
import { useFmTranslation } from '../locale';
|
import { useFmTranslation } from '../locale';
|
||||||
|
|
||||||
// 限制上传文件大小为 10M
|
// 限制上传文件大小为 10M
|
||||||
@ -8,6 +9,8 @@ export const FILE_LIMIT_SIZE = 10 * 1024 * 1024;
|
|||||||
export const useUploadFiles = () => {
|
export const useUploadFiles = () => {
|
||||||
const { service } = useBlockRequestContext();
|
const { service } = useBlockRequestContext();
|
||||||
const { t } = useFmTranslation();
|
const { t } = useFmTranslation();
|
||||||
|
const { setVisible } = useActionContext();
|
||||||
|
const { setSelectedRows } = useContext(RecordPickerContext) || {};
|
||||||
const uploadingFiles = {};
|
const uploadingFiles = {};
|
||||||
|
|
||||||
let pendingNumber = 0;
|
let pendingNumber = 0;
|
||||||
@ -32,13 +35,21 @@ export const useUploadFiles = () => {
|
|||||||
pendingNumber++;
|
pendingNumber++;
|
||||||
uploadingFiles[file.uid] = true;
|
uploadingFiles[file.uid] = true;
|
||||||
}
|
}
|
||||||
if (file.status === 'done' && uploadingFiles[file.uid]) {
|
if (file.status !== 'uploading' && uploadingFiles[file.uid]) {
|
||||||
delete uploadingFiles[file.uid];
|
delete uploadingFiles[file.uid];
|
||||||
if (--pendingNumber === 0) {
|
if (--pendingNumber === 0) {
|
||||||
service?.refresh?.();
|
service?.refresh?.();
|
||||||
|
setSelectedRows?.((preRows) => [
|
||||||
|
...preRows,
|
||||||
|
...fileList.filter((file) => file.status === 'done').map((file) => file.response.data),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (fileList.every((file) => file.status === 'done')) {
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -12,7 +12,7 @@ export const UploadActionInitializer = (props) => {
|
|||||||
'x-component': 'Action',
|
'x-component': 'Action',
|
||||||
'x-decorator': 'ACLActionProvider',
|
'x-decorator': 'ACLActionProvider',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
openMode: 'modal',
|
openMode: 'drawer',
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
icon: 'UploadOutlined',
|
icon: 'UploadOutlined',
|
||||||
},
|
},
|
||||||
@ -27,10 +27,11 @@ export const UploadActionInitializer = (props) => {
|
|||||||
title: '{{ t("Upload files") }}',
|
title: '{{ t("Upload files") }}',
|
||||||
'x-component': 'Upload.DraggerV2',
|
'x-component': 'Upload.DraggerV2',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
height: '60vh',
|
height: '50vh',
|
||||||
action: `${collection.name}:create`,
|
action: `${collection.name}:create`,
|
||||||
useProps: '{{useUploadFiles}}',
|
useProps: '{{useUploadFiles}}',
|
||||||
multiple: true,
|
multiple: true,
|
||||||
|
listType: 'picture',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user