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:
被雨水过滤的空气-Rairn 2023-04-09 16:46:53 +08:00 committed by GitHub
parent f35da4a2a0
commit 2050eb5317
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 273 additions and 79 deletions

View File

@ -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') {

View File

@ -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') {

View File

@ -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') {

View File

@ -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}`;

View File

@ -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: {

View File

@ -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;

View File

@ -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>
{/* 预览图片的弹框 */} {/* 预览图片的弹框 */}

View File

@ -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>

View File

@ -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 && (

View File

@ -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>

View File

@ -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';

View File

@ -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);
}
}, },
}; };
}; };

View File

@ -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',
}, },
}, },
}, },