From a43af46802569301f6cbbc18c8736d8363d682be Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 18 Jul 2024 15:10:34 +0200 Subject: [PATCH 1/9] refactor(MoreMenu): Convert into typescript --- src/components/{MoreMenu.jsx => MoreMenu.tsx} | 19 +++++++++++++------ .../nextcloud/components/NextcloudToolbar.jsx | 5 ++++- 2 files changed, 17 insertions(+), 7 deletions(-) rename src/components/{MoreMenu.jsx => MoreMenu.tsx} (70%) diff --git a/src/components/MoreMenu.jsx b/src/components/MoreMenu.tsx similarity index 70% rename from src/components/MoreMenu.jsx rename to src/components/MoreMenu.tsx index 4b40cc5b5b..d461713f6b 100644 --- a/src/components/MoreMenu.jsx +++ b/src/components/MoreMenu.tsx @@ -1,10 +1,17 @@ -import React, { useState, useCallback, useRef } from 'react' +import React, { useState, useCallback, useRef, RefObject, FC } from 'react' import ActionsMenu from 'cozy-ui/transpiled/react/ActionsMenu' +import { Action } from 'cozy-ui/transpiled/react/ActionsMenu/Actions' -import { BarRightOnMobile } from 'components/Bar' +import { File } from './FolderPicker/types' import MoreButton from 'components/Button/MoreButton' +interface MoreMenuProps { + actions: Record[] + docs?: File[] + disabled?: boolean +} + /** * Renders a MoreMenu component. * @@ -13,14 +20,14 @@ import MoreButton from 'components/Button/MoreButton' * @param disabled - Indicates whether the menu is disabled. * @returns The rendered MoreMenu component. */ -const MoreMenu = ({ actions, docs, disabled }) => { +const MoreMenu: FC = ({ actions, docs = [], disabled }) => { const [isMenuOpened, setMenuOpened] = useState(false) - const moreButtonRef = useRef(null) + const moreButtonRef: RefObject = useRef(null) const openMenu = useCallback(() => setMenuOpened(true), [setMenuOpened]) const closeMenu = useCallback(() => setMenuOpened(false), [setMenuOpened]) return ( - + <>
@@ -38,7 +45,7 @@ const MoreMenu = ({ actions, docs, disabled }) => { }} /> ) : null} -
+ ) } diff --git a/src/modules/nextcloud/components/NextcloudToolbar.jsx b/src/modules/nextcloud/components/NextcloudToolbar.jsx index 021dcf6e8e..e5ef8c3c11 100644 --- a/src/modules/nextcloud/components/NextcloudToolbar.jsx +++ b/src/modules/nextcloud/components/NextcloudToolbar.jsx @@ -12,6 +12,7 @@ import PlusIcon from 'cozy-ui/transpiled/react/Icons/Plus' import ShareIcon from 'cozy-ui/transpiled/react/Icons/Share' import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' +import { BarRightOnMobile } from 'components/Bar' import { MoreMenu } from 'components/MoreMenu' import { selectable } from 'modules/actions/selectable' import { addFolder } from 'modules/nextcloud/components/actions/addFolder' @@ -80,7 +81,9 @@ const NextcloudToolbar = () => { }} /> ) : null} - + + + ) } From 150d0e853282202b677937f482be128e0c9f7258 Mon Sep 17 00:00:00 2001 From: cballevre Date: Tue, 16 Jul 2024 19:08:58 +0200 Subject: [PATCH 2/9] refactor(selectable): Convert into typescript --- src/declarations.d.ts | 2 +- .../{selectable.jsx => components/selectable.tsx} | 13 +++++++++++-- .../nextcloud/components/NextcloudToolbar.jsx | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) rename src/modules/actions/{selectable.jsx => components/selectable.tsx} (73%) diff --git a/src/declarations.d.ts b/src/declarations.d.ts index 40e2faa103..76c58b17b9 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -90,7 +90,7 @@ declare module 'cozy-ui/transpiled/react/ActionsMenu/Actions' { action?: ( docs: import('cozy-client/types/types').IOCozyFile[], opts: { handleAction: HandleActionCallback } - ) => Promise + ) => Promise | void Component: ForwardRefExoticComponent> } diff --git a/src/modules/actions/selectable.jsx b/src/modules/actions/components/selectable.tsx similarity index 73% rename from src/modules/actions/selectable.jsx rename to src/modules/actions/components/selectable.tsx index e313b9fbdc..1ebc26ede7 100644 --- a/src/modules/actions/selectable.jsx +++ b/src/modules/actions/components/selectable.tsx @@ -1,12 +1,21 @@ import React, { forwardRef } from 'react' +import { Action } from 'cozy-ui/transpiled/react/ActionsMenu/Actions' import ActionsMenuItem from 'cozy-ui/transpiled/react/ActionsMenu/ActionsMenuItem' import Icon from 'cozy-ui/transpiled/react/Icon' import CheckSquareIcon from 'cozy-ui/transpiled/react/Icons/CheckSquare' import ListItemIcon from 'cozy-ui/transpiled/react/ListItemIcon' import ListItemText from 'cozy-ui/transpiled/react/ListItemText' -export const selectable = ({ t, showSelectionBar }) => { +interface selectableProps { + t: (key: string, options?: Record) => string + showSelectionBar: () => void +} + +export const selectable = ({ + t, + showSelectionBar +}: selectableProps): Action => { const label = t('toolbar.menu_select') const icon = CheckSquareIcon @@ -14,7 +23,7 @@ export const selectable = ({ t, showSelectionBar }) => { name: 'selectable', label, icon, - action: () => { + action: (): void => { showSelectionBar() }, Component: forwardRef(function Selectable(props, ref) { diff --git a/src/modules/nextcloud/components/NextcloudToolbar.jsx b/src/modules/nextcloud/components/NextcloudToolbar.jsx index e5ef8c3c11..67dffb3684 100644 --- a/src/modules/nextcloud/components/NextcloudToolbar.jsx +++ b/src/modules/nextcloud/components/NextcloudToolbar.jsx @@ -14,7 +14,7 @@ import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' import { BarRightOnMobile } from 'components/Bar' import { MoreMenu } from 'components/MoreMenu' -import { selectable } from 'modules/actions/selectable' +import { selectable } from 'modules/actions/components/selectable' import { addFolder } from 'modules/nextcloud/components/actions/addFolder' import { downloadNextcloudFolder } from 'modules/nextcloud/components/actions/downloadNextcloudFolder' import { openWithinNextcloud } from 'modules/nextcloud/components/actions/openWithinNextcloud' From 1e4cba7429b4a1337c4267a311306825684c22db Mon Sep 17 00:00:00 2001 From: cballevre Date: Thu, 18 Jul 2024 16:05:41 +0200 Subject: [PATCH 3/9] refactor(trash): Use route for empty trash confirmation modal This is preparatory work to share code between IOCozyFile routes and the Nextcloud routes. This also makes it easier to go backwards on Android. --- src/declarations.d.ts | 2 +- src/locales/en.json | 13 ++- src/locales/fr.json | 13 ++- src/modules/actions/utils.js | 16 --- src/modules/navigation/AppRoute.jsx | 17 +-- src/modules/trash/Toolbar.jsx | 102 ------------------ src/modules/trash/Toolbar.spec.jsx | 53 --------- .../trash/components/EmptyTrashConfirm.jsx | 54 ---------- .../trash/components/EmptyTrashConfirm.tsx | 92 ++++++++++++++++ .../trash/components/TrashToolbar.spec.jsx | 45 ++++++++ src/modules/trash/components/TrashToolbar.tsx | 62 +++++++++++ .../trash/components/actions/emptyTrash.tsx | 40 +++++++ src/modules/views/Trash/TrashEmptyView.tsx | 24 +++++ src/modules/views/Trash/TrashFolderView.jsx | 2 +- 14 files changed, 291 insertions(+), 244 deletions(-) delete mode 100644 src/modules/trash/Toolbar.jsx delete mode 100644 src/modules/trash/Toolbar.spec.jsx delete mode 100644 src/modules/trash/components/EmptyTrashConfirm.jsx create mode 100644 src/modules/trash/components/EmptyTrashConfirm.tsx create mode 100644 src/modules/trash/components/TrashToolbar.spec.jsx create mode 100644 src/modules/trash/components/TrashToolbar.tsx create mode 100644 src/modules/trash/components/actions/emptyTrash.tsx create mode 100644 src/modules/views/Trash/TrashEmptyView.tsx diff --git a/src/declarations.d.ts b/src/declarations.d.ts index 76c58b17b9..520f989261 100644 --- a/src/declarations.d.ts +++ b/src/declarations.d.ts @@ -99,7 +99,7 @@ declare module 'cozy-ui/transpiled/react/ActionsMenu/Actions' { export function makeActions( arg1: ((props?: T) => Action)[], T - ): Record Action> + ): Record[] } declare module 'cozy-sharing' { diff --git a/src/locales/en.json b/src/locales/en.json index dbda50ea68..6318ccd20f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -56,7 +56,6 @@ "menu_open_cozy": "Open in my Cozy", "menu_create_note": "Note", "menu_create_shortcut": "Shortcut", - "empty_trash": "Empty trash", "share": "Share", "trash": "Remove", "leave": "Leave shared folder & delete it", @@ -279,12 +278,15 @@ "cancel": "Cancel", "delete": "Remove" }, - "emptytrashconfirmation": { + "EmptyTrashConfirm": { "title": "Permanently delete?", "forbidden": "You won't be able to access these files anymore.", "restore": "You won't be able to restore these files if you didn't make a backup.", "cancel": "Cancel", - "delete": "Delete all" + "delete": "Delete all", + "processing": "Your trash is being emptied. This might take a few moments.", + "success": "The trash has been emptied.", + "error": "An error occurred, please try again." }, "DestroyConfirm": { "title": "Delete %{filename}? |||| Delete %{smart_count} %{type}?", @@ -332,8 +334,6 @@ "restore_file_success": "The selection has been successfully restored.", "trash_file_success": "The selection has been moved to the Trash.", "destroy_file_success": "The selection has been deleted permanently.", - "empty_trash_progress": "Your trash is being emptied. This might take a few moments.", - "empty_trash_success": "The trash has been emptied.", "folder_name": "The element %{folderName} already exists, please choose a new name.", "file_name": "The element %{fileName} already exists, please choose a new name.", "file_name_missing": "The file name is missing, please choose a new name.", @@ -828,5 +828,8 @@ "add": "%{filename} has been added to favorites |||| These items have been added to favorites", "remove": "%{filename} has been removed from favorites |||| These items have been removed from favorites" } + }, + "TrashToolbar": { + "emptyTrash": "Empty trash" } } diff --git a/src/locales/fr.json b/src/locales/fr.json index f2d94f90da..ea5cf761dd 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -56,7 +56,6 @@ "menu_open_cozy": "Ouvrir dans mon Cozy", "menu_create_note": "Note", "menu_create_shortcut": "Raccourci", - "empty_trash": "Vider la corbeille", "share": "Partager", "trash": "Supprimer", "leave": "Quitter le dossier partagé et le supprimer", @@ -280,12 +279,15 @@ "cancel": "Annuler", "delete": "Supprimer" }, - "emptytrashconfirmation": { + "EmptyTrashConfirm": { "title": "Supprimer définitivement ?", "forbidden": "Vous ne pourrez plus accéder à ces fichiers.", "restore": "Vous ne pourrez pas restaurer ces fichiers.", "cancel": "Annuler", - "delete": "Tout supprimer" + "delete": "Tout supprimer", + "processing": "Votre corbeille est en train de se vider. Cela peut prendre quelques instants.", + "success": "La corbeille a été vidée.", + "error": "Une erreur est survenue, merci de réessayer." }, "DestroyConfirm": { "title": "Supprimer %{filename} ? |||| Supprimer %{smart_count} %{type} ?", @@ -333,8 +335,6 @@ "restore_file_success": "La sélection a été restaurée avec succès.", "trash_file_success": "La sélection a été déplacée dans la Corbeille.", "destroy_file_success": "La sélection a été supprimée définitivement.", - "empty_trash_progress": " Votre corbeille est en train de se vider. Cela peut prendre quelques instants.", - "empty_trash_success": "La corbeille a été vidée.", "folder_name": "L'élément %{folderName} existe déjà, merci de choisir un nouveau nom.", "file_name": "L'élément %{fileName} existe déjà, utilisez un nouveau nom", "file_name_missing": "Le nom du fichier est manquant, veuillez choisir un nouveau nom.", @@ -829,5 +829,8 @@ "add": "%{filename} a été ajouté aux favoris |||| Ces éléments ont été ajoutés aux favoris", "remove": "%{filename} a été retiré des favoris |||| Ces éléments ont été retirés des favoris" } + }, + "TrashToolbar": { + "emptyTrash": "Vider la corbeille" } } diff --git a/src/modules/actions/utils.js b/src/modules/actions/utils.js index 1f18f05051..9482ce37e7 100644 --- a/src/modules/actions/utils.js +++ b/src/modules/actions/utils.js @@ -126,19 +126,3 @@ export const deleteFilesPermanently = async (client, files) => { await client.collection(DOCTYPE_FILES).deleteFilePermanently(file.id) } } - -export const emptyTrash = async (client, { showAlert, t }) => { - showAlert({ - message: t('alert.empty_trash_progress'), - severity: 'secondary' - }) - try { - await client.collection(DOCTYPE_FILES).emptyTrash() - } catch (err) { - showAlert({ message: t('alert.try_again'), severity: 'error' }) - } - showAlert({ - message: t('alert.empty_trash_success'), - severity: 'secondary' - }) -} diff --git a/src/modules/navigation/AppRoute.jsx b/src/modules/navigation/AppRoute.jsx index abc3cc2258..b7221b9c00 100644 --- a/src/modules/navigation/AppRoute.jsx +++ b/src/modules/navigation/AppRoute.jsx @@ -19,7 +19,7 @@ import SharingsFolderView from '../views/Sharings/SharingsFolderView' import FilesViewerTrash from '../views/Trash/FilesViewerTrash' import TrashFolderView from '../views/Trash/TrashFolderView' import FileHistory from 'components/FileHistory' -import { ROOT_DIR_ID } from 'constants/config' +import { ROOT_DIR_ID, TRASH_DIR_ID } from 'constants/config' import { UploaderComponent } from 'modules//views/Upload/UploaderComponent' import Layout from 'modules/layout/Layout' import FileOpenerExternal from 'modules/viewer/FileOpenerExternal' @@ -33,6 +33,7 @@ import { ShareFileView } from 'modules/views/Modal/ShareFileView' import { NextcloudDeleteView } from 'modules/views/Nextcloud/NextcloudDeleteView' import { NextcloudFolderView } from 'modules/views/Nextcloud/NextcloudFolderView' import { NextcloudMoveView } from 'modules/views/Nextcloud/NextcloudMoveView' +import { TrashEmptyView } from 'modules/views/Trash/TrashEmptyView' const FilesRedirect = () => { const { folderId } = useParams() @@ -88,12 +89,14 @@ const AppRoute = () => ( } /> - - } /> - }> - } /> - } /> - + } + /> + }> + } /> + } /> + } /> diff --git a/src/modules/trash/Toolbar.jsx b/src/modules/trash/Toolbar.jsx deleted file mode 100644 index a7db6a9b08..0000000000 --- a/src/modules/trash/Toolbar.jsx +++ /dev/null @@ -1,102 +0,0 @@ -import React, { useState, useCallback } from 'react' - -import { BarRight } from 'cozy-bar' -import { useClient } from 'cozy-client' -import Button from 'cozy-ui/transpiled/react/Buttons' -import Icon from 'cozy-ui/transpiled/react/Icon' -import TrashIcon from 'cozy-ui/transpiled/react/Icons/Trash' -import ActionMenu, { - ActionMenuItem -} from 'cozy-ui/transpiled/react/deprecated/ActionMenu' -import { useAlert } from 'cozy-ui/transpiled/react/providers/Alert' -import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints' -import { useI18n } from 'cozy-ui/transpiled/react/providers/I18n' - -import EmptyTrashConfirm from './components/EmptyTrashConfirm' -import SelectableItem from '../drive/Toolbar/selectable/SelectableItem' -import { MoreButton } from 'components/Button' -import { useModalContext } from 'lib/ModalContext' -import { emptyTrash } from 'modules/actions/utils' -import SearchButton from 'modules/drive/Toolbar/components/SearchButton' -import { useSelectionContext } from 'modules/selection/SelectionProvider' - -import styles from 'styles/toolbar.styl' - -export const Toolbar = ({ disabled }) => { - const { t } = useI18n() - const { showAlert } = useAlert() - const { isMobile } = useBreakpoints() - const client = useClient() - const [menuIsVisible, setMenuVisible] = useState(false) - const anchorRef = React.createRef() - const openMenu = useCallback(() => setMenuVisible(true), [setMenuVisible]) - const closeMenu = useCallback(() => setMenuVisible(false), [setMenuVisible]) - - const { pushModal, popModal } = useModalContext() - const { showSelectionBar, isSelectionBarVisible } = useSelectionContext() - - const onEmptyTrash = useCallback(() => { - pushModal( - { - emptyTrash(client, { showAlert, t }) - }} - /> - ) - }, [pushModal, popModal, client, showAlert, t]) - - return ( -
- {isMobile ? ( - - -
- -
- {menuIsVisible && ( - - {isMobile && ( - <> - } - > - {t('toolbar.empty_trash')} - -
- - )} - -
- )} -
- ) : ( -
- ) -} - -export default Toolbar diff --git a/src/modules/trash/Toolbar.spec.jsx b/src/modules/trash/Toolbar.spec.jsx deleted file mode 100644 index a0099b637d..0000000000 --- a/src/modules/trash/Toolbar.spec.jsx +++ /dev/null @@ -1,53 +0,0 @@ -import { waitForElementToBeRemoved } from '@testing-library/dom' -import { render, fireEvent, act } from '@testing-library/react' -import React from 'react' - -import { createMockClient } from 'cozy-client' -import useBreakpoints from 'cozy-ui/transpiled/react/providers/Breakpoints' - -import { Toolbar } from './Toolbar' -import { ModalContextProvider, ModalStack } from 'lib/ModalContext' -import AppLike from 'test/components/AppLike' - -jest.mock('cozy-ui/transpiled/react/providers/Breakpoints', () => ({ - ...jest.requireActual('cozy-ui/transpiled/react/providers/Breakpoints'), - __esModule: true, - default: jest.fn(), - useBreakpoints: jest.fn() -})) - -describe('Toolbar', () => { - const client = createMockClient({}) - client.collection = jest.fn(() => client) - client.emptyTrash = jest.fn() - - it('asks for confirmation before emptying the trash', async () => { - // TODO: Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, - // interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...); - // However the above resolution makes test failing - jest.spyOn(console, 'error').mockImplementation() - useBreakpoints.mockReturnValue({ isMobile: false }) - - const { getByText } = render( - - - - - - - ) - - const emptyTrashButton = getByText('Empty trash') - act(() => { - fireEvent.click(emptyTrashButton) - }) - - const confirmButton = getByText('Delete all') - act(async () => { - await fireEvent.click(confirmButton) - }) - - expect(client.emptyTrash).toHaveBeenCalled() - await waitForElementToBeRemoved(() => getByText('Delete all')) - }) -}) diff --git a/src/modules/trash/components/EmptyTrashConfirm.jsx b/src/modules/trash/components/EmptyTrashConfirm.jsx deleted file mode 100644 index b6fdb7c413..0000000000 --- a/src/modules/trash/components/EmptyTrashConfirm.jsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useState } from 'react' - -import { ConfirmDialog } from 'cozy-ui/transpiled/react/CozyDialogs' -import Stack from 'cozy-ui/transpiled/react/Stack' -import Button from 'cozy-ui/transpiled/react/deprecated/Button' -import { translate } from 'cozy-ui/transpiled/react/providers/I18n' - -import { Message } from 'modules/confirm/Message' -const EmptyTrashConfirm = ({ t, onConfirm, onClose }) => { - const [isLoading, setIsLoading] = useState(false) - return ( - - - - - } - actions={ - <> -