refactor: note editor relationships (#1821)

This commit is contained in:
Mo
2022-10-18 08:59:24 -05:00
committed by GitHub
parent c83dc48d3f
commit 2b66ff82ee
28 changed files with 357 additions and 299 deletions

View File

@@ -9,6 +9,7 @@ import {
ComponentArea,
ItemMutator,
NoteMutator,
NoteType,
PrefKey,
SNComponent,
SNNote,
@@ -19,10 +20,6 @@ import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
import { createEditorMenuGroups } from './createEditorMenuGroups'
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
import {
transactionForAssociateComponentWithCurrentNote,
transactionForDisassociateComponentWithCurrentNote,
} from '../NoteView/TransactionFunctions'
import { reloadFont } from '../NoteView/FontFunctions'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
@@ -97,42 +94,28 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
}
if (!component) {
if (!note.prefersPlainEditor) {
transactions.push({
itemUuid: note.uuid,
mutate: (m: ItemMutator) => {
const noteMutator = m as NoteMutator
noteMutator.prefersPlainEditor = true
},
})
}
const currentEditor = application.componentManager.editorForNote(note)
if (currentEditor?.isExplicitlyEnabledForItem(note.uuid)) {
transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note))
}
transactions.push({
itemUuid: note.uuid,
mutate: (m: ItemMutator) => {
const noteMutator = m as NoteMutator
noteMutator.noteType = NoteType.Plain
noteMutator.editorIdentifier = undefined
},
})
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
} else if (component.area === ComponentArea.Editor) {
const currentEditor = application.componentManager.editorForNote(note)
if (currentEditor && component.uuid !== currentEditor.uuid) {
transactions.push(transactionForDisassociateComponentWithCurrentNote(currentEditor, note))
}
const prefersPlain = note.prefersPlainEditor
if (prefersPlain) {
transactions.push({
itemUuid: note.uuid,
mutate: (m: ItemMutator) => {
const noteMutator = m as NoteMutator
noteMutator.prefersPlainEditor = false
},
})
}
transactions.push(transactionForAssociateComponentWithCurrentNote(component, note))
} else {
transactions.push({
itemUuid: note.uuid,
mutate: (m: ItemMutator) => {
const noteMutator = m as NoteMutator
noteMutator.noteType = component.noteType
noteMutator.editorIdentifier = component.identifier
},
})
}
await application.mutator.runTransactionalMutations(transactions)
/** Dirtying can happen above */
application.sync.sync().catch(console.error)
setCurrentEditor(application.componentManager.editorForNote(note))
},
[application],

View File

@@ -12,15 +12,13 @@ import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
type EditorGroup = NoteType | 'plain' | 'others'
type EditorGroup = NoteType | 'others'
const getEditorGroup = (featureDescription: FeatureDescription): EditorGroup => {
if (featureDescription.note_type) {
return featureDescription.note_type
} else if (featureDescription.file_type) {
switch (featureDescription.file_type) {
case 'txt':
return 'plain'
case 'html':
return NoteType.RichText
case 'md':
@@ -34,7 +32,7 @@ const getEditorGroup = (featureDescription: FeatureDescription): EditorGroup =>
export const createEditorMenuGroups = (application: WebApplication, editors: SNComponent[]) => {
const editorItems: Record<EditorGroup, EditorMenuItem[]> = {
plain: [
'plain-text': [
{
name: PLAIN_EDITOR_NAME,
isEntitled: true,
@@ -79,7 +77,7 @@ export const createEditorMenuGroups = (application: WebApplication, editors: SNC
icon: 'plain-text',
iconClassName: 'text-accessory-tint-1',
title: 'Plain text',
items: editorItems.plain,
items: editorItems['plain-text'],
},
{
icon: 'rich-text',

View File

@@ -10,15 +10,16 @@ import {
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
NoteViewController,
SNNote,
NoteType,
PayloadEmitSource,
} from '@standardnotes/snjs'
import NoteView from './NoteView'
describe('NoteView', () => {
let noteViewController: NoteViewController
let application: WebApplication
let viewControllerManager: ViewControllerManager
let notesState: NotesController
let notesController: NotesController
const createNoteView = () =>
new NoteView({
@@ -31,11 +32,12 @@ describe('NoteView', () => {
noteViewController = {} as jest.Mocked<NoteViewController>
notesState = {} as jest.Mocked<NotesController>
notesState.setShowProtectedWarning = jest.fn()
notesController = {} as jest.Mocked<NotesController>
notesController.setShowProtectedWarning = jest.fn()
notesController.getSpellcheckStateForNote = jest.fn()
viewControllerManager = {
notesController: notesState,
notesController: notesController,
} as jest.Mocked<ViewControllerManager>
application = {} as jest.Mocked<WebApplication>
@@ -59,7 +61,7 @@ describe('NoteView', () => {
await createNoteView().onAppEvent(ApplicationEvent.UnprotectedSessionExpired)
expect(notesState.setShowProtectedWarning).toHaveBeenCalledWith(true)
expect(notesController.setShowProtectedWarning).toHaveBeenCalledWith(true)
})
it('should postpone the note hiding by correct time if the time passed after its last modification is less than the allowed idle time', async () => {
@@ -77,11 +79,11 @@ describe('NoteView', () => {
jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000)
expect(notesState.setShowProtectedWarning).not.toHaveBeenCalled()
expect(notesController.setShowProtectedWarning).not.toHaveBeenCalled()
jest.advanceTimersByTime(1 * 1000)
expect(notesState.setShowProtectedWarning).toHaveBeenCalledWith(true)
expect(notesController.setShowProtectedWarning).toHaveBeenCalledWith(true)
})
it('should postpone the note hiding by correct time if the user continued editing it even after the protection session has expired', async () => {
@@ -105,10 +107,10 @@ describe('NoteView', () => {
secondsAfterWhichTheNoteShouldHide = ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction
jest.advanceTimersByTime((secondsAfterWhichTheNoteShouldHide - 1) * 1000)
expect(notesState.setShowProtectedWarning).not.toHaveBeenCalled()
expect(notesController.setShowProtectedWarning).not.toHaveBeenCalled()
jest.advanceTimersByTime(1 * 1000)
expect(notesState.setShowProtectedWarning).toHaveBeenCalledWith(true)
expect(notesController.setShowProtectedWarning).toHaveBeenCalledWith(true)
})
})
@@ -120,7 +122,43 @@ describe('NoteView', () => {
await createNoteView().onAppEvent(ApplicationEvent.UnprotectedSessionExpired)
expect(notesState.setShowProtectedWarning).not.toHaveBeenCalled()
expect(notesController.setShowProtectedWarning).not.toHaveBeenCalled()
})
})
describe('editors', () => {
it('should reload editor if noteType changes', async () => {
noteViewController.item = {
noteType: NoteType.Code,
} as jest.Mocked<SNNote>
const view = createNoteView()
view.reloadEditorComponent = jest.fn()
view.setState = jest.fn()
const changedItem = {
noteType: NoteType.Plain,
} as jest.Mocked<SNNote>
view.onNoteInnerChange(changedItem, PayloadEmitSource.LocalChanged)
expect(view.reloadEditorComponent).toHaveBeenCalled()
})
it('should reload editor if editorIdentifier changes', async () => {
noteViewController.item = {
editorIdentifier: 'foo',
} as jest.Mocked<SNNote>
const view = createNoteView()
view.reloadEditorComponent = jest.fn()
view.setState = jest.fn()
const changedItem = {
editorIdentifier: 'bar',
} as jest.Mocked<SNNote>
view.onNoteInnerChange(changedItem, PayloadEmitSource.LocalChanged)
expect(view.reloadEditorComponent).toHaveBeenCalled()
})
})
@@ -142,7 +180,7 @@ describe('NoteView', () => {
await noteView.dismissProtectedWarning()
expect(notesState.setShowProtectedWarning).toHaveBeenCalledWith(false)
expect(notesController.setShowProtectedWarning).toHaveBeenCalledWith(false)
})
it('should not reveal note contents if the authorization has not been passed', async () => {
@@ -155,7 +193,7 @@ describe('NoteView', () => {
await noteView.dismissProtectedWarning()
expect(notesState.setShowProtectedWarning).not.toHaveBeenCalled()
expect(notesController.setShowProtectedWarning).not.toHaveBeenCalled()
})
})
@@ -170,7 +208,7 @@ describe('NoteView', () => {
await noteView.dismissProtectedWarning()
expect(notesState.setShowProtectedWarning).toHaveBeenCalledWith(false)
expect(notesController.setShowProtectedWarning).toHaveBeenCalledWith(false)
})
})
})

View File

@@ -16,6 +16,7 @@ import {
Platform,
EditorLineHeight,
EditorFontSize,
NoteType,
} from '@standardnotes/snjs'
import { debounce, isDesktopApplication, isIOS } from '@/Utils'
import { EditorEventSource } from '../../Types/EditorEventSource'
@@ -88,6 +89,9 @@ type State = {
lineHeight?: EditorLineHeight
fontSize?: EditorFontSize
updateSavingIndicator?: boolean
editorFeatureIdentifier?: string
noteType?: NoteType
}
const PlaintextFontSizeMapping: Record<EditorFontSize, string> = {
@@ -151,6 +155,8 @@ class NoteView extends PureComponent<NoteViewProps, State> {
rightResizerWidth: 0,
rightResizerOffset: 0,
shouldStickyHeader: false,
editorFeatureIdentifier: this.controller.item.editorIdentifier,
noteType: this.controller.item.noteType,
}
this.editorContentRef = createRef<HTMLDivElement>()
@@ -249,7 +255,7 @@ class NoteView extends PureComponent<NoteViewProps, State> {
}
}
private onNoteInnerChange(note: SNNote, source: PayloadEmitSource): void {
onNoteInnerChange(note: SNNote, source: PayloadEmitSource): void {
if (note.uuid !== this.note.uuid) {
throw Error('Editor received changes for non-current note')
}
@@ -288,6 +294,15 @@ class NoteView extends PureComponent<NoteViewProps, State> {
})
}
if (note.editorIdentifier !== this.state.editorFeatureIdentifier || note.noteType !== this.state.noteType) {
this.setState({
editorFeatureIdentifier: note.editorIdentifier,
noteType: note.noteType,
})
void this.reloadEditorComponent()
}
this.reloadSpellcheck().catch(console.error)
const isTemplateNoteInsertedToBeInteractableWithEditor = source === PayloadEmitSource.LocalInserted && note.dirty
@@ -476,18 +491,19 @@ class NoteView extends PureComponent<NoteViewProps, State> {
}
}
private async reloadEditorComponent() {
async reloadEditorComponent() {
if (this.state.showProtectedWarning) {
this.destroyCurrentEditorComponent()
return
}
const newEditor = this.application.componentManager.editorForNote(this.note)
/** Editors cannot interact with template notes so the note must be inserted */
if (newEditor && this.controller.isTemplateNote) {
await this.controller.insertTemplatedNote()
this.associateComponentWithCurrentNote(newEditor).catch(console.error)
}
const currentComponentViewer = this.state.editorComponentViewer
if (currentComponentViewer?.componentUuid !== newEditor?.uuid) {
@@ -800,25 +816,17 @@ class NoteView extends PureComponent<NoteViewProps, State> {
toggleStackComponent = async (component: SNComponent) => {
if (!component.isExplicitlyEnabledForItem(this.note.uuid)) {
await this.associateComponentWithCurrentNote(component)
await this.application.mutator.runTransactionalMutation(
transactionForAssociateComponentWithCurrentNote(component, this.note),
)
} else {
await this.disassociateComponentWithCurrentNote(component)
await this.application.mutator.runTransactionalMutation(
transactionForDisassociateComponentWithCurrentNote(component, this.note),
)
}
this.application.sync.sync().catch(console.error)
}
async disassociateComponentWithCurrentNote(component: SNComponent) {
return this.application.mutator.runTransactionalMutation(
transactionForDisassociateComponentWithCurrentNote(component, this.note),
)
}
async associateComponentWithCurrentNote(component: SNComponent) {
return this.application.mutator.runTransactionalMutation(
transactionForAssociateComponentWithCurrentNote(component, this.note),
)
}
registerKeyboardShortcuts() {
this.removeTrashKeyObserver = this.application.io.addKeyObserver({
key: KeyboardKey.Backspace,

View File

@@ -1,11 +1,3 @@
const {
ApplicationEvent,
ProtectionSessionDurations,
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
} = require('@standardnotes/snjs')
const snjs = require('@standardnotes/snjs')
module.exports = {
ApplicationEvent: ApplicationEvent,
ProtectionSessionDurations: ProtectionSessionDurations,
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
}
module.exports = snjs