feat: Added ability to change the note type of multiple notes at once (#2180)

This commit is contained in:
Aman Harwara
2023-01-25 01:20:43 +05:30
committed by GitHub
parent 0e86a0d171
commit af6ae81e1d
3 changed files with 241 additions and 0 deletions

View File

@@ -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<HTMLButtonElement>(null)
const [isChangeMenuOpen, setIsChangeMenuOpen] = useState(false)
const toggleMenu = () => setIsChangeMenuOpen((open) => !open)
const [disableClickOutside, setDisableClickOutside] = useState(false)
return (
<>
<RoundIconButton label={'Change note type'} onClick={toggleMenu} ref={changeButtonRef} icon="plain-text" />
<Popover
title="Change note type"
togglePopover={toggleMenu}
disableClickOutside={disableClickOutside}
anchorElement={changeButtonRef.current}
open={isChangeMenuOpen}
className="pt-2 md:pt-0"
>
<ChangeMultipleMenu
application={application}
notes={notesController.selectedNotes}
setDisableClickOutside={setDisableClickOutside}
/>
</Popover>
</>
)
}
export default ChangeMultipleButton

View File

@@ -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<EditorMenuItem | undefined>()
const [confirmationQueue, setConfirmationQueue] = useState<SNNote[]>([])
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 (
<>
<Menu isOpen={true} a11yLabel="Change note type">
{groupsWithItems.map((group, index) => (
<Fragment key={getGroupId(group)}>
<div className={`border-0 border-t border-solid border-border py-1 ${index === 0 ? 'border-t-0' : ''}`}>
{group.items.map((item) => {
const onClickEditorItem = () => {
selectItem(item).catch(console.error)
}
return (
<MenuItem key={item.name} onClick={onClickEditorItem} className={'flex-row-reversed py-2'}>
<div className="flex flex-grow items-center justify-between">
<div className={'flex items-center'}>
{group.icon && <Icon type={group.icon} className={`mr-2 ${group.iconClassName}`} />}
{item.name}
{item.isLabs && (
<Pill className="py-0.5 px-1.5" style="success">
Labs
</Pill>
)}
</div>
{!item.isEntitled && <Icon type={PremiumFeatureIconName} className={PremiumFeatureIconClass} />}
</div>
</MenuItem>
)
})}
</div>
</Fragment>
))}
</Menu>
<ModalOverlay isOpen={showSuperImporter} onDismiss={closeCurrentSuperNoteImporter}>
{confirmationQueue[0] && (
<SuperNoteImporter
note={confirmationQueue[0]}
application={application}
onConvertComplete={handleSuperNoteConversionCompletion}
closeDialog={closeCurrentSuperNoteImporter}
/>
)}
</ModalOverlay>
</>
)
}
export default ChangeMultipleMenu

View File

@@ -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 = ({
<div className="flex w-full items-center justify-between p-4">
<h1 className="m-0 text-lg font-bold">{count} selected notes</h1>
<div className="flex">
<div className="mr-3">
<ChangeMultipleButton application={application} notesController={notesController} />
</div>
<div className="mr-3">
<PinNoteButton notesController={notesController} />
</div>