diff --git a/.eslintrc.json b/.eslintrc.json index 9a1264d8b8..5e2e97fff8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -20,5 +20,8 @@ "react/display-name": "off" } } - ] + ], + "parserOptions": { + "project": "tsconfig.json" + } } diff --git a/babel.config.js b/babel.config.js index b4d7fb3463..c5df3bc678 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,3 +1,3 @@ module.exports = { - presets: ['cozy-app', '@babel/env'] + presets: ['cozy-app'] } diff --git a/jest.config.js b/jest.config.js index 8e63556af6..162602ee90 100644 --- a/jest.config.js +++ b/jest.config.js @@ -31,7 +31,7 @@ module.exports = { clearMocks: true, snapshotSerializers: ['enzyme-to-json/serializer'], transform: { - '^.+\\.jsx?$': 'babel-jest', + '^.+\\.(ts|tsx|js|jsx)?$': 'babel-jest', '^.+\\.webapp$': '/test/jestLib/json-transformer.js' }, transformIgnorePatterns: ['node_modules/(?!cozy-ui)/'], diff --git a/package.json b/package.json index 158e907e21..0cfc1a1fd8 100644 --- a/package.json +++ b/package.json @@ -80,13 +80,15 @@ "@testing-library/jest-dom": "5.16.4", "@testing-library/react": "11.2.7", "@testing-library/react-hooks": "8.0.1", + "@types/react-redux": "7.1.26", "@welldone-software/why-did-you-render": "^6.1.4", - "babel-core": "7.0.0-bridge.0", + "babel-preset-cozy-app": "2.1.0", "babel-runtime": "^6.26.0", "bundlemon": "1.3.1", "chrome-remote-interface": "0.31.2", "cordova": "8.1.2", "cordova-android": "9.1.0", + "cozy-tsconfig": "1.2.0", "css-mediaquery": "^0.1.2", "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.6", diff --git a/src/declarations.d.ts b/src/declarations.d.ts new file mode 100644 index 0000000000..ae1aa97a25 --- /dev/null +++ b/src/declarations.d.ts @@ -0,0 +1 @@ +declare module 'cozy-ui/*' diff --git a/src/drive/targets/manifest.webapp b/src/drive/targets/manifest.webapp index 809478d059..e941693939 100644 --- a/src/drive/targets/manifest.webapp +++ b/src/drive/targets/manifest.webapp @@ -181,6 +181,6 @@ "accepted_mime_types": ["*/*"], "max_number_of_files": 1, "max_size_per_file_in_MB": 10, - "route_to_upload": "/#/create?fromFlagshipUpload=true" + "route_to_upload": "/#/upload?fromFlagshipUpload=true" } } diff --git a/src/drive/web/modules/navigation/AppRoute.jsx b/src/drive/web/modules/navigation/AppRoute.jsx index c4ef6b7c14..c07f067d38 100644 --- a/src/drive/web/modules/navigation/AppRoute.jsx +++ b/src/drive/web/modules/navigation/AppRoute.jsx @@ -26,7 +26,7 @@ import SearchView from '../views/Search/SearchView' import OnlyOfficePaywallView from '../views/OnlyOffice/OnlyOfficePaywallView' import FilesViewerRecent from '../views/Recent/FilesViewerRecent' import { ShareDisplayedFolderView } from 'drive/web/modules/views/Modal/ShareDisplayedFolderView' -import { UploadFromFlagship } from '../views/Upload/UploadFromFlagship' +import UploaderComponent from '../views/Upload/UploaderComponent' const FilesRedirect = () => { const { folderId } = useParams() @@ -40,7 +40,7 @@ const AppRoute = () => ( {__TARGET__ === 'mobile' && ( } /> )} - } /> + } /> } /> } /> diff --git a/src/drive/web/modules/upload/index.js b/src/drive/web/modules/upload/index.js index 55027b0d30..ed95361422 100644 --- a/src/drive/web/modules/upload/index.js +++ b/src/drive/web/modules/upload/index.js @@ -375,7 +375,13 @@ export const overwriteFile = async (client, file, path, options = {}) => { } export const uploadFilesFromNative = - (files, folderId, uploadFilesSuccessCallback, { client, vaultClient }) => + ( + files, + folderId, + uploadFilesSuccessCallback, + { client, vaultClient }, + alternateUploader + ) => async dispatch => { dispatch({ type: ADD_TO_UPLOAD_QUEUE, @@ -401,10 +407,17 @@ export const uploadFilesFromNative = contentType: file.file.type }) } else { - await doMobileUpload(client, file.file.fileUrl, { - ...fileOpts, - contentType: file.file.type - }) + if (alternateUploader) { + await alternateUploader(client, file.file.fileUrl, { + ...fileOpts, + contentType: file.file.type + }) + } else { + await doMobileUpload(client, file.file.fileUrl, { + ...fileOpts, + contentType: file.file.type + }) + } } dispatch(removeFileToUploadQueue(file.file)) } catch (error) { diff --git a/src/drive/web/modules/views/Upload/UploadFromFlagship.jsx b/src/drive/web/modules/views/Upload/UploadFromFlagship.jsx deleted file mode 100644 index 4ca80b6585..0000000000 --- a/src/drive/web/modules/views/Upload/UploadFromFlagship.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react' -import { useUploadFromFlagship } from './useUploadFromFlagship' - -export const UploadFromFlagship = () => { - const { loading } = useUploadFromFlagship() - - return ( -
-

Upload From Flagship Page

-

{loading ? 'loading' : 'Ready to call hasFilesToHandle'}

-
- ) -} diff --git a/src/drive/web/modules/views/Upload/UploadTypes.ts b/src/drive/web/modules/views/Upload/UploadTypes.ts new file mode 100644 index 0000000000..4bc7878896 --- /dev/null +++ b/src/drive/web/modules/views/Upload/UploadTypes.ts @@ -0,0 +1,40 @@ +import CozyClient from 'cozy-client' + +export interface Folder { + _id: string +} + +export interface FileForQueue { + file: FileFromNative + isDirectory: false +} + +export interface DumbUploadProps { + t: (key: string, options?: { smart_count: number }) => string + stopMediaBackup: () => void + navigate: (path: string) => void + uploadFilesFromNative: DispatchProps['uploadFilesFromNative'] + client: CozyClient + vaultClient: CozyClient + startMediaBackup: () => void + items: FileFromNative[] +} + +export interface DispatchProps { + uploadFilesFromNative: ( + files: FileForQueue[], + folderId: string, + successCallback: () => void, + options: { client: CozyClient; vaultClient?: CozyClient }, + alternateUploader?: ( + files: FileForQueue[], + folderId: string + ) => Promise + ) => Promise + stopMediaBackup: () => void + startMediaBackup: () => void +} + +export interface FileFromNative { + fileName: string +} diff --git a/src/drive/web/modules/views/Upload/UploadUtils.ts b/src/drive/web/modules/views/Upload/UploadUtils.ts new file mode 100644 index 0000000000..996968c233 --- /dev/null +++ b/src/drive/web/modules/views/Upload/UploadUtils.ts @@ -0,0 +1,5 @@ +import type { FileFromNative, FileForQueue } from './UploadTypes' + +export const generateForQueue = (files: FileFromNative[]): FileForQueue[] => { + return files.map(file => ({ file: file, isDirectory: false })) +} diff --git a/src/drive/web/modules/views/Upload/UploaderComponent.spec.tsx b/src/drive/web/modules/views/Upload/UploaderComponent.spec.tsx new file mode 100644 index 0000000000..841e05ace7 --- /dev/null +++ b/src/drive/web/modules/views/Upload/UploaderComponent.spec.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import { render, RenderResult, waitFor } from '@testing-library/react' +import '@testing-library/jest-dom/extend-expect' +import { DumbUpload } from 'drive/mobile/modules/upload' +import { generateForQueue } from './UploadUtils' + +jest.mock('cozy-keys-lib', () => ({ + withVaultClient: jest.fn().mockReturnValue({}) +})) + +const tSpy = jest.fn() +const uploadFilesFromNativeSpy = jest.fn() + +describe('DumbUpload component', () => { + const defaultItems = [{ fileName: 'File1.pdf' }] + + const setupComponent = (): RenderResult< + typeof import('/home/anc/cozy/cozy-drive/node_modules/@testing-library/dom/types/queries'), + HTMLElement + > => { + const props = { + client: {}, + vaultClient: {}, + t: tSpy, + uploadFilesFromNative: uploadFilesFromNativeSpy, + stopMediaBackup: jest.fn(), + router: jest.fn(), + navigate: jest.fn() + } + + return render() + } + + describe('generateForQueue', () => { + it('should generate the right object for the Drive queue', () => { + const genetaredForQueue = generateForQueue(defaultItems) + expect(genetaredForQueue).toEqual([ + { file: defaultItems[0], isDirectory: false } + ]) + }) + }) + + describe('Upload files', () => { + it('should call uploadFileFromNative with the right arguments', async () => { + const { rerender } = setupComponent() + const folderId = 'io.cozy.root' + + rerender() + + await waitFor(() => { + const genetaredForQueue = generateForQueue(defaultItems) + expect(uploadFilesFromNativeSpy).toHaveBeenCalledWith( + genetaredForQueue, + folderId, + expect.any(Function), + { client: {}, vaultClient: {} } + ) + }) + }) + }) +}) diff --git a/src/drive/web/modules/views/Upload/UploaderComponent.tsx b/src/drive/web/modules/views/Upload/UploaderComponent.tsx new file mode 100644 index 0000000000..4122661c8c --- /dev/null +++ b/src/drive/web/modules/views/Upload/UploaderComponent.tsx @@ -0,0 +1,208 @@ +/* eslint-disable no-console */ +import React, { useCallback, useState } from 'react' +import localforage from 'localforage' +import { connect } from 'react-redux' +import { useNavigate } from 'react-router-dom' +import { Query, useClient } from 'cozy-client' +import Alerter from 'cozy-ui/transpiled/react/deprecated/Alerter' +import { FixedDialog } from 'cozy-ui/transpiled/react/CozyDialogs' +import { useI18n } from 'cozy-ui/transpiled/react' +import { uploadFilesFromNative } from 'drive/web/modules/upload' +import { ROOT_DIR_ID } from 'drive/constants/config' +import Header from 'drive/web/modules/move/Header' +import Explorer from 'drive/web/modules/move/Explorer' +import FileList from 'drive/web/modules/move/FileList' +import Loader from 'drive/web/modules/move/Loader' +import LoadMore from 'drive/web/modules/move/LoadMore' +import Footer from 'drive/web/modules/move/Footer' +import Topbar from 'drive/web/modules/move/Topbar' +import { startMediaBackup } from 'drive/mobile/modules/mediaBackup/duck' +import { buildMoveOrImportQuery, buildOnlyFolderQuery } from '../../queries' +import { generateForQueue } from './UploadUtils' +import { DumbUploadProps, Folder } from './UploadTypes' +import { ThunkDispatch } from 'redux-thunk' +import { useUploadFromFlagship } from './useUploadFromFlagship' + +const TypedUseI18n = useI18n as () => { + t: (str: string, arg?: Record) => string +} +const TypedAlerter = Alerter as { success: (str: string) => void } +const TypedLoadMore = LoadMore as React.FC<{ + hasMore: boolean + fetchMore: () => void +}> + +const DumbUpload = ({ + uploadFilesFromNative +}: DumbUploadProps): JSX.Element | null => { + const [folder, setFolder] = useState({ _id: ROOT_DIR_ID }) + const [uploadInProgress] = useState(false) + const { t } = TypedUseI18n() + const navigate = useNavigate() + const client = useClient() + const { items, resetFilesToHandle, uploadFilesFromFlagship } = + useUploadFromFlagship() + + const callbackSuccess = useCallback(() => { + TypedAlerter.success( + t('ImportToDrive.success', { smart_count: items?.length }) + ) + + resetFilesToHandle() + .then(() => { + return console.log('resetFilesToHandle done') + }) + .catch(e => { + console.log('resetFilesToHandle error', e) + }) + }, [items?.length, resetFilesToHandle, t]) + + const onClose = useCallback(() => { + void localforage.removeItem('importedFiles') + navigate('/') + }, [navigate]) + + const uploadFiles = useCallback(() => { + if (!items || items.length === 0 || !client) return + + const filesForQueue = generateForQueue(items) + uploadFilesFromNative( + filesForQueue, + folder._id, + callbackSuccess, + { client }, + uploadFilesFromFlagship + ) + .then(() => { + return console.log('uploadFilesFromNative done') + }) + .catch(e => { + console.log('uploadFilesFromNative error', e) + }) + + setTimeout(() => navigate(`/folder/${folder._id}`), 50) + }, [ + items, + client, + uploadFilesFromNative, + folder._id, + callbackSuccess, + uploadFilesFromFlagship, + navigate + ]) + + const contentQuery = buildMoveOrImportQuery(folder._id) + const folderQuery = buildOnlyFolderQuery(folder._id) + + if (!items || items.length === 0) { + return null + } + + return ( + +
+ + {({ + data, + fetchStatus + }: { + data: Record[] + fetchStatus: string + }): JSX.Element => ( + + )} + + + } + content={ + + {({ + data, + fetchStatus, + hasMore, + fetchMore + }: { + data: Record[] + fetchStatus: string + hasMore: boolean + fetchMore: () => void + }): JSX.Element => ( + + +
+ + +
+
+
+ )} +
+ } + actions={ +