From af6ae81e1d4af4cc3ecd528eddf101eadc923fc9 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Wed, 25 Jan 2023 01:20:43 +0530 Subject: [PATCH] feat: Added ability to change the note type of multiple notes at once (#2180) --- .../ChangeEditor/ChangeMultipleButton.tsx | 40 ++++ .../ChangeEditor/ChangeMultipleMenu.tsx | 197 ++++++++++++++++++ .../MultipleSelectedNotes.tsx | 4 + 3 files changed, 241 insertions(+) create mode 100644 packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleButton.tsx create mode 100644 packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleButton.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleButton.tsx new file mode 100644 index 000000000..42a5696ca --- /dev/null +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleButton.tsx @@ -0,0 +1,40 @@ +import { WebApplication } from '@/Application/Application' +import { NotesController } from '@/Controllers/NotesController/NotesController' +import { useRef, useState } from 'react' +import RoundIconButton from '../Button/RoundIconButton' +import Popover from '../Popover/Popover' +import ChangeMultipleMenu from './ChangeMultipleMenu' + +type Props = { + application: WebApplication + notesController: NotesController +} + +const ChangeMultipleButton = ({ application, notesController }: Props) => { + const changeButtonRef = useRef(null) + const [isChangeMenuOpen, setIsChangeMenuOpen] = useState(false) + const toggleMenu = () => setIsChangeMenuOpen((open) => !open) + const [disableClickOutside, setDisableClickOutside] = useState(false) + + return ( + <> + + + + + + ) +} + +export default ChangeMultipleButton diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx new file mode 100644 index 000000000..8e3bdd41c --- /dev/null +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeMultipleMenu.tsx @@ -0,0 +1,197 @@ +import { WebApplication } from '@/Application/Application' +import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Constants/Strings' +import { usePremiumModal } from '@/Hooks/usePremiumModal' +import { createEditorMenuGroups } from '@/Utils/createEditorMenuGroups' +import { ComponentArea, NoteMutator, NoteType, SNComponent, SNNote } from '@standardnotes/snjs' +import { Fragment, useCallback, useMemo, useState } from 'react' +import Icon from '../Icon/Icon' +import { PremiumFeatureIconName, PremiumFeatureIconClass } from '../Icon/PremiumFeatureIcon' +import Menu from '../Menu/Menu' +import MenuItem from '../Menu/MenuItem' +import { EditorMenuGroup } from '../NotesOptions/EditorMenuGroup' +import { EditorMenuItem } from '../NotesOptions/EditorMenuItem' +import { SuperNoteImporter } from '../NoteView/SuperEditor/SuperNoteImporter' +import { Pill } from '../Preferences/PreferencesComponents/Content' +import ModalOverlay from '../Shared/ModalOverlay' + +const getGroupId = (group: EditorMenuGroup) => group.title.toLowerCase().replace(/\s/, '-') + +type Props = { + application: WebApplication + notes: SNNote[] + setDisableClickOutside: (value: boolean) => void +} + +const ChangeMultipleMenu = ({ application, notes, setDisableClickOutside }: Props) => { + const premiumModal = usePremiumModal() + + const [itemToBeSelected, setItemToBeSelected] = useState() + const [confirmationQueue, setConfirmationQueue] = useState([]) + + const hasSelectedLockedNotes = useMemo(() => notes.some((note) => note.locked), [notes]) + + const editors = useMemo( + () => + application.componentManager.componentsForArea(ComponentArea.Editor).sort((a, b) => { + return a.displayName.toLowerCase() < b.displayName.toLowerCase() ? -1 : 1 + }), + [application.componentManager], + ) + const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors]) + + const selectComponent = useCallback( + async (component: SNComponent, note: SNNote) => { + if (component.conflictOf) { + void application.mutator.changeAndSaveItem(component, (mutator) => { + mutator.conflictOf = undefined + }) + } + + await application.mutator.changeAndSaveItem(note, (mutator) => { + const noteMutator = mutator as NoteMutator + noteMutator.noteType = component.noteType + noteMutator.editorIdentifier = component.identifier + }) + }, + [application], + ) + + const selectNonComponent = useCallback( + async (item: EditorMenuItem, note: SNNote) => { + await application.mutator.changeAndSaveItem(note, (mutator) => { + const noteMutator = mutator as NoteMutator + noteMutator.noteType = item.noteType + noteMutator.editorIdentifier = undefined + }) + }, + [application], + ) + + const selectItem = useCallback( + async (itemToBeSelected: EditorMenuItem) => { + if (!itemToBeSelected.isEntitled) { + premiumModal.activate(itemToBeSelected.name) + return + } + + if (hasSelectedLockedNotes) { + void application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT) + return + } + + if (itemToBeSelected.noteType === NoteType.Super) { + setDisableClickOutside(true) + setItemToBeSelected(itemToBeSelected) + setConfirmationQueue(notes) + return + } + + if (itemToBeSelected.component) { + const changeRequiresConfirmation = notes.some((note) => { + const editorForNote = application.componentManager.editorForNote(note) + return application.componentManager.doesEditorChangeRequireAlert(editorForNote, itemToBeSelected.component) + }) + + if (changeRequiresConfirmation) { + const canChange = await application.componentManager.showEditorChangeAlert() + if (!canChange) { + return + } + } + + for (const note of notes) { + void selectComponent(itemToBeSelected.component, note) + } + + return + } + + for (const note of notes) { + void selectNonComponent(itemToBeSelected, note) + } + }, + [ + application.alertService, + application.componentManager, + hasSelectedLockedNotes, + notes, + premiumModal, + selectComponent, + selectNonComponent, + setDisableClickOutside, + ], + ) + + const groupsWithItems = groups.filter((group) => group.items && group.items.length) + + const showSuperImporter = itemToBeSelected?.noteType === NoteType.Super && confirmationQueue.length > 0 + + const closeCurrentSuperNoteImporter = useCallback(() => { + const remainingNotes = confirmationQueue.slice(1) + + if (remainingNotes.length === 0) { + setItemToBeSelected(undefined) + setConfirmationQueue([]) + setDisableClickOutside(false) + return + } + + setConfirmationQueue(remainingNotes) + }, [confirmationQueue, setDisableClickOutside]) + + const handleSuperNoteConversionCompletion = useCallback(() => { + if (!itemToBeSelected) { + return + } + + void selectNonComponent(itemToBeSelected, confirmationQueue[0]) + + closeCurrentSuperNoteImporter() + }, [closeCurrentSuperNoteImporter, confirmationQueue, itemToBeSelected, selectNonComponent]) + + return ( + <> + + {groupsWithItems.map((group, index) => ( + +
+ {group.items.map((item) => { + const onClickEditorItem = () => { + selectItem(item).catch(console.error) + } + return ( + +
+
+ {group.icon && } + {item.name} + {item.isLabs && ( + + Labs + + )} +
+ {!item.isEntitled && } +
+
+ ) + })} +
+
+ ))} +
+ + {confirmationQueue[0] && ( + + )} + + + ) +} + +export default ChangeMultipleMenu diff --git a/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx b/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx index 65bd64840..0a865dba5 100644 --- a/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx +++ b/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx @@ -10,6 +10,7 @@ import { NotesController } from '@/Controllers/NotesController/NotesController' import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' import { LinkingController } from '@/Controllers/LinkingController' +import ChangeMultipleButton from '../ChangeEditor/ChangeMultipleButton' type Props = { application: WebApplication @@ -39,6 +40,9 @@ const MultipleSelectedNotes = ({

{count} selected notes

+
+ +