refactor: note editor relationships (#1821)
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user