diff --git a/manifest.webapp b/manifest.webapp
index 7603648d10..99a8166892 100644
--- a/manifest.webapp
+++ b/manifest.webapp
@@ -78,6 +78,11 @@
"action": "OPEN",
"type": ["io.cozy.suggestions"],
"href": "/intents"
+ },
+ {
+ "action": "PICK",
+ "type": ["io.cozy.files"],
+ "href": "/intents"
}
],
"entrypoints": [
diff --git a/src/modules/services/components/FilePicker/FilePickerBody.jsx b/src/modules/services/components/FilePicker/FilePickerBody.jsx
new file mode 100644
index 0000000000..3e571653fa
--- /dev/null
+++ b/src/modules/services/components/FilePicker/FilePickerBody.jsx
@@ -0,0 +1,111 @@
+import PropTypes from 'prop-types'
+import React, { useCallback } from 'react'
+
+import { models, useQuery } from 'cozy-client'
+import List from 'cozy-ui/transpiled/react/List'
+import LoadMore from 'cozy-ui/transpiled/react/LoadMore'
+
+import FilePickerBodyItem from './FilePickerBodyItem'
+import { isValidFile } from './helpers'
+import { buildContentFolderQuery } from './queries'
+
+const {
+ file: { isDirectory }
+} = models
+
+const FilePickerBody = ({
+ navigateTo,
+ folderId,
+ onSelectItemId,
+ itemsIdsSelected,
+ itemTypesAccepted,
+ multiple
+}) => {
+ const contentFolderQuery = buildContentFolderQuery(folderId)
+ const {
+ data: contentFolder,
+ hasMore,
+ fetchMore
+ } = useQuery(contentFolderQuery.definition, contentFolderQuery.options)
+
+ const onCheck = useCallback(
+ itemId => {
+ const isChecked = itemsIdsSelected.some(
+ fileIdSelected => fileIdSelected === itemId
+ )
+ if (isChecked) {
+ onSelectItemId(
+ itemsIdsSelected.filter(fileIdSelected => fileIdSelected !== itemId)
+ )
+ } else onSelectItemId(prev => [...prev, itemId])
+ },
+ [itemsIdsSelected, onSelectItemId]
+ )
+
+ // When click on checkbox/radio area...
+ const handleChoiceClick = useCallback(
+ item => () => {
+ if (multiple) onCheck(item._id)
+ else onSelectItemId(item._id)
+ },
+ [multiple, onCheck, onSelectItemId]
+ )
+
+ // ...when click anywhere on the rest of the line
+ const handleListItemClick = useCallback(
+ item => () => {
+ if (isDirectory(item)) {
+ navigateTo(contentFolder.find(it => it._id === item._id))
+ }
+
+ if (isValidFile(item, itemTypesAccepted)) {
+ if (multiple) onCheck(item._id)
+ else onSelectItemId(item._id)
+ }
+ },
+ [
+ contentFolder,
+ itemTypesAccepted,
+ multiple,
+ navigateTo,
+ onCheck,
+ onSelectItemId
+ ]
+ )
+
+ return (
+
+ {contentFolder &&
+ contentFolder.map((item, idx) => {
+ const hasDivider = contentFolder
+ ? idx !== contentFolder.length - 1
+ : false
+
+ return (
+
+ )
+ })}
+ {hasMore && }
+
+ )
+}
+
+FilePickerBody.propTypes = {
+ onSelectItemId: PropTypes.func.isRequired,
+ itemsIdsSelected: PropTypes.arrayOf(PropTypes.string).isRequired,
+ folderId: PropTypes.string.isRequired,
+ navigateTo: PropTypes.func.isRequired,
+ itemTypesAccepted: PropTypes.arrayOf(PropTypes.string).isRequired
+}
+
+export default FilePickerBody
diff --git a/src/modules/services/components/FilePicker/FilePickerBodyItem.jsx b/src/modules/services/components/FilePicker/FilePickerBodyItem.jsx
new file mode 100644
index 0000000000..d21992299d
--- /dev/null
+++ b/src/modules/services/components/FilePicker/FilePickerBodyItem.jsx
@@ -0,0 +1,126 @@
+import cx from 'classnames'
+import { filesize } from 'filesize'
+import PropTypes from 'prop-types'
+import React from 'react'
+
+import { models } from 'cozy-client'
+import Checkbox from 'cozy-ui/transpiled/react/Checkbox'
+import Divider from 'cozy-ui/transpiled/react/Divider'
+import Icon from 'cozy-ui/transpiled/react/Icon'
+import FileTypeFolder from 'cozy-ui/transpiled/react/Icons/FileTypeFolder'
+import FileTypeText from 'cozy-ui/transpiled/react/Icons/FileTypeText'
+import ListItem from 'cozy-ui/transpiled/react/ListItem'
+import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon'
+import ListItemText from 'cozy-ui/transpiled/react/ListItemText'
+import Radio from 'cozy-ui/transpiled/react/Radios'
+import { makeStyles } from 'cozy-ui/transpiled/react/styles'
+import { useI18n } from 'twake-i18n'
+
+import { isValidFile, isValidFolder } from './helpers'
+import styles from './styles.styl'
+
+const {
+ file: { isDirectory, isFile }
+} = models
+
+const useStyles = makeStyles(() => ({
+ verticalDivider: {
+ height: '2rem',
+ display: 'flex',
+ alignSelf: 'auto',
+ alignItems: 'center',
+ marginLeft: '0.5rem'
+ }
+}))
+
+const FilePickerBodyItem = ({
+ item,
+ itemTypesAccepted,
+ multiple,
+ handleChoiceClick,
+ handleListItemClick,
+ itemsIdsSelected,
+ hasDivider
+}) => {
+ const classes = useStyles()
+ const { f } = useI18n()
+ const hasChoice =
+ isValidFile(item, itemTypesAccepted) ||
+ isValidFolder(item, itemTypesAccepted)
+
+ const Input = multiple ? Checkbox : Radio
+
+ const listItemSecondaryContent = isFile(item)
+ ? `${f(item.updated_at, 'dd LLL yyyy')} - ${filesize(item.size, {
+ base: 10
+ })}`
+ : null
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {isDirectory(item) && hasChoice && (
+
+ )}
+
+ {
+ // handled by onClick on the container
+ }}
+ checked={itemsIdsSelected.includes(item._id)}
+ value={item._id}
+ className={cx('u-p-0', {
+ 'u-o-100': hasChoice,
+ 'u-o-0': !hasChoice
+ })}
+ disabled={!hasChoice}
+ />
+
+
+ {hasDivider && }
+ >
+ )
+}
+
+FilePickerBodyItem.propTypes = {
+ item: PropTypes.object.isRequired,
+ itemTypesAccepted: PropTypes.arrayOf(PropTypes.string).isRequired,
+ multiple: PropTypes.bool,
+ handleChoiceClick: PropTypes.func.isRequired,
+ handleListItemClick: PropTypes.func.isRequired,
+ itemsIdsSelected: PropTypes.arrayOf(PropTypes.string).isRequired,
+ hasDivider: PropTypes.bool.isRequired
+}
+
+export default FilePickerBodyItem
diff --git a/src/modules/services/components/FilePicker/FilePickerBodyItem.spec.jsx b/src/modules/services/components/FilePicker/FilePickerBodyItem.spec.jsx
new file mode 100644
index 0000000000..d0e82d698e
--- /dev/null
+++ b/src/modules/services/components/FilePicker/FilePickerBodyItem.spec.jsx
@@ -0,0 +1,164 @@
+import { render, fireEvent } from '@testing-library/react'
+import { filesize } from 'filesize'
+import React from 'react'
+
+import FilePickerBodyItem from './FilePickerBodyItem'
+import DemoProvider from './docs/DemoProvider'
+import { isValidFile, isValidFolder } from './helpers'
+
+const mockFile01 = {
+ _id: '001',
+ type: 'file',
+ name: 'Filename.pdf',
+ mime: 'application/pdf',
+ updated_at: '2021-01-01T12:00:00.000000+01:00'
+}
+const mockFolder01 = {
+ _id: '002',
+ type: 'directory',
+ name: 'Foldername',
+ updated_at: '2021-01-01T12:00:00.000000+01:00'
+}
+
+jest.mock('filesize', () => jest.fn())
+jest.mock('./helpers', () => ({
+ ...jest.requireActual('./helpers'),
+ isValidFile: jest.fn(),
+ isValidFolder: jest.fn()
+}))
+
+describe('FilePickerBodyItem components:', () => {
+ const mockHandleChoiceClick = jest.fn()
+ const mockHandleListItemClick = jest.fn()
+ filesize.mockReturnValue('111Ko')
+
+ const setup = ({
+ item = mockFile01,
+ multiple = false,
+ validFile = false,
+ validFolder = false
+ } = {}) => {
+ isValidFile.mockReturnValue(validFile)
+ isValidFolder.mockReturnValue(validFolder)
+
+ return render(
+
+
+
+ )
+ }
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it('should be rendered correctly', () => {
+ const { container } = setup()
+
+ expect(container).toBeDefined()
+ })
+
+ it('should display filename', () => {
+ const { getByText } = setup()
+
+ expect(getByText('Filename.pdf'))
+ })
+
+ it('should display foldername', () => {
+ const { getByText } = setup({ item: mockFolder01 })
+
+ expect(getByText('Foldername'))
+ })
+
+ it("should item's line is not disabled when has not valid type & is File", () => {
+ const { getByTestId } = setup({
+ item: mockFile01,
+ validFile: false,
+ validFolder: false
+ })
+ const listItem = getByTestId('list-item')
+
+ expect(listItem).toHaveAttribute('aria-disabled', 'true')
+ })
+
+ it("should item's line is not disabled when has not valid type & is Folder", () => {
+ const { getByTestId } = setup({
+ item: mockFolder01,
+ validFile: false,
+ validFolder: false
+ })
+ const listItem = getByTestId('list-item')
+
+ expect(listItem).toHaveAttribute('aria-disabled', 'false')
+ })
+
+ describe('Functions called', () => {
+ it('should call "handleChoiceClick" function when click on checkbox/radio area', () => {
+ const { getByTestId } = setup({ validFile: true })
+ fireEvent.click(getByTestId('choice-onclick'))
+
+ expect(mockHandleChoiceClick).toHaveBeenCalled()
+ })
+
+ it('should NOT call "handleChoiceClick" function when click on checkbox/radio area, if is Folder & not accepted', () => {
+ const { getByTestId } = setup({
+ item: mockFolder01,
+ validFolder: false
+ })
+ fireEvent.click(getByTestId('choice-onclick'))
+
+ expect(mockHandleChoiceClick).not.toHaveBeenCalled()
+ })
+ it('should NOT call "handleChoiceClick" function when click on checkbox/radio area, if is File & not accepted', () => {
+ const { getByTestId } = setup({ validFile: false })
+ fireEvent.click(getByTestId('choice-onclick'))
+
+ expect(mockHandleChoiceClick).not.toHaveBeenCalled()
+ })
+
+ it('should call "handleListItemClick" function when click on ListItem node', () => {
+ const { getByTestId } = setup()
+ fireEvent.click(getByTestId('listitem-onclick'))
+
+ expect(mockHandleListItemClick).toHaveBeenCalled()
+ })
+ })
+
+ describe('Attribute "multiple"', () => {
+ it('should radio button exists if "multiple" atribute is False', () => {
+ const { getByTestId } = setup()
+ const radioBtn = getByTestId('radio-btn')
+ expect(radioBtn).not.toBeNull()
+ })
+
+ it('should checkbox button exists if "multiple" atribute is True', () => {
+ const { getByTestId } = setup({ multiple: true })
+ const checkboxBtn = getByTestId('checkbox-btn')
+ expect(checkboxBtn).not.toBeNull()
+ })
+ })
+
+ describe('Radio/Checkbox button', () => {
+ it('should disable and not display the Radio button if it is a File and is not accepted', () => {
+ const { getByTestId } = setup({ validFile: false })
+ const radioBtn = getByTestId('radio-btn')
+
+ expect(radioBtn.getAttribute('disabled')).toBe(null)
+ })
+
+ it('should disable and not display the Radio button if it is a Folder and is not accepted', () => {
+ const { getByTestId } = setup({ item: mockFolder01 })
+ const radioBtn = getByTestId('radio-btn')
+
+ expect(radioBtn.getAttribute('disabled')).toBe(null)
+ })
+ })
+})
diff --git a/src/modules/services/components/FilePicker/FilePickerBreadcrumb.jsx b/src/modules/services/components/FilePicker/FilePickerBreadcrumb.jsx
new file mode 100644
index 0000000000..2f219d0d10
--- /dev/null
+++ b/src/modules/services/components/FilePicker/FilePickerBreadcrumb.jsx
@@ -0,0 +1,55 @@
+import PropTypes from 'prop-types'
+import React, { Fragment, useCallback, memo } from 'react'
+
+import Icon from 'cozy-ui/transpiled/react/Icon'
+import RightIcon from 'cozy-ui/transpiled/react/Icons/Right'
+import Typography from 'cozy-ui/transpiled/react/Typography'
+import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints'
+
+import styles from './styles.styl'
+
+const FilePickerBreadcrumb = ({ path, onBreadcrumbClick }) => {
+ const { isMobile } = useBreakpoints()
+ const hasPath = path && path.length > 0
+
+ const navigateTo = useCallback(
+ folder => () => onBreadcrumbClick(folder),
+ [onBreadcrumbClick]
+ )
+
+ return (
+
+ {hasPath
+ ? isMobile
+ ? path[path.length - 1].name
+ : path.map((folder, idx) => {
+ if (idx < path.length - 1) {
+ return (
+
+
+ {folder.name}
+
+
+
+ )
+ } else {
+ return {folder.name}
+ }
+ })
+ : null}
+
+ )
+}
+
+FilePickerBreadcrumb.propTypes = {
+ path: PropTypes.array,
+ onBreadcrumbClick: PropTypes.func
+}
+
+export default memo(FilePickerBreadcrumb)
diff --git a/src/modules/services/components/FilePicker/FilePickerFooter.jsx b/src/modules/services/components/FilePicker/FilePickerFooter.jsx
new file mode 100644
index 0000000000..83b591c15e
--- /dev/null
+++ b/src/modules/services/components/FilePicker/FilePickerFooter.jsx
@@ -0,0 +1,42 @@
+import PropTypes from 'prop-types'
+import React, { memo } from 'react'
+
+import Button from 'cozy-ui/transpiled/react/Buttons'
+import { createUseI18n } from 'twake-i18n'
+
+import en from './locales/en.json'
+import fr from './locales/fr.json'
+import ru from './locales/ru.json'
+import vi from './locales/vi.json'
+
+const locales = { en, fr, ru, vi }
+const useI18n = createUseI18n(locales)
+
+const FilePickerFooter = ({ onConfirm, onClose, disabledConfirm }) => {
+ const { t } = useI18n()
+
+ return (
+ <>
+
+
+ >
+ )
+}
+
+FilePickerFooter.propTypes = {
+ onConfirm: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired,
+ disabledConfirm: PropTypes.bool.isRequired
+}
+
+export default memo(FilePickerFooter)
diff --git a/src/modules/services/components/FilePicker/FilePickerFooter.spec.jsx b/src/modules/services/components/FilePicker/FilePickerFooter.spec.jsx
new file mode 100644
index 0000000000..25fe64b9f7
--- /dev/null
+++ b/src/modules/services/components/FilePicker/FilePickerFooter.spec.jsx
@@ -0,0 +1,41 @@
+import { render, fireEvent } from '@testing-library/react'
+import React from 'react'
+
+import FilePickerFooter from './FilePickerFooter'
+import DemoProvider from './docs/DemoProvider'
+
+describe('FilePickerFooter components:', () => {
+ const mockOnConfirm = jest.fn()
+ const mockOnClose = jest.fn()
+
+ const setup = (disabledConfirm = false) => {
+ return render(
+
+
+
+ )
+ }
+ it('should be rendered correctly', () => {
+ const { container } = setup()
+
+ expect(container).toBeDefined()
+ })
+
+ it('should confirm button have been called', () => {
+ const { getByTestId } = setup()
+ fireEvent.click(getByTestId('confirm-btn'))
+
+ expect(mockOnConfirm).toHaveBeenCalled()
+ })
+
+ it('should close button have been called', () => {
+ const { getByTestId } = setup()
+ fireEvent.click(getByTestId('close-btn'))
+
+ expect(mockOnClose).toHaveBeenCalled()
+ })
+})
diff --git a/src/modules/services/components/FilePicker/FilePickerHeader.jsx b/src/modules/services/components/FilePicker/FilePickerHeader.jsx
new file mode 100644
index 0000000000..13e5e73684
--- /dev/null
+++ b/src/modules/services/components/FilePicker/FilePickerHeader.jsx
@@ -0,0 +1,89 @@
+import get from 'lodash/get'
+import uniqBy from 'lodash/uniqBy'
+import PropTypes from 'prop-types'
+import React, { useCallback, memo } from 'react'
+
+import { useQuery, hasQueryBeenLoaded } from 'cozy-client'
+import Icon from 'cozy-ui/transpiled/react/Icon'
+import IconButton from 'cozy-ui/transpiled/react/IconButton'
+import Previous from 'cozy-ui/transpiled/react/Icons/Previous'
+import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints'
+
+import { ROOT_DIR_ID } from '.'
+import FilePickerBreadcrumb from './FilePickerBreadcrumb'
+import { buildCurrentFolderQuery } from './queries'
+
+/**
+ * @param {IOCozyFolder} displayedFolder - An io.cozy.files folder
+ * @returns {{id: string, name: string}[]}
+ */
+const getBreadcrumbPath = displayedFolder => {
+ return uniqBy(
+ [
+ {
+ id: ROOT_DIR_ID
+ },
+ {
+ id: get(displayedFolder, 'dir_id')
+ },
+ {
+ id: displayedFolder.id,
+ name: displayedFolder.name
+ }
+ ],
+ 'id'
+ )
+ .filter(({ id }) => Boolean(id))
+ .map(breadcrumb => ({
+ id: breadcrumb.id,
+ name: breadcrumb.name || (breadcrumb.id === ROOT_DIR_ID ? 'Drive' : '…')
+ }))
+}
+
+const FilePickerHeader = ({ navigateTo, folderId, onClose }) => {
+ const { isMobile } = useBreakpoints()
+
+ const currentFolderQuery = buildCurrentFolderQuery(folderId)
+ const { data: currentFolder, ...restCurrentFolder } = useQuery(
+ currentFolderQuery.definition,
+ currentFolderQuery.options
+ )
+
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ const path = hasQueryBeenLoaded(restCurrentFolder)
+ ? getBreadcrumbPath(currentFolder[0])
+ : []
+
+ const onBack = useCallback(path => navigateTo(path), [navigateTo])
+
+ const handleClick = useCallback(() => {
+ path.length > 1 && isMobile ? onBack(path[path.length - 2]) : onClose()
+ }, [isMobile, path, onBack, onClose])
+
+ return (
+
+ {isMobile && (
+
+
+
+ )}
+
+
+ )
+}
+
+FilePickerHeader.propTypes = {
+ folderId: PropTypes.string.isRequired,
+ navigateTo: PropTypes.func.isRequired
+}
+
+export default memo(FilePickerHeader)
diff --git a/src/modules/services/components/FilePicker/helpers.js b/src/modules/services/components/FilePicker/helpers.js
new file mode 100644
index 0000000000..614f8b804b
--- /dev/null
+++ b/src/modules/services/components/FilePicker/helpers.js
@@ -0,0 +1,46 @@
+import mimeTypes from 'mime-types'
+
+import { isDirectory, isFile } from 'cozy-client/dist/models/file'
+
+/**
+ * @param {string} types - Types we wish to accept ("folder" and/or "extensions/mime" of file), separated by commas
+ * @returns {string[]} All the valid types, if the parameter is undefined or if no type is valid, return an empty array
+ */
+export const getCompliantTypes = types => {
+ if (types) {
+ return types
+ .replaceAll(' ', '')
+ .split(',')
+ .filter(type =>
+ type !== 'folder' ? !!mimeTypes.contentType(type) : true
+ )
+ }
+
+ return []
+}
+
+/**
+ * Check if Item is a file with accepted extension/mime
+ *
+ * @param {object} item - file or folder
+ * @param {string[]} validTypes - List of accepted types
+ * @returns {boolean}
+ */
+export const isValidFile = (item, validTypes) => {
+ const fileTypesAccepted =
+ validTypes.includes(`.${item.name.split('.').pop()}`) ||
+ validTypes.includes(item.mime)
+
+ return isFile(item) && (fileTypesAccepted || validTypes.length === 0)
+}
+
+/**
+ * Check if Item is a folder with accepted type
+ *
+ * @param {object} item - file or folder
+ * @param {string[]} validTypes - List of accepted types
+ * @returns {boolean}
+ */
+export const isValidFolder = (item, validTypes) => {
+ return isDirectory(item) && validTypes.includes(`folder`)
+}
diff --git a/src/modules/services/components/FilePicker/helpers.spec.js b/src/modules/services/components/FilePicker/helpers.spec.js
new file mode 100644
index 0000000000..0b1dd4a6b7
--- /dev/null
+++ b/src/modules/services/components/FilePicker/helpers.spec.js
@@ -0,0 +1,84 @@
+import { isValidFile, isValidFolder, getCompliantTypes } from './helpers'
+
+const makeMockFile = ({
+ extension = '.pdf',
+ mime = 'application/pdf'
+} = {}) => ({
+ _id: '123',
+ type: 'file',
+ mime: mime,
+ name: `mockFile${extension}`
+})
+const makeMockFolder = () => ({
+ _id: '789',
+ type: 'directory',
+ name: 'mockDir'
+})
+
+describe('getCompliantTypes', () => {
+ it('should an array with all valid types', () => {
+ const res = getCompliantTypes('.pdf, text/plain, .not')
+
+ expect(res).toStrictEqual(['.pdf', 'text/plain'])
+ })
+})
+
+describe('isValidFile', () => {
+ it('should be valid when item has no type providen', () => {
+ const item = makeMockFile()
+ const validTypesAccepted = []
+
+ expect(isValidFile(item, validTypesAccepted)).toBe(true)
+ })
+
+ it('should be valid when item has an accepted extension', () => {
+ const item = makeMockFile({ extension: '.png' })
+ const validTypesAccepted = ['.png', '.pdf']
+
+ expect(isValidFile(item, validTypesAccepted)).toBe(true)
+ })
+
+ it('should not be valid when item has not an accepted extension', () => {
+ const item = makeMockFile({ extension: '.png' })
+ const validTypesAccepted = ['.pdf']
+
+ expect(isValidFile(item, validTypesAccepted)).toBe(false)
+ })
+
+ it('should be valid when item has an accepted mime', () => {
+ const item = makeMockFile({ mime: 'image/png' })
+ const validTypesAccepted = ['application/pdf', 'image/png']
+
+ expect(isValidFile(item, validTypesAccepted)).toBe(true)
+ })
+
+ it('should not be valid when item has not an accepted mime', () => {
+ const item = makeMockFile({ mime: 'image/png' })
+ const validTypesAccepted = ['application/pdf']
+
+ expect(isValidFile(item, validTypesAccepted)).toBe(false)
+ })
+
+ it('should not be valid when item is not an file', () => {
+ const item = makeMockFolder()
+ const validTypesAccepted = []
+
+ expect(isValidFile(item, validTypesAccepted)).toBe(false)
+ })
+})
+
+describe('isValidFolder', () => {
+ it('should be valid when item is an folder', () => {
+ const item = makeMockFolder()
+ const validTypesAccepted = ['folder']
+
+ expect(isValidFolder(item, validTypesAccepted)).toBe(true)
+ })
+
+ it('should not be valid when item is not an folder', () => {
+ const item = makeMockFile()
+ const validTypesAccepted = ['folder']
+
+ expect(isValidFolder(item, validTypesAccepted)).toBe(false)
+ })
+})
diff --git a/src/modules/services/components/FilePicker/index.jsx b/src/modules/services/components/FilePicker/index.jsx
new file mode 100644
index 0000000000..a657a4b650
--- /dev/null
+++ b/src/modules/services/components/FilePicker/index.jsx
@@ -0,0 +1,94 @@
+import PropTypes from 'prop-types'
+import React, { useState, memo } from 'react'
+
+import { FixedDialog } from 'cozy-ui/transpiled/react/CozyDialogs'
+import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints'
+import { makeStyles } from 'cozy-ui/transpiled/react/styles'
+
+import FilePickerBody from './FilePickerBody'
+import FilePickerFooter from './FilePickerFooter'
+import FilePickerHeader from './FilePickerHeader'
+import { getCompliantTypes } from './helpers'
+
+const useStyles = makeStyles(() => ({
+ paper: {
+ height: '100%'
+ }
+}))
+
+export const ROOT_DIR_ID = 'io.cozy.files.root-dir'
+
+const FilePicker = ({ onClose, onChange, accept, multiple }) => {
+ const { isMobile } = useBreakpoints()
+ const classes = useStyles()
+ const [folderId, setFolderId] = useState(ROOT_DIR_ID)
+ const [itemsIdsSelected, setItemsIdsSelected] = useState([])
+
+ const onSelectItemId = fileId => {
+ if (!multiple) {
+ handleConfirm(null, fileId)
+ } else {
+ setItemsIdsSelected(fileId)
+ }
+ }
+
+ const navigateTo = folder => setFolderId(folder.id)
+
+ const handleConfirm = (_, fileId) => {
+ onChange(fileId ? fileId : itemsIdsSelected)
+ onClose()
+ }
+ const itemTypesAccepted = getCompliantTypes(accept)
+
+ return (
+
+ }
+ content={
+
+ }
+ actions={
+ multiple ? (
+
+ ) : null
+ }
+ />
+ )
+}
+
+FilePicker.propTypes = {
+ onClose: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ accept: PropTypes.string,
+ multiple: PropTypes.bool
+}
+
+FilePicker.defaultProps = {
+ accept: '',
+ multiple: false
+}
+
+export default memo(FilePicker)
diff --git a/src/modules/services/components/FilePicker/locales/en.json b/src/modules/services/components/FilePicker/locales/en.json
new file mode 100644
index 0000000000..ef1df79e61
--- /dev/null
+++ b/src/modules/services/components/FilePicker/locales/en.json
@@ -0,0 +1,8 @@
+{
+ "footer": {
+ "buttons": {
+ "cancel": "Cancel",
+ "confirm": "Select"
+ }
+ }
+}
diff --git a/src/modules/services/components/FilePicker/locales/fr.json b/src/modules/services/components/FilePicker/locales/fr.json
new file mode 100644
index 0000000000..6d8ed7fed8
--- /dev/null
+++ b/src/modules/services/components/FilePicker/locales/fr.json
@@ -0,0 +1,8 @@
+{
+ "footer": {
+ "buttons": {
+ "cancel": "Annuler",
+ "confirm": "Sélectionner"
+ }
+ }
+}
diff --git a/src/modules/services/components/FilePicker/locales/ru.json b/src/modules/services/components/FilePicker/locales/ru.json
new file mode 100644
index 0000000000..84bc26aa81
--- /dev/null
+++ b/src/modules/services/components/FilePicker/locales/ru.json
@@ -0,0 +1,8 @@
+{
+ "footer": {
+ "buttons": {
+ "cancel": "Отменить",
+ "confirm": "Выбрать"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/modules/services/components/FilePicker/locales/vi.json b/src/modules/services/components/FilePicker/locales/vi.json
new file mode 100644
index 0000000000..6d75a6bcc2
--- /dev/null
+++ b/src/modules/services/components/FilePicker/locales/vi.json
@@ -0,0 +1,8 @@
+{
+ "footer": {
+ "buttons": {
+ "cancel": "Hủy",
+ "confirm": "Chọn"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/modules/services/components/FilePicker/queries.js b/src/modules/services/components/FilePicker/queries.js
new file mode 100644
index 0000000000..318ad1e821
--- /dev/null
+++ b/src/modules/services/components/FilePicker/queries.js
@@ -0,0 +1,37 @@
+import { Q, fetchPolicies } from 'cozy-client'
+
+const FILES_DOCTYPE = 'io.cozy.files'
+
+const defaultFetchPolicy = fetchPolicies.olderThan(30 * 1000)
+const TRASH_DIR_ID = `${FILES_DOCTYPE}.trash-dir`
+const SHARED_DRIVES_DIR_ID = `${FILES_DOCTYPE}.shared-drives-dir`
+
+export const buildCurrentFolderQuery = folderId => ({
+ definition: () => Q(FILES_DOCTYPE).getById(folderId),
+ options: {
+ as: `onlyfolder-${folderId}`,
+ fetchPolicy: defaultFetchPolicy
+ }
+})
+
+export const buildContentFolderQuery = dirId => ({
+ definition: () =>
+ Q(FILES_DOCTYPE)
+ .where({
+ dir_id: dirId,
+ type: { $gt: null },
+ name: { $gt: null }
+ })
+ .partialIndex({
+ _id: {
+ $nin: [SHARED_DRIVES_DIR_ID, TRASH_DIR_ID]
+ }
+ })
+ .indexFields(['dir_id', 'type', 'name'])
+ .sortBy([{ dir_id: 'asc' }, { type: 'asc' }, { name: 'asc' }])
+ .limitBy(20),
+ options: {
+ as: `buildContentFolderQuery-${dirId}`,
+ fetchPolicy: defaultFetchPolicy
+ }
+})
diff --git a/src/modules/services/components/FilePicker/styles.styl b/src/modules/services/components/FilePicker/styles.styl
new file mode 100644
index 0000000000..b6e2b35180
--- /dev/null
+++ b/src/modules/services/components/FilePicker/styles.styl
@@ -0,0 +1,18 @@
+.filePickerBreadcrumb-previousPath
+ color var(--actionColorActive)
+ cursor pointer
+
+.filePickerBreadcrumb-icon
+ color var(--actionColorActive)
+ margin 0 0.25rem
+
+@supports (display: contents)
+ .filePickerBreadcrumb-wrapper
+ display contents
+
+@supports not (display: contents)
+ .filePickerBreadcrumb-wrapper
+ display flex
+ flex 1 1 auto
+ align-items center
+ width 100%
diff --git a/src/modules/services/components/IntentHandler.jsx b/src/modules/services/components/IntentHandler.jsx
index 40d4897934..6776191c18 100644
--- a/src/modules/services/components/IntentHandler.jsx
+++ b/src/modules/services/components/IntentHandler.jsx
@@ -4,7 +4,8 @@ import { useClient } from 'cozy-client'
import Intents from 'cozy-interapp'
import logger from 'cozy-logger'
-import Embeder from './Embeder'
+// import Embeder from './Embeder'
+import Picker from './Picker'
const IntentHandler = ({ intentId }) => {
const client = useClient()
@@ -27,11 +28,19 @@ const IntentHandler = ({ intentId }) => {
service = await intents.createService(intentId, window)
intent = service.getIntent()
+ // This is highly inefficient as it imply to load both part whatever the intent is
+ // breaking potential tree shaking
+ // if (
+ // intent.attributes.action === 'OPEN' &&
+ // intent.attributes.type === 'io.cozy.files'
+ // ) {
+ // component = Embeder
+ // } else
if (
- intent.attributes.action === 'OPEN' &&
+ intent.attributes.action === 'PICK' &&
intent.attributes.type === 'io.cozy.files'
) {
- component = Embeder
+ component = Picker
}
setState({
diff --git a/src/modules/services/components/Picker.jsx b/src/modules/services/components/Picker.jsx
new file mode 100644
index 0000000000..25c3dc27e8
--- /dev/null
+++ b/src/modules/services/components/Picker.jsx
@@ -0,0 +1,45 @@
+import React from 'react'
+
+import { useClient } from 'cozy-client'
+import { makeSharingLink } from 'cozy-client/dist/models/sharing'
+
+import FilePicker from './FilePicker'
+
+const Picker = ({ service }) => {
+ const client = useClient()
+
+ const handleClick = async fileId => {
+ // TODO: check multiple sharing link issues
+ // const [shareLinkTTL, shareLinkPermanent] = await Promise.all([
+ // makeSharingLink(client, [fileId], { ttl: '5m' }),
+ // makeSharingLink(client, [fileId])
+ // ])
+
+ // "http://drive.alice.cozy.localhost:8080/public?sharecode=efiOchmwc9oU#/" }
+ // POST "http://alice.cozy.localhost:8080/files/downloads?Id=01c6d8cb446b41c3a71fe846bc0dcd46&Filename=La%20Suite%20documentation%20technique.pdf"
+ // avec en Bearer le sharecode
+ // GET "http://alice.cozy.localhost:8080/files/downloads/bd5db658030a6bd3/La%20Suite%20documentation%20technique.pdf?Dl=1"
+
+ const sharingLink = await makeSharingLink(client, [fileId])
+
+ // const url = new URL(sharingLink)
+ // const searchParams = new URLSearchParams(url.search)
+ // const sharecode = searchParams.get('sharecode')
+
+ // const publicClient = new CozyClient({
+ // cozyUrl: client.getStackClient().uri,
+ // token: sharecode,
+ // useCustomStore: true
+ // })
+
+ // const downloadLink = await publicClient
+ // .collection('io.cozy.files')
+ // .getDownloadLinkById(fileId, 'test.pdf')
+
+ service.terminate({ id: fileId, sharingLink })
+ }
+
+ return
+}
+
+export default Picker
diff --git a/src/targets/intents/index.jsx b/src/targets/intents/index.jsx
index c5048ddac5..c917a9b304 100644
--- a/src/targets/intents/index.jsx
+++ b/src/targets/intents/index.jsx
@@ -2,8 +2,9 @@
import 'cozy-ui/transpiled/react/stylesheet.css'
import 'cozy-ui/dist/cozy-ui.utils.min.css'
-import 'cozy-viewer/dist/stylesheet.css'
-import 'cozy-sharing/dist/stylesheet.css'
+// import 'cozy-ui-plus/dist/stylesheet.css'
+// import 'cozy-viewer/dist/stylesheet.css'
+// import 'cozy-sharing/dist/stylesheet.css'
import 'whatwg-fetch'
import React from 'react'