From f659d23d22d49c84957125e3827d6d16a38eb464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=B6=E6=9E=AB?= Date: Wed, 11 Sep 2019 21:46:56 +0800 Subject: [PATCH] feat: upload add download icon (#18664) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: upload add download icon * feat: clear up * feat: log * empty * feat: 理解错误 * feat: test * test * feat: clean up * Update components.test.js.snap * Update demo.test.js.snap * Update demo.test.js.snap * Update demo.test.js.snap * Update demo.test.js.snap * Update work-with-us.zh-CN.md * feat: clean up * fix: showDownloadIcon=false 24px * fix: More rigorous * fix: test * fix: test * fix: [] key * fix: hover css * fix: test --- .../__snapshots__/components.test.js.snap | 237 +++-- components/locale/default.tsx | 1 + components/locale/zh_CN.tsx | 1 + components/upload/Upload.tsx | 5 +- components/upload/UploadList.tsx | 93 +- .../__tests__/__snapshots__/demo.test.js.snap | 732 +++++++++---- .../__snapshots__/uploadlist.test.js.snap | 977 ++++++++++++------ components/upload/__tests__/upload.test.js | 29 +- .../upload/__tests__/uploadlist.test.js | 114 +- components/upload/index.en-US.md | 3 +- components/upload/index.zh-CN.md | 3 +- components/upload/interface.tsx | 5 + components/upload/style/index.less | 49 +- 13 files changed, 1653 insertions(+), 596 deletions(-) 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" >
foo.png + + + + + + +
- - -
@@ -137,7 +145,7 @@ exports[`Upload List should be uploading when upload a file 1`] = ` class="ant-upload-list ant-upload-list-text" >
foo.png + + + + + + +
- - -
@@ -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;