diff --git a/components/locale/types.ts b/components/locale/types.ts index bbe8912c75..f4cb30870a 100644 --- a/components/locale/types.ts +++ b/components/locale/types.ts @@ -156,6 +156,12 @@ export interface BaseLocale extends LocaleConfig { upload: { delete: string; }; + image: { + cancel: string; + addPhoto: string; + download: string; + delete: string; + }; }; Search: { buttonText: string; diff --git a/components/upload/__docs__/adaptor/index.jsx b/components/upload/__docs__/adaptor/index.jsx deleted file mode 100644 index 679398a6ce..0000000000 --- a/components/upload/__docs__/adaptor/index.jsx +++ /dev/null @@ -1,257 +0,0 @@ -import React from 'react'; -import { Types, parseData, NodeType } from '@alifd/adaptor-helper'; -import { Upload } from '@alifd/next'; -import locale from '../../../locale/en-us'; - -const getSize = (size) => { - if (!size) return 0; - const UNIT = ['T', 'G', 'MB', 'KB', 'B']; - for (let i = 0; i < UNIT.length; i++) { - const unit = UNIT[i]; - const index = size.indexOf(unit); - if (index !== -1) { - const number = Number(size.substring(0, index)) || 0; - switch (i) { - case 0: - return number * 1024 * 1024 * 1024 * 1024; - case 1: - return number * 1024 * 1024 * 1024; - case 2: - return number * 1024 * 1024; - case 3: - return number * 1024; - case 4: - return number; - } - } - } -}; -export default { - name: 'Upload', - shape: [{ - label: 'File Upload', - value: 'normal' - }, 'image', 'card', 'drag'], - editor: (shape = 'normal') => { - return { - normal: { - props: [{ - name: 'state', - label: 'Status', - type: Types.enum, - options: ['uploading', 'done', 'error'], - default: 'uploading' - }, { - name: 'close', - label: 'Close Included', - type: Types.bool, - default: false - }, { - name: 'width', - type: Types.number, - default: 500 - }, { - name: 'progress', - type: Types.number, - default: 50 - }, { - name: 'filesize', - type: Types.string, - default: '11MB' - }, { - name: 'filename', - type: Types.string, - default: 'xxx.png' - }] - }, - image: { - props: [{ - name: 'state', - label: 'Status', - type: Types.enum, - options: ['uploading', 'done', 'error'], - default: 'uploading' - }, { - name: 'close', - label: 'Close Included', - type: Types.bool, - default: false - }, { - name: 'width', - type: Types.number, - default: 500 - }, { - name: 'progress', - type: Types.number, - default: 50 - }, { - name: 'filesize', - type: Types.string, - default: '11MB' - }, { - name: 'filename', - type: Types.string, - default: 'xxx.png' - }] - }, - card: { - props: [{ - name: 'state', - label: 'Status', - type: Types.enum, - options: ['normal', 'disabled', 'uploading', 'done', 'error'], - default: 'string' - }, { - name: 'progress', - type: Types.number, - default: 20 - }, { - name: 'filename', - type: Types.string, - default: 'xxx.png' - }] - }, - drag: { - props: [{ - name: 'state', - label: 'Status', - type: Types.enum, - options: ['normal', 'over', 'uploading', 'disabled', 'done', 'error'], - default: 'over' - }, { - name: 'width', - type: Types.number, - default: 500 - }, { - name: 'height', - type: Types.number, - default: 200 - }, { - name: 'title', - type: Types.string, - default: 'Click or Drag the file to this area to upload' - }, { - name: 'description', - type: Types.string, - default: 'Support docx, xls, PDF, rar, zip, PNG, JPG and other files upload' - }, { - name: 'progress', - type: Types.number, - default: 20 - }], - data: { - default: 'xxx.png\nxx2.png' - } - } - }[shape]; - }, - adaptor: ({ - shape, - state, - close, - width, - height, - progress, - filesize, - filename, - title, - description, - style, - data, - ...others - }) => { - - if (shape === 'normal') { - return ( - - ); - } - - if (shape === 'image') { - return ( - - ); - } - - if (shape === 'card') { - return ( - - ); - } - - const list = parseData(data).filter(({ type }) => type === NodeType.node); - - - return ( - { - return { - name: item.value, - state, - percent: progress - }; - }) : []} - /> - ); - - } -}; diff --git a/components/upload/__docs__/adaptor/index.tsx b/components/upload/__docs__/adaptor/index.tsx new file mode 100644 index 0000000000..2f669dd419 --- /dev/null +++ b/components/upload/__docs__/adaptor/index.tsx @@ -0,0 +1,315 @@ +import React, { type CSSProperties } from 'react'; +import { Types, parseData, NodeType } from '@alifd/adaptor-helper'; +import { Upload } from '@alifd/next'; +import locale from '../../../locale/en-us'; + +const getSize = (size?: string) => { + if (!size) return 0; + const UNIT = ['T', 'G', 'MB', 'KB', 'B']; + for (let i = 0; i < UNIT.length; i++) { + const unit = UNIT[i]; + const index = size.indexOf(unit); + if (index !== -1) { + const number = Number(size.substring(0, index)) || 0; + switch (i) { + case 0: + return number * 1024 * 1024 * 1024 * 1024; + case 1: + return number * 1024 * 1024 * 1024; + case 2: + return number * 1024 * 1024; + case 3: + return number * 1024; + case 4: + return number; + } + } + } +}; +export default { + name: 'Upload', + shape: [ + { + label: 'File Upload', + value: 'normal', + }, + 'image', + 'card', + 'drag', + ], + editor: (shape = 'normal') => { + return { + normal: { + props: [ + { + name: 'state', + label: 'Status', + type: Types.enum, + options: ['uploading', 'done', 'error'], + default: 'uploading', + }, + { + name: 'close', + label: 'Close Included', + type: Types.bool, + default: false, + }, + { + name: 'width', + type: Types.number, + default: 500, + }, + { + name: 'progress', + type: Types.number, + default: 50, + }, + { + name: 'filesize', + type: Types.string, + default: '11MB', + }, + { + name: 'filename', + type: Types.string, + default: 'xxx.png', + }, + ], + }, + image: { + props: [ + { + name: 'state', + label: 'Status', + type: Types.enum, + options: ['uploading', 'done', 'error'], + default: 'uploading', + }, + { + name: 'close', + label: 'Close Included', + type: Types.bool, + default: false, + }, + { + name: 'width', + type: Types.number, + default: 500, + }, + { + name: 'progress', + type: Types.number, + default: 50, + }, + { + name: 'filesize', + type: Types.string, + default: '11MB', + }, + { + name: 'filename', + type: Types.string, + default: 'xxx.png', + }, + ], + }, + card: { + props: [ + { + name: 'state', + label: 'Status', + type: Types.enum, + options: ['normal', 'disabled', 'uploading', 'done', 'error'], + default: 'string', + }, + { + name: 'progress', + type: Types.number, + default: 20, + }, + { + name: 'filename', + type: Types.string, + default: 'xxx.png', + }, + ], + }, + drag: { + props: [ + { + name: 'state', + label: 'Status', + type: Types.enum, + options: ['normal', 'over', 'uploading', 'disabled', 'done', 'error'], + default: 'over', + }, + { + name: 'width', + type: Types.number, + default: 500, + }, + { + name: 'height', + type: Types.number, + default: 200, + }, + { + name: 'title', + type: Types.string, + default: 'Click or Drag the file to this area to upload', + }, + { + name: 'description', + type: Types.string, + default: + 'Support docx, xls, PDF, rar, zip, PNG, JPG and other files upload', + }, + { + name: 'progress', + type: Types.number, + default: 20, + }, + ], + data: { + default: 'xxx.png\nxx2.png', + }, + }, + }[shape]; + }, + adaptor: ({ + shape, + state, + close, + width, + height, + progress, + filesize, + filename, + title, + description, + style, + data, + ...others + }: { + shape?: 'normal' | 'image' | 'card' | 'drag'; + state: 'uploading' | 'done' | 'error' | 'disabled' | 'over'; + close: boolean; + width: number; + height: number; + progress: number; + filesize: string; + filename: string; + title: string; + description: string; + style: CSSProperties; + data: string; + [key: string]: any; + }) => { + if (shape === 'normal') { + return ( + + ); + } + + if (shape === 'image') { + return ( + + ); + } + + if (shape === 'card') { + return ( + + ); + } + + const list = parseData(data).filter(({ type }: any) => type === NodeType.node); + + return ( + { + return { + name: item.value as string, + state, + percent: progress, + }; + }) + : [] + } + /> + ); + }, +}; diff --git a/components/upload/__docs__/demo/accessibility/index.tsx b/components/upload/__docs__/demo/accessibility/index.tsx index 69903d3c2f..f9ec7200d3 100644 --- a/components/upload/__docs__/demo/accessibility/index.tsx +++ b/components/upload/__docs__/demo/accessibility/index.tsx @@ -1,6 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Button } from '@alifd/next'; +import { type UploadProps } from '@alifd/next/types/upload'; + +const onChange: UploadProps['onChange'] = info => { + console.log('onChange callback : ', info); +}; ReactDOM.render( [ @@ -19,6 +24,3 @@ ReactDOM.render( ], mountNode ); -function onChange(info) { - console.log('onChange callback : ', info); -} diff --git a/components/upload/__docs__/demo/after-select/index.tsx b/components/upload/__docs__/demo/after-select/index.tsx index f4b47252c9..91f866a633 100644 --- a/components/upload/__docs__/demo/after-select/index.tsx +++ b/components/upload/__docs__/demo/after-select/index.tsx @@ -1,9 +1,10 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Button, Dialog } from '@alifd/next'; +import { type UploadProps, type UploadFile } from '@alifd/next/types/upload'; -const afterSelect = file => { - return new Promise((resolve, reject) => { +const afterSelect: UploadProps['afterSelect'] = (file: UploadFile) => { + return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const img = new Image(); @@ -13,15 +14,14 @@ const afterSelect = file => { } else { Dialog.alert({ content: `Image width must be 1200px now ${img.width}px!`, - closable: false, title: 'Warning', }); reject(); } }; - img.src = reader.result; + img.src = reader.result as string; }; - reader.readAsDataURL(file.originFileObj); + reader.readAsDataURL(file.originFileObj!); }); }; diff --git a/components/upload/__docs__/demo/base/index.tsx b/components/upload/__docs__/demo/base/index.tsx index c30342310f..78c0f35da4 100644 --- a/components/upload/__docs__/demo/base/index.tsx +++ b/components/upload/__docs__/demo/base/index.tsx @@ -1,8 +1,22 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Button, Icon } from '@alifd/next'; +import { type UploadProps } from '@alifd/next/types/upload'; const style = { display: 'inline-block', marginRight: 10 }; + +const beforeUpload: UploadProps['beforeUpload'] = info => { + console.log('beforeUpload : ', info); +}; + +const onChange: UploadProps['onChange'] = info => { + console.log('onChange : ', info); +}; + +const onSuccess: UploadProps['onSuccess'] = info => { + console.log('onSuccess : ', info); +}; + ReactDOM.render(
- Upload File -
@@ -42,15 +54,3 @@ ReactDOM.render(
, mountNode ); - -function beforeUpload(info) { - console.log('beforeUpload : ', info); -} - -function onChange(info) { - console.log('onChange : ', info); -} - -function onSuccess(info) { - console.log('onSuccess : ', info); -} diff --git a/components/upload/__docs__/demo/beforeupload/index.tsx b/components/upload/__docs__/demo/beforeupload/index.tsx index 4514fe1d0c..362ab21661 100644 --- a/components/upload/__docs__/demo/beforeupload/index.tsx +++ b/components/upload/__docs__/demo/beforeupload/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Button } from '@alifd/next'; +import { type UploadProps } from '@alifd/next/types/upload'; const requestOpts = { action: 'https://www.easy-mock.com/mock/5b713974309d0d7d107a74a3/alifd/upload', @@ -8,23 +9,23 @@ const requestOpts = { headers: { 'X-Requested-With': 12345 }, }; -function beforeUpload(file, options) { +const beforeUpload: UploadProps['beforeUpload'] = (file, options) => { console.log('beforeUpload callback : ', file, options); return requestOpts; -} +}; -async function asyncBeforeUpload(file, options) { +const asyncBeforeUpload: UploadProps['beforeUpload'] = async (file, options) => { console.log('beforeUpload callback : ', file, options); return await new Promise(resolve => { setTimeout(() => { resolve(requestOpts); }, 1e3); }); -} +}; -function onChange(file) { +const onChange: UploadProps['onChange'] = file => { console.log('onChange callback : ', file); -} +}; ReactDOM.render( [ diff --git a/components/upload/__docs__/demo/card/index.tsx b/components/upload/__docs__/demo/card/index.tsx index 75351caa9d..d2fd982a19 100644 --- a/components/upload/__docs__/demo/card/index.tsx +++ b/components/upload/__docs__/demo/card/index.tsx @@ -1,6 +1,23 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload } from '@alifd/next'; +import { type UploadProps } from '@alifd/next/types/upload'; + +const onPreview: UploadProps['onPreview'] = info => { + console.log('onPreview callback : ', info); +}; + +const onChange: UploadProps['onChange'] = info => { + console.log('onChange callback : ', info); +}; + +const onSuccess: UploadProps['onSuccess'] = (res, file) => { + console.log('onSuccess callback : ', res, file); +}; + +const onError: UploadProps['onError'] = file => { + console.log('onError callback : ', file); +}; ReactDOM.render( , mountNode ); - -function onPreview(info) { - console.log('onPreview callback : ', info); -} - -function onChange(info) { - console.log('onChange callback : ', info); -} - -function onSuccess(res, file) { - console.log('onSuccess callback : ', res, file); -} - -function onError(file) { - console.log('onError callback : ', file); -} diff --git a/components/upload/__docs__/demo/crop/index.tsx b/components/upload/__docs__/demo/crop/index.tsx index e6e88bb614..1218f8f0e6 100644 --- a/components/upload/__docs__/demo/crop/index.tsx +++ b/components/upload/__docs__/demo/crop/index.tsx @@ -1,8 +1,10 @@ -import React from 'react'; +import React, { Component } from 'react'; import ReactDOM from 'react-dom'; import { Upload, Button, Dialog } from '@alifd/next'; +// @ts-expect-error Cropper 这个包的types包没有安装,暂不处理 import Cropper from 'react-cropper'; import 'cropperjs/dist/cropper.css'; +import { type UploadOptions } from '@alifd/next/types/upload'; // plan 1: [not work in IE/Edge] IE don't support File Constructor // function dataURL2File(dataURL, filename) { @@ -20,9 +22,9 @@ import 'cropperjs/dist/cropper.css'; // } // plan 2: base64 -> Blob -> File, IE9+ -function dataURL2Blob2File(dataURL, fileName) { +const dataURL2Blob2File = (dataURL: string, fileName: string) => { const arr = dataURL.split(','), - mime = arr[0].match(/:(.*?);/)[1], + mime = arr![0]!.match(/:(.*?);/)![1], bstr = atob(arr[1]), u8arr = new Uint8Array(bstr.length); let n = bstr.length; @@ -32,13 +34,24 @@ function dataURL2Blob2File(dataURL, fileName) { const blob = new Blob([u8arr], { type: mime }); // Blob to File // set lastModifiedDate and name + // @ts-expect-error Bolb没有lastModifiedDate属性,此处是强制转换 blob.lastModifiedDate = new Date(); + // @ts-expect-error Bolb没有name属性,此处是强制转换 blob.name = fileName; - return blob; -} + return blob as File; +}; -class App extends React.Component { - constructor(props) { +class App extends Component< + object, + { + img: string; + visible: boolean; + src: string | null | ArrayBuffer; + } +> { + uploader; + cropperRef: InstanceType; + constructor(props: object) { super(props); this.uploader = new Upload.Uploader({ action: 'http://127.0.0.1:6001/upload.do', @@ -48,19 +61,19 @@ class App extends React.Component { } state = { - src: null, + src: '', visible: false, - img: null, + img: '', }; - onSuccess = value => { + onSuccess: UploadOptions['onSuccess'] = (value: { url: string }) => { console.log(value); this.setState({ img: value.url, }); }; - onSelect = files => { + onSelect = (files: File[]) => { const reader = new FileReader(); reader.onload = () => { this.setState({ @@ -79,7 +92,7 @@ class App extends React.Component { onOk = () => { const data = this.cropperRef.getCroppedCanvas().toDataURL(); - const file = dataURL2Blob2File(data, 'test.png'); + const file: File = dataURL2Blob2File(data, 'test.png'); // start upload this.uploader.startUpload(file); @@ -89,7 +102,7 @@ class App extends React.Component { }); }; - saveCropperrRef = ref => { + saveCropperrRef = (ref: InstanceType) => { this.cropperRef = ref; }; diff --git a/components/upload/__docs__/demo/data/index.tsx b/components/upload/__docs__/demo/data/index.tsx index e5aa395521..c53aaa752a 100644 --- a/components/upload/__docs__/demo/data/index.tsx +++ b/components/upload/__docs__/demo/data/index.tsx @@ -1,6 +1,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Button } from '@alifd/next'; +import { type UploadProps } from '@alifd/next/types/upload'; + +const beforeUpload: UploadProps['beforeUpload'] = info => { + console.log('beforeUpload callback : ', info); +}; + +const onChange: UploadProps['onChange'] = info => { + console.log('onChange callback : ', info); +}; ReactDOM.render( , mountNode ); - -function beforeUpload(info) { - console.log('beforeUpload callback : ', info); -} - -function onChange(info) { - console.log('onChange callback : ', info); -} diff --git a/components/upload/__docs__/demo/directory/index.tsx b/components/upload/__docs__/demo/directory/index.tsx index 6512173029..008d138490 100644 --- a/components/upload/__docs__/demo/directory/index.tsx +++ b/components/upload/__docs__/demo/directory/index.tsx @@ -1,6 +1,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Button, Icon } from '@alifd/next'; +import { type UploadProps } from '@alifd/next/types/upload'; + +const onChange: UploadProps['onChange'] = info => { + console.log('onChange : ', info); +}; + +const onSuccess: UploadProps['onSuccess'] = info => { + console.log('onSuccess : ', info); +}; ReactDOM.render( , mountNode ); - -function onChange(info) { - console.log('onChange : ', info); -} - -function onSuccess(info) { - console.log('onSuccess : ', info); -} diff --git a/components/upload/__docs__/demo/dragger/index.tsx b/components/upload/__docs__/demo/dragger/index.tsx index 80a0c311ff..88ef4b95bf 100644 --- a/components/upload/__docs__/demo/dragger/index.tsx +++ b/components/upload/__docs__/demo/dragger/index.tsx @@ -1,11 +1,22 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Icon, Button } from '@alifd/next'; +import { type DraggerProps } from '@alifd/next/types/upload'; +import { type ButtonProps } from '@alifd/next/types/button'; -function handleClick(e) { +const handleClick: ButtonProps['onClick'] = e => { e.stopPropagation(); // download template -} +}; + +const onDragOver: DraggerProps['onDragOver'] = () => { + console.log('dragover callback'); +}; + +const onDrop: DraggerProps['onDrop'] = fileList => { + console.log('drop callback : ', fileList); +}; + ReactDOM.render(
{ +const showImg = (url: string) => { Dialog.show({ title: 'img preview', content: , @@ -10,7 +11,7 @@ const showImg = url => { }); }; -const actionRender = file => { +const actionRender: UploadProps['actionRender'] = file => { console.log(file); return ( @@ -18,7 +19,7 @@ const actionRender = file => { text onClick={e => { e.preventDefault(); - showImg(file.url); + showImg(file.url!); }} size="large" > @@ -31,7 +32,7 @@ const actionRender = file => { ); }; -const itemRender = (file, { remove }) => { +const itemRender: CardProps['itemRender'] = (file, { remove }) => { console.log(file); return (
@@ -53,7 +54,7 @@ const itemRender = (file, { remove }) => { showImg(file.url)} + onClick={() => showImg(file.url!)} /> 06:08
@@ -76,6 +77,14 @@ const data = [ }, ]; +const beforeUpload: UploadProps['beforeUpload'] = info => { + console.log('beforeUpload callback : ', info); +}; + +const onChange: UploadProps['onChange'] = info => { + console.log('onChange callback : ', info); +}; + ReactDOM.render(
( + fileNameRender={(file: File) => ( {file.name} @@ -112,11 +121,3 @@ ReactDOM.render(
, mountNode ); - -function beforeUpload(info) { - console.log('beforeUpload callback : ', info); -} - -function onChange(info) { - console.log('onChange callback : ', info); -} diff --git a/components/upload/__docs__/demo/image/index.tsx b/components/upload/__docs__/demo/image/index.tsx index dad4f575e1..76f6dd3f5a 100644 --- a/components/upload/__docs__/demo/image/index.tsx +++ b/components/upload/__docs__/demo/image/index.tsx @@ -1,6 +1,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Button } from '@alifd/next'; +import { type UploadProps } from '@alifd/next/types/upload'; + +const beforeUpload: UploadProps['beforeUpload'] = info => { + console.log('beforeUpload callback : ', info); +}; + +const onChange: UploadProps['onChange'] = info => { + console.log('onChange callback : ', info); +}; ReactDOM.render( , mountNode ); - -function beforeUpload(info) { - console.log('beforeUpload callback : ', info); -} - -function onChange(info) { - console.log('onChange callback : ', info); -} diff --git a/components/upload/__docs__/demo/limit/index.tsx b/components/upload/__docs__/demo/limit/index.tsx index 18fab78630..a49fc85e9b 100644 --- a/components/upload/__docs__/demo/limit/index.tsx +++ b/components/upload/__docs__/demo/limit/index.tsx @@ -1,8 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Button } from '@alifd/next'; +import type { UploadProps, UploadError } from '@alifd/next/types/upload'; -const onError = (file, fileList) => { +const onError: UploadProps['onError'] = (file: UploadError, fileList: File[]) => { console.log('Exceed limit', file, fileList); }; diff --git a/components/upload/__docs__/demo/maxsize/index.tsx b/components/upload/__docs__/demo/maxsize/index.tsx index eaad3c9f07..2105fda401 100644 --- a/components/upload/__docs__/demo/maxsize/index.tsx +++ b/components/upload/__docs__/demo/maxsize/index.tsx @@ -1,15 +1,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Dialog, Button } from '@alifd/next'; +import { type UploadProps } from '@alifd/next/types/upload'; -const beforeUpload = file => { - return new Promise((resolve, reject) => { +const beforeUpload: UploadProps['beforeUpload'] = file => { + return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = e => { if (e.total > 2 * 1024 * 1024) { Dialog.alert({ content: `File size must be < 2M`, - closable: false, title: 'Warning', }); reject(); @@ -22,15 +22,14 @@ const beforeUpload = file => { } else { Dialog.alert({ content: `Image width ${img.width}px, Exceed limits!`, - closable: false, title: 'Warning', }); reject(); } }; - img.src = reader.result; + img.src = reader.result as string; }; - reader.readAsDataURL(file); + reader.readAsDataURL(file as File); }); }; diff --git a/components/upload/__docs__/demo/oss/index.tsx b/components/upload/__docs__/demo/oss/index.tsx index 39bb52bba2..0854e3fe22 100644 --- a/components/upload/__docs__/demo/oss/index.tsx +++ b/components/upload/__docs__/demo/oss/index.tsx @@ -1,10 +1,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload } from '@alifd/next'; +import { type UploadProps } from '@alifd/next/types/upload'; class App extends React.Component { - beforeUpload = (file, options) => { - return new Promise((resolve, reject) => { + beforeUpload: UploadProps['beforeUpload'] = (file, options) => { + return new Promise(resolve => { setTimeout(() => { // document: https://help.aliyun.com/document_detail/181756.html?#h2-u6D4Fu89C8u5668u7AEFu76F4u4F20u4EE3u78015 // mock ajax to get host/OSSAccessKeyId/policy/signature/key @@ -36,13 +37,16 @@ class App extends React.Component { }, 300); }); }; - onSuccess = (file, value) => { + + onSuccess = (file: File, value: any) => { console.log(file, value); }; - formatter = (res, file) => ({ + + formatter: UploadProps['formatter'] = (res, file) => ({ success: true, url: file.tempUrl, }); + render() { return ( { + uploaderRef: ReturnType['getInstance']>; + onPaste: InputProps['onPaste'] = e => { e.preventDefault(); - const files = e.clipboardData.files; - files.length && this.uploaderRef.selectFiles(files); + const files = Array.from(e.clipboardData!.files); + files!.length && this.uploaderRef!.selectFiles!(files); }; - saveUploaderRef = ref => { + saveUploaderRef = (ref: InstanceType) => { if (!ref) return; this.uploaderRef = ref.getInstance(); }; - onChange = value => { + onChange: UploadProps['onChange'] = value => { console.log(value); }; diff --git a/components/upload/__docs__/demo/submit/index.tsx b/components/upload/__docs__/demo/submit/index.tsx index ede7303571..66c32bd5db 100644 --- a/components/upload/__docs__/demo/submit/index.tsx +++ b/components/upload/__docs__/demo/submit/index.tsx @@ -1,9 +1,12 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Button, Icon } from '@alifd/next'; +import { type UploadProps } from '@alifd/next/types/upload'; class App extends React.Component { - saveUploaderRef = ref => { + uploaderRef: ReturnType['getInstance']>; + + saveUploaderRef = (ref: InstanceType | null) => { if (!ref) return; this.uploaderRef = ref.getInstance(); }; @@ -11,10 +14,12 @@ class App extends React.Component { onSubmit = () => { this.uploaderRef.startUpload(); }; - beforeUpload(info, options) { + + beforeUpload: UploadProps['beforeUpload'] = (info, options) => { console.log('beforeUpload callback : ', info, options); return options; - } + }; + render() { return (
diff --git a/components/upload/__docs__/demo/text/index.tsx b/components/upload/__docs__/demo/text/index.tsx index cfe6605883..89893d2dcb 100644 --- a/components/upload/__docs__/demo/text/index.tsx +++ b/components/upload/__docs__/demo/text/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Upload, Button } from '@alifd/next'; +import { type UploadProps } from '@alifd/next/types/upload'; const defaultValue = [ { @@ -40,6 +41,18 @@ const defaultValue = [ }, ]; +const beforeUpload: UploadProps['beforeUpload'] = info => { + console.log('beforeUpload : ', info); +}; + +const onChange: UploadProps['onChange'] = info => { + console.log('onChange : ', info); +}; + +const onSuccess: UploadProps['onSuccess'] = info => { + console.log('onSuccess : ', info); +}; + ReactDOM.render( , mountNode ); - -function beforeUpload(info) { - console.log('beforeUpload : ', info); -} - -function onChange(info) { - console.log('onChange : ', info); -} - -function onSuccess(info) { - console.log('onSuccess : ', info); -} diff --git a/components/upload/__docs__/theme/index.jsx b/components/upload/__docs__/theme/index.jsx deleted file mode 100644 index 1201f6430a..0000000000 --- a/components/upload/__docs__/theme/index.jsx +++ /dev/null @@ -1,293 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import '../../../demo-helper/style'; -import {Demo, DemoGroup, initDemo} from '../../../demo-helper'; -import '../../style'; -import Upload from '../../index'; -import Field from '../../../field'; -import '../../../dialog/style'; -import Dialog from '../../../dialog'; -import zhCN from '../../../locale/zh-cn'; -import enUS from '../../../locale/en-us'; -import ConfigProvider from '../../../config-provider'; - -// import demo helper - -// import upload - -// import dialog - - -const Card = Upload.Card; -const Dragger = Upload.Dragger; - -const style = { - width: '500px' -}; - - -const demo1 = { - closeable: { - label: '关闭按钮', - value: 'true', - enum: [{label: '显示', value: 'true'}, {label: '隐藏', value: 'false'}] - }, - size: { - label: '附件大小', - value: 'true', - enum: [{label: '显示', value: 'true'}, {label: '隐藏', value: 'false'}] - }, - errMsg: { - label: '错误提示', - value: 'true', - enum: [{label: '显示', value: 'true'}, {label: '隐藏', value: 'false'}] - } -}; - -const demo2 = { - closeable: { - label: '关闭按钮', - value: 'true', - enum: [{label: '显示', value: 'true'}, {label: '隐藏', value: 'false'}] - }, - size: { - label: '附件大小', - value: 'true', - enum: [{label: '显示', value: 'true'}, {label: '隐藏', value: 'false'}] - }, - errMsg: { - label: '错误提示', - value: 'true', - enum: [{label: '显示', value: 'true'}, {label: '隐藏', value: 'false'}] - } -}; - -const demo3 = { - listType: { - label: '列表', - value: 'true', - enum: [{label: '显示', value: 'true'}, {label: '隐藏', value: 'false'}] - } -}; - -const list = { - name: 'IMG_20140109_121958.jpg', - state: 'done', - percent: 50, - url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', -}; - -class FunctionDemo extends React.Component { - field = new Field(this); - - onPreview = (file) => { - Dialog.alert({ - content: {file.name}, - title: file.name, - // onOk: () => {}, - }); - } - - render() { - const {init, getValue} = this.field; - - return (
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
); - } -} - -window.renderDemo = function (lang = 'en-us') { - ReactDOM.render(( - - - - ), document.getElementById('container')); -}; - -window.renderDemo(); - -initDemo('upload'); diff --git a/components/upload/__docs__/theme/index.tsx b/components/upload/__docs__/theme/index.tsx new file mode 100644 index 0000000000..0a6b55c735 --- /dev/null +++ b/components/upload/__docs__/theme/index.tsx @@ -0,0 +1,406 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import type { CardProps } from '@alifd/next/types/upload'; + +import '../../../demo-helper/style'; +import { Demo, DemoGroup, initDemo } from '../../../demo-helper'; +import '../../style'; +import Upload from '../../index'; +import Field from '../../../field'; +import '../../../dialog/style'; +import Dialog from '../../../dialog'; +import zhCN from '../../../locale/zh-cn'; +import enUS from '../../../locale/en-us'; +import ConfigProvider from '../../../config-provider'; + +// import demo helper + +// import upload + +// import dialog + +const Card = Upload.Card; +const Dragger = Upload.Dragger; + +const style = { + width: '500px', +}; + +const demo1 = { + closeable: { + label: '关闭按钮', + value: 'true', + enum: [ + { label: '显示', value: 'true' }, + { label: '隐藏', value: 'false' }, + ], + }, + size: { + label: '附件大小', + value: 'true', + enum: [ + { label: '显示', value: 'true' }, + { label: '隐藏', value: 'false' }, + ], + }, + errMsg: { + label: '错误提示', + value: 'true', + enum: [ + { label: '显示', value: 'true' }, + { label: '隐藏', value: 'false' }, + ], + }, +}; + +const demo2 = { + closeable: { + label: '关闭按钮', + value: 'true', + enum: [ + { label: '显示', value: 'true' }, + { label: '隐藏', value: 'false' }, + ], + }, + size: { + label: '附件大小', + value: 'true', + enum: [ + { label: '显示', value: 'true' }, + { label: '隐藏', value: 'false' }, + ], + }, + errMsg: { + label: '错误提示', + value: 'true', + enum: [ + { label: '显示', value: 'true' }, + { label: '隐藏', value: 'false' }, + ], + }, +}; + +const demo3 = { + listType: { + label: '列表', + value: 'true', + enum: [ + { label: '显示', value: 'true' }, + { label: '隐藏', value: 'false' }, + ], + }, +}; + +const list = { + name: 'IMG_20140109_121958.jpg', + state: 'done', + percent: 50, + url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', +}; + +class FunctionDemo extends React.Component { + field = new Field(this); + + onPreview: CardProps['onPreview'] = file => { + Dialog.alert({ + content: {file.name}, + title: file.name, + // onOk: () => {}, + }); + }; + + render() { + const { init, getValue } = this.field; + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); + } +} + +window.renderDemo = function (lang = 'en-us') { + ReactDOM.render( + + + , + document.getElementById('container') + ); +}; + +window.renderDemo(); + +initDemo('upload'); diff --git a/components/upload/__tests__/card-spec.js b/components/upload/__tests__/card-spec.js deleted file mode 100644 index e7e0042380..0000000000 --- a/components/upload/__tests__/card-spec.js +++ /dev/null @@ -1,289 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme, { shallow, mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import sinon from 'sinon'; -import Upload from '../index'; -import request from '../runtime/request'; -import { func } from '../../util'; - -Enzyme.configure({ adapter: new Adapter() }); - -const CardUpload = Upload.Card; -const DragUpload = Upload.Dragger; - -const defaultValue = [ - { - name: 'IMG.png', - state: 'done', - size: 1024, - url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - }, -]; - -function fixBinary(bin) { - const length = bin.length; - const buf = new ArrayBuffer(length); - const arr = new Uint8Array(buf); - for (let i = 0; i < length; i++) { - arr[i] = bin.charCodeAt(i); - } - return buf; -} - -function buildFile(filename = 'test') { - const base64 = - 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggkFBTzlUWEwwWTRPSHdBQUFBQkpSVTVFcmtKZ2dnPT0='; - const binary = fixBinary(atob(base64)); - const blob = new Blob([binary], { type: 'image/png' }); - const file = new File([blob], `${filename}.png`, { type: 'image/png' }); - return file; -} - -function triggerUploadEvent(wrapper, done, callback) { - if (typeof atob === 'function' && typeof Blob === 'function' && typeof File === 'function') { - // 模拟文件上传 - const file = buildFile(); - wrapper.find('input').simulate('change', { target: { files: [file] } }); - callback && callback(wrapper); - } else { - done(); - } -} - -describe('CardUpload', () => { - let requests; - let xhr; - - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = req => requests.push(req); - }); - - afterEach(() => { - xhr.restore(); - }); - - describe('[behavior]', () => { - it('should support prefix', () => { - const wrapper = mount(); - assert(wrapper.find('div.test-upload').length === 1); - }); - it('should support controlled `value`', () => { - const wrapper = mount(); - assert(wrapper.props().value.length === 0); - assert(wrapper.find('div.next-upload-list-item').length === 1); - - wrapper.setProps({ - value: [ - { - name: 'IMG_20140109_121958.jpg', - state: 'uploading', - url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - }, - ], - }); - - assert(wrapper.props().value.length === 1); - assert(wrapper.find('div.next-upload-list-item').length === 2); - }); - it('should support showDownload', () => { - const wrapper = mount(); - wrapper.setProps({ - value: [ - { - name: 'IMG_20140109_121958.jpg', - state: 'done', - url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - }, - ], - }); - - assert(wrapper.find('i.next-upload-tool-download-icon').length === 1); - - wrapper.setProps({ - showDownload: false, - }); - - assert(wrapper.find('i.next-upload-tool-download-icon').length === 0); - }); - it('should support reUpload', () => { - const wrapper = mount(); - wrapper.setProps({ - value: [ - { - name: 'IMG_20140109_121958.jpg', - state: 'error', - url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - }, - ], - }); - - assert(wrapper.find('i.next-upload-tool-reupload-icon').length === 0); - - wrapper.setProps({ - reUpload: true, - }); - - assert(wrapper.find('i.next-upload-tool-reupload-icon').length === 1); - }); - }); - - describe('[request]', () => { - it('should support header', done => { - const formatter = res => { - assert(res.test === 123); - done(); - }; - const wrapper = mount( - - ); - triggerUploadEvent(wrapper, done, () => { - const request = requests[0]; - - assert(request.requestHeaders['test-head'] === 'test-head'); - assert(request.requestBody.get('test-data') === 'test-data'); - - requests[0].respond( - 200, - {}, - '{"success": true, "url":"https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg", "test": 123}' - ); - }); - }); - it('should support onChange/onRemove events', done => { - const onChange = sinon.spy(); - const onRemove = sinon.spy(); - const wrapper = mount( - - ); - - wrapper - .find('i.next-icon-ashbin') - .at(0) - .simulate('click'); - assert(onRemove.calledOnce); - assert(onChange.calledOnce); - assert(wrapper.find('.next-upload-list-item-wrapper').length === 2); - done(); - }); - it('should support onChange/onCancel events', done => { - const onChange = sinon.spy(); - const onCancel = sinon.spy(); - const wrapper = mount( - - ); - - wrapper - .find('.next-upload-list-item-handler .next-btn') - .at(0) - .simulate('click'); - assert(onCancel.calledOnce); - assert(onChange.calledOnce); - assert(wrapper.find('.next-upload-list-item-wrapper').length === 2); - done(); - }); - it('should support change Data/Action/Headers in BeforeUpload', done => { - class App extends React.Component { - constructor() { - super(); - this.state = { - action: '/upload/files/123', - data: { 'test-data': 'test-data' }, - headers: { 'test-head': 'test-head' }, - }; - } - - beforeUpload = () => { - return { - action: '/upload/files/beforeUpload', - data: { 'test-data': 'beforeUpload' }, - headers: { 'test-head': 'beforeUpload' }, - }; - }; - render() { - return ; - } - } - const wrapper = mount(); - - triggerUploadEvent(wrapper, done, () => { - const request = requests[0]; - assert(request.url === '/upload/files/beforeUpload'); - assert(request.requestHeaders['test-head'] === 'beforeUpload'); - assert(request.requestBody.get('test-data') === 'beforeUpload'); - done(); - }); - }); - }); -}); diff --git a/components/upload/__tests__/card-spec.tsx b/components/upload/__tests__/card-spec.tsx new file mode 100644 index 0000000000..b647414789 --- /dev/null +++ b/components/upload/__tests__/card-spec.tsx @@ -0,0 +1,258 @@ +import React from 'react'; +import Upload from '../index'; +import { type UploadFile } from '../types'; + +const CardUpload = Upload.Card; + +function fixBinary(bin: string) { + const length = bin.length; + const buf = new ArrayBuffer(length); + const arr = new Uint8Array(buf); + for (let i = 0; i < length; i++) { + arr[i] = bin.charCodeAt(i); + } + return buf; +} + +function buildFile(filename = 'test') { + const base64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggkFBTzlUWEwwWTRPSHdBQUFBQkpSVTVFcmtKZ2dnPT0='; + const binary = fixBinary(atob(base64)); + const blob = new Blob([binary], { type: 'image/png' }); + const file = new File([blob], `${filename}.png`, { type: 'image/png' }); + return file; +} + +describe('CardUpload', () => { + describe('[behavior]', () => { + it('should support prefix', () => { + cy.mount(); + cy.get('div.test-upload').should('have.length', 1); + }); + it('should support controlled `value`', () => { + cy.mount().as('Demo'); + + cy.get('div.next-upload-list-item').should('have.length', 1); + + const files = [ + { + name: 'IMG_20140109_121958.jpg', + state: 'uploading', + url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + }, + ]; + cy.rerender('Demo', { value: files }); + + cy.get('div.next-upload-list-item').should('have.length', 2); + }); + it.only('should support showDownload', () => { + const files = [ + { + name: 'IMG_20140109_121958.jpg', + state: 'done', + url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + }, + ]; + + cy.mount().as('Demo'); + + cy.get('i.next-upload-tool-download-icon').should('have.length', 1); + + cy.rerender('Demo', { value: files, showDownload: false }); + + cy.get('i.next-upload-tool-download-icon').should('have.length', 0); + }); + + it('should support reUpload', () => { + const files = [buildFile()]; + cy.mount().as('Demo'); + + cy.get('i.next-upload-tool-reupload-icon').should('have.length', 0); + + cy.rerender('Demo', { value: files, reUpload: true }); + + cy.get('i.next-upload-tool-reupload-icon').should('have.length', 1); + }); + }); + + describe('[request]', () => { + it('should support header', () => { + const testUrl = '/upload-endpoint'; + cy.intercept('POST', testUrl, req => { + expect(req.headers['test-head']).to.equal('test-head'); + expect(req.body.get('test-data')).to.equal('test-data'); + req.reply({ + statusCode: 200, + body: { + success: true, + url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + test: 123, + }, + }); + }); + + cy.mount( + + ); + + cy.get('input[type="file"]').trigger('change', { + target: { files: [buildFile()] }, + force: true, + }); + }); + + it('should support onChange/onRemove events', () => { + const onChange = cy.spy().as('onChangeSpy'); + const onRemove = cy.spy().as('onRemoveSpy'); + + cy.mount( + + ); + cy.get('i.next-icon-ashbin').first().trigger('click', { force: true }); + + cy.get('@onChangeSpy').should('have.been.calledOnce'); + cy.get('@onRemoveSpy').should('have.been.calledOnce'); + + cy.get('.next-upload-list-item-wrapper').should('have.length', 2); + }); + + it('should support onChange/onCancel events', () => { + const onChange = cy.spy().as('onChangeSpy'); + const onCancel = cy.spy().as('onCancelSpy'); + + cy.mount( + + ); + + cy.get('.next-upload-list-item-handler .next-btn').first().trigger('click'); + cy.get('@onCancelSpy').should('have.been.calledOnce'); + cy.get('@onChangeSpy').should('have.been.calledOnce'); + cy.get('.next-upload-list-item-wrapper').should('have.length', 2); + }); + + it('should support change Data/Action/Headers in BeforeUpload', () => { + class App extends React.Component { + beforeUpload = () => { + return { + action: '/upload/files/beforeUpload', + data: { 'test-data': 'beforeUpload' }, + headers: { 'test-head': 'beforeUpload' }, + }; + }; + render() { + return ( + + ); + } + } + cy.intercept('POST', '/upload/files/beforeUpload', req => { + expect(req.headers['test-head']).to.equal('beforeUpload'); + + // 解析请求体字符串,这里简化处理,仅针对文本数据 + const bodyParts = req.body.split('\r\n\r\n'); + const testDataIndex = bodyParts.findIndex((part: string) => + part.includes('name="test-data"') + ); + const testDataPart = bodyParts[testDataIndex + 1].split('\r\n')[0]; + const testDataValue = testDataPart.trim(); + expect(testDataValue).to.equal('beforeUpload'); + + req.reply({ + statusCode: 200, + body: { success: true }, + }); + }).as('uploadRequest'); + + cy.mount(); + + cy.get('input[type="file"]').trigger('change', { + target: { files: [buildFile()] }, + force: true, + }); + + cy.wait('@uploadRequest').its('response.statusCode').should('eq', 200); + }); + }); +}); diff --git a/components/upload/__tests__/file-list-spec.js b/components/upload/__tests__/file-list-spec.tsx similarity index 70% rename from components/upload/__tests__/file-list-spec.js rename to components/upload/__tests__/file-list-spec.tsx index f5208caa8c..2ffc63427c 100644 --- a/components/upload/__tests__/file-list-spec.js +++ b/components/upload/__tests__/file-list-spec.tsx @@ -1,30 +1,10 @@ import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme, { shallow, mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import sinon from 'sinon'; import Upload from '../index'; -Enzyme.configure({ adapter: new Adapter() }); - describe('listType', () => { - let requests; - let xhr; - - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = req => requests.push(req); - }); - - afterEach(() => { - xhr.restore(); - }); - describe('render', () => { it('should not render upload file list', () => { - const wrapper = mount( + cy.mount( { name: 'IMG.png', state: 'done', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, { @@ -42,7 +23,8 @@ describe('listType', () => { percent: 50, state: 'uploading', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, { @@ -50,16 +32,17 @@ describe('listType', () => { name: 'IMG.png', state: 'error', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, ]} /> ); - assert(wrapper.exists('.next-upload-list') === false); + cy.get('.next-upload-list').should('not.exist'); }); it('should render text upload file list', () => { - const wrapper = mount( + cy.mount( { name: 'IMG.png', state: 'done', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, { @@ -77,7 +61,8 @@ describe('listType', () => { percent: 50, state: 'uploading', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, { @@ -85,17 +70,18 @@ describe('listType', () => { name: 'IMG.png', state: 'error', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, ]} /> ); - assert(wrapper.exists('.next-upload-list') === true); - assert(wrapper.find('.next-upload-list-item').length === 3); + cy.get('.next-upload-list').should('exist'); + cy.get('.next-upload-list-item').should('have.length', 3); }); it('should render card upload file list', () => { - const wrapper = mount( + cy.mount( { name: 'IMG.png', state: 'done', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, { @@ -113,7 +100,8 @@ describe('listType', () => { percent: 50, state: 'uploading', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, { @@ -121,17 +109,18 @@ describe('listType', () => { name: 'IMG.png', state: 'error', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, ]} /> ); - assert(wrapper.exists('.next-upload-list') === true); - assert(wrapper.find('.next-upload-list-item').length === 3); + cy.get('.next-upload-list').should('exist'); + cy.get('.next-upload-list-item').should('have.length', 3); }); it('should render image upload file list', () => { - const wrapper = mount( + cy.mount( { name: 'IMG.png', state: 'done', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, { @@ -149,7 +139,8 @@ describe('listType', () => { percent: 50, state: 'uploading', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, { @@ -157,14 +148,15 @@ describe('listType', () => { name: 'IMG.png', state: 'error', url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - downloadURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + downloadURL: + 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', imgURL: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', }, ]} /> ); - assert(wrapper.exists('.next-upload-list') === true); - assert(wrapper.find('.next-upload-list-item').length === 3); + cy.get('.next-upload-list').should('exist'); + cy.get('.next-upload-list-item').should('have.length', 3); }); }); }); diff --git a/components/upload/__tests__/iframe-spec.js b/components/upload/__tests__/iframe-spec.js deleted file mode 100644 index ef4e9a9845..0000000000 --- a/components/upload/__tests__/iframe-spec.js +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme, { shallow, mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import Upload from '../runtime/iframe-uploader'; - -Enzyme.configure({ adapter: new Adapter() }); - -describe('Iframe Upload', () => { - describe('should render without crash', () => { - const wrapper = mount(); - - assert(wrapper.find('form').length); - }); -}); diff --git a/components/upload/__tests__/iframe-spec.tsx b/components/upload/__tests__/iframe-spec.tsx new file mode 100644 index 0000000000..89372c524f --- /dev/null +++ b/components/upload/__tests__/iframe-spec.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import Upload from '../runtime/iframe-uploader'; + +describe('Iframe Upload', () => { + it('should render without crash', () => { + cy.mount(); + + // 检查页面中是否存在form元素 + cy.get('form').should('exist'); + }); +}); diff --git a/components/upload/__tests__/image-spec.js b/components/upload/__tests__/image-spec.js deleted file mode 100644 index 7ca1764ccd..0000000000 --- a/components/upload/__tests__/image-spec.js +++ /dev/null @@ -1,139 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme, { shallow, mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import sinon from 'sinon'; -import Upload from '../index'; -import request from '../runtime/request'; -import { func } from '../../util'; - -Enzyme.configure({ adapter: new Adapter() }); - -const CardUpload = Upload.Card; -const DragUpload = Upload.Dragger; - -const defaultValue = [ - { - name: 'IMG.png', - state: 'done', - size: 1024, - url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - }, -]; - -function fixBinary(bin) { - const length = bin.length; - const buf = new ArrayBuffer(length); - const arr = new Uint8Array(buf); - for (let i = 0; i < length; i++) { - arr[i] = bin.charCodeAt(i); - } - return buf; -} - -function buildFile(filename = 'test') { - const base64 = - 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggkFBTzlUWEwwWTRPSHdBQUFBQkpSVTVFcmtKZ2dnPT0='; - const binary = fixBinary(atob(base64)); - const blob = new Blob([binary], { type: 'image/png' }); - const file = new File([blob], `${filename}.png`, { type: 'image/png' }); - return file; -} - -function triggerUploadEvent(wrapper, done, callback) { - if (typeof atob === 'function' && typeof Blob === 'function' && typeof File === 'function') { - // 模拟文件上传 - const file = buildFile(); - wrapper.find('input').simulate('change', { target: { files: [file] } }); - callback && callback(wrapper); - } else { - done(); - } -} - -describe('ImageUpload', () => { - let requests; - let xhr; - - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = req => requests.push(req); - }); - - afterEach(() => { - xhr.restore(); - }); - - describe('render', () => { - it('should render a imageList upload', () => { - const wrapper = mount( - - ); - assert(wrapper.find('.next-upload-list.next-upload-list-image').length === 1); - assert(wrapper.find('.next-upload-list-item').length === 3); - ['next-upload-list-item-done', 'next-upload-list-item-uploading', 'next-upload-list-item-error'].forEach( - className => { - assert(wrapper.find(`.${className}`).length === 1); - } - ); - }); - it('should render a imageList upload with error msg', () => { - const wrapper = mount( - - ); - assert(wrapper.find('.next-upload-list-item-error-with-msg').length === 1); - assert(wrapper.find('.next-upload-list-item-error').length === 1); - assert(wrapper.find('.next-upload-list-item-error-msg').length === 1); - assert( - wrapper - .find('.next-upload-list-item-error-msg') - .at(0) - .text() === 'ErrorText' - ); - }); - }); -}); diff --git a/components/upload/__tests__/image-spec.tsx b/components/upload/__tests__/image-spec.tsx new file mode 100644 index 0000000000..5ca058b516 --- /dev/null +++ b/components/upload/__tests__/image-spec.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import Upload from '../index'; + +describe('ImageUpload', () => { + describe('render', () => { + it('should render a imageList upload', () => { + cy.mount( + + ); + cy.get('.next-upload-list.next-upload-list-image'); + cy.get('.next-upload-list-item').should('have.length', 3); + [ + 'next-upload-list-item-done', + 'next-upload-list-item-uploading', + 'next-upload-list-item-error', + ].forEach(className => { + cy.get(`.${className}`); + }); + }); + it('should render a imageList upload with error msg', () => { + cy.mount( + + ); + cy.get('.next-upload-list-item-error-with-msg'); + cy.get('.next-upload-list-item-error'); + cy.get('.next-upload-list-item-error-msg'); + cy.get('.next-upload-list-item-error-msg').first().should('have.text', 'ErrorText'); + }); + }); +}); diff --git a/components/upload/__tests__/index-spec.js b/components/upload/__tests__/index-spec.js deleted file mode 100644 index 4f91ccf1d4..0000000000 --- a/components/upload/__tests__/index-spec.js +++ /dev/null @@ -1,306 +0,0 @@ -import React from 'react'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import sinon from 'sinon'; -import Upload from '../index'; -import request from '../runtime/request'; -import '../style'; - -Enzyme.configure({ adapter: new Adapter() }); - -const CardUpload = Upload.Card; -const DragUpload = Upload.Dragger; - -const defaultValue = [ - { - name: 'IMG.png', - state: 'done', - size: 1024, - url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - }, -]; - -function fixBinary(bin) { - const length = bin.length; - const buf = new ArrayBuffer(length); - const arr = new Uint8Array(buf); - for (let i = 0; i < length; i++) { - arr[i] = bin.charCodeAt(i); - } - return buf; -} - -function buildFile(filename = 'test') { - const base64 = - 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggkFBTzlUWEwwWTRPSHdBQUFBQkpSVTVFcmtKZ2dnPT0='; - const binary = fixBinary(atob(base64)); - const blob = new Blob([binary], { type: 'image/png' }); - const file = new File([blob], `${filename}.png`, { type: 'image/png' }); - return file; -} - -function triggerUploadEvent(wrapper, done, callback) { - if (typeof atob === 'function' && typeof Blob === 'function' && typeof File === 'function') { - // 模拟文件上传 - const file = buildFile(); - wrapper.find('input').simulate('change', { target: { files: [file] } }); - callback && callback(wrapper); - } else { - done(); - } -} - -describe('Upload', () => { - let requests; - let xhr; - - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = req => requests.push(req); - }); - - afterEach(() => { - xhr.restore(); - }); - - describe('render', () => { - it('should render a wrapper upload', () => { - const wrapper = mount(); - assert(wrapper.find('.next-upload').length === 1); - // remove item - assert(wrapper.find('.next-upload-list-item').length === 1); - }); - }); - - describe('behavior', () => { - it('should support defaultValue and can be changed', done => { - const wrapper = mount( - { - assert(value.length === 2); - done(); - }} - /> - ); - - assert(wrapper.find('.next-upload-list-item').length === 1); - triggerUploadEvent(wrapper, done, () => { - requests[0].respond( - 200, - {}, - '{"success": true, "url":"https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg"}' - ); - }); - }); - it('should support limit', done => { - // limit = 2 上传4个文件 结果应该是 2个成功 2个失败 - if (!(typeof atob === 'function' && typeof Blob === 'function' && typeof File === 'function')) { - return done(); - } - - let success = 0, - fail = 0; - const isPass = () => { - if (success === 2 && fail === 2) { - done(); - } - }; - const onSuccess = () => { - success++; - isPass(); - }; - const onError = () => { - fail++; - isPass(); - }; - - const files = [1, 2, 3, 4].map(value => buildFile(value)); - - const wrapper = mount( - - ); - wrapper.find('input').simulate('change', { target: { files: files } }); - requests.forEach(req => - req.respond( - 200, - {}, - '{"success": true, "url":"https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg"}' - ) - ); - }); - - it('should support onPreview events when listType is set to card and isPreview is set to true', done => { - const onPreview = sinon.spy(); - const wrapper = mount( - - ); - wrapper - .find('.next-upload-list-item-thumbnail > img') - .at(0) - .simulate('click'); - assert(onPreview.calledOnce); - done(); - }); - - it('should support onChange/onRemove events', done => { - const onChange = sinon.spy(); - const onRemove = sinon.spy(); - const wrapper = mount( - - ); - wrapper - .find('i.next-icon-close') - .at(0) - .simulate('click'); - assert(onChange.calledOnce); - assert(onRemove.calledOnce); - done(); - }); - }); - - describe('[render] drag', () => { - it('should render a drag upload', done => { - const drag = mount(); - assert(drag.find('.next-upload-drag').length === 1); - - if (typeof Blob === 'function' && typeof File === 'function') { - let blob = new Blob(['hello'], { type: 'text/plain' }); - let file = new File([blob], 'test.png', { type: 'image/png' }); - - drag.find('div.next-upload-inner').simulate('dragover', { - dataTransfer: { files: [file] }, - }); - drag.find('div.next-upload-inner').simulate('dragleave', { - dataTransfer: { files: [file] }, - }); - drag.find('div.next-upload-inner').simulate('drop', { - dataTransfer: { files: [file] }, - }); - // expect(drag.find('.next-upload-list-item-done')).to.have.length(1) - } - - done(); - }); - // issue: Shell phone model menu icon should hidde, close #3886 - it('should hidden upload Dragger when file length === limit', done => { - const drag = document.createElement('div'); - document.body.appendChild(drag); - mount( - , - {attachTo: drag} - ); - assert(document.querySelectorAll('.next-upload-drag').length === 1); - const uploadInner = document.querySelectorAll('.next-upload-inner'); - assert(uploadInner.length === 1 ) - assert(uploadInner[0].offsetHeight === 0); - done(); - }); - }); - - describe('[behavior] Upload Request', () => { - it('should support header', done => { - const formatter = res => { - assert(res.test === 123); - done(); - }; - const wrapper = mount( - - ); - triggerUploadEvent(wrapper, done, () => { - const request = requests[0]; - assert(request.requestHeaders['test-head'] === 'test-head'); - assert(request.requestBody.get('test-data') === 'test-data'); - requests[0].respond( - 200, - {}, - '{"success": true, "url":"https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg", "test": 123}' - ); - }); - }); - it('should support custom request', done => { - const pass = { isPass: false }; - const customRequest = function customRequest(options) { - pass.isPass = true; - return request(options); - }; - const onSuccess = function onSuccess() { - assert(pass.isPass); - done(); - }; - const wrapper = mount(); - triggerUploadEvent(wrapper, done, () => { - requests[0].respond( - 200, - {}, - '{"success": true, "url":"https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg", "test": 123}' - ); - }); - }); - it('should throw error when response json invalid', done => { - const onError = function onError() { - done(); - }; - const wrapper = mount(); - triggerUploadEvent(wrapper, done, () => { - requests[0].respond(200, {}, '{"succe3}'); - }); - }); - it('should throw error when response.success=false', done => { - const onError = function onError() { - done(); - }; - const wrapper = mount(); - triggerUploadEvent(wrapper, done, () => { - requests[0].respond(200, {}, '{"success": false}'); - }); - }); - it('should throw error when return false in BeforeUpload', done => { - const beforeUpload = () => false; - const onError = function onError() { - done(); - }; - const wrapper = mount(); - triggerUploadEvent(wrapper, done); - }); - it('should throw error when return Promise.resolve(false) in BeforeUpload', done => { - const beforeUpload = () => Promise.resolve(false); - const onError = function onError() { - done(); - }; - const wrapper = mount(); - triggerUploadEvent(wrapper, done); - }); - it('should throw error when return Promise.reject() in BeforeUpload', done => { - const beforeUpload = () => Promise.reject({}); - const onError = function onError() { - done(); - }; - const wrapper = mount(); - triggerUploadEvent(wrapper, done); - }); - }); -}); diff --git a/components/upload/__tests__/index-spec.tsx b/components/upload/__tests__/index-spec.tsx new file mode 100644 index 0000000000..c1968bb2c6 --- /dev/null +++ b/components/upload/__tests__/index-spec.tsx @@ -0,0 +1,313 @@ +import React from 'react'; +import Upload from '../index'; +import type { ObjectFile } from '../types'; + +const DragUpload = Upload.Dragger; + +const defaultValue: ObjectFile[] = [ + { + name: 'IMG.png', + state: 'done', + size: 1024, + url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + }, +]; + +function fixBinary(bin: string) { + const length = bin.length; + const buf = new ArrayBuffer(length); + const arr = new Uint8Array(buf); + for (let i = 0; i < length; i++) { + arr[i] = bin.charCodeAt(i); + } + return buf; +} + +function buildFile(filename = 'test') { + const base64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggkFBTzlUWEwwWTRPSHdBQUFBQkpSVTVFcmtKZ2dnPT0='; + const binary = fixBinary(atob(base64)); + const blob = new Blob([binary], { type: 'image/png' }); + const file = new File([blob], `${filename}.png`, { type: 'image/png' }); + return file; +} + +function triggerUploadEvent(filename?: string) { + cy.get('input[type="file"]').trigger('change', { + target: { files: [buildFile(filename)] }, + force: true, + }); +} + +describe('Upload', () => { + describe('render', () => { + it('should render a wrapper upload', () => { + cy.mount(); + cy.get('.next-upload'); + cy.get('.next-upload-list-item').should('have.length', 1); + }); + }); + describe('behavior', () => { + it('should support defaultValue', () => { + cy.mount( + + ); + cy.get('.next-upload-list-item').should('have.length', 1); + cy.get('.next-upload-list-item-name').first().should('contain', 'IMG.png'); + }); + it('can be changed by uploading a new file', () => { + cy.mount( + + ); + triggerUploadEvent(); + cy.get('.next-upload-list-item').should('have.length', 2); + cy.get('.next-upload-list-item-name').first().should('contain', 'IMG.png'); + }); + + it('should support limit', () => { + // limit = 2 上传4个文件 结果应该是 2个成功 2个失败 + if ( + !( + typeof atob === 'function' && + typeof Blob === 'function' && + typeof File === 'function' + ) + ) { + return; + } + + let success = 0, + fail = 0; + const isPass = () => { + expect(success).to.equal(2); + expect(fail).to.equal(2); + }; + const onSuccess = () => { + success++; + isPass(); + }; + const onError = () => { + fail++; + isPass(); + }; + + const files = ['1', '2', '3', '4'].map(value => buildFile(value)); + + cy.mount( + + ); + cy.get('input').trigger('change', { target: { files: files }, force: true }); + }); + + it('should support onPreview events when listType is set to card and isPreview is set to true', () => { + const onPreview = cy.spy().as('onPreviewSpy'); + cy.mount( + + ); + cy.get('.next-upload-list-item-thumbnail > img').first().trigger('click'); + cy.get('@onPreviewSpy').should('have.been.calledOnce'); + }); + + it('should support onChange/onRemove events', () => { + const onChange = cy.spy().as('onChangeSpy'); + const onRemove = cy.spy().as('onRemoveSpy'); + + cy.mount( + + ); + cy.get('i.next-icon-close').first().trigger('click', { force: true }); + cy.get('@onChangeSpy').should('have.been.calledOnce'); + cy.get('@onRemoveSpy').should('have.been.calledOnce'); + }); + }); + + describe('[render] drag', () => { + it('should render a drag upload', () => { + cy.mount(); + cy.get('.next-upload-drag'); + + if (typeof Blob === 'function' && typeof File === 'function') { + const blob = new Blob(['hello'], { type: 'text/plain' }); + const file = new File([blob], 'test.png', { type: 'image/png' }); + + cy.get('div.next-upload-inner').trigger('dragover', { + dataTransfer: { files: [file] }, + }); + cy.get('div.next-upload-inner').trigger('dragleave', { + dataTransfer: { files: [file] }, + }); + cy.get('div.next-upload-inner').trigger('drop', { + dataTransfer: { files: [file] }, + }); + } + }); + + // issue: Shell phone model menu icon should hidde, close #3886 + it('should hidden upload Dragger when file length === limit', () => { + cy.mount( + + ); + + cy.get('.next-upload-drag').should('have.length', 1); + cy.get('.next-upload-inner').should('have.length', 1); + cy.get('.next-upload-inner').should('have.class', 'next-hidden'); + }); + }); + + describe('[behavior] Upload Request', () => { + it('should support header', () => { + const testUrl = '/upload-endpoint'; + cy.intercept('POST', testUrl, req => { + expect(req.headers['test-head']).to.equal('test-head'); + // 解析请求体字符串,这里简化处理,仅针对文本数据 + const bodyParts = req.body.split('\r\n\r\n'); + const testDataIndex = bodyParts.findIndex((part: string) => + part.includes('name="test-data"') + ); + const testDataPart = bodyParts[testDataIndex + 1].split('\r\n')[0]; + const testDataValue = testDataPart.trim(); + expect(testDataValue).to.equal('test-data'); + + req.reply({ + statusCode: 200, + body: { + success: true, + url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + test: 123, + }, + }); + }).as('fileUpload'); + + cy.mount( + + ); + + triggerUploadEvent(); + cy.wait('@fileUpload'); + }); + + it('should support custom request', () => { + const testUrl = '/upload-endpoint'; + const customRequest = cy.spy().as('customRequest'); + + cy.intercept('POST', testUrl, req => { + req.reply({ + statusCode: 200, + body: { + success: true, + url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + test: 123, + }, + }); + }).as('fileUpload'); + + cy.mount(); + + triggerUploadEvent(); + cy.get('@customRequest').should('have.been.called'); + }); + + it('should throw error when response json invalid', () => { + const testUrl = '/upload/files/beforeUpload'; + + cy.intercept('POST', testUrl, { + statusCode: 200, + body: '{"succe3}', + }).as('uploadRequest'); + + const onErrorSpy = cy.spy().as('onErrorSpy'); + + cy.mount(); + + triggerUploadEvent(); + cy.wait('@uploadRequest'); + + cy.get('@onErrorSpy').should('have.been.called'); + }); + + it('should throw error when response.success=false', () => { + cy.intercept('POST', '/upload-endpoint', { + statusCode: 200, + body: '{"success": false}', + }).as('uploadRequest'); + + const onErrorSpy = cy.spy().as('onErrorSpy'); + + cy.mount(); + + triggerUploadEvent(); + cy.wait('@uploadRequest'); + + cy.get('@onErrorSpy').should('have.been.called'); + }); + + it('should throw error when return false in BeforeUpload', () => { + const beforeUpload = () => false; + const onErrorSpy = cy.spy().as('onErrorSpy'); + + cy.mount(); + triggerUploadEvent(); + cy.get('@onErrorSpy').should('have.been.called'); + }); + + it('should throw error when return Promise.resolve(false) in BeforeUpload', () => { + const beforeUpload = () => Promise.resolve(false); + const onErrorSpy = cy.spy().as('onErrorSpy'); + + cy.mount(); + triggerUploadEvent(); + cy.get('@onErrorSpy').should('have.been.called'); + }); + + it('should throw error when return Promise.reject() in BeforeUpload', () => { + const beforeUpload = () => Promise.reject({}); + const onErrorSpy = cy.spy().as('onErrorSpy'); + + cy.mount(); + triggerUploadEvent(); + cy.get('@onErrorSpy').should('have.been.called'); + }); + }); +}); diff --git a/components/upload/__tests__/request-spec.js b/components/upload/__tests__/request-spec.js deleted file mode 100644 index 1fb1901866..0000000000 --- a/components/upload/__tests__/request-spec.js +++ /dev/null @@ -1,106 +0,0 @@ -import assert from 'power-assert'; -import sinon from 'sinon'; -import request from '../runtime/request'; - -let xhr; -let requests; - -const empty = () => {}; -const option = { - onSuccess: empty, - action: 'upload.do', - data: { a: 1, b: 2 }, - filename: 'a.png', - file: { - name: 'a.png', - }, - headers: { from: 'hello' }, -}; - -describe('request', () => { - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = req => requests.push(req); - - option.onError = empty; - option.onSuccess = empty; - }); - - afterEach(() => { - xhr.restore(); - }); - - it('upload request success', done => { - if (typeof FormData !== 'undefined') { - option.onError = done; - option.onSuccess = ret => { - assert.deepEqual(ret, { success: true }); - done(); - }; - request(option); - requests[0].respond(200, {}, '{"success": true}'); - } else { - done(); - } - }); - - it('upload request timeout for 5e3', done => { - if (typeof FormData !== 'undefined') { - // option.onError = ret => { - // done(); - // }; - option.onSuccess = ret => { - assert.deepEqual(ret, { success: true }); - done(); - }; - option.timeout = 5e3; - request(option); - requests[0].respond(200, {}, '{"success": true}'); - } else { - done(); - } - }); - - // it('upload request timeout for 1e3', done => { - // if (typeof FormData !== 'undefined') { - // option.onError = ret => { - // assert(ret.toString() === 'Error: Upload abort for exceeding time (timeout: 1000ms)'); - // done(); - // } - // option.onSuccess = ret => { - // assert.deepEqual(ret, { success: true }); - // done(); - // }; - // option.timeout = 1e3; - // request(option); - // } else { - // done(); - // } - // }); - - it('40x code should be error', function(done) { - if (typeof FormData !== 'undefined') { - option.onError = e => { - assert(e.toString().indexOf('404') !== -1); - done(); - }; - - // option.onSuccess = () => done('404 should throw error'); - request(option); - requests[0].respond(404, {}, 'Not found'); - } else { - done(); - } - }); - - it('get headers', () => { - if (typeof FormData !== 'undefined') { - request(option); - assert.deepEqual(requests[0].requestHeaders, { - 'X-Requested-With': 'XMLHttpRequest', - from: 'hello', - }); - } - }); -}); diff --git a/components/upload/__tests__/request-spec.ts b/components/upload/__tests__/request-spec.ts new file mode 100644 index 0000000000..b83a509c61 --- /dev/null +++ b/components/upload/__tests__/request-spec.ts @@ -0,0 +1,102 @@ +import request from '../runtime/request'; +import type { UploadOptions } from '../types'; + +function fixBinary(bin: string) { + const length = bin.length; + const buf = new ArrayBuffer(length); + const arr = new Uint8Array(buf); + for (let i = 0; i < length; i++) { + arr[i] = bin.charCodeAt(i); + } + return buf; +} + +function buildFile(filename = 'test') { + const base64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggkFBTzlUWEwwWTRPSHdBQUFBQkpSVTVFcmtKZ2dnPT0='; + const binary = fixBinary(atob(base64)); + const blob = new Blob([binary], { type: 'image/png' }); + const file = new File([blob], `${filename}.png`, { type: 'image/png' }); + return file; +} + +const empty = () => {}; +const option: UploadOptions = { + onSuccess: empty, + onError: empty, + action: '/upload/files/beforeUpload', + data: { a: '1', b: ' 2' }, + filename: 'a.png', + file: buildFile('a.png'), + headers: { from: 'hello' }, +}; + +describe('request', () => { + beforeEach(() => { + cy.intercept('POST', '/upload/files/beforeUpload', { success: true }).as('getData'); + }); + + it('handles upload request success', () => { + const onSuccess = cy.spy().as('onSuccess'); + option.onSuccess = onSuccess; + request(option); + + cy.wait('@getData').then(() => { + cy.get('@onSuccess').should('have.been.calledOnce'); + }); + }); + + it('upload request timeout for 5e3', () => { + option.timeout = 5e3; + const onSuccess = cy.spy().as('onSuccess'); + option.onSuccess = onSuccess; + request(option); + + cy.wait('@getData').then(() => { + cy.get('@onSuccess').should('have.been.calledOnce'); + }); + }); + + // it('upload request timeout for 1e3', done => { + // if (typeof FormData !== 'undefined') { + // option.onError = ret => { + // assert(ret.toString() === 'Error: Upload abort for exceeding time (timeout: 1000ms)'); + // done(); + // } + // option.onSuccess = ret => { + // assert.deepEqual(ret, { success: true }); + // done(); + // }; + // option.timeout = 1e3; + // request(option); + // } else { + // done(); + // } + // }); + + it('get headers', () => { + request(option); + cy.wait('@getData').then(({ request }) => { + expect(request.headers).to.have.property('x-requested-with', 'XMLHttpRequest'); + expect(request.headers).to.have.property('from', 'hello'); + }); + }); +}); + +describe('request error', () => { + beforeEach(() => { + cy.intercept('POST', '/upload/files/beforeUpload', { forceNetworkError: true }).as( + 'getData' + ); + }); + + it('40x code should be error', () => { + const onError = cy.spy().as('onError'); + option.onError = onError; + request(option); + + cy.wait('@getData').then(() => { + cy.get('@onError').should('have.been.calledOnce'); + }); + }); +}); diff --git a/components/upload/__tests__/text-spec.js b/components/upload/__tests__/text-spec.js deleted file mode 100644 index 1e56cec189..0000000000 --- a/components/upload/__tests__/text-spec.js +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import Enzyme, { shallow, mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import sinon from 'sinon'; -import Upload from '../index'; -import request from '../runtime/request'; -import { func } from '../../util'; - -Enzyme.configure({ adapter: new Adapter() }); - -const defaultValue = [ - { - name: 'IMG.png', - state: 'done', - size: 1024, - url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', - }, -]; - -function fixBinary(bin) { - const length = bin.length; - const buf = new ArrayBuffer(length); - const arr = new Uint8Array(buf); - for (let i = 0; i < length; i++) { - arr[i] = bin.charCodeAt(i); - } - return buf; -} - -function buildFile(filename = 'test') { - const base64 = - 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggkFBTzlUWEwwWTRPSHdBQUFBQkpSVTVFcmtKZ2dnPT0='; - const binary = fixBinary(atob(base64)); - const blob = new Blob([binary], { type: 'image/png' }); - const file = new File([blob], `${filename}.png`, { type: 'image/png' }); - return file; -} - -function triggerUploadEvent(wrapper, done, callback) { - if (typeof atob === 'function' && typeof Blob === 'function' && typeof File === 'function') { - // 模拟文件上传 - const file = buildFile(); - wrapper.find('input').simulate('change', { target: { files: [file] } }); - callback && callback(wrapper); - } else { - done(); - } -} - -describe('TextUpload', () => { - let requests; - let xhr; - - beforeEach(() => { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = req => requests.push(req); - }); - - afterEach(() => { - xhr.restore(); - }); - - describe('render', () => { - it('should render a wrapper upload', () => { - const wrapper = mount(); - assert(wrapper.find('.next-upload').length === 1); - assert(wrapper.find('.next-upload-list-item').length === 1); - }); - it('should render a error item without text', () => { - const wrapper = mount( - - ); - assert(wrapper.find('.next-upload').length === 1); - assert(wrapper.find('.next-upload-list-item-error').length === 1); - assert(wrapper.find('.next-upload-list-item-error-with-text').length === 0); - }); - it('should render a upload item', () => { - const wrapper = mount( - - ); - assert(wrapper.find('.next-upload').length === 1); - assert(wrapper.find('.next-upload-list-item-uploading').length === 1); - }); - it('should render a error item with text', () => { - const wrapper = mount( - - ); - assert(wrapper.find('.next-upload').length === 1); - assert(wrapper.find('.next-upload-list-item-error').length === 1); - assert(wrapper.find('.next-upload-list-item-error-with-msg').length === 1); - assert(wrapper.find('.next-upload-list-item-error-msg').length === 1); - assert( - wrapper - .find('.next-upload-list-item-error-msg') - .at(0) - .text() === 'error text' - ); - }); - }); -}); diff --git a/components/upload/__tests__/text-spec.tsx b/components/upload/__tests__/text-spec.tsx new file mode 100644 index 0000000000..766ad0f15d --- /dev/null +++ b/components/upload/__tests__/text-spec.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import Upload from '../index'; + +const defaultValue = [ + { + name: 'IMG.png', + state: 'done', + size: 1024, + url: 'https://img.alicdn.com/tps/TB19O79MVXXXXcZXVXXXXXXXXXX-1024-1024.jpg', + }, +]; + +describe('TextUpload', () => { + describe('render', () => { + it('should render a wrapper upload', () => { + cy.mount(); + cy.get('.next-upload'); + cy.get('.next-upload-list-item'); + }); + it('should render a error item without text', () => { + cy.mount( + + ); + cy.get('.next-upload'); + cy.get('.next-upload-list-item-error'); + cy.get('.next-upload-list-item-error-with-text').should('not.exist'); + }); + it('should render a upload item', () => { + cy.mount( + + ); + cy.get('.next-upload'); + cy.get('.next-upload-list-item-uploading'); + }); + it('should render a error item with text', () => { + cy.mount( + + ); + cy.get('.next-upload'); + cy.get('.next-upload-list-item-error'); + cy.get('.next-upload-list-item-error-with-msg'); + cy.get('.next-upload-list-item-error-msg'); + cy.get('.next-upload-list-item-error-msg').first().should('have.text', 'error text'); + }); + }); +}); diff --git a/components/upload/__tests__/upload-spec.js b/components/upload/__tests__/upload-spec.ts similarity index 100% rename from components/upload/__tests__/upload-spec.js rename to components/upload/__tests__/upload-spec.ts diff --git a/components/upload/__tests__/util-spec.js b/components/upload/__tests__/util-spec.ts similarity index 83% rename from components/upload/__tests__/util-spec.js rename to components/upload/__tests__/util-spec.ts index 9a6190ced6..094814256e 100644 --- a/components/upload/__tests__/util-spec.js +++ b/components/upload/__tests__/util-spec.ts @@ -1,5 +1,5 @@ -import assert from 'power-assert'; import { uid, fileToObject, getFileItem, removeFileItem, previewFile, errorCode } from '../util'; +import type { UploadFile } from '../types'; describe('util function test', () => { it('uid generate', () => { @@ -10,7 +10,7 @@ describe('util function test', () => { it('fileToObject', () => { const file = { lastModified: 22334, - lastModifiedDate: 11223, + lastModifiedDate: new Date(), name: 'cjk.png', size: 12345, type: 'image/png', @@ -18,7 +18,7 @@ describe('util function test', () => { // error: null, percent: 0, // originFileObj: null, - }; + } as UploadFile; assert(fileToObject(file).uid === file.uid); }); @@ -31,17 +31,19 @@ describe('util function test', () => { it('removeFileItem remove one', () => { const file = { uid: 1, 1: 1 }; const files = [file, { uid: 2, 2: 2 }]; - assert(removeFileItem(file, files).length === files.length - 1); + assert(removeFileItem(file, files)!.length === files.length - 1); }); it('removeFileItem not find one to remove', () => { const file = { uid: 1, 1: 1 }; - const files = [{ uid: 3, 3: 3 }, { uid: 2, 2: 2 }]; + const files: { uid: number; [key: number]: number }[] = [ + { uid: 3, 3: 3 }, + { uid: 2, 2: 2 }, + ]; assert(removeFileItem(file, files) === null); }); - it('previewFile', done => { + it('previewFile', () => { if (!window.FileReader || !window.File || !window.Blob) { - done(); return; } @@ -50,7 +52,6 @@ describe('util function test', () => { previewFile(blob, dataurl => { const data = 'data:application/json;base64,ewogICJoZWxsbyI6ICJ3b3JsZCIKfQ=='; assert(dataurl === data); - done(); }); }); it('errCode', () => { diff --git a/components/upload/base.jsx b/components/upload/base.jsx deleted file mode 100644 index 37e38c998e..0000000000 --- a/components/upload/base.jsx +++ /dev/null @@ -1,29 +0,0 @@ -import { Component } from 'react'; - -export default class Base extends Component { - /* istanbul ignore next */ - abort(file) { - /* istanbul ignore next */ - this.uploaderRef.abort(file); - } - /* istanbul ignore next */ - startUpload() { - /* istanbul ignore next */ - this.uploaderRef.startUpload(); - } - - saveUploaderRef = ref => { - /* istanbul ignore if */ - if (ref && typeof ref.getInstance === 'function') { - this.uploaderRef = ref.getInstance(); - } else { - this.uploaderRef = ref; - } - }; - - /* istanbul ignore next */ - isUploading() { - /* istanbul ignore next */ - return this.uploaderRef.isUploading(); - } -} diff --git a/components/upload/base.tsx b/components/upload/base.tsx new file mode 100644 index 0000000000..7c68b551d4 --- /dev/null +++ b/components/upload/base.tsx @@ -0,0 +1,26 @@ +import { Component } from 'react'; +import type { UploaderRef } from './types'; + +export default class Base extends Component { + uploaderRef: UploaderRef; + + abort(file: File) { + this.uploaderRef.abort(file); + } + + startUpload() { + this.uploaderRef.startUpload(); + } + + saveUploaderRef = (ref: UploaderRef | { getInstance: () => UploaderRef } | null) => { + if (ref && typeof (ref as { getInstance: () => UploaderRef }).getInstance === 'function') { + this.uploaderRef = (ref as { getInstance: () => UploaderRef }).getInstance(); + } else { + this.uploaderRef = ref as UploaderRef; + } + }; + + isUploading() { + return this.uploaderRef.isUploading(); + } +} diff --git a/components/upload/card.jsx b/components/upload/card.tsx similarity index 76% rename from components/upload/card.jsx rename to components/upload/card.tsx index eadd86877f..30ca338d8f 100644 --- a/components/upload/card.jsx +++ b/components/upload/card.tsx @@ -8,12 +8,13 @@ import { func, obj } from '../util'; import Base from './base'; import List from './list'; import Upload from './upload'; +import type { CardProps, CardState, ObjectFile, UploadFile } from './types'; /** * Upload.Card - * @description 继承 Upload 的 API,除非特别说明 + * 继承 Upload 的 API,除非特别说明 */ -class Card extends Base { +class Card extends Base { static displayName = 'Card'; static propTypes = { @@ -22,43 +23,13 @@ class Card extends Base { children: PropTypes.object, value: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), defaultValue: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), - /** - * 点击图片回调 - */ onPreview: PropTypes.func, - /** - * 改变时候的回调 - */ onChange: PropTypes.func, - /** - * 点击移除的回调 - */ onRemove: PropTypes.func, - /** - * 取消上传的回调 - */ onCancel: PropTypes.func, - /** - * 自定义成功和失败的列表渲染方式 - * @param {File} file 文件对象 - * @param {Object} obj {remove: 删除回调} - * @retuns {ReactNode} React元素 - * @version 1.21 - */ itemRender: PropTypes.func, - /** - * 选择新文件上传并替换 - * @version 1.24 - */ reUpload: PropTypes.bool, - /** - * 展示下载按钮 - * @version 1.24 - */ showDownload: PropTypes.bool, - /** - * 上传中 - */ onProgress: PropTypes.func, isPreview: PropTypes.bool, renderPreview: PropTypes.func, @@ -73,11 +44,10 @@ class Card extends Base { onProgress: func.noop, }; - constructor(props) { + constructor(props: CardProps) { super(props); let value; - /* istanbul ignore else */ if ('value' in props) { value = props.value; } else { @@ -90,6 +60,8 @@ class Card extends Base { }; } + uploaderRef: InstanceType; + componentDidMount() { this.updateUploaderRef(this.uploaderRef); } @@ -101,43 +73,45 @@ class Card extends Base { } } - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps(nextProps: CardProps, prevState: CardState) { const isUploading = prevState.uploaderRef && prevState.uploaderRef.isUploading(); if ('value' in nextProps && nextProps.value !== prevState.value && !isUploading) { return { - value: !Array.isArray(nextProps.value) ? [] : [].concat(nextProps.value), + value: !Array.isArray(nextProps.value) + ? [] + : ([] as ObjectFile[]).concat(nextProps.value), }; } return null; } - onProgress = (value, targetItem) => { + onProgress = (value: UploadFile[], targetItem: UploadFile) => { this.setState({ value, }); - this.props.onProgress(value, targetItem); + this.props.onProgress!(value, targetItem); }; - onChange = (value, file) => { + onChange = (value: Array, file: UploadFile) => { if (!('value' in this.props)) { this.setState({ value, }); } - this.props.onChange(value, file); + this.props.onChange!(value, file); }; isUploading() { return this.uploaderRef.isUploading(); } - saveRef(ref) { + saveRef(ref: InstanceType | null) { this.saveUploaderRef(ref); } - updateUploaderRef(uploaderRef) { + updateUploaderRef(uploaderRef: InstanceType) { this.setState({ uploaderRef }); } @@ -161,23 +135,23 @@ class Card extends Base { showDownload, } = this.props; - const isExceedLimit = this.state.value.length >= limit; + const isExceedLimit = this.state.value.length >= limit!; const uploadButtonCls = classNames({ [`${prefix}upload-list-item`]: true, [`${prefix}hidden`]: isExceedLimit, }); - const children = this.props.children || locale.card.addPhoto; + const children = this.props.children || locale!.card!.addPhoto; const onRemoveFunc = disabled ? func.prevent : onRemove; const othersForList = obj.pickOthers(Card.propTypes, this.props); - const othersForUpload = obj.pickOthers(List.propTypes, othersForList); + const othersForUpload = obj.pickOthers(List.propTypes!, othersForList); if (isPreview) { if (typeof renderPreview === 'function') { const previewCls = classNames({ [`${prefix}form-preview`]: true, - [className]: !!className, + [className!]: !!className, }); return (
@@ -194,7 +168,7 @@ class Card extends Base { listType="card" closable locale={locale} - value={this.state.value} + value={this.state.value as UploadFile[]} onRemove={onRemoveFunc} onCancel={onCancel} onPreview={onPreview} diff --git a/components/upload/dragger.jsx b/components/upload/dragger.tsx similarity index 66% rename from components/upload/dragger.jsx rename to components/upload/dragger.tsx index af235320bf..c053d535dd 100644 --- a/components/upload/dragger.jsx +++ b/components/upload/dragger.tsx @@ -1,20 +1,18 @@ -import React from 'react'; +import React, { type DragEvent, Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import Icon from '../icon'; import { func } from '../util'; import zhCN from '../locale/zh-cn'; import Upload from './upload'; +import type { DraggerProps } from './types'; /** * Upload.Dragger - * @description IE10+ 支持。继承 Upload 的 API,除非特别说明 + * IE10+ 支持。继承 Upload 的 API,除非特别说明 */ -class Dragger extends React.Component { +class Dragger extends Component { static propTypes = { - /** - * 样式前缀 - */ prefix: PropTypes.string, locale: PropTypes.object, shape: PropTypes.string, @@ -38,51 +36,57 @@ class Dragger extends React.Component { locale: zhCN.Upload, }; + uploaderRef: InstanceType; + state = { dragOver: false, }; - onDragOver = e => { + onDragOver = (e: DragEvent) => { if (!this.state.dragOver) { this.setState({ dragOver: true, }); } - this.props.onDragOver(e); + this.props.onDragOver!(e); }; - onDragLeave = e => { + onDragLeave = (e: DragEvent) => { this.setState({ dragOver: false, }); - this.props.onDragLeave(e); + this.props.onDragLeave!(e); }; - onDrop = e => { + onDrop = (e: File[]) => { this.setState({ dragOver: false, }); - this.props.onDrop(e); + this.props.onDrop!(e); }; - /* istanbul ignore next */ - abort(file) { - /* istanbul ignore next */ + abort(file: File) { this.uploaderRef.abort(file); } - /* istanbul ignore next */ + startUpload() { - /* istanbul ignore next */ this.uploaderRef.startUpload(); } - saveUploaderRef = ref => { - /* istanbul ignore if */ - if (ref && typeof ref.getInstance === 'function') { - this.uploaderRef = ref.getInstance(); + saveUploaderRef = ( + ref: InstanceType | { getInstance: () => InstanceType } | null + ) => { + if ( + ref && + typeof (ref as { getInstance: () => InstanceType }).getInstance === + 'function' + ) { + this.uploaderRef = ( + ref as { getInstance: () => InstanceType } + ).getInstance(); } else { - this.uploaderRef = ref; + this.uploaderRef = ref as InstanceType; } }; @@ -92,7 +96,7 @@ class Dragger extends React.Component { const cls = classNames({ [`${prefixCls}`]: true, [`${prefixCls}-over`]: this.state.dragOver, - [className]: !!className, + [className!]: !!className, }); const children = this.props.children || ( @@ -100,8 +104,8 @@ class Dragger extends React.Component {

-

{locale.drag.text}

-

{locale.drag.hint}

+

{locale!.drag!.text}

+

{locale!.drag!.hint}

); diff --git a/components/upload/index.d.ts b/components/upload/index.d.ts deleted file mode 100644 index 48d6b08cc3..0000000000 --- a/components/upload/index.d.ts +++ /dev/null @@ -1,544 +0,0 @@ -/// - -import React from 'react'; -import { CommonProps } from '../util'; -import { ProgressProps } from '../progress'; - -interface HTMLAttributesWeak extends React.HTMLAttributes { - onError?: any; - onSelect?: any; - defaultValue?: any; - onChange?: any; -} - -export interface CardProps extends HTMLAttributesWeak, CommonProps { - /** - * 上传的地址 - */ - action?: string; - - /** - * 是否支持多选文件,`ie10+` 支持。开启后按住 ctrl 可选择多个文件 - */ - multiple?: boolean; - - /** - * 展示下载按钮 - */ - showDownload?: boolean; - - /** - * 接受上传的文件类型 (image/png, image/jpg, .doc, .ppt) 详见 [input accept attribute](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input#attr-accept) - */ - accept?: string; - - /** - * 上传额外传参 - */ - data?: any | (() => void); - - /** - * 设置上传的请求头部 - */ - headers?: any; - - /** - * 是否允许请求携带 cookie - */ - withCredentials?: boolean; - - /** - * 可选参数, 详见 [beforeUpload](#beforeUpload) - */ - beforeUpload?: (file: {}, options: {}) => boolean | {} | any; - - /** - * 上传中 - */ - onProgress?: () => void; - - /** - * 可选参数,上传成功回调函数,参数为请求下响应信息以及文件 - */ - onSuccess?: (file: {}, value: Array) => void; - - /** - * 可选参数,上传失败回调函数,参数为上传失败的信息、响应信息以及文件 - */ - onError?: (file: {}, value: Array) => void; - - /** - * 子元素 - */ - children?: React.ReactNode; - - /** - * 设置上传超时,单位ms - */ - timeout?: number; - - /** - * 上传方法 - */ - method?: 'post' | 'put'; - - /** - * 自定义上传方法 - */ - request?: (option: {}) => void; - - /** - * 文件名字段 - */ - name?: string; - - /** - * 选择文件回调 - */ - onSelect?: () => void; - - /** - * 放文件 - */ - onDrop?: () => void; - - /** - * 样式前缀 - */ - prefix?: string; - - /** - * 文件列表 - */ - value?: Array; - - /** - * 默认文件列表 - */ - defaultValue?: Array; - - /** - * 上传按钮形状 - */ - shape?: 'card'; - - /** - * 上传列表的样式 - */ - listType?: 'text' | 'image' | 'card'; - - /** - * 数据格式化函数,配合自定义 action 使用,参数为服务器的响应数据,详见 [formatter](#formater) - */ - formatter?: (response: {}, file: any) => void; - - /** - * 最大文件上传个数 - */ - limit?: number; - - /** - * 可选参数,是否支持拖拽上传,`ie10+` 支持。 - */ - dragable?: boolean; - - /** - * 可选参数,是否本地预览 - */ - useDataURL?: boolean; - - /** - * 可选参数,是否禁用上传功能 - */ - disabled?: boolean; - - /** - * 改变时候的回调 - */ - onChange?: (value: File[]) => void; - - /** - * 可选参数, 用于校验文件,afterSelect仅在 autoUpload=false 的时候生效,autoUpload=true时,可以使用beforeUpload完全可以替代该功能. - */ - afterSelect?: (file: {}) => boolean; - - /** - * 点击移除的回调 - */ - onRemove?: () => void; - - /** - * 自定义class - */ - className?: string; - - /** - * 自定义内联样式 - */ - style?: React.CSSProperties; - - /** - * 自动上传 - */ - autoUpload?: boolean; - - /** - * 透传给Progress props - */ - progressProps?: ProgressProps; - - /** - * 点击图片回调 - */ - onPreview?: () => void; - - /** - * 取消上传的回调 - */ - onCancel?: () => void; - /** - * 调用系统设备媒体 - */ - capture?: string; - - /** - * 自定义成功和失败的列表渲染方式 - */ - itemRender?: (file: File, obj: { remove?: () => void }) => React.ReactNode; - - /** - * 选择新文件上传并替换 - */ - reUpload?: boolean; -} - -export class Card extends React.Component {} - -export class Dragger extends React.Component {} - -export interface SelecterProps extends HTMLAttributesWeak, CommonProps { - /** - * 是否禁用上传功能 - */ - disabled?: boolean; - - /** - * 是否支持多选文件,`ie10+` 支持。开启后按住 ctrl 可选择多个文件 - */ - multiple?: boolean; - - /** - * 是否支持拖拽上传,`ie10+` 支持。 - */ - dragable?: boolean; - - /** - * 接受上传的文件类型 (image/png, image/jpg, .doc, .ppt) 详见 [input accept attribute](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input#attr-accept) - */ - accept?: string; - - /** - * 文件选择回调 - */ - onSelect?: (e: React.ChangeEvent) => void; - - /** - * 拖拽经过回调 - */ - onDragOver?: () => void; - - /** - * 拖拽离开回调 - */ - onDragLeave?: () => void; - - /** - * 拖拽完成回调 - */ - onDrop?: () => void; - - /** - * 是否支持上传文件夹,仅在 chorme 下生效 - */ - webkitdirectory?: boolean; -} - -export class Selecter extends React.Component {} - -export class Uploader { - /** - * @param options 配置 - */ - constructor(options?: any); - - /** - * 配置选项 - * @param options 配置 - */ - setOptions(options: any): void; - /** - * 开始上传 - * @param files 文件列表 - */ - startUpload(files: Array): void; - /** - * 中断某个文件上传 - * @param file 文件 - */ - abort(file: any): void; -} - -export interface UploadProps extends HTMLAttributesWeak, CommonProps { - /** - * 上传的地址 - */ - action?: string; - - /** - * 接受上传的文件类型 (image/png, image/jpg, .doc, .ppt) 详见 [input accept attribute](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input#attr-accept) - */ - accept?: string; - - /** - * 上传额外传参 - */ - data?: any | (() => void); - - /** - * 是否支持多选文件,`ie10+` 支持。开启后按住 ctrl 可选择多个文件 - */ - multiple?: boolean; - - /** - * 设置上传的请求头部 - */ - headers?: any; - - /** - * 是否允许请求携带 cookie - */ - withCredentials?: boolean; - - /** - * 可选参数, 详见 [beforeUpload](#beforeUpload) - */ - beforeUpload?: (file: any, options: any) => boolean | {} | any; - - /** - * 上传中 - */ - onProgress?: () => void; - - /** - * 可选参数,上传成功回调函数,参数为请求下响应信息以及文件 - */ - onSuccess?: (file: any, value: Array) => void; - - /** - * 可选参数,上传失败回调函数,参数为上传失败的信息、响应信息以及文件 - */ - onError?: (file: any, value: Array) => void; - - /** - * 子元素 - */ - children?: React.ReactNode; - - /** - * 设置上传超时,单位ms - */ - timeout?: number; - - /** - * 上传方法 - */ - method?: 'post' | 'put'; - - /** - * 自定义上传方法 - */ - request?: (option: any) => any; - - /** - * 文件名字段 - */ - name?: string; - - /** - * 选择文件回调 - */ - onSelect?: (uploadFiles: Array, value: Array) => void; - - /** - * 放文件 - */ - onDrop?: () => void; - - /** - * 样式前缀 - */ - prefix?: string; - - /** - * 文件列表 - */ - value?: Array; - - /** - * 默认文件列表 - */ - defaultValue?: Array; - - /** - * 上传按钮形状 - */ - shape?: 'card'; - - /** - * 上传列表的样式 - */ - listType?: 'text' | 'image' | 'card'; - - /** - * 数据格式化函数,配合自定义 action 使用,参数为服务器的响应数据,详见 [formatter](#formater) - */ - formatter?: (response: any, file: any) => void; - - /** - * 最大文件上传个数 - */ - limit?: number; - - /** - * 可选参数,是否支持拖拽上传,`ie10+` 支持。 - */ - dragable?: boolean; - - /** - * 可选参数,是否本地预览 - */ - useDataURL?: boolean; - - /** - * 可选参数,是否禁用上传功能 - */ - disabled?: boolean; - - /** - * 上传文件改变时的状态 - */ - onChange?: (value: File[]) => void; - - /** - * 可选参数, 用于校验文件,afterSelect仅在 autoUpload=false 的时候生效,autoUpload=true时,可以使用beforeUpload完全可以替代该功能. - */ - afterSelect?: (file: any) => boolean; - - /** - * 移除文件回调函数 - */ - onRemove?: (file: any) => boolean | any; - - /** - * 自定义额外渲染 - */ - extraRender?: (file: File) => any; - /** - * 自定义文件名渲染 - */ - fileNameRender?: (file: File) => any; - /** - * 自定义操作区域渲染 - */ - actionRender?: (file: File) => any; - /** - * 自定义class - */ - className?: string; - - /** - * 自定义内联样式 - */ - style?: React.CSSProperties; - - /** - * 自动上传 - */ - autoUpload?: boolean; - - /** - * 透传给Progress props - */ - progressProps?: ProgressProps; - - /** - * 是否为预览态 - */ - isPreview?: boolean; - - /** - * 预览态模式下渲染的内容 - */ - renderPreview?: (value: number) => void; - - /** - * 文件对象的 key name - */ - fileKeyName?: string; - /** - * 点击文件名时触发 onPreview - * @version 1.24 - */ - previewOnFileName?: boolean; - - /** - * 自定义成功和失败的列表渲染方式 - */ - itemRender?: (file: File, obj: { remove?: () => void }) => React.ReactNode; - - /** - * 选择新文件上传并替换 - */ - reUpload?: boolean; -} - -export enum ErrorCode { - EXCEED_LIMIT = 'EXCEED_LIMIT', - BEFOREUPLOAD_REJECT = 'BEFOREUPLOAD_REJECT', - RESPONSE_FAIL = 'RESPONSE_FAIL', -} - -export default class Upload extends React.Component { - static Card: typeof Card; - static Dragger: typeof Dragger; - static Selecter: typeof Selecter; - static Uploader: typeof Uploader; - static ErrorCode: typeof ErrorCode; - /** - * 添加文件 - * @param files - */ - selectFiles: (file: File) => void; - /** - * 控制文件上传 - */ - startUpload: () => void; - /** - * 控制文件上传 - * @param file 文件 - */ - uploadFiles: (file: File) => void; - /** - * 替换文件 - */ - replaceFiles: (old: object, current: object) => void; - /** - * 上传状态 - */ - isUploading: () => boolean; - /** - * 中断某个文件上传 - * @param file 文件 - */ - abort: (file: File) => void; -} diff --git a/components/upload/index.jsx b/components/upload/index.jsx deleted file mode 100644 index 880a1ce0b1..0000000000 --- a/components/upload/index.jsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from 'react'; -import ConfigProvider from '../config-provider'; -import { log } from '../util'; -import { errorCode } from './util'; -import transform from './transform'; -import Upload from './upload'; -import List from './list'; -import Card from './card'; -import Dragger from './dragger'; -import Selecter from './runtime/selecter'; -import Uploader from './runtime/uploader'; - -Upload.Card = ConfigProvider.config(Card, { componentName: 'Upload' }); -Upload.Dragger = ConfigProvider.config(Dragger, { componentName: 'Upload' }); -Upload.Selecter = Selecter; -Upload.Uploader = Uploader; -Upload.ErrorCode = errorCode; - -// compatible with 0.x version -Upload.ImageUpload = ConfigProvider.config(Card, { - componentName: 'Upload', - transform: /* istanbul ignore next */ (props, deprecated) => { - deprecated('Upload.ImageUpload', 'Upload.Card', 'Upload'); - const newprops = transform(props, () => {}); - if (newprops.locale && newprops.locale.image) { - newprops.locale.card = newprops.locale.image; - } - - return newprops; - }, -}); - -// compatible with 0.x version -Upload.DragUpload = ConfigProvider.config(Dragger, { - componentName: 'Upload', - transform: /* istanbul ignore next */ (props, deprecated) => { - deprecated('Upload.DragUpload', 'Upload.Dragger', 'Upload'); - const newprops = transform(props, () => {}); - if (!newprops.listType) { - newprops.listType = 'card'; - } - - return newprops; - }, -}); - -// compatible with 0.x version -/* istanbul ignore next */ -Upload.Core = class Core extends React.Component { - constructor(props) { - super(props); - // eslint-disable-next-line - const { - action, - name, - method, - beforeUpload, - onProgress, - onError, - withCredentials, - headers, - data, - onSuccess, - } = this.props; - - this.uploader = new Uploader({ - action, - name, - method, - beforeUpload, - onProgress, - onError, - withCredentials, - headers, - data, - onSuccess, - }); - } - - abort() { - this.uploader.abort(); - } - - handleSelect = files => { - this.uploader.startUpload(files); - }; - - render() { - log.deprecated('Upload.Core', 'Upload.Selecter and Upload.Uploader', 'Upload'); - - // eslint-disable-next-line - const { - action, - name, - method, - beforeUpload, - onProgress, - onError, - withCredentials, - headers, - data, - onSuccess, - ...others - } = this.props; - - const props = others; - - return {})} onSelect={this.handleSelect} />; - } -}; - -Upload.List = List; - -// compatible with 0.x version -/* istanbul ignore next */ -Upload.CropUpload = function() { - log.deprecated('Upload.CropUpload', '@alife/bc-next-crop-upload', 'Upload'); - return null; -}; - -export default ConfigProvider.config(Upload, { - transform, -}); diff --git a/components/upload/index.tsx b/components/upload/index.tsx new file mode 100644 index 0000000000..e5c04402fd --- /dev/null +++ b/components/upload/index.tsx @@ -0,0 +1,135 @@ +import React, { Component } from 'react'; +import ConfigProvider from '../config-provider'; +import { log } from '../util'; +import { errorCode } from './util'; +import transform from './transform'; +import Upload from './upload'; +import List from './list'; +import Card from './card'; +import Dragger from './dragger'; +import Selecter from './runtime/selecter'; +import Uploader from './runtime/uploader'; +import { assignSubComponent } from '../util/component'; +import type { CardProps, CoreProps, UploadFile, ListType } from './types'; + +export type { + UploadProps, + CardProps, + SelecterProps, + DraggerProps, + UploadFile, + UploadOptions, + UploadError, +} from './types'; + +class Core extends Component { + uploader: Uploader; + constructor(props: CoreProps) { + super(props); + const { + action, + name, + method, + beforeUpload, + onProgress, + onError, + withCredentials, + headers, + data, + onSuccess, + } = this.props; + + this.uploader = new Uploader({ + action, + name, + method, + beforeUpload, + onProgress, + onError, + withCredentials, + headers, + data, + onSuccess, + }); + } + + abort() { + this.uploader.abort(); + } + + handleSelect = (files: UploadFile[]) => { + this.uploader.startUpload(files); + }; + + render() { + log.deprecated('Upload.Core', 'Upload.Selecter and Upload.Uploader', 'Upload'); + + const { + action, + name, + method, + beforeUpload, + onProgress, + onError, + withCredentials, + headers, + data, + onSuccess, + ...others + } = this.props; + + const props = others; + + return {})} onSelect={this.handleSelect} />; + } +} + +const UploadWithSub = assignSubComponent(Upload, { + List: List, + Card: ConfigProvider.config(Card, { componentName: 'Upload' }), + Dragger: ConfigProvider.config(Dragger, { + componentName: 'Upload', + }), + Selecter: Selecter, + Uploader: Uploader, + ErrorCode: errorCode, + // compatible with 0.x version + ImageUpload: ConfigProvider.config(Card, { + componentName: 'Upload', + transform: (props, deprecated) => { + deprecated!('Upload.ImageUpload', 'Upload.Card', 'Upload'); + const newprops: CardProps = transform(props, () => {}); + if (newprops.locale && newprops.locale.image) { + newprops.locale.card = newprops.locale.image; + } + + return newprops; + }, + }), + // compatible with 0.x version + DragUpload: ConfigProvider.config(Dragger, { + componentName: 'Upload', + transform: (props, deprecated) => { + deprecated!('Upload.DragUpload', 'Upload.Dragger', 'Upload'); + const newprops: { listType?: ListType; [key: string]: unknown } = transform( + props, + () => {} + ); + if (!newprops.listType) { + newprops.listType = 'card'; + } + + return newprops; + }, + }), + Core: Core, + // compatible with 0.x version + CropUpload: function () { + log.deprecated('Upload.CropUpload', '@alife/bc-next-crop-upload', 'Upload'); + return null; + }, +}); + +export default ConfigProvider.config(UploadWithSub, { + transform, +}); diff --git a/components/upload/list.jsx b/components/upload/list.tsx similarity index 78% rename from components/upload/list.jsx rename to components/upload/list.tsx index c151a654d5..3696b4e82b 100644 --- a/components/upload/list.jsx +++ b/components/upload/list.tsx @@ -1,6 +1,7 @@ -import React, { Component } from 'react'; +import React, { Component, type KeyboardEvent, type MouseEvent } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; + import ConfigProvider from '../config-provider'; import Progress from '../progress'; import Icon from '../icon'; @@ -9,72 +10,30 @@ import { func, obj, KEYCODE, env } from '../util'; import zhCN from '../locale/zh-cn'; import { previewFile } from './util'; import transform from './transform'; -import Item from '../menu/view/item'; import Selecter from './runtime/selecter'; +import type { ListProps, UploadFile, ImageError } from './types'; const isIE9 = env.ieVersion === 9; -class List extends Component { +class List extends Component { static propTypes = { prefix: PropTypes.string, - /** - * 多语言 - */ locale: PropTypes.object, - /** - * 文件列表,数据格式请参考 文件对象 - */ listType: PropTypes.oneOf(['text', 'image', 'card']), - /** - * 文件列表 - */ value: PropTypes.array, closable: PropTypes.bool, - /** - * 删除文件回调(支持Promise) - */ onRemove: PropTypes.func, - /** - * 取消上传回调(支持Promise) - */ onCancel: PropTypes.func, - /** - * 头像加载出错回调 - */ onImageError: PropTypes.func, - /** - * 点击图片回调 - */ onPreview: PropTypes.func, - /** - * 点击文件名时触发 onPreview - */ previewOnFileName: PropTypes.bool, - /** - * 自定义额外渲染 - */ extraRender: PropTypes.func, - /** - * 自定义操作渲染 - */ actionRender: PropTypes.func, - /** - * 卡片自定义渲染(目前只支持 Card) - * @param {Object} file 文件对象 - * @param {Object} {remove} remove:删除回调 - * @retuns {ReactNode} React元素 - */ itemRender: PropTypes.func, - /** - * 透传给Progress props - */ progressProps: PropTypes.object, children: PropTypes.node, uploader: PropTypes.any, showDownload: PropTypes.bool, - /** - * 可选参数,是否本地预览 - */ useDataURL: PropTypes.bool, rtl: PropTypes.bool, isPreview: PropTypes.bool, @@ -94,7 +53,7 @@ class List extends Component { actionRender: func.noop, onImageError: func.noop, progressProps: {}, - fileNameRender: file => file.name, + fileNameRender: (file: File) => file.name, previewOnFileName: false, }; @@ -118,37 +77,37 @@ class List extends Component { } file.imgURL = ''; previewFile(file.originFileObj, previewDataUrl => { - file.imgURL = previewDataUrl; + file.imgURL = previewDataUrl as string; this.forceUpdate(); }); }); } - handleClose = file => { + handleClose = (file: UploadFile) => { const { onRemove, uploader } = this.props; - const remove = onRemove(file); + const remove = onRemove!(file); func.promiseCall(remove, () => { uploader && uploader.removeFile(file); }); }; - handleCancel = file => { + handleCancel = (file: UploadFile) => { const { onCancel, uploader } = this.props; - const cancel = onCancel(file); + const cancel = onCancel!(file); func.promiseCall(cancel, () => { uploader && uploader.abort(file); }); }; - onImageError = (file, obj) => { + onImageError = (file: UploadFile, obj: ImageError) => { obj.onerror = null; - this.props.onImageError(obj, file); + this.props.onImageError!(obj, file); }; - onPreview(file, e) { + onPreview(file: UploadFile, e: MouseEvent) { const { onPreview } = this.props; if (!onPreview) { @@ -158,11 +117,11 @@ class List extends Component { return onPreview(file, e); } - getInfo(file) { + getInfo(file: UploadFile) { const prefixCls = `${this.props.prefix}upload`; const downloadURL = file.downloadURL || file.url; const imgURL = file.imgURL || file.url; - const size = this.sizeCaculator(file.size); + const size = this.sizeCaculator(file.size as unknown as string); const itemCls = classNames({ [`${prefixCls}-list-item`]: true, [`${prefixCls}-list-item-${file.state}`]: file.state, @@ -172,8 +131,8 @@ class List extends Component { return { prefixCls, downloadURL, imgURL, size, itemCls, alt }; } // transfer size from number to xx K/ XxxM / xxG - sizeCaculator(size) { - let fileSize = parseFloat(size, 10); + sizeCaculator(size: string) { + let fileSize = parseFloat(size); // fileSize为浮点数 用 < 0.000001 替代 === 0 if (isNaN(fileSize) || fileSize < 0.0000001) { return 0; @@ -190,16 +149,25 @@ class List extends Component { } const suffix = SIZE_SUFFIX[suffixIndex]; - fileSize = fileSize.toFixed(2); + fileSize = fileSize.toFixed(2) as unknown as number; return `${fileSize}${suffix}`; } - getTextList(file) { - const { locale, extraRender, actionRender, progressProps, rtl, fileNameRender, previewOnFileName } = this.props; + getTextList(file: UploadFile) { + const { + locale, + extraRender, + actionRender, + progressProps, + rtl, + fileNameRender, + previewOnFileName, + } = this.props; const { prefixCls, downloadURL, size, itemCls } = this.getInfo(file); - const onClick = () => (file.state === 'uploading' ? this.handleCancel(file) : this.handleClose(file)); - const onKeyDown = e => { + const onClick = () => + file.state === 'uploading' ? this.handleCancel(file) : this.handleClose(file); + const onKeyDown = (e: KeyboardEvent) => { if (e.keyCode === KEYCODE.ENTER) { onClick(); } @@ -211,16 +179,19 @@ class List extends Component { onClick={previewOnFileName ? this.onPreview.bind(this, file) : func.noop} href={downloadURL} target="_blank" - style={{ pointerEvents: downloadURL ? '' : 'none' }} + style={{ pointerEvents: (downloadURL ? '' : 'none') as 'auto' | 'none' }} className={`${prefixCls}-list-item-name`} > - {fileNameRender(file)} + {fileNameRender!(file)} {!!size && ( - + ({size}) )} - {extraRender(file)} + {extraRender!(file)}
{file.state === 'uploading' ? ( @@ -238,14 +209,14 @@ class List extends Component {
{file.errorMsg}
) : null} - {actionRender(file)} + {actionRender!(file)} {this.props.closable ? ( @@ -255,15 +226,17 @@ class List extends Component { ); } - getImageList(file) { - const { extraRender, actionRender, progressProps, rtl, fileNameRender, previewOnFileName } = this.props; + getImageList(file: UploadFile) { + const { extraRender, actionRender, progressProps, rtl, fileNameRender, previewOnFileName } = + this.props; const { prefixCls, downloadURL, imgURL, size, itemCls, alt } = this.getInfo(file); let img = null; - const onClick = () => (file.state === 'uploading' ? this.handleCancel(file) : this.handleClose(file)); - const onKeyDown = e => { + const onClick = () => + file.state === 'uploading' ? this.handleCancel(file) : this.handleClose(file); + const onKeyDown = (e: KeyboardEvent) => { if (e.keyCode === KEYCODE.ENTER) { onClick(); } @@ -278,7 +251,7 @@ class List extends Component { {alt} @@ -289,12 +262,12 @@ class List extends Component {
{img}
- {actionRender(file)} + {actionRender!(file)} {this.props.closable ? ( - {fileNameRender(file)} + {fileNameRender!(file)} {!!size && ( - + ({size}) )} - {extraRender(file)} + {extraRender!(file)} {file.state === 'uploading' ? (
- +
) : null} {file.state === 'error' && file.errorMsg ? ( @@ -328,12 +309,12 @@ class List extends Component { ); } - onSelect = (oldfile, files) => { + onSelect = (oldfile: UploadFile, files: UploadFile[]) => { const uploader = this.props.uploader; uploader && files.length && uploader.replaceWithNewFile(oldfile, files[0]); }; - getPictureCardList(file, isPreview) { + getPictureCardList(file: UploadFile, isPreview?: boolean) { const { locale, progressProps, fileNameRender, itemRender, showDownload } = this.props; const { prefixCls, downloadURL, imgURL, itemCls, alt } = this.getInfo(file); @@ -346,7 +327,7 @@ class List extends Component {
); @@ -360,7 +341,7 @@ class List extends Component { img = ( {alt} this.handleClose(file); - const onKeyDownClose = e => { + const onKeyDownClose = (e: KeyboardEvent) => { if (e.keyCode === KEYCODE.ENTER) { onClose(); } @@ -382,7 +363,12 @@ class List extends Component { {img}
,
- +
, ]; } else { @@ -392,7 +378,10 @@ class List extends Component { item = itemRender(file, { remove: onClose }); } else { const Uploader = this.props.uploader || { props: {} }; - const UploaderProps = Uploader.props; + const UploaderProps = Uploader.props as { + accept: string | undefined; + fileKeyName: string | undefined; + }; // TODO: 2.x 中逻辑会修改为,只要有showDownload,那就有下载按钮(不管有没有downloadURL) item = [ @@ -408,7 +397,7 @@ class List extends Component { > @@ -417,8 +406,8 @@ class List extends Component { {this.props.reUpload && !isPreview && !isIE9 ? ( @@ -429,8 +418,8 @@ class List extends Component {
{item}
- {fileNameRender(file)} + {fileNameRender!(file)}
); } @@ -459,17 +448,17 @@ class List extends Component { if (isPreview) { const previewCls = classNames({ [`${prefix}form-preview`]: true, - [className]: !!className, + [className as string]: !!className, }); - list = this.props.value.map(file => { + list = this.props.value.map((file, index) => { if (!file) { return null; } - const { downloadURL, imgURL, name } = file; + const { downloadURL, name } = file; if (listType === 'text') { return ( -
+
{name} @@ -525,5 +514,6 @@ class List extends Component { // https://github.com/alibaba-fusion/next/blob/build/1.13.9/src/upload/upload.jsx#L521 export default ConfigProvider.config(List, { componentName: 'Upload', + // @ts-expect-error 类型不匹配 transform, }); diff --git a/components/upload/mobile/index.jsx b/components/upload/mobile/index.tsx similarity index 100% rename from components/upload/mobile/index.jsx rename to components/upload/mobile/index.tsx diff --git a/components/upload/runtime/html5-uploader.jsx b/components/upload/runtime/html5-uploader.tsx similarity index 73% rename from components/upload/runtime/html5-uploader.jsx rename to components/upload/runtime/html5-uploader.tsx index 313eccc109..4e8e9a9f82 100644 --- a/components/upload/runtime/html5-uploader.jsx +++ b/components/upload/runtime/html5-uploader.tsx @@ -3,56 +3,22 @@ import PropTypes from 'prop-types'; import { func } from '../../util'; import Uploader from './uploader'; import Selecter from './selecter'; +import type { Html5Props, UploadFile } from '../types'; -export default class Html5Uploader extends Component { +export default class Html5Uploader extends Component { static propTypes = { ...Selecter.propTypes, - /** - * 上传的地址 - */ action: PropTypes.string, - /** - * 接受上传的文件类型 (image/png, image/jpg, .doc, .ppt) 详见 [input accept attribute](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input#attr-accept) - */ accept: PropTypes.string, - /** - * 上传额外传参 - */ data: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * 设置上传的请求头部 - */ headers: PropTypes.object, - /** - * 是否允许请求携带 cookie - */ withCredentials: PropTypes.bool, - /** - * 上传文件之前 - * @param {Object} file 文件对象 - * @return {Boolean} `false` 停止上传 - */ beforeUpload: PropTypes.func, - /** - * 正在上传文件的钩子,参数为上传的事件以及文件 - */ onProgress: PropTypes.func, - /** - * 上传成功回调函数,参数为请求下响应信息以及文件 - */ onSuccess: PropTypes.func, - /** - * 上传失败回调函数,参数为上传失败的信息、响应信息以及文件 - */ onError: PropTypes.func, children: PropTypes.node, - /** - * 上传超时,单位ms - */ timeout: PropTypes.number, - /** - * 上传方法 - */ method: PropTypes.oneOf(['post', 'put']), request: PropTypes.func, }; @@ -73,6 +39,7 @@ export default class Html5Uploader extends Component { onAbort: func.noop, method: 'post', }; + uploader: Uploader; componentDidMount() { const { props } = this; @@ -80,7 +47,7 @@ export default class Html5Uploader extends Component { this.uploader = new Uploader(options); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Html5Props) { const preOptions = this.getUploadOptions(prevProps); const options = this.getUploadOptions(this.props); @@ -99,15 +66,15 @@ export default class Html5Uploader extends Component { this.abort(); } - abort(file) { + abort(file?: UploadFile) { this.uploader.abort(file); } - startUpload(fileList) { + startUpload(fileList: UploadFile[]) { this.uploader.startUpload(fileList); } - getUploadOptions = props => ({ + getUploadOptions = (props: Html5Props): Record => ({ action: props.action, name: props.name, timeout: props.timeout, diff --git a/components/upload/runtime/iframe-uploader.jsx b/components/upload/runtime/iframe-uploader.tsx similarity index 76% rename from components/upload/runtime/iframe-uploader.jsx rename to components/upload/runtime/iframe-uploader.tsx index abfd89366f..9af82e0652 100644 --- a/components/upload/runtime/iframe-uploader.jsx +++ b/components/upload/runtime/iframe-uploader.tsx @@ -4,8 +4,9 @@ import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import { log, func, obj } from '../../util'; import { uid } from '../util'; +import type { IframeUploaderProps, ObjectFile, RequestOption, UploadFile } from '../types'; -const INPUT_STYLE = { +const INPUT_STYLE: React.CSSProperties = { position: 'absolute', top: 0, right: 0, @@ -16,7 +17,7 @@ const INPUT_STYLE = { cursor: 'pointer', }; -class IframeUploader extends React.Component { +class IframeUploader extends React.Component { static propTypes = { style: PropTypes.object, action: PropTypes.string.isRequired, @@ -44,8 +45,15 @@ class IframeUploader extends React.Component { onError: func.noop, onAbort: func.noop, }; + domain: string; + iFrameEl: HTMLIFrameElement; + inputEl: HTMLInputElement; + formEl: HTMLFormElement; + dataEl: HTMLSpanElement; + file: UploadFile | object = {}; + uid = ''; - constructor(props) { + constructor(props: IframeUploaderProps) { super(props); this.domain = typeof document !== 'undefined' && document.domain ? document.domain : ''; this.uid = uid(); @@ -63,9 +71,6 @@ class IframeUploader extends React.Component { this.updateInputWH(); } - file = {}; - - uid = ''; onLoad = () => { if (!this.state.uploading) { return; @@ -74,33 +79,35 @@ class IframeUploader extends React.Component { let response; try { const doc = this.iFrameEl.contentDocument; - const script = doc.getElementsByTagName('script')[0]; - if (script && script.parentNode === doc.body) { - doc.body.removeChild(script); + const script = doc!.getElementsByTagName('script')[0]; + if (script && script.parentNode === doc!.body) { + doc!.body.removeChild(script); } - response = doc.body.innerHTML; - props.onSuccess(response, file); + response = doc!.body.innerHTML; + props.onSuccess!(response, file as ObjectFile); } catch (err) { - log.warning('cross domain error for Upload. Maybe server should return document.domain script.'); + log.warning( + 'cross domain error for Upload. Maybe server should return document.domain script.' + ); response = 'cross-domain'; - props.onError(err, null, file); + props.onError!(err, null, file as ObjectFile); } this.endUpload(); }; - onSelect = e => { + onSelect = (e: React.ChangeEvent) => { this.file = { uid: uid(), - name: e.target.value, + name: (e.target as HTMLInputElement)!.value, }; - this.props.onSelect([this.file]); + this.props.onSelect!([this.file]); }; startUpload() { - this.upload(this.file); + this.upload(this.file as UploadFile); } - upload(file) { + upload(file: UploadFile) { if (!this.state.uploading) { // eslint-disable-next-line this.state.uploading = true; @@ -117,9 +124,9 @@ class IframeUploader extends React.Component { data, }; const before = beforeUpload(file, requestData); - if (before && before.then) { - before.then( - data => { + if (before && (before as Promise).then) { + (before as Promise).then( + (data: object) => { this.post(file, data); }, () => { @@ -143,19 +150,19 @@ class IframeUploader extends React.Component { } updateInputWH() { - const rootNode = ReactDOM.findDOMNode(this); + const rootNode = ReactDOM.findDOMNode(this) as HTMLElement; const inputNode = this.inputEl; inputNode.style.height = `${rootNode.offsetHeight}px`; inputNode.style.width = `${rootNode.offsetWidth}px`; } - abort(file) { + abort(file: UploadFile) { if (file) { - let uid = file; + let uid: UploadFile | string | number = file; if (file && file.uid) { uid = file.uid; } - if (uid === this.file.uid) { + if (uid === (this.file as UploadFile).uid) { this.endUpload(); } } else { @@ -163,7 +170,7 @@ class IframeUploader extends React.Component { } } - post(file, requestOption = {}) { + post(file: UploadFile, requestOption: RequestOption = {}) { const formNode = this.formEl; const dataSpan = this.dataEl; const fileInput = this.inputEl; @@ -188,32 +195,32 @@ class IframeUploader extends React.Component { const inputs = document.createDocumentFragment(); for (const key in propsData) { - if (data.hasOwnProperty(key)) { + if (data!.hasOwnProperty(key)) { const input = document.createElement('input'); input.setAttribute('name', key); - input.value = propsData[key]; + input.value = (propsData as Record)[key]; inputs.appendChild(input); } } dataSpan.appendChild(inputs); formNode.submit(); dataSpan.innerHTML = ''; - this.props.onStart(file); + this.props.onStart!(file); } - saveIFrameRef = ref => { + saveIFrameRef = (ref: HTMLIFrameElement) => { this.iFrameEl = ref; }; - saveFormRef = ref => { + saveFormRef = (ref: HTMLFormElement) => { this.formEl = ref; }; - saveDataRef = ref => { + saveDataRef = (ref: HTMLSpanElement) => { this.dataEl = ref; }; - saveInputRef = ref => { + saveInputRef = (ref: HTMLInputElement) => { this.inputEl = ref; }; diff --git a/components/upload/runtime/index.jsx b/components/upload/runtime/index.tsx similarity index 62% rename from components/upload/runtime/index.jsx rename to components/upload/runtime/index.tsx index 9f2e4538a4..418da2332c 100644 --- a/components/upload/runtime/index.jsx +++ b/components/upload/runtime/index.tsx @@ -1,11 +1,16 @@ import React from 'react'; import Html5Uploader from './html5-uploader'; import IframeUploader from './iframe-uploader'; +import type { UploadFile } from '../types'; -export default class Uploader extends React.Component { +export default class Uploader extends React.Component< + unknown, + { Component: typeof Html5Uploader | typeof IframeUploader } +> { state = { Component: Html5Uploader, }; + uploaderRef: InstanceType | InstanceType; componentDidMount() { if (typeof File === 'undefined') { @@ -16,15 +21,15 @@ export default class Uploader extends React.Component { } } - abort(file) { + abort(file: UploadFile) { this.uploaderRef.abort(file); } - startUpload(files) { + startUpload(files: UploadFile[]) { this.uploaderRef.startUpload(files); } - saveUploaderRef = ref => { + saveUploaderRef = (ref: InstanceType) => { this.uploaderRef = ref; }; diff --git a/components/upload/runtime/request.jsx b/components/upload/runtime/request.tsx similarity index 69% rename from components/upload/runtime/request.jsx rename to components/upload/runtime/request.tsx index c9caf127d3..9012eb152d 100644 --- a/components/upload/runtime/request.jsx +++ b/components/upload/runtime/request.tsx @@ -2,16 +2,18 @@ * clone from https://github.com/react-component/upload/blob/master/src/request.js */ -function getError(option, xhr, msg) { +import type { UploadOptions, UploadError, UploadProgressEvent } from '../types'; + +function getError(option: UploadOptions, xhr: XMLHttpRequest, msg?: string) { msg = msg || `cannot post ${option.action} ${xhr.status}'`; - const err = new Error(msg); + const err: UploadError = new Error(msg); err.status = xhr.status; err.method = option.method; err.url = option.action; return err; } -function getBody(xhr) { +function getBody(xhr: XMLHttpRequest) { const text = xhr.responseText || xhr.response; if (!text) { return text; @@ -37,15 +39,15 @@ function getBody(xhr) { // method: String // timeout: Number // } -export default function upload(option) { +export default function upload(option: UploadOptions) { const xhr = new XMLHttpRequest(); if (option.onProgress && xhr.upload) { - xhr.upload.onprogress = function progress(e) { + xhr.upload.onprogress = function progress(e: UploadProgressEvent) { if (e.total > 0) { e.percent = (e.loaded / e.total) * 100; } - option.onProgress(e); + option.onProgress!(e); }; } @@ -53,31 +55,33 @@ export default function upload(option) { if (option.data) { Object.keys(option.data).forEach(key => { - formData.append(key, option.data[key]); + // @ts-expect-error data 类型不兼容 + formData.append(key, option.data![key]); }); } if (option.file instanceof Blob) { - formData.append(option.filename, option.file, option.file.name); + formData.append(option.filename!, option.file, option.file.name); } else { - formData.append(option.filename, option.file); + formData.append(option.filename!, option.file!); } - xhr.onerror = function error(e) { - option.onError(e); + xhr.onerror = function error(e: ProgressEvent) { + option.onError!(e); }; xhr.onload = function onload() { // allow success when 2xx status // see https://github.com/react-component/upload/issues/34 if (xhr.status < 200 || xhr.status >= 300) { - return option.onError(getError(option, xhr), getBody(xhr)); + return option.onError!(getError(option, xhr), getBody(xhr)); } - option.onSuccess(getBody(xhr), xhr); + option.onSuccess!(getBody(xhr), xhr); }; option.method = option.method || 'POST'; - xhr.open(option.method, option.action, true); + + xhr.open(option.method, option.action!, true); // In Internet Explorer, the timeout property may be set only after calling the open() method and before calling the send() method. // see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/timeout @@ -87,7 +91,7 @@ export default function upload(option) { xhr.timeout = timeout; xhr.ontimeout = () => { const msg = `Upload abort for exceeding time (timeout: ${timeout}ms)`; - option.onError(getError(option, xhr, msg), getBody(xhr)); + option.onError!(getError(option, xhr, msg), getBody(xhr)); }; } @@ -96,7 +100,7 @@ export default function upload(option) { xhr.withCredentials = true; } - const headers = option.headers || {}; + const headers: { 'X-Requested-With'?: string; [key: string]: unknown } = option.headers || {}; // when set headers['X-Requested-With'] = null , can close default XHR header // see https://github.com/react-component/upload/issues/33 @@ -106,7 +110,7 @@ export default function upload(option) { for (const h in headers) { if (headers.hasOwnProperty(h) && headers[h] !== null) { - xhr.setRequestHeader(h, headers[h]); + xhr.setRequestHeader(h, headers[h] as string); } } xhr.send(formData); diff --git a/components/upload/runtime/selecter.jsx b/components/upload/runtime/selecter.tsx similarity index 67% rename from components/upload/runtime/selecter.jsx rename to components/upload/runtime/selecter.tsx index 4d6eaa8d79..6128fadfe9 100644 --- a/components/upload/runtime/selecter.jsx +++ b/components/upload/runtime/selecter.tsx @@ -1,58 +1,29 @@ -import React from 'react'; +import React, { type ChangeEvent, Component, type KeyboardEvent, type DragEvent } from 'react'; import PropTypes from 'prop-types'; import { func } from '../../util'; import { uid } from '../util'; +import type { UploadFile, SelecterProps } from '../types'; const { noop } = func; /** * Upload.Selecter - * @description [底层能力] 可自定义样式的文件选择器 + * [底层能力] 可自定义样式的文件选择器 */ -export default class Selecter extends React.Component { +export default class Selecter extends Component { static propTypes = { id: PropTypes.string, style: PropTypes.object, className: PropTypes.string, - /** - * 是否禁用上传功能 - */ disabled: PropTypes.bool, - /** - * 是否支持多选文件,`ie10+` 支持。开启后按住 ctrl 可选择多个文件 - */ multiple: PropTypes.bool, - /** - * 是否支持上传文件夹,仅在 chorme 下生效 - */ webkitdirectory: PropTypes.bool, - /** - * 调用系统设备媒体 - */ capture: PropTypes.string, - /** - * 是否支持拖拽上传,`ie10+` 支持。 - */ dragable: PropTypes.bool, - /** - * 接受上传的文件类型 (image/png, image/jpg, .doc, .ppt) 详见 [input accept attribute](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input#attr-accept) - */ accept: PropTypes.string, - /** - * 文件选择回调 - */ onSelect: PropTypes.func, - /** - * 拖拽经过回调 - */ onDragOver: PropTypes.func, - /** - * 拖拽离开回调 - */ onDragLeave: PropTypes.func, - /** - * 拖拽完成回调 - */ onDrop: PropTypes.func, children: PropTypes.node, name: PropTypes.string, @@ -67,20 +38,22 @@ export default class Selecter extends React.Component { onDrop: noop, }; - onSelect = e => { - const files = e.target.files; - const filesArr = files.length ? Array.prototype.slice.call(files) : [files]; + fileRef: HTMLInputElement; - filesArr.forEach(file => { + onSelect = (e: ChangeEvent) => { + const files = e.target!.files; + const filesArr = files!.length ? Array.prototype.slice.call(files) : [files]; + + filesArr.forEach((file: UploadFile) => { file.uid = uid(); }); - this.props.onSelect(filesArr); + this.props.onSelect!(filesArr); }; /** * 点击上传按钮 - * @return {void} + * */ onClick = () => { const el = this.fileRef; @@ -94,10 +67,10 @@ export default class Selecter extends React.Component { /** * 键盘事件 - * @param {SyntheticEvent} e - * @return {void} + * e + * */ - onKeyDown = e => { + onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter') { this.onClick(); } @@ -105,24 +78,24 @@ export default class Selecter extends React.Component { /** * 拖拽 - * @param {SyntheticEvent} e - * @return {void} + * e + * */ - onDrop = e => { + onDrop = (e: DragEvent) => { e.preventDefault(); const files = e.dataTransfer.files; const filesArr = Array.prototype.slice.call(files); - this.props.onDrop(filesArr); + this.props.onDrop!(filesArr); }; - onDragOver = e => { + onDragOver = (e: DragEvent) => { e.preventDefault(); - this.props.onDragOver(e); + this.props.onDragOver!(e); }; - saveFileRef = ref => { + saveFileRef = (ref: HTMLInputElement) => { this.fileRef = ref; }; @@ -159,7 +132,10 @@ export default class Selecter extends React.Component { ); } - const otherProps = {}; + const otherProps: { + webkitdirectory?: boolean | string; + capture?: string; + } = {}; if (webkitdirectory) { otherProps.webkitdirectory = ''; } diff --git a/components/upload/runtime/uploader.js b/components/upload/runtime/uploader.ts similarity index 54% rename from components/upload/runtime/uploader.js rename to components/upload/runtime/uploader.ts index af25a27a04..68ae9c5174 100644 --- a/components/upload/runtime/uploader.js +++ b/components/upload/runtime/uploader.ts @@ -1,11 +1,18 @@ import { func, obj } from '../../util'; import { uid, errorCode } from '../util'; import defaultRequest from './request'; +import type { UploadFile, UploadOptions, UploadError } from '../types'; const noop = func.noop; export default class Uploader { - constructor(options) { + options: UploadOptions; + reqs: { + [x: string]: { + abort?: () => void; + }; + }; + constructor(options: UploadOptions) { this.options = { beforeUpload: noop, onProgress: noop, @@ -19,42 +26,43 @@ export default class Uploader { this.reqs = {}; } - setOptions(options) { + setOptions(options: UploadOptions) { Object.assign(this.options, options); } - startUpload(files) { - const filesArr = files.length ? Array.prototype.slice.call(files) : [files]; - filesArr.forEach(file => { + startUpload(files: UploadFile[] | UploadFile) { + const filesArr = Array.isArray(files) ? Array.prototype.slice.call(files) : [files]; + filesArr.forEach((file: UploadFile) => { file.uid = file.uid || uid(); this.upload(file); }); } - abort(file) { + abort(file?: UploadFile | string) { const { reqs } = this; if (file) { - let uid = file; - if (file && file.uid) { - uid = file.uid; + let uid: UploadFile | string | undefined | number = file; + if (file && (file as UploadFile).uid) { + uid = (file as UploadFile).uid; } - if (reqs[uid] && reqs[uid].abort) { - reqs[uid].abort(); + if (reqs[uid as string] && reqs[uid as string].abort) { + reqs[uid as string].abort!(); } - delete reqs[uid]; + delete reqs[uid as string]; } else { Object.keys(reqs).forEach(uid => { if (reqs[uid] && reqs[uid].abort) { - reqs[uid].abort(); + reqs[uid].abort!(); } delete reqs[uid]; }); } } - upload(file) { - const { beforeUpload, action, name, headers, timeout, withCredentials, method, data } = this.options; - const before = beforeUpload(file, { + upload(file: UploadFile) { + const { beforeUpload, action, name, headers, timeout, withCredentials, method, data } = + this.options; + const before = beforeUpload!(file, { action, name, headers, @@ -66,28 +74,28 @@ export default class Uploader { func.promiseCall( before, - options => { + (options: unknown) => { if (options === false) { - const err = new Error(errorCode.BEFOREUPLOAD_REJECT); + const err: UploadError = new Error(errorCode.BEFOREUPLOAD_REJECT); err.code = errorCode.BEFOREUPLOAD_REJECT; - return this.options.onError(err, null, file); + return this.options.onError!(err, null, file); } this.post(file, obj.isPlainObject(options) ? options : undefined); }, - error => { - let err; + (error: Error) => { + let err: UploadError; if (error) { err = error; } else { err = new Error(errorCode.BEFOREUPLOAD_REJECT); err.code = errorCode.BEFOREUPLOAD_REJECT; } - this.options.onError(err, null, file); + this.options.onError!(err, null, file); } ); } - post(file, options = {}) { + post(file: UploadFile, options = {}) { const requestOptions = { ...this.options, ...options, @@ -106,13 +114,14 @@ export default class Uploader { let data = requestOptions.data; if (typeof data === 'function') { - data = data(file); + data = (data as (file: UploadFile) => { [key: string]: string | Blob })(file); } const { uid } = file; - const request = typeof requestOptions.request === 'function' ? requestOptions.request : defaultRequest; - this.reqs[uid] = request({ + const request = + typeof requestOptions.request === 'function' ? requestOptions.request : defaultRequest; + this.reqs[uid!] = request({ action, filename: name, file, @@ -121,16 +130,16 @@ export default class Uploader { headers, withCredentials, method, - onProgress: e => { - onProgress(e, file); + onProgress: (e: ProgressEvent) => { + onProgress!(e, file); }, - onSuccess: ret => { - delete this.reqs[uid]; - onSuccess(ret, file); + onSuccess: (ret: unknown) => { + delete this.reqs[uid!]; + onSuccess!(ret, file); }, - onError: (err, ret) => { - delete this.reqs[uid]; - onError(err, ret, file); + onError: (err: Error, ret: unknown) => { + delete this.reqs[uid!]; + onError!(err, ret, file); }, }); } diff --git a/components/upload/style.js b/components/upload/style.ts similarity index 100% rename from components/upload/style.js rename to components/upload/style.ts diff --git a/components/upload/transform.js b/components/upload/transform.ts similarity index 51% rename from components/upload/transform.js rename to components/upload/transform.ts index a57ebb8763..2f2f3a48c3 100644 --- a/components/upload/transform.js +++ b/components/upload/transform.ts @@ -1,26 +1,30 @@ // compatible with 0.x version -/* istanbul ignore next */ -const transform = (props, deprecated) => { +import type { TransformNewProps, TransformProps } from './types'; + +const transform = ( + props: T, + deprecated: (param0: string, param1: string, param2: string) => void +) => { const { listType, defaultFileList, fileList, ...others } = props; - const newprops = others; + const newprops: TransformNewProps = others; if (listType === 'text-image') { - deprecated('listType=text-image', 'listType=image', 'Upload'); + deprecated!('listType=text-image', 'listType=image', 'Upload'); newprops.listType = 'image'; } else if (listType === 'picture-card') { - deprecated('listType=picture-card', 'listType=card', 'Upload'); + deprecated!('listType=picture-card', 'listType=card', 'Upload'); newprops.listType = 'card'; } else { newprops.listType = listType; } if ('defaultFileList' in props) { - deprecated('defaultFileList', 'defaultValue', 'Upload'); + deprecated!('defaultFileList', 'defaultValue', 'Upload'); newprops.defaultValue = defaultFileList; } if ('fileList' in props) { - deprecated('fileList', 'value', 'Upload'); + deprecated!('fileList', 'value', 'Upload'); newprops.value = fileList; } diff --git a/components/upload/types.ts b/components/upload/types.ts new file mode 100644 index 0000000000..3805440bbf --- /dev/null +++ b/components/upload/types.ts @@ -0,0 +1,810 @@ +import type { CSSProperties, DragEvent, ReactNode, MouseEvent, SyntheticEvent } from 'react'; +import type { ReactWrapper, ShallowWrapper } from 'enzyme'; + +import type { ProgressProps } from '../progress'; +import type { CommonProps } from '../util'; +import type { Locale } from '../locale/types'; + +/** + * @api Upload + */ +export interface UploadProps extends UploadCommonProps { + /** + * 上传的地址 + * @en Upload address + */ + action?: string; + + /** + * 上传按钮形状 + * @en Upload button shape + */ + shape?: 'card'; + + /** + * 接受上传的文件类型 (image/png, image/jpg, .doc, .ppt) 详见 [input accept attribute](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input#attr-accept) + * @en Accepted file types, see [input accept attribute](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input#attr-accept) + */ + accept?: string; + + /** + * 上传额外传参 + * @en Upload extra parameters + */ + data?: object | (() => void); + + /** + * 设置上传的请求头部 + * @en Set the request header + */ + headers?: { + 'X-Requested-With'?: string | undefined; + [key: string]: unknown; + }; + + /** + * 是否允许请求携带 cookie + * @en Whether to allow requests to carry cookies + * @defaultValue true + */ + withCredentials?: boolean; + + /** + * 是否支持上传文件夹,仅在 chorme 下生效 + * @en Whether to support upload folder, only in chrome + * @skip + */ + webkitdirectory?: boolean; + + /** + * 可选参数, 详见 [beforeUpload](#beforeUpload) + * @en Optional parameters, see [beforeUpload](#beforeUpload) + * @param file - 所有文件 - all file + * @param options - 参数 - parameters + * @defaultValue func.noop + */ + beforeUpload?: (file: ObjectFile, options: BeforeUploadOption) => boolean | object | unknown; + + /** + * 上传中 + * @en onProgress Callback + * @defaultValue func.noop + */ + onProgress?: (file: ObjectFile[], e: ObjectFile) => void; + + /** + * 可选参数,上传成功回调函数,参数为请求下响应信息以及文件 + * @en Optional parameters, upload success callback function, the parameter is the response information and file + * @param file - 文件 - file + * @param value - 值 - value + * @defaultValue func.noop + */ + onSuccess?: (file: ObjectFile, value?: ObjectFile[]) => void; + + /** + * 可选参数,上传失败回调函数,参数为上传失败的信息、响应信息以及文件 + * @en Optional parameters, upload failure callback function, the parameter is the upload failure information, response information and file + * @param file - 出错的文件 - error file + * @param value - 当前值 - current value + * @defaultValue func.noop + */ + onError?: (file: UploadError, value: ObjectFile[]) => void; + + /** + * 子元素 + * @en Child elements + */ + children?: ReactNode; + + /** + * 设置上传超时,单位ms + * @en Upload timeout, unit ms + */ + timeout?: number; + + /** + * 上传方法 + * @en Upload method + * @defaultValue 'post' + */ + method?: 'post' | 'put' | 'POST' | 'PUT'; + + /** + * 自定义上传方法 + * @en Custom upload method + * @param option - 参数 - parameters + * @returns - 返回值 - object with abort method + */ + request?: (option: object) => object; + + /** + * 文件名字段 + * @en File name field + */ + name?: string; + + /** + * 选择文件回调 + * @en Select file callback + * @defaultValue func.noop + */ + onSelect?: (uploadFiles: Array, value: Array) => void; + + /** + * 放文件 + * @en Drop file + * @defaultValue func.noop + */ + onDrop?: (files: UploadFile[]) => void; + + /** + * 文件列表 + * @en File list + */ + value?: Array | ObjectFile; + + /** + * 默认文件列表 + * @en Default file list + */ + defaultValue?: Array | ObjectFile; + + /** + * 上传列表的样式 + * @en Upload list style + */ + listType?: ListType; + + /** + * 数据格式化函数,配合自定义 action 使用,参数为服务器的响应数据,详见 [formatter](#formater) + * @en Data formatting function, used with custom action, the parameter is the response data of the server, see [formatter](#formater) + * @param response - 返回 - return + * @param file - 文件对象 - file object + */ + formatter?: (response: UploadResponse, file: ObjectFile) => UploadResponse; + + /** + * 最大文件上传个数 + * @en Maximum number of file uploads + * @defaultValue Infinity + */ + limit?: number; + + /** + * 可选参数,是否支持拖拽上传,`ie10+` 支持。 + * @en Optional parameters, whether to support drag and drop upload, `ie10+` supports. + */ + dragable?: boolean; + + /** + * 可选参数,是否本地预览 + * @en Optional parameters, whether to locally preview + */ + useDataURL?: boolean; + + /** + * 可选参数,是否禁用上传功能 + * @en Optional parameters, whether to disable upload + */ + disabled?: boolean; + + /** + * 上传文件改变时的状态 + * @en Upload file change status + * @param value - 所有文件 - value + * @param file - 文件对象 - file object + * @defaultValue func.noop + */ + onChange?: (value: ObjectFile[], file: ObjectFile | ObjectFile[]) => void; + + /** + * 可选参数, 用于校验文件,afterSelect仅在 autoUpload=false 的时候生效,autoUpload=true时,可以使用beforeUpload完全可以替代该功能. + * @en Optional parameters, used to validate files, afterSelect only takes effect when autoUpload=false, autoUpload=true can be used to replace this function + * @param file - 文件 - file + * @returns - 返回false会阻止上传,其他则表示正常 - return false will block upload, other means normal + * @defaultValue func.noop + */ + afterSelect?: (file: object) => boolean | Promise; + + /** + * 移除文件回调函数 + * @en Remove file callback function + * @param file - 文件 - file + * @returns - 返回 false、Promise.resolve(false)、 Promise.reject() 将阻止文件删除 - return false、Promise.resolve(false)、 Promise.reject() will block file deletion + * @defaultValue func.noop + */ + onRemove?: (file: object) => void; + + /** + * 自动上传 + * @en Automatic upload + * @defaultValue true + */ + autoUpload?: boolean; + + /** + * 透传给Progress props + * @en Pass to Progress props + */ + progressProps?: ProgressProps; + + /** + * 是否为预览态 + * @en Is preview + */ + isPreview?: boolean; + + /** + * 预览态模式下渲染的内容 + * @en Preview mode + * @param value - 文件 - file + */ + renderPreview?: (value: ObjectFile | ObjectFile[], props: UploadProps) => void; + + /** + * 文件对象的 key name + * @en File object key name + * @version 1.21 + */ + fileKeyName?: string; + + /** + * 自定义文件名渲染 + * @en Custom file name rendering + * @param file - 文件 - file + * @returns - react node - react node + */ + fileNameRender?: (file: object) => ReactNode; + + /** + * 自定义操作区域渲染 + * @en Custom operation area rendering + * @param file - 文件 - file + * @returns - react node - react node + */ + actionRender?: (file: UploadFile) => ReactNode; + + /** + * 自定义额外渲染 + * @skip + */ + extraRender?: (file: File) => unknown; + /** + * 自定义class + * @skip + */ + className?: string; + + /** + * 自定义内联样式 + * @skip + */ + style?: CSSProperties; + + /** + * @skip + */ + onPreview?: (file: UploadFile, e?: MouseEvent) => void; + + /** + * 点击文件名时触发 onPreview + * @en Click file name + * @version 1.24 + */ + previewOnFileName?: boolean; + + /** + * 自定义成功和失败的列表渲染方式 + */ + itemRender?: (file: UploadFile, obj: { remove?: () => void }) => ReactNode; + + /** + * 选择新文件上传并替换 + */ + reUpload?: boolean; + + /** + * 是否自动上传 + * @skip + */ + autoUplod?: boolean; + + /** + * 是否支持多选文件,`ie10+` 支持。开启后按住 ctrl 可选择多个文件 + * @skip + */ + multiple?: boolean; + + /** + * 是否可以关闭 + * @skip + */ + closable?: boolean; + + /** + * 是否只读 + * @skip + */ + readonly?: boolean; + + /** + * list 值 + * @skip + */ + list?: unknown; + /** + * + * @skip + */ + rtl?: boolean; + /** + * 取消事件 + * @skip + */ + onCancel?: (file: UploadFile) => void; + + /** + * 样式前缀 + * @skip + */ + prefix?: string; +} +/** + * @api Upload.Card + * 继承 Upload 的 API,除非特别说明 + */ +export interface CardProps extends UploadProps { + /** + * 点击图片回调 + * @en Click image callback + * @defaultValue func.noop + */ + onPreview?: (file: UploadFile, e?: MouseEvent) => void; + + /** + * 改变时候的回调 + * @en Change callback + * @defaultValue func.noop + */ + onChange?: (value: UploadFile[], file: UploadFile) => void; + + /** + * 点击移除的回调 + * @en Click remove callback + */ + onRemove?: (file: object) => void; + + /** + * 取消上传的回调 + * @en Cancel upload callback + */ + onCancel?: () => void; + + /** + * 自定义成功和失败的列表渲染方式 + * @en Customize success and failure list rendering + * @param file - 文件对象 - file object + * @param obj - 包含属性remove: 删除回调 - contains properties remove: delete callback + * @version 1.21 + */ + itemRender?: (file: UploadFile, obj: { remove?: () => void }) => ReactNode; + + /** + * 选择新文件上传并替换 + * @en Select new file upload and replace + * @version 1.24 + */ + reUpload?: boolean; + + /** + * 展示下载按钮 + * @en Show download button + * @defaultValue true + * @version 1.24 + */ + showDownload?: boolean; + + /** + * 上传中 + * @en onProgress Callback + * @defaultValue func.noop + */ + onProgress?: (file: UploadFile[], e: UploadFile) => void; +} + +/** + * @api Upload.Dragger + * IE10+ 支持。继承 Upload 的 API,除非特别说明 + */ +export interface DraggerProps extends UploadProps { + /** + * 拖拽进入回调 + * @skip + */ + onDragOver?: (e: DragEvent) => void; + + /** + * 拖拽离开回调 + * @skip + */ + onDragLeave?: (e: DragEvent) => void; +} + +export interface CardState { + uploaderRef: UploaderRef; + value: Array; +} + +/** + * @api Upload.Selecter + * [底层能力] 可自定义样式的文件选择器 + */ +export interface SelecterProps extends UploadProps { + /** + * 是否禁用上传功能 + * @en Whether to disable upload + */ + disabled?: boolean; + + /** + * 是否支持多选文件,`ie10+` 支持。开启后按住 ctrl 可选择多个文件 + * @en Whether to support multiple file selection, `ie10+` supports + * @defaultValue false + */ + multiple?: boolean; + + /** + * 是否支持上传文件夹,仅在 chorme 下生效 + * @en Whether to support upload folder, only in chrome + */ + webkitdirectory?: boolean; + + /** + * 调用系统设备媒体 + * @en Call system device media + */ + capture?: string; + + /** + * 是否支持拖拽上传,`ie10+` 支持。 + * @en Whether to support drag and drop upload, `ie10+` supports + */ + dragable?: boolean; + + /** + * 接受上传的文件类型 (image/png, image/jpg, .doc, .ppt) 详见 [input accept attribute](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input#attr-accept) + * @en Accepted file types + */ + accept?: string; + + /** + * 文件选择回调 + * @en File selection callback + * @defaultValue func.noop + */ + onSelect?: (e: UploadFile[]) => void; + + /** + * 拖拽经过回调 + * @en Drag over callback + * @defaultValue func.noop + */ + onDragOver?: (e: DragEvent) => void; + + /** + * 拖拽离开回调 + * @en Drag leave callback + * @defaultValue func.noop + */ + onDragLeave?: () => void; + + /** + * 拖拽完成回调 + * @en Drag complete callback + * @defaultValue func.noop + */ + onDrop?: (fiels: File[]) => void; +} + +export interface UploadState { + value: Array; + uploading: boolean; +} + +export interface ListProps extends UploadCommonProps { + prefix?: string; + /** + * 文件列表,数据格式请参考 文件对象 + */ + listType?: ListType; + reUpload?: boolean; + /** + * 文件列表 + */ + value: Array; + closable?: boolean; + /** + * 删除文件回调(支持Promise) + */ + onRemove?: (file: unknown) => boolean | unknown; + /** + * 取消上传回调(支持Promise) + */ + onCancel?: (file?: UploadFile) => void; + /** + * 头像加载出错回调 + */ + onImageError?: (obj: object, file: UploadFile) => void; + /** + * 点击图片回调 + */ + onPreview?: (file: UploadFile, e?: MouseEvent) => void; + /** + * 点击文件名时触发 onPreview + */ + previewOnFileName?: boolean; + /** + * 自定义额外渲染 + */ + extraRender?: (file: UploadFile) => void; + /** + * 自定义操作渲染 + */ + actionRender?: (file: UploadFile) => void; + /** + * 卡片自定义渲染(目前只支持 Card) + * Object file 文件对象 + * Object remove remove:删除回调 + * ReactNode React元素 + */ + itemRender?: (file: UploadFile, { remove }: { remove: () => void }) => void; + /** + * 透传给Progress props + */ + progressProps?: object; + children?: ReactNode; + showDownload?: boolean; + /** + * 可选参数,是否本地预览 + */ + useDataURL?: boolean; + rtl?: boolean; + isPreview?: boolean; + fileNameRender?: (file: UploadFile) => void; + uploader?: { + removeFile: (file: UploadFile) => void; + abort: (file: UploadFile) => void; + replaceWithNewFile: (oldfile: UploadFile, newfile: UploadFile) => void; + props: { + accept?: string; + fileKeyName?: string; + }; + }; +} + +export interface Html5Props extends SelecterProps {} + +export interface CoreProps extends UploadOptions {} + +export interface UploadOptions { + beforeUpload?: (file: UploadFile, data: unknown) => boolean | object | unknown; + onProgress?: (e: UploadProgressEvent, file?: UploadFile) => void; + onSuccess?: (ret: unknown, xhr?: XMLHttpRequest | UploadFile) => void; + onError?: (err: ProgressEvent | UploadError, param?: unknown, file?: UploadFile) => void; + data?: + | { [key: string]: string | Blob } + | ((file: UploadFile) => { [key: string]: string | Blob }); + name?: string; + method?: 'post' | 'put' | 'POST' | 'PUT'; + action?: string; + headers?: { [key: string]: unknown; 'X-Requested-With'?: string | undefined }; + withCredentials?: boolean; + request?: (option: object) => { abort?: (() => void) | undefined }; + file?: UploadFile; + filename?: string; + timeout?: number; +} + +export interface UploadCommonProps extends Omit { + locale?: Locale['Upload']; + id?: string; + style?: CSSProperties; + className?: string; +} + +export enum ErrorCode { + EXCEED_LIMIT = 'EXCEED_LIMIT', + BEFOREUPLOAD_REJECT = 'BEFOREUPLOAD_REJECT', + RESPONSE_FAIL = 'RESPONSE_FAIL', +} + +export interface UploadFile extends OriginalFile, File { + filename?: string; + lastModifiedDate?: Date; + originFileObj?: File; + imgURL?: string; + downloadURL?: string; + url?: string; + state?: string; + errorMsg?: string; + alt?: string; +} + +export interface ObjectFile extends OriginalFile { + lastModified?: number; + lastModifiedDate?: Date; + size?: number; + type?: string; + originFileObj?: UploadFile; + imgURL?: string; + downloadURL?: string; + url?: string; + errorMsg?: string; + errorText?: string; + fileURL?: string; + tempUrl?: string; +} + +export interface OriginalFile { + uid?: string | number; + error?: unknown; + percent?: number; + state?: string; + readonly name: string; +} + +export interface UploadError extends Error { + code?: string; + status?: number; + method?: string; + url?: string; +} + +export interface UploadProgressEvent extends ProgressEvent { + percent?: number; +} + +export interface NObject { + [key: string]: string | number | undefined | null | void; +} + +export interface IframeUploaderProps { + name?: string; + /** + * 上传的地址 + */ + action?: string; + /** + * 接受上传的文件类型 (image/png, image/jpg, .doc, .ppt) 详见 [input accept attribute](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/Input#attr-accept) + */ + accept?: string; + /** + * 上传额外传参 + */ + data?: object | ((file: unknown) => void); + disabled?: boolean; + className?: string; + style?: CSSProperties; + /** + * 是否支持多选文件,`ie10+` 支持。开启后按住 ctrl 可选择多个文件 + */ + multiple?: boolean; + /** + * 设置上传的请求头部 + */ + headers?: { [key: string]: unknown; 'X-Requested-With'?: string | undefined }; + /** + * 是否允许请求携带 cookie + */ + withCredentials?: boolean; + /** + * 可选参数, 详见 [beforeUpload](#beforeUpload) + */ + beforeUpload?: (file: unknown, options: unknown) => boolean | object | Promise; + /** + * 上传中 + */ + onProgress?: () => void; + /** + * 可选参数,上传成功回调函数,参数为请求下响应信息以及文件 + */ + onSuccess?: (param: unknown, file: ObjectFile | UploadFile) => void; + /** + * 可选参数,上传失败回调函数 + * err + * params + * file + */ + onError?: (err: UploadError, params: unknown, file: ObjectFile) => void; + onSelect?: (file: IframeFile[]) => void; + onStart?: (file: UploadFile) => void; +} + +export interface RequestOption { + action?: string; + name?: string; + data?: object; +} + +/** + * iframe 组件使用的file类型 + */ +export interface IframeFile { + uid?: string; + name?: string; +} + +export interface TransformProps { + listType?: ListType; + defaultFileList?: Array; + fileList?: Array; + [key: string]: unknown; +} + +export interface TransformNewProps { + listType?: ListType; + defaultFileList?: Array; + value?: Array; + [key: string]: unknown; +} + +export interface ImageError extends SyntheticEvent { + onerror?: null | ((this: HTMLDivElement, ev: ImageError) => unknown); +} + +export interface UploadResponse { + success: boolean; + message?: string; + downloadURL?: string; + url?: string; + imgURL?: string; +} + +// export interface FakeXMLHttpRequest extends XMLHttpRequest { +// onCreate: (xhr: FakeXMLHttpRequest) => void; +// restore: () => void; +// respond: (status: number, headers: Record, body: unknown) => void; +// requestHeaders: Record; +// requestBody: unknown; +// url: string; +// } + +export interface OptionType { + onSuccess: (ret?: unknown) => void; + onError?: (e?: unknown) => void; // onError 是可选的,因为它是后来添加的 + action: string; + data: Record; + filename: string; + file: { + name: string; + }; + headers: Record; + timeout?: number; +} + +export type ListType = + | 'text' + | 'text-image' + | 'image' + | 'card' + | 'picture' + | 'picture-card' + | 'none'; + +export type Wrapper = ReactWrapper | ShallowWrapper; // 根据wrapper的实际类型调整 + +export type UploaderRef = { + abort: (file: File) => void; + startUpload: (fileList?: (UploadFile | File | undefined)[]) => void; + isUploading?: () => boolean; + [key: string]: unknown; +}; + +export type BeforeUploadOption = { + action?: string; + headers?: object; + timeout?: number; + withCredentials?: boolean; + method?: string; + data?: object; +}; diff --git a/components/upload/upload.jsx b/components/upload/upload.tsx similarity index 71% rename from components/upload/upload.jsx rename to components/upload/upload.tsx index b0775799f3..9dd200505a 100644 --- a/components/upload/upload.jsx +++ b/components/upload/upload.tsx @@ -10,184 +10,61 @@ import Uploader from './runtime/index'; import html5Uploader from './runtime/html5-uploader'; import List from './list'; import { fileToObject, getFileItem, errorCode } from './util'; +import type { + ObjectFile, + UploadError, + UploadFile, + UploadProgressEvent, + UploadProps, + UploadResponse, + UploadState, +} from './types'; const noop = func.noop; -/** - * Upload - */ -class Upload extends Base { +class Upload extends Base { static displayName = 'Upload'; static propTypes = { ...html5Uploader.propTypes, ...List.propTypes, - /** - * 样式前缀 - */ prefix: PropTypes.string.isRequired, - /** - * 上传的地址 - */ action: PropTypes.string, - /** - * 文件列表 - */ value: PropTypes.array, - /** - * 默认文件列表 - */ defaultValue: PropTypes.array, - /** - * 上传按钮形状 - */ shape: PropTypes.oneOf(['card']), - /** - * 上传列表的样式 - * @enumdesc 文字, 图文, 卡片 - */ listType: PropTypes.oneOf(['text', 'image', 'card', 'none']), list: PropTypes.any, - /** - * 文件名字段 - */ name: PropTypes.string, - /** - * 上传额外传参 - */ data: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), - /** - * 数据格式化函数,配合自定义 action 使用,参数为服务器的响应数据,详见 [formatter](#formater) - * @param {Object} response 返回 - * @param {File} file 文件对象 - */ formatter: PropTypes.func, - /** - * 最大文件上传个数 - */ limit: PropTypes.number, - /** - * 设置上传超时,单位ms - */ timeout: PropTypes.number, - /** - * 可选参数,是否支持拖拽上传,`ie10+` 支持。 - */ dragable: PropTypes.bool, closable: PropTypes.bool, - /** - * 可选参数,是否本地预览 - */ useDataURL: PropTypes.bool, - /** - * 可选参数,是否禁用上传功能 - */ disabled: PropTypes.bool, - /** - * 选择文件回调 - */ onSelect: PropTypes.func, - /** - * 上传中 - */ onProgress: PropTypes.func, - /** - * 上传文件改变时的状态 - * @param {Object} info 文件事件对象 - */ onChange: PropTypes.func, - /** - * 可选参数,上传成功回调函数,参数为请求下响应信息以及文件 - * @param {Object} file 文件 - * @param {Array} value 值 - */ onSuccess: PropTypes.func, - /** - * 可选参数, 用于校验文件,afterSelect仅在 autoUpload=false 的时候生效,autoUpload=true时,可以使用beforeUpload完全可以替代该功能. - * @param {Object} file - * @returns {Boolean} 返回false会阻止上传,其他则表示正常 - */ afterSelect: PropTypes.func, - /** - * 移除文件回调函数 - * @param {Object} file 文件 - * @returns {Boolean|Promise} 返回 false、Promise.resolve(false)、 Promise.reject() 将阻止文件删除 - */ onRemove: PropTypes.func, - /** - * 可选参数,上传失败回调函数,参数为上传失败的信息、响应信息以及文件 - * @param {Object} file 出错的文件 - * @param {Array} value 当前值 - */ onError: PropTypes.func, - /** - * 可选参数, 详见 [beforeUpload](#beforeUpload) - * @param {Object} file 所有文件 - * @param {Object} options 参数 - * @returns {Boolean|Object|Promise} 返回值作用见demo - */ beforeUpload: PropTypes.func, - /** - * 放文件 - */ onDrop: PropTypes.func, - /** - * 自定义class - */ className: PropTypes.string, - /** - * 自定义内联样式 - */ style: PropTypes.object, - /** - * 子元素 - */ children: PropTypes.node, - /** - * 自动上传 - */ autoUpload: PropTypes.bool, - /** - * 自定义上传方法 - * @param {Object} option - * @return {Object} object with abort method - */ request: PropTypes.func, - /** - * 透传给Progress props - */ progressProps: PropTypes.object, rtl: PropTypes.bool, - /** - * 是否为预览态 - */ isPreview: PropTypes.bool, - /** - * 预览态模式下渲染的内容 - * @param {number} value 评分值 - */ renderPreview: PropTypes.func, - /** - * 文件对象的 key name - * @version 1.21 - */ fileKeyName: PropTypes.string, - /** - * list 的自定义文件名渲染 - * @param {Object} file 文件 - * @return {Node} react node - */ fileNameRender: PropTypes.func, - /** - * 操作区域额外渲染 - * @param {Object} file 文件 - * @return {Node} react node - */ actionRender: PropTypes.func, - /** - * 点击文件名时触发 onPreview - * @version 1.24 - */ previewOnFileName: PropTypes.bool, }; @@ -209,7 +86,7 @@ class Upload extends Base { previewOnFileName: false, }; - constructor(props) { + constructor(props: UploadProps) { super(props); let value; @@ -225,7 +102,7 @@ class Upload extends Base { }; } - static getDerivedStateFromProps(nextProps, prevState) { + static getDerivedStateFromProps(nextProps: UploadProps, prevState: UploadState) { // 上传中不允许做受控修改 if ('value' in nextProps && nextProps.value !== prevState.value && !prevState.uploading) { return { @@ -236,12 +113,12 @@ class Upload extends Base { return null; } - onSelect = files => { + onSelect = (files: Array) => { const { autoUpload, afterSelect, onSelect, limit } = this.props; // 总数 const total = this.state.value.length + files.length; // 差额 - const less = limit - this.state.value.length; + const less = limit! - this.state.value.length; if (less <= 0) { // 差额不足 则不上传 return; @@ -255,8 +132,8 @@ class Upload extends Base { // 默认全量上传 let uploadFiles = fileList; - let discardFiles = []; - if (total > limit) { + let discardFiles: Array = []; + if (total > limit!) { // 全量上传总数会超过limit 但是 还有差额 uploadFiles = fileList.slice(0, less); discardFiles = fileList.slice(less); @@ -265,48 +142,49 @@ class Upload extends Base { const value = this.state.value.concat(fileList); /* eslint-disable-next */ + // @ts-expect-error 无法为“value”赋值,因为它是只读属性。 this.state.value = value; if (autoUpload) { this.uploadFiles(uploadFiles); } - onSelect(uploadFiles, value); + onSelect!(uploadFiles, value); discardFiles.forEach(file => { // 丢弃的文件 - const err = new Error(errorCode.EXCEED_LIMIT); + const err: UploadError = new Error(errorCode.EXCEED_LIMIT); err.code = errorCode.EXCEED_LIMIT; this.onError(err, null, file); }); if (!autoUpload) { uploadFiles.forEach(file => { - const isPassed = afterSelect(file); - func.promiseCall(isPassed, func.noop, error => { - this.onError(error, null, file); // TODO: handle error message + const isPassed = afterSelect!(file); + func.promiseCall(isPassed, func.noop, (error: UploadError) => { + this.onError(error, null, file); }); }); this.onChange(value, uploadFiles); } }; - onDrop = files => { + onDrop = (files: UploadFile[]) => { this.onSelect(files); - this.props.onDrop(files); + this.props.onDrop!(files); }; /** * 对外暴露API, 添加文件 - * @param files */ - selectFiles(files) { + selectFiles(files: File[]) { const filesArr = files.length ? Array.prototype.slice.call(files) : [files]; this.onSelect(filesArr); } - uploadFiles(files) { + uploadFiles(files: (UploadFile | ObjectFile)[]) { // NOTE: drag上传,当鼠标松开的时候回执行 onDrop,但此时onChange还没出发所以 value=[], 必须提前标识上传中 + // @ts-expect-error 无法为“uploading”赋值,因为它是只读属性。 this.state.uploading = true; const fileList = files .filter(file => { @@ -319,7 +197,6 @@ class Upload extends Base { .map(file => { return file.originFileObj; }); - fileList.length && this.uploaderRef.startUpload(fileList); } @@ -330,7 +207,7 @@ class Upload extends Base { this.uploadFiles(this.state.value); } - replaceFiles(old, current) { + replaceFiles(old: ObjectFile, current: UploadFile) { const targetItem = getFileItem(old, this.state.value); if (!targetItem) { return; @@ -341,7 +218,7 @@ class Upload extends Base { } // 替换掉队列里面的文件 - replaceWithNewFile = (old, current) => { + replaceWithNewFile = (old: ObjectFile, current: UploadFile) => { const newFile = fileToObject(current); newFile.state = 'selected'; @@ -364,7 +241,8 @@ class Upload extends Base { return this.state.uploading; } - onProgress = (e, file) => { + onProgress = (e: UploadProgressEvent, file: UploadFile) => { + //@ts-expect-error 无法为“uploading”赋值,因为它是只读属性。 this.state.uploading = true; const value = this.state.value; @@ -383,10 +261,10 @@ class Upload extends Base { value, }); - this.props.onProgress(value, targetItem); + this.props.onProgress!(value, targetItem); }; - onSuccess = (response, file) => { + onSuccess = (response: UploadResponse, file: ObjectFile) => { const { formatter } = this.props; if (formatter) { @@ -403,7 +281,7 @@ class Upload extends Base { } if (response.success === false) { - const err = new Error(response.message || errorCode.RESPONSE_FAIL); + const err: UploadError = new Error(response.message || errorCode.RESPONSE_FAIL); err.code = errorCode.RESPONSE_FAIL; return this.onError(err, response, file); } @@ -429,10 +307,10 @@ class Upload extends Base { this.updateUploadingState(); this.onChange(value, targetItem); - this.props.onSuccess(targetItem, value); + this.props.onSuccess!(targetItem, value); }; - onError = (err, response, file) => { + onError = (err: UploadError, response: UploadResponse | null, file: ObjectFile) => { const value = this.state.value; const targetItem = getFileItem(file, value); @@ -449,15 +327,13 @@ class Upload extends Base { this.updateUploadingState(); this.onChange(value, targetItem); - this.props.onError(targetItem, value); + this.props.onError!(targetItem as UploadError, value); }; /** * 删除文件 - * @param {File} file - * @return {void} */ - removeFile = file => { + removeFile = (file: UploadFile) => { file.state = 'removed'; this.uploaderRef.abort(file); // 删除组件时调用组件的 `abort` 方法中断上传 @@ -473,16 +349,15 @@ class Upload extends Base { updateUploadingState = () => { const inProgress = this.state.value.some(i => i.state === 'uploading'); if (!inProgress) { + // @ts-expect-error 无法为“uploading”赋值,因为它是只读属性。 this.state.uploading = false; } }; /** * 取消上传 - * @param {File} file - * @return {void} */ - abort = file => { + abort = (file: File) => { const fileList = this.state.value; const targetItem = getFileItem(file, fileList); const index = fileList.indexOf(targetItem); @@ -493,11 +368,11 @@ class Upload extends Base { this.uploaderRef.abort(file); // 取消上传时调用组件的 `abort` 方法中断上传 }; - onChange = (value, file) => { + onChange = (value: Array, file: ObjectFile | Array) => { this.setState({ value, }); - this.props.onChange(value, file); + this.props.onChange!(value, file); }; render() { @@ -536,10 +411,9 @@ class Upload extends Base { [`${prefix}upload-dragable`]: dragable, [`${prefix}disabled`]: disabled, [`${prefix}readonly`]: readonly, - [className]: className, + [className!]: className, }); - - const isExceedLimit = this.state.value.length >= limit; + const isExceedLimit = this.state.value.length >= limit!; const innerCls = classNames({ [`${prefix}upload-inner`]: true, [`${prefix}hidden`]: isExceedLimit, @@ -554,7 +428,7 @@ class Upload extends Base { children = (
-
+
{children}
@@ -565,7 +439,7 @@ class Upload extends Base { if (typeof renderPreview === 'function') { const previewCls = classNames({ [`${prefix}form-preview`]: true, - [className]: !!className, + [className!]: !!className, }); return (
@@ -581,7 +455,7 @@ class Upload extends Base { listType={listType} style={style} className={className} - value={this.state.value} + value={this.state.value as UploadFile[]} onPreview={onPreview} /> ); @@ -618,7 +492,7 @@ class Upload extends Base { actionRender={actionRender} uploader={this} listType={listType} - value={this.state.value} + value={this.state.value as UploadFile[]} closable={closable} onRemove={onRemoveFunc} progressProps={progressProps} diff --git a/components/upload/util.js b/components/upload/util.ts similarity index 70% rename from components/upload/util.js rename to components/upload/util.ts index 77090f2f1b..ea2b25ea66 100644 --- a/components/upload/util.js +++ b/components/upload/util.ts @@ -1,14 +1,12 @@ +import type { ObjectFile, UploadFile } from './types'; + let now = +new Date(); -/** - * 生成唯一的id - * @return {String} uid - */ export function uid() { return (now++).toString(36); } -export function fileToObject(file) { +export function fileToObject(file: UploadFile): ObjectFile { if (!file.uid) { file.uid = uid(); } @@ -26,12 +24,18 @@ export function fileToObject(file) { }; } -export function getFileItem(file, fileList) { +export function getFileItem< + T extends { uid?: unknown; name?: unknown }, + U extends { uid?: unknown; name?: unknown }, +>(file: T, fileList: U[]): U { const matchKey = file.uid !== undefined ? 'uid' : 'name'; return fileList.filter(item => item[matchKey] === file[matchKey])[0]; } -export function removeFileItem(file, fileList) { +export function removeFileItem( + file: T, + fileList: T[] +) { const matchKey = file.uid !== undefined ? 'uid' : 'name'; const removed = fileList.filter(item => item[matchKey] !== file[matchKey]); if (removed.length === fileList.length) { @@ -41,7 +45,7 @@ export function removeFileItem(file, fileList) { } // https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL -export function previewFile(file, callback) { +export function previewFile(file: Blob, callback: (arg0: string | ArrayBuffer | null) => void) { const reader = new FileReader(); reader.onloadend = () => callback(reader.result); reader.readAsDataURL(file);