Repository navigation
WPB-29385: Preview moving selected Drive items #22767
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
Changes from all commits
cbc84c2
ff08f0b
d0dbb19
eb72897
431067e
85c4cfc
3e6d67c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /* | ||
| * Wire | ||
| * Copyright (C) 2026 Wire Swiss GmbH | ||
| * | ||
| * This program is free software: you can redistribute it and/or modify | ||
| * it under the terms of the GNU General Public License as published by | ||
| * the Free Software Foundation, either version 3 of the License, or | ||
| * (at your option) any later version. | ||
| * | ||
| * This program is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| * GNU General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU General Public License | ||
| * along with this program. If not, see http://www.gnu.org/licenses/. | ||
| * | ||
| */ | ||
|
|
||
| import {QualifiedId} from '@wireapp/api-client/lib/user/'; | ||
|
|
||
| import {CellsRepository} from 'Repositories/cells/cellsRepository'; | ||
| import {CellNode} from 'src/script/types/cellNode'; | ||
|
|
||
| import {CellsBulkMovePreviewPresentation} from './cellsBulkMovePreviewPresentation'; | ||
| import {useBulkMoveDestinationSelection} from './useBulkMoveDestinationSelection'; | ||
|
|
||
| interface CellsBulkMovePreviewModalProps { | ||
| isOpen: boolean; | ||
| onClose: () => void; | ||
| selectedNodes: CellNode[]; | ||
| cellsRepository: CellsRepository; | ||
| conversationQualifiedId: QualifiedId; | ||
| conversationName: string; | ||
| } | ||
|
|
||
| export const CellsBulkMovePreviewModal = ({ | ||
| isOpen, | ||
| onClose, | ||
| selectedNodes, | ||
| cellsRepository, | ||
| conversationQualifiedId, | ||
| conversationName, | ||
| }: CellsBulkMovePreviewModalProps) => { | ||
| const destination = useBulkMoveDestinationSelection({ | ||
| isOpen, | ||
| selectedNodes, | ||
| cellsRepository, | ||
| conversationQualifiedId, | ||
| }); | ||
|
|
||
| return ( | ||
| <CellsBulkMovePreviewPresentation | ||
| isOpen={isOpen} | ||
| onClose={onClose} | ||
| selectedNodes={selectedNodes} | ||
| conversationName={conversationName} | ||
| destination={destination} | ||
| /> | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| /* | ||
| * Wire | ||
| * Copyright (C) 2026 Wire Swiss GmbH | ||
| * | ||
| * This program is free software: you can redistribute it and/or modify | ||
| * it under the terms of the GNU General Public License as published by | ||
| * the Free Software Foundation, either version 3 of the License, or | ||
| * (at your option) any later version. | ||
| * | ||
| * This program is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| * GNU General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU General Public License | ||
| * along with this program. If not, see http://www.gnu.org/licenses/. | ||
| * | ||
| */ | ||
|
|
||
| import {CellsModal} from 'Components/conversation/conversationCells/common/cellsModal/cellsModal'; | ||
| import {useApplicationContext} from 'src/script/page/rootProvider'; | ||
| import {CellNode} from 'src/script/types/cellNode'; | ||
|
|
||
| import {BulkMoveDestinationSelection} from './useBulkMoveDestinationSelection'; | ||
|
|
||
| import {CellsFoldersListModalContent} from '../cellsTableColumns/cellsTableRowOptions/cellsMoveNodeModal/cellsFoldersListModalContent/cellsFoldersListModalContent'; | ||
|
|
||
| interface CellsBulkMovePreviewPresentationProps { | ||
| isOpen: boolean; | ||
| onClose: () => void; | ||
| selectedNodes: CellNode[]; | ||
| conversationName: string; | ||
| destination: BulkMoveDestinationSelection; | ||
| } | ||
|
|
||
| export const CellsBulkMovePreviewPresentation = ({ | ||
| isOpen, | ||
| onClose, | ||
| selectedNodes, | ||
| conversationName, | ||
| destination, | ||
| }: CellsBulkMovePreviewPresentationProps) => { | ||
| const {translate} = useApplicationContext(); | ||
|
|
||
| return ( | ||
| <CellsModal isOpen={isOpen} onClose={onClose} size="large"> | ||
| <CellsModal.Header>{translate('cells.bulkMove.previewTitle', {count: selectedNodes.length})}</CellsModal.Header> | ||
| <p>{translate('cells.bulkMove.previewDescription')}</p> | ||
| <section aria-labelledby="bulk-move-selected-items-heading"> | ||
| <h3 id="bulk-move-selected-items-heading">{translate('cells.bulkMove.selectedItems')}</h3> | ||
| <ul> | ||
| {selectedNodes.map(node => { | ||
| return <li key={node.id}>{node.name}</li>; | ||
| })} | ||
| </ul> | ||
| </section> | ||
| <p aria-live="polite"> | ||
| {translate('cells.bulkMove.currentDestination', {destination: destination.currentDestination})} | ||
| </p> | ||
| <CellsFoldersListModalContent | ||
| items={destination.folders} | ||
| status={destination.status} | ||
| shouldShowLoadingSpinner={destination.shouldShowLoadingSpinner} | ||
| conversationName={conversationName} | ||
| currentPath={destination.currentPath} | ||
| onPathChange={destination.handlePathChange} | ||
| onSelectDestination={destination.handleDestinationSelect} | ||
| selectedDestinationPath={destination.selectedDestinationPath} | ||
| disabledDestinationPaths={destination.disabledDestinationPaths} | ||
| disabledDestinationLabels={destination.disabledDestinationLabels} | ||
| /> | ||
| <CellsModal.Actions> | ||
| <CellsModal.SecondaryButton onClick={onClose}> | ||
| {translate('cells.moveNodeModal.cancelButton')} | ||
| </CellsModal.SecondaryButton> | ||
| </CellsModal.Actions> | ||
| </CellsModal> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| /* | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This entire hook here will be gone once we integrate with the moving API, for now this only give us a visual feedback on what's about to be moved |
||
| * Wire | ||
| * Copyright (C) 2026 Wire Swiss GmbH | ||
| * | ||
| * This program is free software: you can redistribute it and/or modify | ||
| * it under the terms of the GNU General Public License as published by | ||
| * the Free Software Foundation, either version 3 of the License, or | ||
| * (at your option) any later version. | ||
| * | ||
| * This program is distributed in the hope that it will be useful, | ||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| * GNU General Public License for more details. | ||
| * | ||
| * You should have received a copy of the GNU General Public License | ||
| * along with this program. If not, see http://www.gnu.org/licenses/. | ||
| * | ||
| */ | ||
|
|
||
| import {useMemo, useState} from 'react'; | ||
|
|
||
| import {QualifiedId} from '@wireapp/api-client/lib/user/'; | ||
|
|
||
| import {getCellsFilesPath} from 'Components/conversation/conversationCells/common/getCellsFilesPath/getCellsFilesPath'; | ||
| import {CellsRepository} from 'Repositories/cells/cellsRepository'; | ||
| import {useApplicationContext} from 'src/script/page/rootProvider'; | ||
| import {CellNode, CellNodeType} from 'src/script/types/cellNode'; | ||
|
|
||
| import {useGetCellsFolders} from '../cellsTableColumns/cellsTableRowOptions/cellsMoveNodeModal/useGetCellsFolders/useGetCellsFolders'; | ||
|
|
||
| interface UseBulkMoveDestinationSelectionProps { | ||
| isOpen: boolean; | ||
| selectedNodes: CellNode[]; | ||
| cellsRepository: CellsRepository; | ||
| conversationQualifiedId: QualifiedId; | ||
| } | ||
|
|
||
| export interface BulkMoveDestinationSelection { | ||
| currentDestination: string; | ||
| currentPath: string; | ||
| disabledDestinationLabels: Readonly<Record<string, string>>; | ||
| disabledDestinationPaths: readonly string[]; | ||
| folders: {id: string; name: string; path: string}[]; | ||
| handleDestinationSelect: (path: string) => void; | ||
| handlePathChange: (path: string) => void; | ||
| selectedDestinationPath: string; | ||
| shouldShowLoadingSpinner: boolean; | ||
| status: 'idle' | 'loading' | 'success' | 'error'; | ||
| } | ||
|
|
||
| const normalizeDestinationPath = (path: string, rootPath: string): string => { | ||
| const normalizedRootPath = rootPath.replace(/\/+$/, ''); | ||
| const normalizedPath = path.replace(/^\/+|\/+$/g, ''); | ||
| if (normalizedPath === normalizedRootPath) { | ||
| return ''; | ||
| } | ||
|
|
||
| if (normalizedRootPath.length > 0 && normalizedPath.startsWith(`${normalizedRootPath}/`)) { | ||
| return normalizedPath.slice(normalizedRootPath.length + 1); | ||
| } | ||
|
|
||
| return normalizedPath; | ||
| }; | ||
|
Comment on lines
+51
to
+63
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you check if we have similar function already in the code. |
||
|
|
||
| const getDestinationPath = (path: string, rootPath: string): string => { | ||
| const normalizedRootPath = rootPath.replace(/\/+$/, ''); | ||
| const relativePath = normalizeDestinationPath(path, rootPath); | ||
| return relativePath.length > 0 ? [normalizedRootPath, relativePath].filter(Boolean).join('/') : rootPath; | ||
| }; | ||
|
|
||
| export const useBulkMoveDestinationSelection = ({ | ||
| isOpen, | ||
| selectedNodes, | ||
| cellsRepository, | ||
| conversationQualifiedId, | ||
| }: UseBulkMoveDestinationSelectionProps): BulkMoveDestinationSelection => { | ||
| const {clock, fireAndForgetInvoker, translate} = useApplicationContext(); | ||
| const rootPath = getCellsFilesPath(); | ||
| const [currentPath, setCurrentPath] = useState(rootPath); | ||
| const [selectedDestinationPath, setSelectedDestinationPath] = useState(rootPath); | ||
| const {folders, status, shouldShowLoadingSpinner} = useGetCellsFolders({ | ||
| cellsRepository, | ||
| conversationQualifiedId, | ||
| currentPath, | ||
| enabled: isOpen, | ||
| fireAndForgetInvoker, | ||
| clock, | ||
| }); | ||
|
|
||
| const selectedFolderPaths = useMemo(() => { | ||
| return selectedNodes | ||
| .filter(node => { | ||
| return node.type === CellNodeType.FOLDER; | ||
| }) | ||
| .map(node => { | ||
| return node.path; | ||
| }); | ||
| }, [selectedNodes]); | ||
| const disabledDestinationPaths = useMemo(() => { | ||
| return [...selectedFolderPaths, currentPath]; | ||
| }, [selectedFolderPaths, currentPath]); | ||
| const disabledDestinationLabels = useMemo(() => { | ||
| return Object.fromEntries([ | ||
| ...selectedFolderPaths.map(path => { | ||
| return [path, translate('cells.bulkMove.selected')]; | ||
| }), | ||
| [currentPath, translate('cells.bulkMove.currentLocation')], | ||
| ]); | ||
| }, [selectedFolderPaths, currentPath, translate]); | ||
|
|
||
| const handlePathChange = (path: string): void => { | ||
| setCurrentPath(path); | ||
| setSelectedDestinationPath(getDestinationPath(path, rootPath)); | ||
| }; | ||
| const handleDestinationSelect = (path: string): void => { | ||
| setSelectedDestinationPath(getDestinationPath(path, rootPath)); | ||
| }; | ||
|
|
||
| const driveLabel = translate('cells.bulkMove.drive'); | ||
| const relativeDestinationPath = normalizeDestinationPath(selectedDestinationPath, rootPath); | ||
| const currentDestination = | ||
| relativeDestinationPath.length > 0 | ||
| ? `${driveLabel} / ${relativeDestinationPath.split('/').filter(Boolean).join(' / ')}` | ||
| : driveLabel; | ||
|
|
||
| return { | ||
| currentDestination, | ||
| currentPath, | ||
| disabledDestinationLabels, | ||
| disabledDestinationPaths, | ||
| folders, | ||
| handleDestinationSelect, | ||
| handlePathChange, | ||
| selectedDestinationPath, | ||
| shouldShowLoadingSpinner, | ||
| status, | ||
| }; | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nine out of ten props are passed through
destinationobject. Can we modifyCellsFoldersListModalContentto receive complete object?