diff --git a/components/config-provider/__tests__/__snapshots__/components.test.js.snap b/components/config-provider/__tests__/__snapshots__/components.test.js.snap
index 9a2be2ad9ca0..f34630449f49 100644
--- a/components/config-provider/__tests__/__snapshots__/components.test.js.snap
+++ b/components/config-provider/__tests__/__snapshots__/components.test.js.snap
@@ -17309,7 +17309,7 @@ exports[`ConfigProvider components Upload configProvider 1`] = `
class="config-upload-list config-upload-list-text"
>
@@ -17379,7 +17412,7 @@ exports[`ConfigProvider components Upload normal 1`] = `
class="ant-upload-list ant-upload-list-text"
>
@@ -17449,7 +17515,7 @@ exports[`ConfigProvider components Upload prefixCls 1`] = `
class="ant-upload-list ant-upload-list-text"
>
diff --git a/components/locale/default.tsx b/components/locale/default.tsx
index eabf51c069f4..012f9b4643cc 100644
--- a/components/locale/default.tsx
+++ b/components/locale/default.tsx
@@ -42,6 +42,7 @@ export default {
removeFile: 'Remove file',
uploadError: 'Upload error',
previewFile: 'Preview file',
+ downloadFile: 'Download file',
},
Empty: {
description: 'No Data',
diff --git a/components/locale/zh_CN.tsx b/components/locale/zh_CN.tsx
index 6dd924069e3f..67534a324a1f 100644
--- a/components/locale/zh_CN.tsx
+++ b/components/locale/zh_CN.tsx
@@ -42,6 +42,7 @@ export default {
removeFile: '删除文件',
uploadError: '上传错误',
previewFile: '预览文件',
+ downloadFile: '下载文件',
},
Empty: {
description: '暂无数据',
diff --git a/components/upload/Upload.tsx b/components/upload/Upload.tsx
index fa4a6ba59b71..8e024c8e4c3c 100644
--- a/components/upload/Upload.tsx
+++ b/components/upload/Upload.tsx
@@ -253,11 +253,12 @@ class Upload extends React.Component {
showUploadList,
listType,
onPreview,
+ onDownload,
previewFile,
disabled,
locale: propLocale,
} = this.props;
- const { showRemoveIcon, showPreviewIcon } = showUploadList as any;
+ const { showRemoveIcon, showPreviewIcon, showDownloadIcon } = showUploadList as any;
const { fileList } = this.state;
return (
{
items={fileList}
previewFile={previewFile}
onPreview={onPreview}
+ onDownload={onDownload}
onRemove={this.handleManualRemove}
showRemoveIcon={!disabled && showRemoveIcon}
showPreviewIcon={showPreviewIcon}
+ showDownloadIcon={showDownloadIcon}
locale={{ ...locale, ...propLocale }}
/>
);
diff --git a/components/upload/UploadList.tsx b/components/upload/UploadList.tsx
index 5ffcf7e0868b..3b6a77b86191 100644
--- a/components/upload/UploadList.tsx
+++ b/components/upload/UploadList.tsx
@@ -16,6 +16,7 @@ export default class UploadList extends React.Component {
showInfo: false,
},
showRemoveIcon: true,
+ showDownloadIcon: true,
showPreviewIcon: true,
previewFile: previewImage,
};
@@ -56,6 +57,15 @@ export default class UploadList extends React.Component {
return onPreview(file);
};
+ handleDownload = (file: UploadFile) => {
+ const { onDownload } = this.props;
+ if (typeof onDownload === 'function') {
+ onDownload(file);
+ } else if (file.url) {
+ window.open(file.url);
+ }
+ };
+
handleClose = (file: UploadFile) => {
const { onRemove } = this.props;
if (onRemove) {
@@ -70,6 +80,7 @@ export default class UploadList extends React.Component {
listType,
showPreviewIcon,
showRemoveIcon,
+ showDownloadIcon,
locale,
progressAttr,
} = this.props;
@@ -125,28 +136,63 @@ export default class UploadList extends React.Component {
const infoUploadingClass = classNames({
[`${prefixCls}-list-item`]: true,
[`${prefixCls}-list-item-${file.status}`]: true,
+ [`${prefixCls}-list-item-list-type-${listType}`]: true,
});
const linkProps =
typeof file.linkProps === 'string' ? JSON.parse(file.linkProps) : file.linkProps;
- const preview = file.url ? (
- this.handlePreview(file, e)}
+
+ const removeIcon = showRemoveIcon ? (
+ this.handleClose(file)} />
+ ) : null;
+ const downloadIcon =
+ showDownloadIcon && file.status === 'done' ? (
+ this.handleDownload(file)}
+ />
+ ) : null;
+ const downloadOrDelete = listType !== 'picture-card' && (
+
- {file.name}
-
+ {downloadIcon && {downloadIcon}}
+ {removeIcon && {removeIcon}}
+
+ );
+ const listItemNameClass = classNames({
+ [`${prefixCls}-list-item-name`]: true,
+ [`${prefixCls}-list-item-name-icon-count-${
+ [downloadIcon, removeIcon].filter(x => x).length
+ }`]: true,
+ });
+ const preview = file.url ? (
+ [
+ this.handlePreview(file, e)}
+ >
+ {file.name}
+ ,
+ downloadOrDelete,
+ ]
) : (
this.handlePreview(file, e)}
title={file.name}
>
{file.name}
+ {downloadOrDelete}
);
const style: React.CSSProperties = {
@@ -165,21 +211,14 @@ export default class UploadList extends React.Component {
) : null;
- const removeIcon = showRemoveIcon ? (
- this.handleClose(file)} />
- ) : null;
- const removeIconClose = showRemoveIcon ? (
- this.handleClose(file)} />
- ) : null;
- const actions =
- listType === 'picture-card' && file.status !== 'uploading' ? (
-
- {previewIcon}
- {removeIcon}
-
- ) : (
- removeIconClose
- );
+
+ const actions = listType === 'picture-card' && file.status !== 'uploading' && (
+
+ {previewIcon}
+ {file.status === 'done' && downloadIcon}
+ {removeIcon}
+
+ );
let message;
if (file.response && typeof file.response === 'string') {
message = file.response;
diff --git a/components/upload/__tests__/__snapshots__/demo.test.js.snap b/components/upload/__tests__/__snapshots__/demo.test.js.snap
index d681b37ab3a0..b2b0219df6e6 100644
--- a/components/upload/__tests__/__snapshots__/demo.test.js.snap
+++ b/components/upload/__tests__/__snapshots__/demo.test.js.snap
@@ -34,7 +34,7 @@ exports[`renders ./components/upload/demo/defaultFileList.md correctly 1`] = `
class="ant-upload-list ant-upload-list-text"
>
@@ -251,7 +325,7 @@ exports[`renders ./components/upload/demo/fileList.md correctly 1`] = `
class="ant-upload-list ant-upload-list-text"
>
@@ -324,7 +431,7 @@ exports[`renders ./components/upload/demo/picture-card.md correctly 1`] = `
class="ant-upload-list ant-upload-list-picture-card"
>
-
-
-
@@ -870,7 +1148,7 @@ exports[`renders ./components/upload/demo/picture-style.md correctly 1`] = `
class="ant-upload-list ant-upload-list-picture"
>
-
-
-
-
-
-
diff --git a/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap b/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap
index 20a9c1020284..b07d0ae5d025 100644
--- a/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap
+++ b/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap
@@ -28,7 +28,7 @@ exports[`Upload List handle error 1`] = `
class="ant-upload-list ant-upload-list-text"
>
-
-
-
@@ -137,7 +145,7 @@ exports[`Upload List should be uploading when upload a file 1`] = `
class="ant-upload-list ant-upload-list-text"
>
-
-
-
@@ -246,7 +262,7 @@ exports[`Upload List should be uploading when upload a file 2`] = `
class="ant-upload-list ant-upload-list-text"
>
-
-
-
@@ -355,7 +404,7 @@ exports[`Upload List should non-image format file preview 1`] = `
class="ant-upload-list ant-upload-list-picture"
>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/components/upload/__tests__/upload.test.js b/components/upload/__tests__/upload.test.js
index 483df225950b..367982907972 100644
--- a/components/upload/__tests__/upload.test.js
+++ b/components/upload/__tests__/upload.test.js
@@ -417,7 +417,7 @@ describe('Upload', () => {
const wrapper = mount();
- wrapper.find('div.ant-upload-list-item i.anticon-close').simulate('click');
+ wrapper.find('div.ant-upload-list-item i.anticon-delete').simulate('click');
setImmediate(() => {
wrapper.update();
@@ -429,6 +429,33 @@ describe('Upload', () => {
});
});
+ it('should not stop download when return use onDownload', done => {
+ const mockRemove = jest.fn(() => false);
+ const props = {
+ onRemove: mockRemove,
+ fileList: [
+ {
+ uid: '-1',
+ name: 'foo.png',
+ status: 'done',
+ url: 'http://www.baidu.com/xxx.png',
+ },
+ ],
+ };
+
+ const wrapper = mount( {}} />);
+
+ wrapper.find('div.ant-upload-list-item i.anticon-download').simulate('click');
+
+ setImmediate(() => {
+ wrapper.update();
+
+ expect(props.fileList).toHaveLength(1);
+ expect(props.fileList[0].status).toBe('done');
+ done();
+ });
+ });
+
// https://github.com/ant-design/ant-design/issues/14439
it('should allow call abort function through upload instance', () => {
const wrapper = mount(
diff --git a/components/upload/__tests__/uploadlist.test.js b/components/upload/__tests__/uploadlist.test.js
index 0f0b73c968e1..d126b609f30d 100644
--- a/components/upload/__tests__/uploadlist.test.js
+++ b/components/upload/__tests__/uploadlist.test.js
@@ -121,7 +121,7 @@ describe('Upload List', () => {
wrapper
.find('.ant-upload-list-item')
.at(0)
- .find('.anticon-close')
+ .find('.anticon-delete')
.simulate('click');
await sleep(400);
wrapper.update();
@@ -202,6 +202,36 @@ describe('Upload List', () => {
expect(handleChange.mock.calls[0][0].fileList).toHaveLength(3);
});
+ it('In the case of listType=picture, the error status does not show the download.', () => {
+ const file = { status: 'error', uid: 'file' };
+ const wrapper = mount(
+
+
+ ,
+ );
+ expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0);
+ });
+
+ it('In the case of listType=picture-card, the error status does not show the download.', () => {
+ const file = { status: 'error', uid: 'file' };
+ const wrapper = mount(
+
+
+ ,
+ );
+ expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0);
+ });
+
+ it('In the case of listType=text, the error status does not show the download.', () => {
+ const file = { status: 'error', uid: 'file' };
+ const wrapper = mount(
+
+
+ ,
+ );
+ expect(wrapper.find('div.ant-upload-list-item i.anticon-download').length).toBe(0);
+ });
+
it('should support onPreview', () => {
const handlePreview = jest.fn();
const wrapper = mount(
@@ -248,6 +278,52 @@ describe('Upload List', () => {
expect(handleChange.mock.calls.length).toBe(2);
});
+ it('should support onDownload', async () => {
+ const handleDownload = jest.fn();
+ const wrapper = mount(
+
+
+ ,
+ );
+ wrapper
+ .find('.anticon-download')
+ .at(0)
+ .simulate('click');
+ });
+
+ it('should support no onDownload', async () => {
+ const wrapper = mount(
+
+
+ ,
+ );
+ wrapper
+ .find('.anticon-download')
+ .at(0)
+ .simulate('click');
+ });
+
describe('should generate thumbUrl from file', () => {
[
{ width: 100, height: 200, name: 'height large than width' },
@@ -431,6 +507,13 @@ describe('Upload List', () => {
expect(wrapper.handlePreview()).toBe(undefined);
});
+ it('return when prop onDownload not exists', () => {
+ const file = new File([''], 'test.txt', { type: 'text/plain' });
+ const items = [{ uid: 'upload-list-item', url: '' }];
+ const wrapper = mount().instance();
+ expect(wrapper.handleDownload(file)).toBe(undefined);
+ });
+
it('previewFile should work correctly', async () => {
const file = new File([''], 'test.txt', { type: 'text/plain' });
const items = [{ uid: 'upload-list-item', url: '' }];
@@ -440,6 +523,20 @@ describe('Upload List', () => {
return wrapper.props.previewFile(file);
});
+ it('downloadFile should work correctly', async () => {
+ const file = new File([''], 'test.txt', { type: 'text/plain' });
+ const items = [{ uid: 'upload-list-item', url: '' }];
+ const wrapper = mount(
+ {}}
+ locale={{ downloadFile: '' }}
+ />,
+ ).instance();
+ return wrapper.props.onDownload(file);
+ });
+
it('extname should work correctly when url not exists', () => {
const items = [{ uid: 'upload-list-item', url: '' }];
const wrapper = mount(
@@ -448,6 +545,21 @@ describe('Upload List', () => {
expect(wrapper.find('.ant-upload-list-item-thumbnail').length).toBe(2);
});
+ it('extname should work correctly when url exists', () => {
+ const items = [{ status: 'done', uid: 'upload-list-item', url: '/example' }];
+ const wrapper = mount(
+ {
+ expect(file.url).toBe('/example');
+ }}
+ items={items}
+ locale={{ downloadFile: '' }}
+ />,
+ );
+ wrapper.find('div.ant-upload-list-item i.anticon-download').simulate('click');
+ });
+
it('when picture-card is loading, icon should render correctly', () => {
const items = [{ status: 'uploading', uid: 'upload-list-item' }];
const wrapper = mount(
diff --git a/components/upload/index.en-US.md b/components/upload/index.en-US.md
index c0b1e33e1223..62671e73eed8 100644
--- a/components/upload/index.en-US.md
+++ b/components/upload/index.en-US.md
@@ -32,13 +32,14 @@ Uploading is the process of publishing information (web pages, text, pictures, v
| multiple | Whether to support selected multiple file. `IE10+` supported. You can select multiple files with CTRL holding down while multiple is set to be true | boolean | false | |
| name | The name of uploading file | string | 'file' | |
| previewFile | Customize preview file logic | (file: File \| Blob) => Promise | - | 3.17.0 |
-| showUploadList | Whether to show default upload list, could be an object to specify `showPreviewIcon` and `showRemoveIcon` individually | Boolean or { showPreviewIcon?: boolean, showRemoveIcon?: boolean } | true | |
+| showUploadList | Whether to show default upload list, could be an object to specify `showPreviewIcon`, `showRemoveIcon` and `showDownloadIcon` individually | Boolean or { showPreviewIcon?: boolean, showDownloadIcon?: boolean, showRemoveIcon?: boolean } | true | |
| supportServerRender | Need to be turned on while the server side is rendering | boolean | false | |
| withCredentials | ajax upload with cookie sent | boolean | false | |
| openFileDialogOnClick | click open file dialog | boolean | true | 3.10.0 |
| onChange | A callback function, can be executed when uploading state is changing, see [onChange](#onChange) | Function | - | |
| onPreview | A callback function, will be executed when file link or preview icon is clicked | Function(file) | - | |
| onRemove | A callback function, will be executed when removing file button is clicked, remove event will be prevented when return value is `false` or a Promise which resolve(false) or reject | Function(file): `boolean | Promise` | - | |
+| onDownload | Click the method to download the file, pass the method to perform the method logic, do not pass the default jump to the new TAB. | Function(file): void | Jump to new TAB | |
| transformFile | Customize transform file before request | Function(file): `string | Blob | File | Promise` | - | 3.21.0 |
### onChange
diff --git a/components/upload/index.zh-CN.md b/components/upload/index.zh-CN.md
index 3a1d5f5b528e..38a732fc4d4b 100644
--- a/components/upload/index.zh-CN.md
+++ b/components/upload/index.zh-CN.md
@@ -33,13 +33,14 @@ title: Upload
| multiple | 是否支持多选文件,`ie10+` 支持。开启后按住 ctrl 可选择多个文件 | boolean | false | |
| name | 发到后台的文件参数名 | string | 'file' | |
| previewFile | 自定义文件预览逻辑 | (file: File \| Blob) => Promise | 无 | 3.17.0 |
-| showUploadList | 是否展示文件列表, 可设为一个对象,用于单独设定 `showPreviewIcon` 和 `showRemoveIcon` | Boolean or { showPreviewIcon?: boolean, showRemoveIcon?: boolean } | true | |
+| showUploadList | 是否展示文件列表, 可设为一个对象,用于单独设定 `showPreviewIcon`, `showRemoveIcon` 和 `showDownloadIcon` | Boolean or { showPreviewIcon?: boolean, showRemoveIcon?: boolean, showDownloadIcon?: boolean } | true | |
| supportServerRender | 服务端渲染时需要打开这个 | boolean | false | |
| withCredentials | 上传请求时是否携带 cookie | boolean | false | |
| openFileDialogOnClick | 点击打开文件对话框 | boolean | true | 3.10.0 |
| onChange | 上传文件改变时的状态,详见 [onChange](#onChange) | Function | 无 | |
| onPreview | 点击文件链接或预览图标时的回调 | Function(file) | 无 | |
| onRemove | 点击移除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象,Promise 对象 resolve(false) 或 reject 时不移除。 | Function(file): `boolean | Promise` | 无 | |
+| onDownload | 点击下载文件时的回调,如果没有指定,则默认跳转到文件 url 对应的标签页。 | Function(file): void | 跳转新标签页 | |
| transformFile | 在上传之前转换文件。支持返回一个 Promise 对象 | Function(file): `string | Blob | File | Promise` | 无 | 3.21.0 |
### onChange
diff --git a/components/upload/interface.tsx b/components/upload/interface.tsx
index ac6ffe7491de..d9b454234ddc 100755
--- a/components/upload/interface.tsx
+++ b/components/upload/interface.tsx
@@ -40,11 +40,13 @@ export interface UploadChangeParam {
export interface ShowUploadListInterface {
showRemoveIcon?: boolean;
showPreviewIcon?: boolean;
+ showDownloadIcon?: boolean;
}
export interface UploadLocale {
uploading?: string;
removeFile?: string;
+ downloadFile?: string;
uploadError?: string;
previewFile?: string;
}
@@ -74,6 +76,7 @@ export interface UploadProps {
listType?: UploadListType;
className?: string;
onPreview?: (file: UploadFile) => void;
+ onDownload?: (file: UploadFile) => void;
onRemove?: (file: UploadFile) => void | boolean | Promise;
supportServerRender?: boolean;
style?: React.CSSProperties;
@@ -96,11 +99,13 @@ export interface UploadState {
export interface UploadListProps {
listType?: UploadListType;
onPreview?: (file: UploadFile) => void;
+ onDownload?: (file: UploadFile) => void;
onRemove?: (file: UploadFile) => void | boolean;
items?: Array;
progressAttr?: Object;
prefixCls?: string;
showRemoveIcon?: boolean;
+ showDownloadIcon?: boolean;
showPreviewIcon?: boolean;
locale: UploadLocale;
previewFile?: PreviewFileHandler;
diff --git a/components/upload/style/index.less b/components/upload/style/index.less
index 328278335903..84605f16538a 100644
--- a/components/upload/style/index.less
+++ b/components/upload/style/index.less
@@ -140,6 +140,16 @@
.@{upload-prefix-cls}-list {
.reset-component;
.clearfix;
+ &-item-list-type-text {
+ &:hover {
+ .@{upload-prefix-cls}-list-item-name-icon-count-1 {
+ padding-right: 14px;
+ }
+ .@{upload-prefix-cls}-list-item-name-icon-count-2 {
+ padding-right: 28px;
+ }
+ }
+ }
&-item {
position: relative;
height: 22px;
@@ -154,6 +164,25 @@
text-overflow: ellipsis;
}
+ &-name-icon-count-1 {
+ padding-right: 14px;
+ }
+
+ &-card-actions {
+ position: absolute;
+ right: 0;
+ opacity: 0;
+ &.picture {
+ top: 25px;
+ line-height: 1;
+ opacity: 1;
+ }
+ .anticon {
+ padding-right: 5px;
+ color: rgba(0, 0, 0, 0.45);
+ }
+ }
+
&-info {
height: 100%;
padding: 0 12px 0 4px;
@@ -196,14 +225,21 @@
opacity: 1;
}
+ &:hover &-card-actions {
+ opacity: 1;
+ }
+
&-error,
&-error .@{iconfont-css-prefix}-paper-clip,
&-error &-name {
color: @error-color;
}
- &-error .@{iconfont-css-prefix}-close {
- color: @error-color !important;
+ &-error &-card-actions {
+ .anticon {
+ padding-right: 5px;
+ color: @error-color;
+ }
opacity: 1;
}
@@ -290,6 +326,14 @@
transition: all 0.3s;
}
+ .@{upload-item}-name-icon-count-1 {
+ padding-right: 18px;
+ }
+
+ .@{upload-item}-name-icon-count-2 {
+ padding-right: 36px;
+ }
+
.@{upload-item}-uploading .@{upload-item}-name {
line-height: 28px;
}
@@ -353,6 +397,7 @@
transition: all 0.3s;
.@{iconfont-css-prefix}-eye-o,
+ .@{iconfont-css-prefix}-download,
.@{iconfont-css-prefix}-delete {
z-index: 10;
width: 16px;