diff --git a/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx b/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx index 20d441181..50a2005ca 100644 --- a/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/app/assets/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -193,7 +193,10 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio <> - + = ({ await toggleAttachedFilesMenu() }, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal]) - const deleteFile = async (file: FileItem) => { - const shouldDelete = await confirmDialog({ - text: `Are you sure you want to permanently delete "${file.name}"?`, - confirmButtonStyle: 'danger', - }) - if (shouldDelete) { - const deletingToastId = addToast({ - type: ToastType.Loading, - message: `Deleting file "${file.name}"...`, - }) - await application.files.deleteFile(file) - addToast({ - type: ToastType.Success, - message: `Deleted file "${file.name}"`, - }) - dismissToast(deletingToastId) - } - } - - const downloadFile = async (file: FileItem) => { - viewControllerManager.filesController.downloadFile(file).catch(console.error) - } - const attachFileToNote = useCallback( async (file: FileItem) => { if (!note) { @@ -143,98 +118,6 @@ const AttachedFilesButton: FunctionComponent = ({ [application.items, note], ) - const detachFileFromNote = async (file: FileItem) => { - if (!note) { - addToast({ - type: ToastType.Error, - message: 'Could not attach file because selected note was deleted', - }) - return - } - await application.items.disassociateFileWithNote(file, note) - } - - const toggleFileProtection = async (file: FileItem) => { - let result: FileItem | undefined - if (file.protected) { - keepMenuOpen(true) - result = await application.mutator.unprotectFile(file) - keepMenuOpen(false) - buttonRef.current?.focus() - } else { - result = await application.mutator.protectFile(file) - } - const isProtected = result ? result.protected : file.protected - return isProtected - } - - const authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => { - const authorizedFiles = await application.protections.authorizeProtectedActionForItems([file], challengeReason) - const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file) - return isAuthorized - } - - const renameFile = async (file: FileItem, fileName: string) => { - await application.items.renameFile(file, fileName) - } - - const handleFileAction = async (action: PopoverFileItemAction) => { - const file = action.type !== PopoverFileItemActionType.RenameFile ? action.payload : action.payload.file - let isAuthorizedForAction = true - - if (file.protected && action.type !== PopoverFileItemActionType.ToggleFileProtection) { - keepMenuOpen(true) - isAuthorizedForAction = await authorizeProtectedActionForFile(file, ChallengeReason.AccessProtectedFile) - keepMenuOpen(false) - buttonRef.current?.focus() - } - - if (!isAuthorizedForAction) { - return false - } - - switch (action.type) { - case PopoverFileItemActionType.AttachFileToNote: - await attachFileToNote(file) - break - case PopoverFileItemActionType.DetachFileToNote: - await detachFileFromNote(file) - break - case PopoverFileItemActionType.DeleteFile: - await deleteFile(file) - break - case PopoverFileItemActionType.DownloadFile: - await downloadFile(file) - break - case PopoverFileItemActionType.ToggleFileProtection: { - const isProtected = await toggleFileProtection(file) - action.callback(isProtected) - break - } - case PopoverFileItemActionType.RenameFile: - await renameFile(file, action.payload.name) - break - case PopoverFileItemActionType.PreviewFile: { - keepMenuOpen(true) - const otherFiles = currentTab === PopoverTabs.AllFiles ? allFiles : attachedFiles - viewControllerManager.filePreviewModalController.activate( - file, - otherFiles.filter((file) => !file.protected), - ) - break - } - } - - if ( - action.type !== PopoverFileItemActionType.DownloadFile && - action.type !== PopoverFileItemActionType.PreviewFile - ) { - application.sync.sync().catch(console.error) - } - - return true - } - const [isDraggingFiles, setIsDraggingFiles] = useState(false) const dragCounter = useRef(0) @@ -400,12 +283,11 @@ const AttachedFilesButton: FunctionComponent = ({ {open && ( diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx index 12197f05a..a827a0fc3 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx @@ -1,6 +1,5 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { WebApplication } from '@/Application/Application' -import { ViewControllerManager } from '@/Services/ViewControllerManager' import { FileItem } from '@standardnotes/snjs' import { FilesIllustration } from '@standardnotes/icons' import { observer } from 'mobx-react-lite' @@ -8,29 +7,28 @@ import { Dispatch, FunctionComponent, SetStateAction, useRef, useState } from 'r import Button from '@/Components/Button/Button' import Icon from '@/Components/Icon/Icon' import PopoverFileItem from './PopoverFileItem' -import { PopoverFileItemAction, PopoverFileItemActionType } from './PopoverFileItemAction' +import { PopoverFileItemActionType } from './PopoverFileItemAction' import { PopoverTabs } from './PopoverTabs' +import { FilesController } from '@/Controllers/FilesController' type Props = { application: WebApplication - viewControllerManager: ViewControllerManager + filesController: FilesController allFiles: FileItem[] attachedFiles: FileItem[] closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void currentTab: PopoverTabs - handleFileAction: (action: PopoverFileItemAction) => Promise isDraggingFiles: boolean setCurrentTab: Dispatch> } const AttachedFilesPopover: FunctionComponent = ({ application, - viewControllerManager, + filesController, allFiles, attachedFiles, closeOnBlur, currentTab, - handleFileAction, isDraggingFiles, setCurrentTab, }) => { @@ -45,20 +43,31 @@ const AttachedFilesPopover: FunctionComponent = ({ : filesList const handleAttachFilesClick = async () => { - const uploadedFiles = await viewControllerManager.filesController.uploadNewFile() + const uploadedFiles = await filesController.uploadNewFile() if (!uploadedFiles) { return } if (currentTab === PopoverTabs.AttachedFiles) { uploadedFiles.forEach((file) => { - handleFileAction({ - type: PopoverFileItemActionType.AttachFileToNote, - payload: file, - }).catch(console.error) + filesController + .handleFileAction({ + type: PopoverFileItemActionType.AttachFileToNote, + payload: { file }, + }) + .catch(console.error) }) } } + const previewHandler = (file: FileItem) => { + filesController + .handleFileAction({ + type: PopoverFileItemActionType.PreviewFile, + payload: { file, otherFiles: currentTab === PopoverTabs.AllFiles ? allFiles : attachedFiles }, + }) + .catch(console.error) + } + return (
= ({ key={file.uuid} file={file} isAttachedToNote={attachedFiles.includes(file)} - handleFileAction={handleFileAction} + handleFileAction={filesController.handleFileAction} getIconType={application.iconsController.getIconForFileType} closeOnBlur={closeOnBlur} + previewHandler={previewHandler} /> ) }) diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx index 5715ce576..0c6a0982d 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx @@ -2,7 +2,15 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { KeyboardKey } from '@/Services/IOService' import { formatSizeToReadableString } from '@standardnotes/filepicker' import { FileItem } from '@standardnotes/snjs' -import { FormEventHandler, FunctionComponent, KeyboardEventHandler, useEffect, useRef, useState } from 'react' +import { + FormEventHandler, + FunctionComponent, + KeyboardEventHandler, + useCallback, + useEffect, + useRef, + useState, +} from 'react' import Icon from '@/Components/Icon/Icon' import { PopoverFileItemActionType } from './PopoverFileItemAction' import PopoverFileSubmenu from './PopoverFileSubmenu' @@ -15,6 +23,7 @@ const PopoverFileItem: FunctionComponent = ({ handleFileAction, getIconType, closeOnBlur, + previewHandler, }) => { const [fileName, setFileName] = useState(file.name) const [isRenamingFile, setIsRenamingFile] = useState(false) @@ -27,37 +36,48 @@ const PopoverFileItem: FunctionComponent = ({ } }, [isRenamingFile]) - const renameFile = async (file: FileItem, name: string) => { - await handleFileAction({ - type: PopoverFileItemActionType.RenameFile, - payload: { - file, - name, - }, - }) - setIsRenamingFile(false) - } + const renameFile = useCallback( + async (file: FileItem, name: string) => { + if (name.length < 1) { + return + } - const handleFileNameInput: FormEventHandler = (event) => { + await handleFileAction({ + type: PopoverFileItemActionType.RenameFile, + payload: { + file, + name, + }, + }) + setIsRenamingFile(false) + }, + [handleFileAction], + ) + + const handleFileNameInput: FormEventHandler = useCallback((event) => { setFileName((event.target as HTMLInputElement).value) - } + }, []) - const handleFileNameInputKeyDown: KeyboardEventHandler = (event) => { - if (event.key === KeyboardKey.Enter) { - itemRef.current?.focus() - } - } + const handleFileNameInputKeyDown: KeyboardEventHandler = useCallback( + (event) => { + if (fileName.length > 0 && event.key === KeyboardKey.Enter) { + itemRef.current?.focus() + } + }, + [fileName.length], + ) - const handleFileNameInputBlur = () => { + const handleFileNameInputBlur = useCallback(() => { renameFile(file, fileName).catch(console.error) - } + }, [file, fileName, renameFile]) - const clickPreviewHandler = () => { - handleFileAction({ - type: PopoverFileItemActionType.PreviewFile, - payload: file, - }).catch(console.error) - } + const handleClick = useCallback(() => { + if (isRenamingFile) { + return + } + + previewHandler(file) + }, [file, isRenamingFile, previewHandler]) return (
= ({ className="flex items-center justify-between p-3 focus:shadow-none" tabIndex={FOCUSABLE_BUT_NOT_TABBABLE} > -
+
{getFileIconComponent(getIconType(file.mimeType), 'w-8 h-8 flex-shrink-0')}
{isRenamingFile ? ( @@ -97,7 +117,7 @@ const PopoverFileItem: FunctionComponent = ({ handleFileAction={handleFileAction} setIsRenamingFile={setIsRenamingFile} closeOnBlur={closeOnBlur} - previewHandler={clickPreviewHandler} + previewHandler={previewHandler} />
) diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemAction.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemAction.tsx index b1e9368e6..4a0a5ec7c 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemAction.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemAction.tsx @@ -14,13 +14,19 @@ export type PopoverFileItemAction = | { type: Exclude< PopoverFileItemActionType, - PopoverFileItemActionType.RenameFile | PopoverFileItemActionType.ToggleFileProtection + | PopoverFileItemActionType.RenameFile + | PopoverFileItemActionType.ToggleFileProtection + | PopoverFileItemActionType.PreviewFile > - payload: FileItem + payload: { + file: FileItem + } } | { type: PopoverFileItemActionType.ToggleFileProtection - payload: FileItem + payload: { + file: FileItem + } callback: (isProtected: boolean) => void } | { @@ -30,3 +36,10 @@ export type PopoverFileItemAction = name: string } } + | { + type: PopoverFileItemActionType.PreviewFile + payload: { + file: FileItem + otherFiles: FileItem[] + } + } diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemProps.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemProps.tsx index 35cc40679..4f55f8b72 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemProps.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileItemProps.tsx @@ -1,10 +1,21 @@ import { IconType, FileItem } from '@standardnotes/snjs' +import { Dispatch, SetStateAction } from 'react' import { PopoverFileItemAction } from './PopoverFileItemAction' -export type PopoverFileItemProps = { +type CommonProps = { file: FileItem isAttachedToNote: boolean - handleFileAction: (action: PopoverFileItemAction) => Promise - getIconType(type: string): IconType + handleFileAction: (action: PopoverFileItemAction) => Promise<{ + didHandleAction: boolean + }> closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void + previewHandler: (file: FileItem) => void +} + +export type PopoverFileItemProps = CommonProps & { + getIconType(type: string): IconType +} + +export type PopoverFileSubmenuProps = CommonProps & { + setIsRenamingFile: Dispatch> } diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx index 29d055f2b..6622dcf43 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx @@ -1,19 +1,14 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { calculateSubmenuStyle, SubmenuStyle } from '@/Utils/CalculateSubmenuStyle' import { Disclosure, DisclosureButton, DisclosurePanel } from '@reach/disclosure' -import { Dispatch, FunctionComponent, SetStateAction, useCallback, useEffect, useRef, useState } from 'react' +import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' import Icon from '@/Components/Icon/Icon' import Switch from '@/Components/Switch/Switch' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' -import { PopoverFileItemProps } from './PopoverFileItemProps' +import { PopoverFileSubmenuProps } from './PopoverFileItemProps' import { PopoverFileItemActionType } from './PopoverFileItemAction' -type Props = Omit & { - setIsRenamingFile: Dispatch> - previewHandler: () => void -} - -const PopoverFileSubmenu: FunctionComponent = ({ +const PopoverFileSubmenu: FunctionComponent = ({ file, isAttachedToNote, handleFileAction, @@ -88,7 +83,7 @@ const PopoverFileSubmenu: FunctionComponent = ({ onBlur={closeOnBlur} className="sn-dropdown-item focus:bg-info-backdrop" onClick={() => { - previewHandler() + previewHandler(file) closeMenu() }} > @@ -102,7 +97,7 @@ const PopoverFileSubmenu: FunctionComponent = ({ onClick={() => { handleFileAction({ type: PopoverFileItemActionType.DetachFileToNote, - payload: file, + payload: { file }, }).catch(console.error) closeMenu() }} @@ -117,7 +112,7 @@ const PopoverFileSubmenu: FunctionComponent = ({ onClick={() => { handleFileAction({ type: PopoverFileItemActionType.AttachFileToNote, - payload: file, + payload: { file }, }).catch(console.error) closeMenu() }} @@ -132,7 +127,7 @@ const PopoverFileSubmenu: FunctionComponent = ({ onClick={() => { handleFileAction({ type: PopoverFileItemActionType.ToggleFileProtection, - payload: file, + payload: { file }, callback: (isProtected: boolean) => { setIsFileProtected(isProtected) }, @@ -157,7 +152,7 @@ const PopoverFileSubmenu: FunctionComponent = ({ onClick={() => { handleFileAction({ type: PopoverFileItemActionType.DownloadFile, - payload: file, + payload: { file }, }).catch(console.error) closeMenu() }} @@ -181,7 +176,7 @@ const PopoverFileSubmenu: FunctionComponent = ({ onClick={() => { handleFileAction({ type: PopoverFileItemActionType.DeleteFile, - payload: file, + payload: { file }, }).catch(console.error) closeMenu() }} diff --git a/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx b/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx index 8861bd965..ecd639ac1 100644 --- a/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx +++ b/app/assets/javascripts/Components/FileContextMenu/FileContextMenu.tsx @@ -1,20 +1,19 @@ import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants' +import { FilesController } from '@/Controllers/FilesController' +import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { useCloseOnBlur } from '@/Hooks/useCloseOnBlur' import { useCloseOnClickOutside } from '@/Hooks/useCloseOnClickOutside' -import { ViewControllerManager } from '@/Services/ViewControllerManager' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' -import { PopoverFileItemAction } from '../AttachedFilesPopover/PopoverFileItemAction' -import { PopoverTabs } from '../AttachedFilesPopover/PopoverTabs' import FileMenuOptions from './FileMenuOptions' type Props = { - viewControllerManager: ViewControllerManager + filesController: FilesController + selectionController: SelectedItemsController } -const FileContextMenu: FunctionComponent = observer(({ viewControllerManager }) => { - const { selectedFiles, showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = - viewControllerManager.filesController +const FileContextMenu: FunctionComponent = observer(({ filesController, selectionController }) => { + const { showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = filesController const [contextMenuStyle, setContextMenuStyle] = useState({ top: 0, @@ -24,9 +23,7 @@ const FileContextMenu: FunctionComponent = observer(({ viewControllerMana const [contextMenuMaxHeight, setContextMenuMaxHeight] = useState('auto') const contextMenuRef = useRef(null) const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => setShowFileContextMenu(open)) - useCloseOnClickOutside(contextMenuRef, () => viewControllerManager.filesController.setShowFileContextMenu(false)) - - const selectedFile = selectedFiles[0] + useCloseOnClickOutside(contextMenuRef, () => filesController.setShowFileContextMenu(false)) const reloadContextMenuLayout = useCallback(() => { const { clientHeight } = document.documentElement @@ -86,17 +83,6 @@ const FileContextMenu: FunctionComponent = observer(({ viewControllerMana } }, [reloadContextMenuLayout]) - const handleFileAction = useCallback( - async (action: PopoverFileItemAction) => { - const { didHandleAction } = await viewControllerManager.filesController.handleFileAction( - action, - PopoverTabs.AllFiles, - ) - return didHandleAction - }, - [viewControllerManager.filesController], - ) - return (
= observer(({ viewControllerMana }} > setShowFileContextMenu(false)} shouldShowRenameOption={false} @@ -120,8 +106,9 @@ const FileContextMenu: FunctionComponent = observer(({ viewControllerMana FileContextMenu.displayName = 'FileContextMenu' -const FileContextMenuWrapper: FunctionComponent = ({ viewControllerManager }) => { - const { selectedFiles, showFileContextMenu } = viewControllerManager.filesController +const FileContextMenuWrapper: FunctionComponent = ({ filesController, selectionController }) => { + const { showFileContextMenu } = filesController + const { selectedFiles } = selectionController const selectedFile = selectedFiles[0] @@ -129,7 +116,7 @@ const FileContextMenuWrapper: FunctionComponent = ({ viewControllerManage return null } - return + return } export default observer(FileContextMenuWrapper) diff --git a/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx b/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx index 136312de4..d129174ce 100644 --- a/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx +++ b/app/assets/javascripts/Components/FileContextMenu/FileMenuOptions.tsx @@ -1,16 +1,17 @@ +import { FunctionComponent, useCallback, useMemo } from 'react' +import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' -import { FileItem } from '@standardnotes/snjs' -import { FunctionComponent } from 'react' -import { PopoverFileItemAction, PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction' import Icon from '@/Components/Icon/Icon' import Switch from '@/Components/Switch/Switch' +import { observer } from 'mobx-react-lite' +import { FilesController } from '@/Controllers/FilesController' +import { SelectedItemsController } from '@/Controllers/SelectedItemsController' type Props = { closeMenu: () => void closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void - file: FileItem - fileProtectionToggleCallback?: (isProtected: boolean) => void - handleFileAction: (action: PopoverFileItemAction) => Promise + filesController: FilesController + selectionController: SelectedItemsController isFileAttachedToNote?: boolean renameToggleCallback?: (isRenamingFile: boolean) => void shouldShowRenameOption: boolean @@ -20,72 +21,73 @@ type Props = { const FileMenuOptions: FunctionComponent = ({ closeMenu, closeOnBlur, - file, - fileProtectionToggleCallback, - handleFileAction, + filesController, + selectionController, isFileAttachedToNote, renameToggleCallback, shouldShowRenameOption, shouldShowAttachOption, }) => { + const { selectedFiles } = selectionController + const { handleFileAction } = filesController + + const hasProtectedFiles = useMemo(() => selectedFiles.some((file) => file.protected), [selectedFiles]) + + const onPreview = useCallback(() => { + void handleFileAction({ + type: PopoverFileItemActionType.PreviewFile, + payload: { + file: selectedFiles[0], + otherFiles: selectedFiles.length > 1 ? selectedFiles : filesController.allFiles, + }, + }) + closeMenu() + }, [closeMenu, filesController.allFiles, handleFileAction, selectedFiles]) + + const onDetach = useCallback(() => { + const file = selectedFiles[0] + void handleFileAction({ + type: PopoverFileItemActionType.DetachFileToNote, + payload: { file }, + }) + closeMenu() + }, [closeMenu, handleFileAction, selectedFiles]) + + const onAttach = useCallback(() => { + const file = selectedFiles[0] + void handleFileAction({ + type: PopoverFileItemActionType.AttachFileToNote, + payload: { file }, + }) + closeMenu() + }, [closeMenu, handleFileAction, selectedFiles]) + return ( <> - - {isFileAttachedToNote ? ( - - ) : shouldShowAttachOption ? ( - - ) : null} + {selectedFiles.length === 1 && ( + <> + {isFileAttachedToNote ? ( + + ) : shouldShowAttachOption ? ( + + ) : null} + + )}
+
+
+ ) +} + +export default observer(MultipleSelectedFiles) diff --git a/app/assets/javascripts/Components/NoteGroupView/NoteGroupView.tsx b/app/assets/javascripts/Components/NoteGroupView/NoteGroupView.tsx index ef09fcc27..0814233ab 100644 --- a/app/assets/javascripts/Components/NoteGroupView/NoteGroupView.tsx +++ b/app/assets/javascripts/Components/NoteGroupView/NoteGroupView.tsx @@ -3,10 +3,12 @@ import { PureComponent } from '@/Components/Abstract/PureComponent' import { WebApplication } from '@/Application/Application' import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes' import NoteView from '@/Components/NoteView/NoteView' +import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFiles' import { ElementIds } from '@/Constants/ElementIDs' type State = { showMultipleSelectedNotes: boolean + showMultipleSelectedFiles: boolean controllers: NoteViewController[] } @@ -21,6 +23,7 @@ class NoteGroupView extends PureComponent { super(props, props.application) this.state = { showMultipleSelectedNotes: false, + showMultipleSelectedFiles: false, controllers: [], } } @@ -37,11 +40,21 @@ class NoteGroupView extends PureComponent { }) this.autorun(() => { + if (!this.viewControllerManager) { + return + } + if (this.viewControllerManager && this.viewControllerManager.notesController) { this.setState({ showMultipleSelectedNotes: this.viewControllerManager.notesController.selectedNotesCount > 1, }) } + + if (this.viewControllerManager.selectionController) { + this.setState({ + showMultipleSelectedFiles: this.viewControllerManager.selectionController.selectedFilesCount > 1, + }) + } }) } @@ -59,6 +72,13 @@ class NoteGroupView extends PureComponent { )} + {this.state.showMultipleSelectedFiles && ( + + )} + {!this.state.showMultipleSelectedNotes && ( <> {this.state.controllers.map((controller) => { diff --git a/app/assets/javascripts/Constants/Strings.ts b/app/assets/javascripts/Constants/Strings.ts index cf78fec9b..48c7aaec7 100644 --- a/app/assets/javascripts/Constants/Strings.ts +++ b/app/assets/javascripts/Constants/Strings.ts @@ -106,9 +106,11 @@ export const Strings = { protectingNoteWithoutProtectionSources: 'Access to this note will not be restricted until you set up a passcode or account.', openAccountMenu: 'Open Account Menu', - trashNotesTitle: 'Move to Trash', + trashItemsTitle: 'Move to Trash', trashNotesText: 'Are you sure you want to move these notes to the trash?', + trashFilesText: 'Are you sure you want to move these files to the trash?', enterPasscode: 'Please enter a passcode.', + deleteMultipleFiles: 'Are you sure you want to permanently delete these files?', } export const StringUtils = { @@ -139,6 +141,9 @@ export const StringUtils = { : 'Are you sure you want to move these notes to the trash?' } }, + deleteFile(title: string): string { + return `Are you sure you want to permanently delete ${title}?` + }, archiveLockedNotesAttempt(archive: boolean, notesCount = 1): string { const archiveString = archive ? 'archive' : 'unarchive' return notesCount === 1 diff --git a/app/assets/javascripts/Controllers/FilesController.ts b/app/assets/javascripts/Controllers/FilesController.ts index d09abd878..c58895702 100644 --- a/app/assets/javascripts/Controllers/FilesController.ts +++ b/app/assets/javascripts/Controllers/FilesController.ts @@ -3,9 +3,9 @@ import { PopoverFileItemAction, PopoverFileItemActionType, } from '@/Components/AttachedFilesPopover/PopoverFileItemAction' -import { PopoverTabs } from '@/Components/AttachedFilesPopover/PopoverTabs' import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants' import { confirmDialog } from '@/Services/AlertService' +import { Strings, StringUtils } from '@/Constants/Strings' import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays' import { ClassicFileReader, @@ -16,11 +16,10 @@ import { } from '@standardnotes/filepicker' import { ChallengeReason, ClientDisplayableError, ContentType, FileItem, InternalEventBus } from '@standardnotes/snjs' import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/stylekit' -import { action, computed, makeObservable, observable, reaction } from 'mobx' +import { action, makeObservable, observable, reaction } from 'mobx' import { WebApplication } from '../Application/Application' import { AbstractViewController } from './Abstract/AbstractViewController' import { NotesController } from './NotesController' -import { SelectedItemsController } from './SelectedItemsController' const UnprotectedFileActions = [PopoverFileItemActionType.ToggleFileProtection] const NonMutatingFileActions = [PopoverFileItemActionType.DownloadFile, PopoverFileItemActionType.PreviewFile] @@ -36,14 +35,12 @@ export class FilesController extends AbstractViewController { override deinit(): void { super.deinit() ;(this.notesController as unknown) = undefined - ;(this.selectionController as unknown) = undefined ;(this.filePreviewModalController as unknown) = undefined } constructor( application: WebApplication, private notesController: NotesController, - private selectionController: SelectedItemsController, private filePreviewModalController: FilePreviewModalController, eventBus: InternalEventBus, ) { @@ -55,8 +52,6 @@ export class FilesController extends AbstractViewController { showFileContextMenu: observable, fileContextMenuLocation: observable, - selectedFiles: computed, - reloadAllFiles: action, reloadAttachedFiles: action, setShowFileContextMenu: action, @@ -80,10 +75,6 @@ export class FilesController extends AbstractViewController { ) } - get selectedFiles(): FileItem[] { - return this.selectionController.getSelectedItems(ContentType.File) - } - setShowFileContextMenu = (enabled: boolean) => { this.showFileContextMenu = enabled } @@ -170,11 +161,10 @@ export class FilesController extends AbstractViewController { handleFileAction = async ( action: PopoverFileItemAction, - currentTab: PopoverTabs, ): Promise<{ didHandleAction: boolean }> => { - const file = action.type !== PopoverFileItemActionType.RenameFile ? action.payload : action.payload.file + const file = action.payload.file let isAuthorizedForAction = true const requiresAuthorization = file.protected && !UnprotectedFileActions.includes(action.type) @@ -211,10 +201,7 @@ export class FilesController extends AbstractViewController { await this.renameFile(file, action.payload.name) break case PopoverFileItemActionType.PreviewFile: - this.filePreviewModalController.activate( - file, - currentTab === PopoverTabs.AllFiles ? this.allFiles : this.attachedFiles, - ) + this.filePreviewModalController.activate(file, action.payload.otherFiles) break } @@ -399,4 +386,31 @@ export class FilesController extends AbstractViewController { return undefined } + + deleteFilesPermanently = async (files: FileItem[]) => { + const title = Strings.trashItemsTitle + const text = files.length === 1 ? StringUtils.deleteFile(files[0].name) : Strings.deleteMultipleFiles + + if ( + await confirmDialog({ + title, + text, + confirmButtonStyle: 'danger', + }) + ) { + await Promise.all(files.map((file) => this.application.mutator.deleteItem(file))) + } + } + + setProtectionForFiles = async (protect: boolean, files: FileItem[]) => { + if (protect) { + await this.application.mutator.protectItems(files) + } else { + await this.application.mutator.unprotectItems(files, ChallengeReason.UnprotectFile) + } + } + + downloadFiles = async (files: FileItem[]) => { + await Promise.all(files.map((file) => this.downloadFile(file))) + } } diff --git a/app/assets/javascripts/Controllers/NotesController.ts b/app/assets/javascripts/Controllers/NotesController.ts index f4af058a2..757fa5cf9 100644 --- a/app/assets/javascripts/Controllers/NotesController.ts +++ b/app/assets/javascripts/Controllers/NotesController.ts @@ -257,7 +257,7 @@ export class NotesController extends AbstractViewController { return false } - const title = Strings.trashNotesTitle + const title = Strings.trashItemsTitle let noteTitle = undefined if (this.selectedNotesCount === 1) { const selectedNote = this.getSelectedNotesList()[0] diff --git a/app/assets/javascripts/Controllers/SelectedItemsController.ts b/app/assets/javascripts/Controllers/SelectedItemsController.ts index 599b671fd..e8084fd97 100644 --- a/app/assets/javascripts/Controllers/SelectedItemsController.ts +++ b/app/assets/javascripts/Controllers/SelectedItemsController.ts @@ -35,6 +35,8 @@ export class SelectedItemsController extends AbstractViewController { selectedItems: observable, selectedItemsCount: computed, + selectedFiles: computed, + selectedFilesCount: computed, selectItem: action, setSelectedItems: action, @@ -73,6 +75,14 @@ export class SelectedItemsController extends AbstractViewController { return Object.keys(this.selectedItems).length } + get selectedFiles(): FileItem[] { + return this.getSelectedItems(ContentType.File) + } + + get selectedFilesCount(): number { + return this.selectedFiles.length + } + getSelectedItems = (contentType?: ContentType): T[] => { return Object.values(this.selectedItems).filter((item) => { return !contentType ? true : item.content_type === contentType diff --git a/app/assets/javascripts/Services/ViewControllerManager.ts b/app/assets/javascripts/Services/ViewControllerManager.ts index 06a7d00fe..81cd84213 100644 --- a/app/assets/javascripts/Services/ViewControllerManager.ts +++ b/app/assets/javascripts/Services/ViewControllerManager.ts @@ -97,7 +97,6 @@ export class ViewControllerManager { this.filesController = new FilesController( application, this.notesController, - this.selectionController, this.filePreviewModalController, this.eventBus, )