From e99c7b7c516b0c822f0e663843146c7f5cf1fb86 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Thu, 20 Oct 2022 02:18:52 +0530 Subject: [PATCH] feat: improved file drag-n-drop experience (#1848) --- .../ApplicationView/ApplicationView.tsx | 1 + .../AttachedFilesButton.tsx | 171 ------ .../AttachedFilesPopover.tsx | 206 ------- .../AttachedFilesPopover/PopoverFileItem.tsx | 124 ----- .../PopoverFileItemProps.tsx | 20 - .../PopoverFileSubmenu.tsx | 154 ----- .../AttachedFilesPopover/PopoverTabs.ts | 4 - .../ContentListView/ContentListView.tsx | 47 +- .../ContentListView/FileListItem.tsx | 2 +- .../FileDragNDropProvider.tsx | 131 ++++- .../FilePreview/FilePreviewModal.tsx | 2 +- .../getFileIconComponent.tsx | 0 .../FileView/FileViewWithoutProtection.tsx | 30 +- .../MultipleSelectedNotes.tsx | 21 - .../NoteGroupView/NoteGroupView.tsx | 14 - .../Components/NoteView/NoteView.tsx | 24 +- .../NoteView/NoteViewFileDropTarget.tsx | 42 ++ .../javascripts/Components/Tags/TagsList.tsx | 1 + .../Components/Tags/TagsListItem.tsx | 526 ++++++++++-------- .../Controllers/LinkingController.tsx | 36 +- 20 files changed, 534 insertions(+), 1022 deletions(-) delete mode 100644 packages/web/src/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx delete mode 100644 packages/web/src/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx delete mode 100644 packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx delete mode 100644 packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileItemProps.tsx delete mode 100644 packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx delete mode 100644 packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverTabs.ts rename packages/web/src/javascripts/Components/{AttachedFilesPopover => FilePreview}/getFileIconComponent.tsx (100%) create mode 100644 packages/web/src/javascripts/Components/NoteView/NoteViewFileDropTarget.tsx diff --git a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx index d858217ce..255bd0b3b 100644 --- a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -221,6 +221,7 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio notesController={viewControllerManager.notesController} selectionController={viewControllerManager.selectionController} searchOptionsController={viewControllerManager.searchOptionsController} + linkingController={viewControllerManager.linkingController} /> diff --git a/packages/web/src/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx b/packages/web/src/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx deleted file mode 100644 index efcb7d4d9..000000000 --- a/packages/web/src/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx +++ /dev/null @@ -1,171 +0,0 @@ -import { WebApplication } from '@/Application/Application' -import { observer } from 'mobx-react-lite' -import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react' -import Icon from '@/Components/Icon/Icon' -import AttachedFilesPopover from './AttachedFilesPopover' -import { usePremiumModal } from '@/Hooks/usePremiumModal' -import { PopoverTabs } from './PopoverTabs' -import { NotesController } from '@/Controllers/NotesController' -import { FilePreviewModalController } from '@/Controllers/FilePreviewModalController' -import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { FeaturesController } from '@/Controllers/FeaturesController' -import { FilesController } from '@/Controllers/FilesController' -import { SelectedItemsController } from '@/Controllers/SelectedItemsController' -import { useFileDragNDrop } from '@/Components/FileDragNDropProvider/FileDragNDropProvider' -import { FileItem, SNNote } from '@standardnotes/snjs' -import { addToast, ToastType } from '@standardnotes/toast' -import { classNames } from '@/Utils/ConcatenateClassNames' -import Popover from '../Popover/Popover' - -type Props = { - application: WebApplication - featuresController: FeaturesController - filePreviewModalController: FilePreviewModalController - filesController: FilesController - navigationController: NavigationController - notesController: NotesController - selectionController: SelectedItemsController - onClickPreprocessing?: () => Promise -} - -const AttachedFilesButton: FunctionComponent = ({ - application, - featuresController, - filesController, - navigationController, - notesController, - selectionController, - onClickPreprocessing, -}: Props) => { - const { allFiles, attachedFiles } = filesController - const attachedFilesCount = attachedFiles.length - - const premiumModal = usePremiumModal() - const note: SNNote | undefined = notesController.firstSelectedNote - - const [isOpen, setIsOpen] = useState(false) - const buttonRef = useRef(null) - const containerRef = useRef(null) - - const [currentTab, setCurrentTab] = useState( - navigationController.isInFilesView ? PopoverTabs.AllFiles : PopoverTabs.AttachedFiles, - ) - - const isAttachedTabDisabled = navigationController.isInFilesView || selectionController.selectedItemsCount > 1 - - useEffect(() => { - if (isAttachedTabDisabled && currentTab === PopoverTabs.AttachedFiles) { - setCurrentTab(PopoverTabs.AllFiles) - } - }, [currentTab, isAttachedTabDisabled]) - - const toggleAttachedFilesMenu = useCallback(async () => { - const newOpenState = !isOpen - - if (newOpenState && onClickPreprocessing) { - await onClickPreprocessing() - } - - setIsOpen(newOpenState) - }, [onClickPreprocessing, isOpen]) - - const prospectivelyShowFilesPremiumModal = useCallback(() => { - if (!featuresController.hasFiles) { - premiumModal.activate('Files') - } - }, [featuresController.hasFiles, premiumModal]) - - const toggleAttachedFilesMenuWithEntitlementCheck = useCallback(async () => { - prospectivelyShowFilesPremiumModal() - - await toggleAttachedFilesMenu() - }, [toggleAttachedFilesMenu, prospectivelyShowFilesPremiumModal]) - - const attachFileToNote = useCallback( - async (file: FileItem) => { - if (!note) { - addToast({ - type: ToastType.Error, - message: 'Could not attach file because selected note was unselected or deleted', - }) - return - } - - await application.items.associateFileWithNote(file, note) - }, - [application.items, note], - ) - - const { isDraggingFiles, addFilesDragInCallback, addFilesDropCallback } = useFileDragNDrop() - - useEffect(() => { - if (isDraggingFiles && !isOpen) { - void toggleAttachedFilesMenu() - } - }, [isDraggingFiles, isOpen, toggleAttachedFilesMenu]) - - const filesDragInCallback = useCallback((tab: PopoverTabs) => { - setCurrentTab(tab) - }, []) - - useEffect(() => { - addFilesDragInCallback(filesDragInCallback) - }, [addFilesDragInCallback, filesDragInCallback]) - - const filesDropCallback = useCallback( - (uploadedFiles: FileItem[]) => { - if (currentTab === PopoverTabs.AttachedFiles) { - uploadedFiles.forEach((file) => { - attachFileToNote(file).catch(console.error) - }) - } - }, - [attachFileToNote, currentTab], - ) - - useEffect(() => { - addFilesDropCallback(filesDropCallback) - }, [addFilesDropCallback, filesDropCallback]) - - return ( -
- - - - -
- ) -} - -export default observer(AttachedFilesButton) diff --git a/packages/web/src/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx b/packages/web/src/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx deleted file mode 100644 index 7fca8f36d..000000000 --- a/packages/web/src/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' -import { WebApplication } from '@/Application/Application' -import { FileItem } from '@standardnotes/snjs' -import { FilesIllustration } from '@standardnotes/icons' -import { observer } from 'mobx-react-lite' -import { Dispatch, FunctionComponent, SetStateAction, useRef, useState } from 'react' -import Button from '@/Components/Button/Button' -import Icon from '@/Components/Icon/Icon' -import PopoverFileItem from './PopoverFileItem' -import { PopoverFileItemActionType } from './PopoverFileItemAction' -import { PopoverTabs } from './PopoverTabs' -import { FilesController } from '@/Controllers/FilesController' -import { StreamingFileReader } from '@standardnotes/filepicker' -import ClearInputButton from '../ClearInputButton/ClearInputButton' -import DecoratedInput from '../Input/DecoratedInput' - -type Props = { - application: WebApplication - filesController: FilesController - allFiles: FileItem[] - attachedFiles: FileItem[] - currentTab: PopoverTabs - isDraggingFiles: boolean - setCurrentTab: Dispatch> - attachedTabDisabled: boolean -} - -const AttachedFilesPopover: FunctionComponent = ({ - application, - filesController, - allFiles, - attachedFiles, - currentTab, - isDraggingFiles, - setCurrentTab, - attachedTabDisabled, -}) => { - const fileInputRef = useRef(null) - - const [searchQuery, setSearchQuery] = useState('') - const searchInputRef = useRef(null) - - const filesList = currentTab === PopoverTabs.AttachedFiles ? attachedFiles : allFiles - - const filteredList = - searchQuery.length > 0 - ? filesList.filter((file) => file.name.toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1) - : filesList - - const attachFilesIfRequired = (files: FileItem[]) => { - if (currentTab === PopoverTabs.AttachedFiles) { - files.forEach((file) => { - filesController - .handleFileAction({ - type: PopoverFileItemActionType.AttachFileToNote, - payload: { file }, - }) - .catch(console.error) - }) - } - } - - const handleAttachFilesClick = async () => { - if (!StreamingFileReader.available()) { - fileInputRef.current?.click() - return - } - - const uploadedFiles = await filesController.uploadNewFile() - if (!uploadedFiles) { - return - } - attachFilesIfRequired(uploadedFiles) - } - - const previewHandler = (file: FileItem) => { - filesController - .handleFileAction({ - type: PopoverFileItemActionType.PreviewFile, - payload: { file, otherFiles: currentTab === PopoverTabs.AllFiles ? allFiles : attachedFiles }, - }) - .catch(console.error) - } - - return ( -
-
- - -
-
- {filteredList.length > 0 || searchQuery.length > 0 ? ( -
- 0 && ( - { - setSearchQuery('') - searchInputRef.current?.focus() - }} - /> - ), - ]} - /> -
- ) : null} - {filteredList.length > 0 ? ( - filteredList.map((file: FileItem) => { - return ( - - ) - }) - ) : ( -
-
- -
-
- {searchQuery.length > 0 - ? 'No result found' - : currentTab === PopoverTabs.AttachedFiles - ? 'No files attached to this note' - : 'No files found in this account'} -
- -
Or drop your files here
-
- )} -
- { - const files = event.currentTarget.files - - if (!files) { - return - } - - for (const file of files) { - const uploadedFiles = await filesController.uploadNewFile(file) - if (uploadedFiles) { - attachFilesIfRequired(uploadedFiles) - } - } - }} - /> - {filteredList.length > 0 && ( - - )} -
- ) -} - -export default observer(AttachedFilesPopover) diff --git a/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx b/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx deleted file mode 100644 index 445b9a9a0..000000000 --- a/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileItem.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' -import { KeyboardKey } from '@standardnotes/ui-services' -import { formatSizeToReadableString } from '@standardnotes/filepicker' -import { FileItem } from '@standardnotes/snjs' -import { - FormEventHandler, - FunctionComponent, - KeyboardEventHandler, - useCallback, - useEffect, - useRef, - useState, -} from 'react' -import Icon from '@/Components/Icon/Icon' -import { PopoverFileItemActionType } from './PopoverFileItemAction' -import PopoverFileSubmenu from './PopoverFileSubmenu' -import { getFileIconComponent } from './getFileIconComponent' -import { PopoverFileItemProps } from './PopoverFileItemProps' - -const PopoverFileItem: FunctionComponent = ({ - file, - isAttachedToNote, - handleFileAction, - getIconType, - previewHandler, -}) => { - const [fileName, setFileName] = useState(file.name) - const [isRenamingFile, setIsRenamingFile] = useState(false) - const itemRef = useRef(null) - const fileNameInputRef = useRef(null) - - useEffect(() => { - if (isRenamingFile) { - fileNameInputRef.current?.focus() - } - }, [isRenamingFile]) - - const renameFile = useCallback( - async (file: FileItem, name: string) => { - if (name.length < 1) { - return - } - - 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 = useCallback( - (event) => { - if (fileName.length > 0 && event.key === KeyboardKey.Enter) { - itemRef.current?.focus() - } - }, - [fileName.length], - ) - - const handleFileNameInputBlur = useCallback(() => { - renameFile(file, fileName).catch(console.error) - }, [file, fileName, renameFile]) - - const handleClick = useCallback(() => { - if (isRenamingFile) { - return - } - - previewHandler(file) - }, [file, isRenamingFile, previewHandler]) - - return ( -
-
- {getFileIconComponent(getIconType(file.mimeType), 'w-8 h-8 flex-shrink-0')} -
- {isRenamingFile ? ( - - ) : ( -
- {file.name} - {file.protected && ( - - )} -
- )} -
- {file.created_at.toLocaleString()} ยท {formatSizeToReadableString(file.decryptedSize)} -
-
-
- -
- ) -} - -export default PopoverFileItem diff --git a/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileItemProps.tsx b/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileItemProps.tsx deleted file mode 100644 index b56cf731f..000000000 --- a/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileItemProps.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { IconType, FileItem } from '@standardnotes/snjs' -import { Dispatch, SetStateAction } from 'react' -import { PopoverFileItemAction } from './PopoverFileItemAction' - -type CommonProps = { - file: FileItem - isAttachedToNote: boolean - handleFileAction: (action: PopoverFileItemAction) => Promise<{ - didHandleAction: boolean - }> - previewHandler: (file: FileItem) => void -} - -export type PopoverFileItemProps = CommonProps & { - getIconType(type: string): IconType -} - -export type PopoverFileSubmenuProps = CommonProps & { - setIsRenamingFile: Dispatch> -} diff --git a/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx b/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx deleted file mode 100644 index 6eb4b7cd9..000000000 --- a/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverFileSubmenu.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' -import { FunctionComponent, useCallback, useRef, useState } from 'react' -import Icon from '@/Components/Icon/Icon' -import Switch from '@/Components/Switch/Switch' -import { PopoverFileSubmenuProps } from './PopoverFileItemProps' -import { PopoverFileItemActionType } from './PopoverFileItemAction' -import HorizontalSeparator from '../Shared/HorizontalSeparator' -import { formatSizeToReadableString } from '@standardnotes/filepicker' -import Popover from '../Popover/Popover' - -const PopoverFileSubmenu: FunctionComponent = ({ - file, - isAttachedToNote, - handleFileAction, - setIsRenamingFile, - previewHandler, -}) => { - const menuContainerRef = useRef(null) - const menuButtonRef = useRef(null) - - const [isOpen, setIsOpen] = useState(false) - const [isFileProtected, setIsFileProtected] = useState(file.protected) - - const closeMenu = useCallback(() => { - setIsOpen(false) - }, []) - - const toggleMenu = useCallback(() => { - setIsOpen((isOpen) => !isOpen) - }, []) - - return ( -
- - - - {isAttachedToNote ? ( - - ) : ( - - )} - - - - - - -
-
- File ID: {file.uuid} -
-
- Size: {formatSizeToReadableString(file.decryptedSize)} -
-
-
-
- ) -} - -export default PopoverFileSubmenu diff --git a/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverTabs.ts b/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverTabs.ts deleted file mode 100644 index 98088aed0..000000000 --- a/packages/web/src/javascripts/Components/AttachedFilesPopover/PopoverTabs.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum PopoverTabs { - AttachedFiles = 'attached-files-tab', - AllFiles = 'all-files-tab', -} diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx index 22e673179..21b8bfab1 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx @@ -1,7 +1,7 @@ import { KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services' import { WebApplication } from '@/Application/Application' import { PANEL_NAME_NOTES } from '@/Constants/Constants' -import { PrefKey, SystemViewId } from '@standardnotes/snjs' +import { FileItem, PrefKey, SystemViewId } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react' import ContentList from '@/Components/ContentListView/ContentList' @@ -24,6 +24,8 @@ import SearchBar from '../SearchBar/SearchBar' import { SearchOptionsController } from '@/Controllers/SearchOptionsController' import { classNames } from '@/Utils/ConcatenateClassNames' import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' +import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider' +import { LinkingController } from '@/Controllers/LinkingController' type Props = { accountMenuController: AccountMenuController @@ -35,6 +37,7 @@ type Props = { notesController: NotesController selectionController: SelectedItemsController searchOptionsController: SearchOptionsController + linkingController: LinkingController } const ContentListView: FunctionComponent = ({ @@ -47,12 +50,54 @@ const ContentListView: FunctionComponent = ({ notesController, selectionController, searchOptionsController, + linkingController, }) => { const { isNotesListVisibleOnTablets, toggleAppPane } = useResponsiveAppPane() const fileInputRef = useRef(null) const itemsViewPanelRef = useRef(null) + const { addDragTarget, removeDragTarget } = useFileDragNDrop() + + const fileDropCallback = useCallback( + async (files: FileItem[]) => { + const currentTag = navigationController.selected + + if (!currentTag) { + return + } + + if (navigationController.isInAnySystemView() || navigationController.isInSmartView()) { + console.error('Trying to link uploaded files to smart view') + return + } + + files.forEach(async (file) => { + await linkingController.linkItems(file, currentTag) + }) + }, + [navigationController, linkingController], + ) + + useEffect(() => { + const target = itemsViewPanelRef.current + const currentTag = navigationController.selected + const shouldAddDropTarget = !navigationController.isInAnySystemView() && !navigationController.isInSmartView() + + if (target && shouldAddDropTarget && currentTag) { + addDragTarget(target, { + tooltipText: `Drop your files to upload and link them to tag "${currentTag.title}"`, + callback: fileDropCallback, + }) + } + + return () => { + if (target) { + removeDragTarget(target) + } + } + }, [addDragTarget, fileDropCallback, navigationController, navigationController.selected, removeDragTarget]) + const { completedFullSync, createNewNote, diff --git a/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx b/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx index 87cc0c00d..e3050be11 100644 --- a/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/FileListItem.tsx @@ -1,7 +1,7 @@ import { FileItem } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useRef } from 'react' -import { getFileIconComponent } from '../AttachedFilesPopover/getFileIconComponent' +import { getFileIconComponent } from '../FilePreview/getFileIconComponent' import ListItemConflictIndicator from './ListItemConflictIndicator' import ListItemFlagIcons from './ListItemFlagIcons' import ListItemTags from './ListItemTags' diff --git a/packages/web/src/javascripts/Components/FileDragNDropProvider/FileDragNDropProvider.tsx b/packages/web/src/javascripts/Components/FileDragNDropProvider/FileDragNDropProvider.tsx index 7a5aa5d50..f669e7243 100644 --- a/packages/web/src/javascripts/Components/FileDragNDropProvider/FileDragNDropProvider.tsx +++ b/packages/web/src/javascripts/Components/FileDragNDropProvider/FileDragNDropProvider.tsx @@ -2,19 +2,22 @@ import { WebApplication } from '@/Application/Application' import { FeaturesController } from '@/Controllers/FeaturesController' import { FilesController } from '@/Controllers/FilesController' import { usePremiumModal } from '@/Hooks/usePremiumModal' +import { classNames } from '@/Utils/ConcatenateClassNames' import { isHandlingFileDrag } from '@/Utils/DragTypeCheck' import { StreamingFileReader } from '@standardnotes/filepicker' import { FileItem } from '@standardnotes/snjs' -import { useMemo, useState, createContext, ReactNode, useRef, useCallback, useEffect, useContext } from 'react' -import { PopoverTabs } from '../AttachedFilesPopover/PopoverTabs' +import { useMemo, useState, createContext, ReactNode, useRef, useCallback, useEffect, useContext, memo } from 'react' +import Portal from '../Portal/Portal' -type FilesDragInCallback = (tab: PopoverTabs) => void -type FilesDropCallback = (uploadedFiles: FileItem[]) => void +type FileDragTargetData = { + tooltipText: string + callback: (files: FileItem[]) => void +} type FileDnDContextData = { isDraggingFiles: boolean - addFilesDragInCallback: (callback: FilesDragInCallback) => void - addFilesDropCallback: (callback: FilesDropCallback) => void + addDragTarget: (target: HTMLElement, data: FileDragTargetData) => void + removeDragTarget: (target: HTMLElement) => void } export const FileDnDContext = createContext(null) @@ -36,23 +39,57 @@ type Props = { children: ReactNode } +const FileDragOverlayClassName = + 'overlay pointer-events-none absolute top-0 left-0 z-footer-bar h-full w-full border-2 border-info before:block before:h-full before:w-full before:bg-info before:opacity-20' + +const MemoizedChildren = memo(({ children }: { children: ReactNode }) => { + return <>{children} +}) + const FileDragNDropProvider = ({ application, children, featuresController, filesController }: Props) => { const premiumModal = usePremiumModal() const [isDraggingFiles, setIsDraggingFiles] = useState(false) + const [tooltipText, setTooltipText] = useState('') - const filesDragInCallbackRef = useRef() - const filesDropCallbackRef = useRef() + const fileDragOverlayRef = useRef(null) - const addFilesDragInCallback = useCallback((callback: FilesDragInCallback) => { - filesDragInCallbackRef.current = callback + const addOverlayToElement = useCallback((target: Element) => { + if (fileDragOverlayRef.current) { + const targetBoundingRect = target.getBoundingClientRect() + fileDragOverlayRef.current.style.width = `${targetBoundingRect.width}px` + fileDragOverlayRef.current.style.height = `${targetBoundingRect.height}px` + fileDragOverlayRef.current.style.transform = `translate(${targetBoundingRect.x}px, ${targetBoundingRect.y}px)` + } }, []) - const addFilesDropCallback = useCallback((callback: FilesDropCallback) => { - filesDropCallbackRef.current = callback + const removeOverlayFromElement = useCallback(() => { + if (fileDragOverlayRef.current) { + fileDragOverlayRef.current.style.width = '' + fileDragOverlayRef.current.style.height = '' + fileDragOverlayRef.current.style.transform = '' + } + }, []) + + const dragTargets = useRef>(new Map()) + + const addDragTarget = useCallback((target: HTMLElement, data: FileDragTargetData) => { + target.setAttribute('data-file-drag-target', '') + dragTargets.current.set(target, data) + }, []) + + const removeDragTarget = useCallback((target: HTMLElement) => { + target.removeAttribute('data-file-drag-target') + dragTargets.current.delete(target) }, []) const dragCounter = useRef(0) + const resetState = useCallback(() => { + setIsDraggingFiles(false) + setTooltipText('') + removeOverlayFromElement() + }, [removeOverlayFromElement]) + const handleDrag = useCallback( (event: DragEvent) => { if (isHandlingFileDrag(event, application)) { @@ -72,22 +109,31 @@ const FileDragNDropProvider = ({ application, children, featuresController, file event.preventDefault() event.stopPropagation() - switch ((event.target as HTMLElement).id) { - case PopoverTabs.AllFiles: - filesDragInCallbackRef.current?.(PopoverTabs.AllFiles) - break - case PopoverTabs.AttachedFiles: - filesDragInCallbackRef.current?.(PopoverTabs.AttachedFiles) - break + removeOverlayFromElement() + + let closestDragTarget: Element | null = null + + if (event.target instanceof HTMLElement) { + closestDragTarget = event.target.closest('[data-file-drag-target]') } dragCounter.current = dragCounter.current + 1 if (event.dataTransfer?.items.length) { setIsDraggingFiles(true) + if (closestDragTarget) { + addOverlayToElement(closestDragTarget) + const tooltipText = dragTargets.current.get(closestDragTarget)?.tooltipText + if (tooltipText) { + setTooltipText(tooltipText) + } + } else { + setTooltipText('') + removeOverlayFromElement() + } } }, - [application], + [addOverlayToElement, application, removeOverlayFromElement], ) const handleDragOut = useCallback( @@ -105,22 +151,22 @@ const FileDragNDropProvider = ({ application, children, featuresController, file return } - setIsDraggingFiles(false) + resetState() }, - [application], + [application, resetState], ) const handleDrop = useCallback( (event: DragEvent) => { if (!isHandlingFileDrag(event, application)) { - setIsDraggingFiles(false) + resetState() return } event.preventDefault() event.stopPropagation() - setIsDraggingFiles(false) + resetState() if (!featuresController.hasFiles) { premiumModal.activate('Files') @@ -143,14 +189,22 @@ const FileDragNDropProvider = ({ application, children, featuresController, file return } - filesDropCallbackRef.current?.(uploadedFiles) + let closestDragTarget: Element | null = null + + if (event.target instanceof HTMLElement) { + closestDragTarget = event.target.closest('[data-file-drag-target]') + } + + if (closestDragTarget && dragTargets.current.has(closestDragTarget)) { + dragTargets.current.get(closestDragTarget)?.callback(uploadedFiles) + } }) event.dataTransfer.clearData() dragCounter.current = 0 } }, - [application, featuresController.hasFiles, filesController, premiumModal], + [application, featuresController.hasFiles, filesController, premiumModal, resetState], ) useEffect(() => { @@ -170,12 +224,29 @@ const FileDragNDropProvider = ({ application, children, featuresController, file const contextValue = useMemo(() => { return { isDraggingFiles, - addFilesDragInCallback, - addFilesDropCallback, + addDragTarget, + removeDragTarget, } - }, [addFilesDragInCallback, addFilesDropCallback, isDraggingFiles]) + }, [addDragTarget, isDraggingFiles, removeDragTarget]) - return {children} + return ( + + + {isDraggingFiles ? ( + <> +
+ {tooltipText.length ? tooltipText : 'Drop your files to upload them'} +
+ + ) : null} + +
+ + + ) } export default FileDragNDropProvider diff --git a/packages/web/src/javascripts/Components/FilePreview/FilePreviewModal.tsx b/packages/web/src/javascripts/Components/FilePreview/FilePreviewModal.tsx index 678e8c06a..0232eead3 100644 --- a/packages/web/src/javascripts/Components/FilePreview/FilePreviewModal.tsx +++ b/packages/web/src/javascripts/Components/FilePreview/FilePreviewModal.tsx @@ -1,7 +1,7 @@ import { WebApplication } from '@/Application/Application' import { DialogContent, DialogOverlay } from '@reach/dialog' import { FunctionComponent, KeyboardEventHandler, useCallback, useMemo, useRef, useState } from 'react' -import { getFileIconComponent } from '@/Components/AttachedFilesPopover/getFileIconComponent' +import { getFileIconComponent } from './getFileIconComponent' import Icon from '@/Components/Icon/Icon' import FilePreviewInfoPanel from './FilePreviewInfoPanel' import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' diff --git a/packages/web/src/javascripts/Components/AttachedFilesPopover/getFileIconComponent.tsx b/packages/web/src/javascripts/Components/FilePreview/getFileIconComponent.tsx similarity index 100% rename from packages/web/src/javascripts/Components/AttachedFilesPopover/getFileIconComponent.tsx rename to packages/web/src/javascripts/Components/FilePreview/getFileIconComponent.tsx diff --git a/packages/web/src/javascripts/Components/FileView/FileViewWithoutProtection.tsx b/packages/web/src/javascripts/Components/FileView/FileViewWithoutProtection.tsx index 98245fae0..1f63bfcf8 100644 --- a/packages/web/src/javascripts/Components/FileView/FileViewWithoutProtection.tsx +++ b/packages/web/src/javascripts/Components/FileView/FileViewWithoutProtection.tsx @@ -1,6 +1,6 @@ import { ElementIds } from '@/Constants/ElementIDs' import { observer } from 'mobx-react-lite' -import { ChangeEventHandler, useCallback, useRef, useState } from 'react' +import { ChangeEventHandler, useCallback, useEffect, useRef, useState } from 'react' import FileOptionsPanel from '@/Components/FileContextMenu/FileOptionsPanel' import FilePreview from '@/Components/FilePreview/FilePreview' import { FileViewProps } from './FileViewProps' @@ -10,6 +10,7 @@ import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContaine import Icon from '../Icon/Icon' import Popover from '../Popover/Popover' import FilePreviewInfoPanel from '../FilePreview/FilePreviewInfoPanel' +import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider' const SyncTimeoutNoDebounceMs = 100 const SyncTimeoutDebounceMs = 350 @@ -40,8 +41,33 @@ const FileViewWithoutProtection = ({ application, viewControllerManager, file }: [application, file], ) + const fileDragTargetRef = useRef(null) + + const { addDragTarget, removeDragTarget } = useFileDragNDrop() + + useEffect(() => { + const target = fileDragTargetRef.current + + if (target) { + addDragTarget(target, { + tooltipText: 'Drop your files to upload and link them to the current file', + callback(files) { + files.forEach(async (uploadedFile) => { + await viewControllerManager.linkingController.linkItems(uploadedFile, file) + }) + }, + }) + } + + return () => { + if (target) { + removeDragTarget(target) + } + } + }, [addDragTarget, file, removeDragTarget, viewControllerManager.linkingController]) + return ( -
+

{count} selected notes

-
- -
diff --git a/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx b/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx index b36a0adbf..3c63f7d35 100644 --- a/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx +++ b/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx @@ -4,7 +4,6 @@ import { WebApplication } from '@/Application/Application' import MultipleSelectedNotes from '@/Components/MultipleSelectedNotes/MultipleSelectedNotes' import MultipleSelectedFiles from '../MultipleSelectedFiles/MultipleSelectedFiles' import { ElementIds } from '@/Constants/ElementIDs' -import { FileDnDContext } from '@/Components/FileDragNDropProvider/FileDragNDropProvider' import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import ResponsivePaneContent from '../ResponsivePane/ResponsivePaneContent' import FileView from '../FileView/FileView' @@ -24,9 +23,6 @@ type Props = { } class NoteGroupView extends PureComponent { - static override contextType = FileDnDContext - declare context: React.ContextType - private removeChangeObserver!: () => void constructor(props: Props) { @@ -94,8 +90,6 @@ class NoteGroupView extends PureComponent { } override render() { - const fileDragNDropContext = this.context - const shouldNotShowMultipleSelectedItems = !this.state.showMultipleSelectedNotes && !this.state.showMultipleSelectedFiles @@ -112,10 +106,7 @@ class NoteGroupView extends PureComponent { {this.state.showMultipleSelectedNotes && ( { selectionController={this.viewControllerManager.selectionController} /> )} - {this.viewControllerManager.navigationController.isInFilesView && fileDragNDropContext?.isDraggingFiles && ( -
- Drop your files to upload them -
- )} {shouldNotShowMultipleSelectedItems && hasControllers && canRenderEditorView && ( <> {this.state.controllers.map((controller) => { diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index e5a666463..3ca5186e4 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -30,7 +30,6 @@ import ComponentView from '@/Components/ComponentView/ComponentView' import PanelResizer, { PanelSide, PanelResizeType } from '@/Components/PanelResizer/PanelResizer' import { ElementIds } from '@/Constants/ElementIDs' import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton' -import AttachedFilesButton from '@/Components/AttachedFilesPopover/AttachedFilesButton' import EditingDisabledBanner from './EditingDisabledBanner' import { transactionForAssociateComponentWithCurrentNote, @@ -46,6 +45,7 @@ import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContaine import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator' import { PrefDefaults } from '@/Constants/PrefDefaults' import LinkedItemsButton from '../LinkedItems/LinkedItemsButton' +import NoteViewFileDropTarget from './NoteViewFileDropTarget' const MinimumStatusDuration = 400 const TextareaDebounce = 100 @@ -118,6 +118,7 @@ class NoteView extends PureComponent { private protectionTimeoutId: ReturnType | null = null + private noteViewElementRef: RefObject private editorContentRef: RefObject constructor(props: NoteViewProps) { @@ -159,6 +160,7 @@ class NoteView extends PureComponent { noteType: this.controller.item.noteType, } + this.noteViewElementRef = createRef() this.editorContentRef = createRef() window.addEventListener('scroll', this.handleWindowScroll) @@ -949,7 +951,15 @@ class NoteView extends PureComponent { } return ( -
+
+ {this.note && ( + + )} + {this.state.noteLocked && ( { @@ -1014,16 +1024,6 @@ class NoteView extends PureComponent { linkingController={this.viewControllerManager.linkingController} onClickPreprocessing={this.ensureNoteIsInsertedBeforeUIAction} /> - { + const { isDraggingFiles, addDragTarget, removeDragTarget } = useFileDragNDrop() + + useEffect(() => { + const target = noteViewElement + + if (target) { + addDragTarget(target, { + tooltipText: 'Drop your files to upload and link them to the current note', + callback(files) { + files.forEach(async (uploadedFile) => { + await linkingController.linkItems(uploadedFile, note) + }) + }, + }) + } + + return () => { + if (target) { + removeDragTarget(target) + } + } + }, [addDragTarget, linkingController, note, noteViewElement, removeDragTarget]) + + return isDraggingFiles ? ( + // Required to block drag events to editor iframe +
+ ) : null +} + +export default NoteViewFileDropTarget diff --git a/packages/web/src/javascripts/Components/Tags/TagsList.tsx b/packages/web/src/javascripts/Components/Tags/TagsList.tsx index 74e72159f..8012f7305 100644 --- a/packages/web/src/javascripts/Components/Tags/TagsList.tsx +++ b/packages/web/src/javascripts/Components/Tags/TagsList.tsx @@ -51,6 +51,7 @@ const TagsList: FunctionComponent = ({ viewControllerManager }: Props) => tag={tag} tagsState={tagsState} features={viewControllerManager.featuresController} + linkingController={viewControllerManager.linkingController} onContextMenu={onContextMenu} /> ) diff --git a/packages/web/src/javascripts/Components/Tags/TagsListItem.tsx b/packages/web/src/javascripts/Components/Tags/TagsListItem.tsx index 1673a9f60..b19ec7d9b 100644 --- a/packages/web/src/javascripts/Components/Tags/TagsListItem.tsx +++ b/packages/web/src/javascripts/Components/Tags/TagsListItem.tsx @@ -23,11 +23,15 @@ import { DropItem, DropProps, ItemTypes } from './DragNDrop' import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' import { AppPaneId } from '../ResponsivePane/AppPaneMetadata' import { classNames } from '@/Utils/ConcatenateClassNames' +import { mergeRefs } from '@/Hooks/mergeRefs' +import { useFileDragNDrop } from '../FileDragNDropProvider/FileDragNDropProvider' +import { LinkingController } from '@/Controllers/LinkingController' type Props = { tag: SNTag tagsState: NavigationController features: FeaturesController + linkingController: LinkingController level: number onContextMenu: (tag: SNTag, posX: number, posY: number) => void } @@ -35,288 +39,316 @@ type Props = { const PADDING_BASE_PX = 14 const PADDING_PER_LEVEL_PX = 21 -export const TagsListItem: FunctionComponent = observer(({ tag, features, tagsState, level, onContextMenu }) => { - const { toggleAppPane } = useResponsiveAppPane() +export const TagsListItem: FunctionComponent = observer( + ({ tag, features, tagsState, level, onContextMenu, linkingController }) => { + const { toggleAppPane } = useResponsiveAppPane() - const [title, setTitle] = useState(tag.title || '') - const [subtagTitle, setSubtagTitle] = useState('') - const inputRef = useRef(null) - const subtagInputRef = useRef(null) - const menuButtonRef = useRef(null) + const [title, setTitle] = useState(tag.title || '') + const [subtagTitle, setSubtagTitle] = useState('') + const inputRef = useRef(null) + const subtagInputRef = useRef(null) + const menuButtonRef = useRef(null) - const isSelected = tagsState.selected === tag - const isEditing = tagsState.editingTag === tag - const isAddingSubtag = tagsState.addingSubtagTo === tag - const noteCounts = computed(() => tagsState.getNotesCount(tag)) + const isSelected = tagsState.selected === tag + const isEditing = tagsState.editingTag === tag + const isAddingSubtag = tagsState.addingSubtagTo === tag + const noteCounts = computed(() => tagsState.getNotesCount(tag)) - const childrenTags = computed(() => tagsState.getChildren(tag)).get() - const hasChildren = childrenTags.length > 0 + const childrenTags = computed(() => tagsState.getChildren(tag)).get() + const hasChildren = childrenTags.length > 0 - const hasFolders = features.hasFolders - const hasAtLeastOneFolder = tagsState.hasAtLeastOneFolder + const hasFolders = features.hasFolders + const hasAtLeastOneFolder = tagsState.hasAtLeastOneFolder - const premiumModal = usePremiumModal() + const premiumModal = usePremiumModal() - const [showChildren, setShowChildren] = useState(tag.expanded) - const [hadChildren, setHadChildren] = useState(hasChildren) + const [showChildren, setShowChildren] = useState(tag.expanded) + const [hadChildren, setHadChildren] = useState(hasChildren) - useEffect(() => { - if (!hadChildren && hasChildren) { - setShowChildren(true) - } - setHadChildren(hasChildren) - }, [hadChildren, hasChildren]) + useEffect(() => { + if (!hadChildren && hasChildren) { + setShowChildren(true) + } + setHadChildren(hasChildren) + }, [hadChildren, hasChildren]) - useEffect(() => { - setTitle(tag.title || '') - }, [setTitle, tag]) + useEffect(() => { + setTitle(tag.title || '') + }, [setTitle, tag]) - const toggleChildren: MouseEventHandler = useCallback( - (e) => { - e.stopPropagation() - setShowChildren((x) => { - tagsState.setExpanded(tag, !x) - return !x - }) - }, - [setShowChildren, tag, tagsState], - ) + const toggleChildren: MouseEventHandler = useCallback( + (e) => { + e.stopPropagation() + setShowChildren((x) => { + tagsState.setExpanded(tag, !x) + return !x + }) + }, + [setShowChildren, tag, tagsState], + ) - const selectCurrentTag = useCallback(async () => { - await tagsState.setSelectedTag(tag) - toggleAppPane(AppPaneId.Items) - }, [tagsState, tag, toggleAppPane]) + const selectCurrentTag = useCallback(async () => { + await tagsState.setSelectedTag(tag) + toggleAppPane(AppPaneId.Items) + }, [tagsState, tag, toggleAppPane]) - const onBlur = useCallback(() => { - tagsState.save(tag, title).catch(console.error) - setTitle(tag.title) - }, [tagsState, tag, title, setTitle]) + const onBlur = useCallback(() => { + tagsState.save(tag, title).catch(console.error) + setTitle(tag.title) + }, [tagsState, tag, title, setTitle]) - const onInput: FormEventHandler = useCallback( - (e) => { + const onInput: FormEventHandler = useCallback( + (e) => { + const value = (e.target as HTMLInputElement).value + setTitle(value) + }, + [setTitle], + ) + + const onKeyDown: KeyboardEventHandler = useCallback( + (e) => { + if (e.key === KeyboardKey.Enter) { + inputRef.current?.blur() + e.preventDefault() + } + }, + [inputRef], + ) + + useEffect(() => { + if (isEditing) { + inputRef.current?.focus() + } + }, [inputRef, isEditing]) + + const onSubtagInput: FormEventHandler = useCallback((e) => { const value = (e.target as HTMLInputElement).value - setTitle(value) - }, - [setTitle], - ) + setSubtagTitle(value) + }, []) - const onKeyDown: KeyboardEventHandler = useCallback( - (e) => { - if (e.key === KeyboardKey.Enter) { - inputRef.current?.blur() - e.preventDefault() - } - }, - [inputRef], - ) + const onSubtagInputBlur = useCallback(() => { + tagsState.createSubtagAndAssignParent(tag, subtagTitle).catch(console.error) + setSubtagTitle('') + }, [subtagTitle, tag, tagsState]) - useEffect(() => { - if (isEditing) { - inputRef.current?.focus() - } - }, [inputRef, isEditing]) - - const onSubtagInput: FormEventHandler = useCallback((e) => { - const value = (e.target as HTMLInputElement).value - setSubtagTitle(value) - }, []) - - const onSubtagInputBlur = useCallback(() => { - tagsState.createSubtagAndAssignParent(tag, subtagTitle).catch(console.error) - setSubtagTitle('') - }, [subtagTitle, tag, tagsState]) - - const onSubtagKeyDown: KeyboardEventHandler = useCallback( - (e) => { - if (e.key === KeyboardKey.Enter) { - e.preventDefault() - subtagInputRef.current?.blur() - } - }, - [subtagInputRef], - ) - - useEffect(() => { - if (isAddingSubtag) { - subtagInputRef.current?.focus() - } - }, [subtagInputRef, isAddingSubtag]) - - const [, dragRef] = useDrag( - () => ({ - type: ItemTypes.TAG, - item: { uuid: tag.uuid }, - canDrag: () => { - return true + const onSubtagKeyDown: KeyboardEventHandler = useCallback( + (e) => { + if (e.key === KeyboardKey.Enter) { + e.preventDefault() + subtagInputRef.current?.blur() + } }, - collect: (monitor) => ({ - isDragging: !!monitor.isDragging(), + [subtagInputRef], + ) + + useEffect(() => { + if (isAddingSubtag) { + subtagInputRef.current?.focus() + } + }, [subtagInputRef, isAddingSubtag]) + + const [, dragRef] = useDrag( + () => ({ + type: ItemTypes.TAG, + item: { uuid: tag.uuid }, + canDrag: () => { + return true + }, + collect: (monitor) => ({ + isDragging: !!monitor.isDragging(), + }), }), - }), - [tag], - ) + [tag], + ) - const [{ isOver, canDrop }, dropRef] = useDrop( - () => ({ - accept: ItemTypes.TAG, - canDrop: (item) => { - return tagsState.isValidTagParent(tag, item as SNTag) - }, - drop: (item) => { - if (!hasFolders) { - premiumModal.activate(TAG_FOLDERS_FEATURE_NAME) + const [{ isOver, canDrop }, dropRef] = useDrop( + () => ({ + accept: ItemTypes.TAG, + canDrop: (item) => { + return tagsState.isValidTagParent(tag, item as SNTag) + }, + drop: (item) => { + if (!hasFolders) { + premiumModal.activate(TAG_FOLDERS_FEATURE_NAME) + return + } + tagsState.assignParent(item.uuid, tag.uuid).catch(console.error) + }, + collect: (monitor) => ({ + isOver: !!monitor.isOver(), + canDrop: !!monitor.canDrop(), + }), + }), + [tag, tagsState, hasFolders, premiumModal], + ) + + const readyToDrop = isOver && canDrop + + const toggleContextMenu: MouseEventHandler = useCallback( + (event) => { + event.preventDefault() + event.stopPropagation() + + if (!menuButtonRef.current) { return } - tagsState.assignParent(item.uuid, tag.uuid).catch(console.error) + + const contextMenuOpen = tagsState.contextMenuOpen + const menuButtonRect = menuButtonRef.current?.getBoundingClientRect() + + if (contextMenuOpen) { + tagsState.setContextMenuOpen(false) + } else { + onContextMenu(tag, menuButtonRect.right, menuButtonRect.top) + } }, - collect: (monitor) => ({ - isOver: !!monitor.isOver(), - canDrop: !!monitor.canDrop(), - }), - }), - [tag, tagsState, hasFolders, premiumModal], - ) + [onContextMenu, tagsState, tag], + ) - const readyToDrop = isOver && canDrop + const tagRef = useRef(null) - const toggleContextMenu: MouseEventHandler = useCallback( - (event) => { - event.preventDefault() - event.stopPropagation() + const { addDragTarget, removeDragTarget } = useFileDragNDrop() - if (!menuButtonRef.current) { - return + useEffect(() => { + const target = tagRef.current + + if (target) { + addDragTarget(target, { + tooltipText: `Drop your files to upload and link them to tag "${tag.title}"`, + callback(files) { + files.forEach(async (file) => { + await linkingController.linkItems(file, tag) + }) + }, + }) } - const contextMenuOpen = tagsState.contextMenuOpen - const menuButtonRect = menuButtonRef.current?.getBoundingClientRect() - - if (contextMenuOpen) { - tagsState.setContextMenuOpen(false) - } else { - onContextMenu(tag, menuButtonRect.right, menuButtonRect.top) + return () => { + if (target) { + removeDragTarget(target) + } } - }, - [onContextMenu, tagsState, tag], - ) + }, [addDragTarget, linkingController, removeDragTarget, tag]) - return ( - <> -
{ - e.preventDefault() - onContextMenu(tag, e.clientX, e.clientY) - }} - > -
- {hasAtLeastOneFolder && ( -
- - - -
- )} -
- -
- {isEditing ? ( - - ) : ( -
- {title} -
- )} -
- - - -
{noteCounts.get()}
-
-
- -
- {tag.conflictOf &&
Conflicted Copy {tag.conflictOf}
} -
-
- {isAddingSubtag && ( + return ( + <>
{ + e.preventDefault() + onContextMenu(tag, e.clientX, e.clientY) }} > -
-
-
- +
+ {hasAtLeastOneFolder && ( +
+ + + +
+ )} +
+
- + {isEditing ? ( + + ) : ( +
+ {title} +
+ )} +
+ + + +
{noteCounts.get()}
+
+
+ +
+ {tag.conflictOf &&
Conflicted Copy {tag.conflictOf}
}
- )} - {showChildren && ( - <> - {childrenTags.map((tag) => { - return ( - +
+
+
+ +
+ - ) - })} - - )} - - ) -}) +
+
+ )} + {showChildren && ( + <> + {childrenTags.map((tag) => { + return ( + + ) + })} + + )} + + ) + }, +) TagsListItem.displayName = 'TagsListItem' diff --git a/packages/web/src/javascripts/Controllers/LinkingController.tsx b/packages/web/src/javascripts/Controllers/LinkingController.tsx index b5c151799..516561487 100644 --- a/packages/web/src/javascripts/Controllers/LinkingController.tsx +++ b/packages/web/src/javascripts/Controllers/LinkingController.tsx @@ -282,25 +282,22 @@ export class LinkingController extends AbstractViewController { } } - linkItemToSelectedItem = async (itemToLink: LinkableItem) => { - await this.ensureActiveItemIsInserted() - const activeItem = this.activeItem - - if (activeItem && itemToLink instanceof SNTag) { - await this.addTagToItem(itemToLink, activeItem) - } - - if (activeItem instanceof SNNote) { + linkItems = async (item: LinkableItem, itemToLink: LinkableItem) => { + if (item instanceof SNNote) { if (itemToLink instanceof FileItem) { - await this.application.items.associateFileWithNote(itemToLink, activeItem) + await this.application.items.associateFileWithNote(itemToLink, item) } else if (itemToLink instanceof SNNote && this.isEntitledToNoteLinking) { - await this.application.items.linkNoteToNote(activeItem, itemToLink) + await this.application.items.linkNoteToNote(item, itemToLink) + } else if (itemToLink instanceof SNTag) { + await this.addTagToItem(itemToLink, item) } - } else if (activeItem instanceof FileItem) { + } else if (item instanceof FileItem) { if (itemToLink instanceof SNNote) { - await this.application.items.associateFileWithNote(activeItem, itemToLink) + await this.application.items.associateFileWithNote(item, itemToLink) } else if (itemToLink instanceof FileItem) { - await this.application.items.linkFileToFile(activeItem, itemToLink) + await this.application.items.linkFileToFile(item, itemToLink) + } else if (itemToLink instanceof SNTag) { + await this.addTagToItem(itemToLink, item) } } @@ -308,6 +305,17 @@ export class LinkingController extends AbstractViewController { this.reloadAllLinks() } + linkItemToSelectedItem = async (itemToLink: LinkableItem) => { + await this.ensureActiveItemIsInserted() + const activeItem = this.activeItem + + if (!activeItem) { + return + } + + await this.linkItems(activeItem, itemToLink) + } + createAndAddNewTag = async (title: string) => { await this.ensureActiveItemIsInserted() const activeItem = this.activeItem