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/jestHelpers/setup.js b/jestHelpers/setup.js index 986fe5d593..27be965ac8 100644 --- a/jestHelpers/setup.js +++ b/jestHelpers/setup.js @@ -20,6 +20,10 @@ jest.mock('cozy-bar/transpiled', () => ({ setTheme: () => null })) +jest.mock('cozy-intent', () => ({ + useWebviewIntent: jest.fn() +})) + Enzyme.configure({ adapter: new Adapter() }) // see https://github.com/jsdom/jsdom/issues/1695 window.HTMLElement.prototype.scroll = function () {} diff --git a/package.json b/package.json index 697f87af51..6feb35adfa 100644 --- a/package.json +++ b/package.json @@ -82,14 +82,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-runtime": "^6.26.0", + "babel-preset-cozy-app": "2.1.0", "bundlemon": "1.3.1", "chrome-remote-interface": "0.31.2", "cordova": "8.1.2", "cordova-android": "9.1.0", "cozy-jobs-cli": "^2.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/components/App/App.jsx b/src/components/App/App.jsx index f7af619e45..09467894ae 100644 --- a/src/components/App/App.jsx +++ b/src/components/App/App.jsx @@ -10,10 +10,17 @@ import { ModalContextProvider } from 'drive/lib/ModalContext' import { AcceptingSharingProvider } from 'drive/lib/AcceptingSharingContext' import PushBannerProvider from 'components/PushBanner/PushBannerProvider' import cozyBar from 'lib/cozyBar' +import { onFileUploaded } from 'drive/web/modules/views/Upload/UploadUtils' const App = ({ store, client, lang, polyglot, children }) => { return ( - + + onFileUploaded({ file, isSuccess }, store.dispatch) + }} + > diff --git a/src/declarations.d.ts b/src/declarations.d.ts new file mode 100644 index 0000000000..ceab76a067 --- /dev/null +++ b/src/declarations.d.ts @@ -0,0 +1,25 @@ +declare module 'cozy-ui/*' + +declare module 'cozy-ui/transpiled/react' { + export const Alerter: { + error: (message: string) => void + } + + export const logger: { + info: (message: string, ...rest: unknown[]) => void + } +} + +declare module 'cozy-ui/transpiled/react/providers/I18n' { + export const useI18n: () => { + t: (key: string, options?: Record) => string + } +} + +declare module 'cozy-ui/transpiled/react/deprecated/Alerter' { + const Alerter: { + error: (message: string) => void + } + + export default Alerter +} diff --git a/src/drive/targets/manifest.webapp b/src/drive/targets/manifest.webapp index 97eaf3e5ba..a8c83f1ad7 100644 --- a/src/drive/targets/manifest.webapp +++ b/src/drive/targets/manifest.webapp @@ -175,5 +175,12 @@ "verbs": ["POST"], "description": "Remote-doctype required to send anonymized measures to the DACC shared among mycozy.eu's Cozy." } + }, + "accept_from_flagship": true, + "accept_documents_from_flagship": { + "accepted_mime_types": ["*/*"], + "max_number_of_files": 10, + "max_size_per_file_in_MB": 100, + "route_to_upload": "/#/upload?fromFlagshipUpload=true" } } diff --git a/src/drive/web/modules/drive/helpers.js b/src/drive/web/modules/drive/helpers.js deleted file mode 100644 index f991292acf..0000000000 --- a/src/drive/web/modules/drive/helpers.js +++ /dev/null @@ -1,9 +0,0 @@ -import { makeStyles } from 'cozy-ui/transpiled/react/styles' - -export const useFabStyles = makeStyles(() => ({ - root: { - position: 'fixed', - right: ({ right }) => (right ? right : '1rem'), - bottom: ({ bottom }) => (bottom ? bottom : '1rem') - } -})) diff --git a/src/drive/web/modules/drive/helpers.ts b/src/drive/web/modules/drive/helpers.ts new file mode 100644 index 0000000000..09f01838e0 --- /dev/null +++ b/src/drive/web/modules/drive/helpers.ts @@ -0,0 +1,40 @@ +import { makeStyles } from 'cozy-ui/transpiled/react/styles' + +/* eslint-disable */ +export const useFabStyles = makeStyles(() => ({ + root: { + position: 'fixed', + right: ({ right = '1rem' }) => right, + bottom: ({ bottom = '1rem' }) => bottom + } +})) +/* eslint-enable */ + +interface ErrorWithMessage { + message: string +} + +const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => { + return ( + typeof error === 'object' && + error !== null && + 'message' in error && + typeof (error as Record).message === 'string' + ) +} + +const toErrorWithMessage = (maybeError: unknown): ErrorWithMessage => { + if (isErrorWithMessage(maybeError)) return maybeError + + try { + return new Error(JSON.stringify(maybeError)) + } catch { + // fallback in case there's an error stringifying the maybeError + // like with circular references for example. + return new Error(String(maybeError)) + } +} + +export const getErrorMessage = (error: unknown): string => { + return toErrorWithMessage(error).message +} diff --git a/src/drive/web/modules/navigation/AppRoute.jsx b/src/drive/web/modules/navigation/AppRoute.jsx index ea1641982b..0518e7bed4 100644 --- a/src/drive/web/modules/navigation/AppRoute.jsx +++ b/src/drive/web/modules/navigation/AppRoute.jsx @@ -29,6 +29,7 @@ import { ShareDisplayedFolderView } from 'drive/web/modules/views/Modal/ShareDis import { ShareFileView } from 'drive/web/modules/views/Modal/ShareFileView' import { QualifyFileView } from 'drive/web/modules/views/Modal/QualifyFileView' import { MoveFilesView } from 'drive/web/modules/views/Modal/MoveFilesView' +import { UploaderComponent } from 'drive/web/modules//views/Upload/UploaderComponent' const FilesRedirect = () => { const { folderId } = useParams() @@ -42,6 +43,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..d6bf0952d4 100644 --- a/src/drive/web/modules/upload/index.js +++ b/src/drive/web/modules/upload/index.js @@ -23,8 +23,8 @@ const SLUG = 'upload' export const ADD_TO_UPLOAD_QUEUE = 'ADD_TO_UPLOAD_QUEUE' const UPLOAD_FILE = 'UPLOAD_FILE' const UPLOAD_PROGRESS = 'UPLOAD_PROGRESS' -const RECEIVE_UPLOAD_SUCCESS = 'RECEIVE_UPLOAD_SUCCESS' -const RECEIVE_UPLOAD_ERROR = 'RECEIVE_UPLOAD_ERROR' +export const RECEIVE_UPLOAD_SUCCESS = 'RECEIVE_UPLOAD_SUCCESS' +export const RECEIVE_UPLOAD_ERROR = 'RECEIVE_UPLOAD_ERROR' const PURGE_UPLOAD_QUEUE = 'PURGE_UPLOAD_QUEUE' const CANCEL = 'cancel' diff --git a/src/drive/web/modules/views/Drive/index.jsx b/src/drive/web/modules/views/Drive/index.jsx index 47e8aa3d4e..992b8f1eeb 100644 --- a/src/drive/web/modules/views/Drive/index.jsx +++ b/src/drive/web/modules/views/Drive/index.jsx @@ -9,7 +9,6 @@ import { useQuery, useClient } from 'cozy-client' import { useVaultClient } from 'cozy-keys-lib' import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints' - import Dropzone from 'drive/web/modules/upload/Dropzone' import { ModalContext } from 'drive/lib/ModalContext' import useActions from 'drive/web/modules/actions/useActions' @@ -51,6 +50,7 @@ import FabWithMenuContext from 'drive/web/modules/drive/FabWithMenuContext' import AddMenuProvider from 'drive/web/modules/drive/AddMenu/AddMenuProvider' import useHead from 'components/useHead' import { useSelectionContext } from 'drive/web/modules/selection/SelectionProvider' +import { useResumeUploadFromFlagship } from 'drive/web/modules/views/Upload/useResumeFromFlagship' const desktopExtraColumnsNames = ['carbonCopy', 'electronicSafe'] const mobileExtraColumnsNames = [] @@ -172,6 +172,9 @@ const DriveView = () => { }), [t] ) + + useResumeUploadFromFlagship() + useEffect(() => { if (canWriteToCurrentFolder) { setIsFabDisplayed(isMobile) 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..223ce77b83 --- /dev/null +++ b/src/drive/web/modules/views/Upload/UploadTypes.ts @@ -0,0 +1,44 @@ +import { UseQueryReturnValue } from 'cozy-client/types/types' + +export interface Folder { + _id: string +} + +export interface FileForQueue { + name: string + file?: { name: string } + isDirectory?: false +} + +export interface FileFromNative { + name: string + file: { + weblink: null + text: null + filePath: string + contentUri: string + subject: null + extension: string + fileName: string + mimeType: string + dirId?: string + conflictStrategy?: string + } + status: number +} + +export interface UploadFromFlagship { + items?: FileFromNative['file'][] + uploadFilesFromFlagship: (fileOptions: { + name: string + dirId: string + conflictStrategy: string + }) => Promise + resetFilesToHandle: () => Promise + onClose: () => Promise + uploadInProgress: boolean + contentQuery: UseQueryReturnValue + folderQuery: UseQueryReturnValue + setFolder: React.Dispatch> + folder: Folder +} 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..eb336a5501 --- /dev/null +++ b/src/drive/web/modules/views/Upload/UploadUtils.ts @@ -0,0 +1,86 @@ +import { WebviewService } from 'cozy-intent' +import logger from 'cozy-logger' + +import { + RECEIVE_UPLOAD_ERROR, + RECEIVE_UPLOAD_SUCCESS +} from 'drive/web/modules/upload' +import type { + FileFromNative, + FileForQueue +} from 'drive/web/modules/views/Upload/UploadTypes' + +export const generateForQueue = ( + files: FileFromNative['file'][] +): FileForQueue[] => { + // @ts-expect-error fix types + return files.map(file => ({ file: file, isDirectory: false })) +} + +export const onFileUploaded = ( + data: { + file: FileFromNative + isSuccess: boolean + }, + dispatch: (arg0: { type: string; file: FileFromNative }) => void +): void => { + if (!data.file) return + + // Status 2 means file status is "uploaded", any other status should be considered as an error, + // though it is not intended to receive something else than "uploaded" (2) or "error" (3) + // See OsReceiveFileStatus enum in cozy-flagship + if (data.file.status === 2) { + dispatch({ type: RECEIVE_UPLOAD_SUCCESS, file: data.file }) + } else { + dispatch({ type: RECEIVE_UPLOAD_ERROR, file: data.file }) + } +} + +export const shouldRender = ( + items?: FileFromNative['file'][] +): items is FileFromNative['file'][] => !!items && items.length > 0 + +export const getFilesToHandle = async ( + webviewIntent: WebviewService +): Promise> => { + logger('info', 'getFilesToHandle called') + + const files = (await webviewIntent?.call( + 'getFilesToHandle' + )) as unknown as FileFromNative[] + + if (files?.length === 0) throw new Error('No files to upload') + + if (files.length > 0) { + logger('info', 'getFilesToHandle success') + + return files.map(fileFromNative => ({ + ...fileFromNative.file, + name: fileFromNative.file.fileName + })) + } else { + logger('info', 'getFilesToHandle no files to upload') + throw new Error('No files to upload') + } +} + +export const sendFilesToHandle = async ( + filesForQueue: FileForQueue[], + webviewIntent: WebviewService, + folder: { _id: string } +): Promise => { + for (const file of filesForQueue) { + if (!file.file) throw new Error('No file to upload') + + const fileOptions = { + name: file.file.name, + dirId: folder._id + } + + logger('info', 'uploadFilesFromFlagship called') + + await webviewIntent?.call('uploadFile', JSON.stringify({ fileOptions })) + + logger('info', 'uploadFilesFromFlagship success') + } +} 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..eab7180806 --- /dev/null +++ b/src/drive/web/modules/views/Upload/UploaderComponent.spec.tsx @@ -0,0 +1,72 @@ +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 'drive/web/modules/views/Upload/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', + mimeType: 'application/pdf', + extension: 'pdf', + contentUri: 'file:///path/to/file.pdf', + filePath: '/path/to/file.pdf', + weblink: null, + text: null, + subject: null, + dirId: '123', + conflictStrategy: 'replace' + } + ] + + const setupComponent = (): RenderResult => { + 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..6c5e044e69 --- /dev/null +++ b/src/drive/web/modules/views/Upload/UploaderComponent.tsx @@ -0,0 +1,89 @@ +/* eslint-disable no-console */ +import React from 'react' + +import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' +import { FixedDialog } from 'cozy-ui/transpiled/react/CozyDialogs' +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 Footer from 'drive/web/modules/move/Footer' +import Topbar from 'drive/web/modules/move/Topbar' +import { useUploadFromFlagship } from 'drive/web/modules/views/Upload/useUploadFromFlagship' +import { shouldRender } from 'drive/web/modules/views/Upload/UploadUtils' + +export const UploaderComponent = (): JSX.Element => { + const { t } = useI18n() + const { + items, + uploadFilesFromFlagship, + folderQuery, + contentQuery, + onClose, + folder, + setFolder, + uploadInProgress + } = useUploadFromFlagship() + + return ( + + {shouldRender(items) && ( +
+ )} + + + } + content={ + + +
+ {shouldRender(items) && ( + + )} +
+
+
+ } + actions={ + shouldRender(items) && ( +