Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Handle file upload from Flagship #2969

Merged
merged 17 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@
"react/display-name": "off"
}
}
]
],
"parserOptions": {
"project": "tsconfig.json"
}
}
2 changes: 1 addition & 1 deletion babel.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module.exports = {
presets: ['cozy-app', '@babel/env']
presets: ['cozy-app']
}
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module.exports = {
clearMocks: true,
snapshotSerializers: ['enzyme-to-json/serializer'],
transform: {
'^.+\\.jsx?$': 'babel-jest',
'^.+\\.(ts|tsx|js|jsx)?$': 'babel-jest',
'^.+\\.webapp$': '<rootDir>/test/jestLib/json-transformer.js'
},
transformIgnorePatterns: ['node_modules/(?!cozy-ui)/'],
Expand Down
4 changes: 4 additions & 0 deletions jestHelpers/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 8 additions & 1 deletion src/components/App/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<WebviewIntentProvider setBarContext={cozyBar.setWebviewContext}>
<WebviewIntentProvider
setBarContext={cozyBar.setWebviewContext}
methods={{
onFileUploaded: (file, isSuccess) =>
onFileUploaded({ file, isSuccess }, store.dispatch)
}}
>
<Provider store={store}>
<DriveProvider client={client} lang={lang} polyglot={polyglot}>
<PushBannerProvider>
Expand Down
25 changes: 25 additions & 0 deletions src/declarations.d.ts
Original file line number Diff line number Diff line change
@@ -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, unknown>) => string
}
}

declare module 'cozy-ui/transpiled/react/deprecated/Alerter' {
const Alerter: {
error: (message: string) => void
}

export default Alerter
}
7 changes: 7 additions & 0 deletions src/drive/targets/manifest.webapp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Ldoppea marked this conversation as resolved.
Show resolved Hide resolved
"max_size_per_file_in_MB": 10,
"route_to_upload": "/#/upload?fromFlagshipUpload=true"
}
}
2 changes: 2 additions & 0 deletions src/drive/web/modules/navigation/AppRoute.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -42,6 +43,7 @@ const AppRoute = () => (
{__TARGET__ === 'mobile' && (
<Route path="uploadfrommobile" element={<UploadFromMobile />} />
)}
<Route path="upload" element={<UploaderComponent />} />
<Route path="/files/:folderId" element={<FilesRedirect />} />
<Route path="/" element={<Index />} />

Expand Down
4 changes: 2 additions & 2 deletions src/drive/web/modules/upload/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
5 changes: 4 additions & 1 deletion src/drive/web/modules/views/Drive/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -172,6 +172,9 @@ const DriveView = () => {
}),
[t]
)

useResumeUploadFromFlagship()

useEffect(() => {
if (canWriteToCurrentFolder) {
setIsFabDisplayed(isMobile)
Expand Down
44 changes: 44 additions & 0 deletions src/drive/web/modules/views/Upload/UploadTypes.ts
Original file line number Diff line number Diff line change
@@ -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<void>
resetFilesToHandle: () => Promise<void>
onClose: () => Promise<void>
uploadInProgress: boolean
contentQuery: UseQueryReturnValue
folderQuery: UseQueryReturnValue
setFolder: React.Dispatch<React.SetStateAction<Folder>>
folder: Folder
}
113 changes: 113 additions & 0 deletions src/drive/web/modules/views/Upload/UploadUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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

if (!(data.file.status === 2)) {
dispatch({ type: RECEIVE_UPLOAD_ERROR, file: data.file })
} else {
dispatch({ type: RECEIVE_UPLOAD_SUCCESS, file: data.file })
}
Ldoppea marked this conversation as resolved.
Show resolved Hide resolved
}

export const shouldRender = (
items?: FileFromNative['file'][]
): items is FileFromNative['file'][] => !!items && items.length > 0

interface ErrorWithMessage {
message: string
}

const isErrorWithMessage = (error: unknown): error is ErrorWithMessage => {
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof (error as Record<string, unknown>).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
}
Ldoppea marked this conversation as resolved.
Show resolved Hide resolved

export const getFilesToHandle = async (
webviewIntent: WebviewService
): Promise<Array<FileFromNative['file'] & { name: string }>> => {
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<void> => {
for (const file of filesForQueue) {
if (!file.file) throw new Error('No file to upload')

const fileOptions = {
name: file.file.name,
dirId: folder._id,
conflictStrategy: 'rename'
Ldoppea marked this conversation as resolved.
Show resolved Hide resolved
}

logger('info', 'uploadFilesFromFlagship called')

await webviewIntent?.call('uploadFiles', JSON.stringify({ fileOptions }))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that we pass only one file at a time to uploadFiles. Didn't we want to pass an array here?

If yes we should modify the interface.

If no we should rename this as uploadFile.

Copy link
Contributor Author

@acezard acezard Oct 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it's one by one, it was made like that to mimick the old upload to mobile feature. In this case renaming seems the best approach

done on react-native side too cozy/cozy-flagship-app@5278d76


logger('info', 'uploadFilesFromFlagship success')
}
}
61 changes: 61 additions & 0 deletions src/drive/web/modules/views/Upload/UploaderComponent.spec.tsx
Original file line number Diff line number Diff line change
@@ -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'),
Ldoppea marked this conversation as resolved.
Show resolved Hide resolved
HTMLElement
> => {
const props = {
client: {},
vaultClient: {},
t: tSpy,
uploadFilesFromNative: uploadFilesFromNativeSpy,
stopMediaBackup: jest.fn(),
router: jest.fn(),
navigate: jest.fn()
}

return render(<DumbUpload {...props} />)
}

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(<DumbUpload items={defaultItems} folder={{ _id: folderId }} />)

await waitFor(() => {
const genetaredForQueue = generateForQueue(defaultItems)
expect(uploadFilesFromNativeSpy).toHaveBeenCalledWith(
genetaredForQueue,
folderId,
expect.any(Function),
{ client: {}, vaultClient: {} }
)
})
})
})
})
Loading
Loading