From 2b66ff82eea277cc2a7deebdc2afdf52e5103d81 Mon Sep 17 00:00:00 2001 From: Mo Date: Tue, 18 Oct 2022 08:59:24 -0500 Subject: [PATCH] refactor: note editor relationships (#1821) --- package.json | 2 +- .../features/src/Domain/Component/NoteType.ts | 1 + .../Abstract/Item/Types/AppDataField.ts | 2 +- .../Syncable/Component/Component.spec.ts | 39 ++++++++++ .../Domain/Syncable/Component/Component.ts | 5 ++ .../src/Domain/Syncable/Note/Note.spec.ts | 11 +-- .../models/src/Domain/Syncable/Note/Note.ts | 20 +++-- .../src/Domain/Syncable/Note/NoteContent.ts | 4 +- .../Domain/Syncable/Note/NoteMutator.spec.ts | 24 ++++++ .../src/Domain/Syncable/Note/NoteMutator.ts | 14 ++-- .../Utilities/Payload/AffectorFunction.ts | 55 -------------- .../Payload/PayloadsByDuplicating.ts | 9 --- packages/models/src/Domain/index.ts | 1 - .../Component/ComponentManagerInterface.ts | 2 +- .../lib/Client/NoteViewController.spec.ts | 33 ++++++++ .../snjs/lib/Client/NoteViewController.ts | 4 + .../ComponentManager/ComponentManager.spec.ts | 22 ++++++ .../ComponentManager/ComponentManager.ts | 58 +++++++------- .../lib/Services/Items/ItemManager.spec.ts | 76 ++++++++----------- packages/snjs/lib/Spec/SpecUtils.ts | 29 +++++++ .../snjs/mocha/model_tests/appmodels.test.js | 7 +- .../snjs/mocha/sync_tests/conflicting.test.js | 53 ------------- .../ChangeEditor/ChangeEditorMenu.tsx | 53 +++++-------- .../ChangeEditor/createEditorMenuGroups.ts | 8 +- .../Components/NoteView/NoteView.test.ts | 66 ++++++++++++---- .../Components/NoteView/NoteView.tsx | 42 +++++----- .../__mocks__/@standardnotes/snjs.js | 12 +-- yarn.lock | 4 +- 28 files changed, 357 insertions(+), 299 deletions(-) create mode 100644 packages/models/src/Domain/Syncable/Note/NoteMutator.spec.ts delete mode 100644 packages/models/src/Domain/Utilities/Payload/AffectorFunction.ts create mode 100644 packages/snjs/lib/Client/NoteViewController.spec.ts create mode 100644 packages/snjs/lib/Spec/SpecUtils.ts diff --git a/package.json b/package.json index d8ac0b1e4..61e6372ad 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "css-loader": "^6.7.1", "eslint": "^8.17.0", "eslint-plugin-prettier": "^4.2.1", - "husky": "^8.0.0", + "husky": "^8.0.1", "lint-staged": "^13.0.1", "npm-check-updates": "^14.1.1", "prettier": "^2.6.2", diff --git a/packages/features/src/Domain/Component/NoteType.ts b/packages/features/src/Domain/Component/NoteType.ts index 61b731481..a816b2b16 100644 --- a/packages/features/src/Domain/Component/NoteType.ts +++ b/packages/features/src/Domain/Component/NoteType.ts @@ -5,4 +5,5 @@ export enum NoteType { RichText = 'rich-text', Spreadsheet = 'spreadsheet', Task = 'task', + Plain = 'plain-text', } diff --git a/packages/models/src/Domain/Abstract/Item/Types/AppDataField.ts b/packages/models/src/Domain/Abstract/Item/Types/AppDataField.ts index ea530b0c3..abe599a88 100644 --- a/packages/models/src/Domain/Abstract/Item/Types/AppDataField.ts +++ b/packages/models/src/Domain/Abstract/Item/Types/AppDataField.ts @@ -8,6 +8,6 @@ export enum AppDataField { NotAvailableOnMobile = 'notAvailableOnMobile', MobileActive = 'mobileActive', LastSize = 'lastSize', - PrefersPlainEditor = 'prefersPlainEditor', + LegacyPrefersPlainEditor = 'prefersPlainEditor', ComponentInstallError = 'installError', } diff --git a/packages/models/src/Domain/Syncable/Component/Component.spec.ts b/packages/models/src/Domain/Syncable/Component/Component.spec.ts index 68ccc5399..839aacd2c 100644 --- a/packages/models/src/Domain/Syncable/Component/Component.spec.ts +++ b/packages/models/src/Domain/Syncable/Component/Component.spec.ts @@ -5,6 +5,7 @@ import { FillItemContent } from '../../Abstract/Content/ItemContent' import { SNComponent } from './Component' import { ComponentContent } from './ComponentContent' import { PayloadTimestampDefaults } from '../../Abstract/Payload' +import { NoteType } from '@standardnotes/features' describe('component model', () => { it('valid hosted url should ignore url', () => { @@ -46,4 +47,42 @@ describe('component model', () => { expect(component.hasValidHostedUrl()).toBe(true) expect(component.hosted_url).toBe('http://foo.com') }) + + it('should return noteType as specified in package_info', () => { + const component = new SNComponent( + new DecryptedPayload( + { + uuid: String(Math.random()), + content_type: ContentType.Component, + content: FillItemContent({ + package_info: { + note_type: NoteType.Authentication, + }, + } as ComponentContent), + ...PayloadTimestampDefaults(), + }, + PayloadSource.Constructor, + ), + ) + + expect(component.noteType).toEqual(NoteType.Authentication) + }) + + it('should return plain as noteType if no note type defined in package_info', () => { + const component = new SNComponent( + new DecryptedPayload( + { + uuid: String(Math.random()), + content_type: ContentType.Component, + content: FillItemContent({ + package_info: {}, + } as ComponentContent), + ...PayloadTimestampDefaults(), + }, + PayloadSource.Constructor, + ), + ) + + expect(component.noteType).toEqual(NoteType.Plain) + }) }) diff --git a/packages/models/src/Domain/Syncable/Component/Component.ts b/packages/models/src/Domain/Syncable/Component/Component.ts index 595043d14..29fed92bd 100644 --- a/packages/models/src/Domain/Syncable/Component/Component.ts +++ b/packages/models/src/Domain/Syncable/Component/Component.ts @@ -7,6 +7,7 @@ import { ComponentFlag, ComponentPermission, FindNativeFeature, + NoteType, } from '@standardnotes/features' import { AppDataField } from '../../Abstract/Item/Types/AppDataField' import { ComponentContent, ComponentInterface } from './ComponentContent' @@ -177,6 +178,10 @@ export class SNComponent extends DecryptedItem implements Comp return this.package_info as ThirdPartyFeatureDescription } + public get noteType(): NoteType { + return this.package_info.note_type || NoteType.Plain + } + public get isDeprecated(): boolean { let flags: string[] = this.package_info.flags ?? [] flags = flags.map((flag: string) => flag.toLowerCase()) diff --git a/packages/models/src/Domain/Syncable/Note/Note.spec.ts b/packages/models/src/Domain/Syncable/Note/Note.spec.ts index c5704f1e2..98f5e2946 100644 --- a/packages/models/src/Domain/Syncable/Note/Note.spec.ts +++ b/packages/models/src/Domain/Syncable/Note/Note.spec.ts @@ -29,14 +29,9 @@ describe('SNNote Tests', () => { expect(note.preview_html).toBeFalsy() }) - it('should set mobilePrefersPlainEditor when given a valid choice', () => { - const selected = createNote({ - mobilePrefersPlainEditor: true, - }) + it('should not set default value for note type if none is provided', () => { + const note = createNote({}) - const unselected = createNote() - - expect(selected.mobilePrefersPlainEditor).toBeTruthy() - expect(unselected.mobilePrefersPlainEditor).toBe(undefined) + expect(note.noteType).toBe(undefined) }) }) diff --git a/packages/models/src/Domain/Syncable/Note/Note.ts b/packages/models/src/Domain/Syncable/Note/Note.ts index 21a963bea..f04050c28 100644 --- a/packages/models/src/Domain/Syncable/Note/Note.ts +++ b/packages/models/src/Domain/Syncable/Note/Note.ts @@ -1,7 +1,8 @@ +import { AppDataField } from './../../Abstract/Item/Types/AppDataField' import { ContentType } from '@standardnotes/common' +import { FeatureIdentifier, NoteType } from '@standardnotes/features' import { DecryptedItem } from '../../Abstract/Item/Implementations/DecryptedItem' import { ItemInterface } from '../../Abstract/Item/Interfaces/ItemInterface' -import { AppDataField } from '../../Abstract/Item/Types/AppDataField' import { DecryptedPayloadInterface } from '../../Abstract/Payload/Interfaces/DecryptedPayload' import { NoteContent, NoteContentSpecialized } from './NoteContent' @@ -10,12 +11,14 @@ export const isNote = (x: ItemInterface): x is SNNote => x.content_type === Cont export class SNNote extends DecryptedItem implements NoteContentSpecialized { public readonly title: string public readonly text: string - public readonly mobilePrefersPlainEditor?: boolean public readonly hidePreview: boolean = false public readonly preview_plain: string public readonly preview_html: string - public readonly prefersPlainEditor: boolean public readonly spellcheck?: boolean + public readonly noteType?: NoteType + + /** The package_info.identifier of the editor (not its uuid), such as org.standardnotes.advanced-markdown */ + public readonly editorIdentifier?: FeatureIdentifier | string constructor(payload: DecryptedPayloadInterface) { super(payload) @@ -26,9 +29,14 @@ export class SNNote extends DecryptedItem implements NoteContentSpe this.preview_html = String(this.payload.content.preview_html || '') this.hidePreview = Boolean(this.payload.content.hidePreview) this.spellcheck = this.payload.content.spellcheck + this.noteType = this.payload.content.noteType + this.editorIdentifier = this.payload.content.editorIdentifier - this.prefersPlainEditor = this.getAppDomainValueWithDefault(AppDataField.PrefersPlainEditor, false) - - this.mobilePrefersPlainEditor = this.payload.content.mobilePrefersPlainEditor + if (!this.noteType) { + const prefersPlain = this.getAppDomainValueWithDefault(AppDataField.LegacyPrefersPlainEditor, false) + if (prefersPlain) { + this.noteType = NoteType.Plain + } + } } } diff --git a/packages/models/src/Domain/Syncable/Note/NoteContent.ts b/packages/models/src/Domain/Syncable/Note/NoteContent.ts index 6e4202c55..146a27c2b 100644 --- a/packages/models/src/Domain/Syncable/Note/NoteContent.ts +++ b/packages/models/src/Domain/Syncable/Note/NoteContent.ts @@ -1,13 +1,15 @@ +import { FeatureIdentifier, NoteType } from '@standardnotes/features' import { ItemContent } from '../../Abstract/Content/ItemContent' export interface NoteContentSpecialized { title: string text: string - mobilePrefersPlainEditor?: boolean hidePreview?: boolean preview_plain?: string preview_html?: string spellcheck?: boolean + noteType?: NoteType + editorIdentifier?: FeatureIdentifier | string } export type NoteContent = NoteContentSpecialized & ItemContent diff --git a/packages/models/src/Domain/Syncable/Note/NoteMutator.spec.ts b/packages/models/src/Domain/Syncable/Note/NoteMutator.spec.ts new file mode 100644 index 000000000..310bb8f3a --- /dev/null +++ b/packages/models/src/Domain/Syncable/Note/NoteMutator.spec.ts @@ -0,0 +1,24 @@ +import { NoteMutator } from './NoteMutator' +import { createNote } from './../../Utilities/Test/SpecUtils' +import { MutationType } from '../../Abstract/Item' +import { FeatureIdentifier, NoteType } from '@standardnotes/features' + +describe('note mutator', () => { + it('sets noteType', () => { + const note = createNote({}) + const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps) + mutator.noteType = NoteType.Authentication + const result = mutator.getResult() + + expect(result.content.noteType).toEqual(NoteType.Authentication) + }) + + it('sets editorIdentifier', () => { + const note = createNote({}) + const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps) + mutator.editorIdentifier = FeatureIdentifier.MarkdownProEditor + const result = mutator.getResult() + + expect(result.content.editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor) + }) +}) diff --git a/packages/models/src/Domain/Syncable/Note/NoteMutator.ts b/packages/models/src/Domain/Syncable/Note/NoteMutator.ts index f41152ca0..d618b8427 100644 --- a/packages/models/src/Domain/Syncable/Note/NoteMutator.ts +++ b/packages/models/src/Domain/Syncable/Note/NoteMutator.ts @@ -1,10 +1,10 @@ -import { AppDataField } from '../../Abstract/Item/Types/AppDataField' import { NoteContent } from './NoteContent' import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator' import { SNNote } from './Note' import { NoteToNoteReference } from '../../Abstract/Reference/NoteToNoteReference' import { ContentType } from '@standardnotes/common' import { ContentReferenceType } from '../../Abstract/Item' +import { FeatureIdentifier, NoteType } from '@standardnotes/features' export class NoteMutator extends DecryptedItemMutator { set title(title: string) { @@ -27,14 +27,18 @@ export class NoteMutator extends DecryptedItemMutator { this.mutableContent.preview_html = preview_html } - set prefersPlainEditor(prefersPlainEditor: boolean) { - this.setAppDataItem(AppDataField.PrefersPlainEditor, prefersPlainEditor) - } - set spellcheck(spellcheck: boolean) { this.mutableContent.spellcheck = spellcheck } + set noteType(noteType: NoteType) { + this.mutableContent.noteType = noteType + } + + set editorIdentifier(identifier: FeatureIdentifier | string | undefined) { + this.mutableContent.editorIdentifier = identifier + } + toggleSpellcheck(): void { if (this.mutableContent.spellcheck == undefined) { this.mutableContent.spellcheck = false diff --git a/packages/models/src/Domain/Utilities/Payload/AffectorFunction.ts b/packages/models/src/Domain/Utilities/Payload/AffectorFunction.ts deleted file mode 100644 index a75e37c65..000000000 --- a/packages/models/src/Domain/Utilities/Payload/AffectorFunction.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { DecryptedPayloadInterface } from './../../Abstract/Payload/Interfaces/DecryptedPayload' -import { ComponentContent } from '../../Syncable/Component/ComponentContent' -import { ComponentArea } from '@standardnotes/features' -import { ContentType } from '@standardnotes/common' -import { ComponentMutator, SNComponent } from '../../Syncable/Component' -import { CreateDecryptedItemFromPayload } from '../Item/ItemGenerator' -import { ImmutablePayloadCollection } from '../../Runtime/Collection/Payload/ImmutablePayloadCollection' -import { MutationType } from '../../Abstract/Item/Types/MutationType' -import { FullyFormedPayloadInterface } from '../../Abstract/Payload/Interfaces/UnionTypes' -import { isDecryptedPayload } from '../../Abstract/Payload' -import { SyncResolvedPayload } from '../../Runtime/Deltas/Utilities/SyncResolvedPayload' - -export type AffectorFunction = ( - basePayload: FullyFormedPayloadInterface, - duplicatePayload: FullyFormedPayloadInterface, - baseCollection: ImmutablePayloadCollection, -) => SyncResolvedPayload[] - -const NoteDuplicationAffectedPayloads: AffectorFunction = ( - basePayload: FullyFormedPayloadInterface, - duplicatePayload: FullyFormedPayloadInterface, - baseCollection: ImmutablePayloadCollection, -) => { - /** If note has editor, maintain editor relationship in duplicate note */ - const components = baseCollection - .all(ContentType.Component) - .filter(isDecryptedPayload) - .map((payload) => { - return CreateDecryptedItemFromPayload( - payload as DecryptedPayloadInterface, - ) - }) - - const editor = components - .filter((c) => c.area === ComponentArea.Editor) - .find((e) => { - return e.isExplicitlyEnabledForItem(basePayload.uuid) - }) - - if (!editor) { - return [] - } - - /** Modify the editor to include new note */ - const mutator = new ComponentMutator(editor, MutationType.NoUpdateUserTimestamps) - mutator.associateWithItem(duplicatePayload.uuid) - - const result = mutator.getResult() as SyncResolvedPayload - - return [result] -} - -export const AffectorMapping = { - [ContentType.Note]: NoteDuplicationAffectedPayloads, -} as Partial> diff --git a/packages/models/src/Domain/Utilities/Payload/PayloadsByDuplicating.ts b/packages/models/src/Domain/Utilities/Payload/PayloadsByDuplicating.ts index 755ff980e..dc553e7f1 100644 --- a/packages/models/src/Domain/Utilities/Payload/PayloadsByDuplicating.ts +++ b/packages/models/src/Domain/Utilities/Payload/PayloadsByDuplicating.ts @@ -2,7 +2,6 @@ import { PayloadSource } from './../../Abstract/Payload/Types/PayloadSource' import { extendArray, UuidGenerator } from '@standardnotes/utils' import { ImmutablePayloadCollection } from '../../Runtime/Collection/Payload/ImmutablePayloadCollection' import { ItemContent } from '../../Abstract/Content/ItemContent' -import { AffectorMapping } from './AffectorFunction' import { PayloadsByUpdatingReferencingPayloadReferences } from './PayloadsByUpdatingReferencingPayloadReferences' import { isDecryptedPayload } from '../../Abstract/Payload/Interfaces/TypeCheck' import { FullyFormedPayloadInterface } from '../../Abstract/Payload/Interfaces/UnionTypes' @@ -69,13 +68,5 @@ export function PayloadsByDuplicating(dto: extendArray(results, updatedReferencing) } - const affector = AffectorMapping[payload.content_type] - if (affector) { - const affected = affector(payload, copy, baseCollection) - if (affected) { - extendArray(results, affected) - } - } - return results } diff --git a/packages/models/src/Domain/index.ts b/packages/models/src/Domain/index.ts index eea35ae28..5e4492cef 100644 --- a/packages/models/src/Domain/index.ts +++ b/packages/models/src/Domain/index.ts @@ -74,7 +74,6 @@ export * from './Utilities/Item/FindItem' export * from './Utilities/Item/ItemContentsDiffer' export * from './Utilities/Item/ItemContentsEqual' export * from './Utilities/Item/ItemGenerator' -export * from './Utilities/Payload/AffectorFunction' export * from './Utilities/Payload/CopyPayloadWithContentOverride' export * from './Utilities/Payload/CreatePayload' export * from './Utilities/Payload/FindPayload' diff --git a/packages/services/src/Domain/Component/ComponentManagerInterface.ts b/packages/services/src/Domain/Component/ComponentManagerInterface.ts index 5c7ab5d49..fa4ba18f8 100644 --- a/packages/services/src/Domain/Component/ComponentManagerInterface.ts +++ b/packages/services/src/Domain/Component/ComponentManagerInterface.ts @@ -20,5 +20,5 @@ export interface ComponentManagerInterface { urlOverride?: string, ): ComponentViewerInterface presentPermissionsDialog(_dialog: PermissionDialog): void - getDefaultEditor(): SNComponent + getDefaultEditor(): SNComponent | undefined } diff --git a/packages/snjs/lib/Client/NoteViewController.spec.ts b/packages/snjs/lib/Client/NoteViewController.spec.ts new file mode 100644 index 000000000..07aaef454 --- /dev/null +++ b/packages/snjs/lib/Client/NoteViewController.spec.ts @@ -0,0 +1,33 @@ +import { SNApplication } from './../Application/Application' +import { ContentType } from '@standardnotes/common' +import { MutatorService } from './../Services/Mutator/MutatorService' +import { SNComponentManager } from './../Services/ComponentManager/ComponentManager' +import { NoteType } from '@standardnotes/features' +import { NoteViewController } from './NoteViewController' + +describe('note view controller', () => { + let application: SNApplication + + beforeEach(() => { + application = {} as jest.Mocked + application.streamItems = jest.fn() + + const componentManager = {} as jest.Mocked + componentManager.getDefaultEditor = jest.fn() + Object.defineProperty(application, 'componentManager', { value: componentManager }) + + const mutator = {} as jest.Mocked + mutator.createTemplateItem = jest.fn() + Object.defineProperty(application, 'mutator', { value: mutator }) + }) + + it('should create notes with plaintext note type', async () => { + const controller = new NoteViewController(application) + await controller.initialize(false) + + expect(application.mutator.createTemplateItem).toHaveBeenCalledWith( + ContentType.Note, + expect.objectContaining({ noteType: NoteType.Plain }), + ) + }) +}) diff --git a/packages/snjs/lib/Client/NoteViewController.ts b/packages/snjs/lib/Client/NoteViewController.ts index fa7885f92..2a3e8eb7a 100644 --- a/packages/snjs/lib/Client/NoteViewController.ts +++ b/packages/snjs/lib/Client/NoteViewController.ts @@ -1,3 +1,4 @@ +import { NoteType } from '@standardnotes/features' import { NoteMutator, SNNote, @@ -66,9 +67,12 @@ export class NoteViewController implements ItemViewControllerInterface { async initialize(addTagHierarchy: boolean): Promise { if (!this.item) { + const editor = this.application.componentManager.getDefaultEditor() const note = this.application.mutator.createTemplateItem(ContentType.Note, { text: '', title: this.defaultTitle || '', + noteType: editor?.noteType || NoteType.Plain, + editorIdentifier: editor?.identifier, references: [], }) diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts b/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts index b791e535a..881d58b04 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentManager.spec.ts @@ -3,12 +3,14 @@ */ import { SNPreferencesService } from '../Preferences/PreferencesService' +import { createNote } from './../../Spec/SpecUtils' import { ComponentAction, ComponentPermission, FeatureDescription, FindNativeFeature, FeatureIdentifier, + NoteType, } from '@standardnotes/features' import { ContentType } from '@standardnotes/common' import { GenericItem, SNComponent, Environment, Platform } from '@standardnotes/models' @@ -306,6 +308,26 @@ describe('featuresService', () => { }) }) + describe('editors', () => { + it('getEditorForNote should return undefined is note type is plain', () => { + const note = createNote({ + noteType: NoteType.Plain, + }) + const manager = createManager(Environment.Web, Platform.MacWeb) + + expect(manager.editorForNote(note)).toBe(undefined) + }) + + it('getEditorForNote should call legacy function if no note editorIdentifier or noteType', () => { + const note = createNote({}) + const manager = createManager(Environment.Web, Platform.MacWeb) + manager['legacyGetEditorForNote'] = jest.fn() + manager.editorForNote(note) + + expect(manager['legacyGetEditorForNote']).toHaveBeenCalled() + }) + }) + describe('editor change alert', () => { it('should not require alert switching from plain editor', () => { const manager = createManager(Environment.Web, Platform.MacWeb) diff --git a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts index 4c41ab47a..a99e86507 100644 --- a/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts +++ b/packages/snjs/lib/Services/ComponentManager/ComponentManager.ts @@ -17,7 +17,14 @@ import { import { SNSyncService } from '@Lib/Services/Sync/SyncService' import find from 'lodash/find' import uniq from 'lodash/uniq' -import { ComponentArea, ComponentAction, ComponentPermission, FindNativeFeature } from '@standardnotes/features' +import { + ComponentArea, + ComponentAction, + ComponentPermission, + FindNativeFeature, + NoteType, + FeatureIdentifier, +} from '@standardnotes/features' import { Copy, filterFromArray, removeFromArray, sleep, assert } from '@standardnotes/utils' import { UuidString } from '@Lib/Types/UuidString' import { AllowedBatchContentTypes } from '@Lib/Services/ComponentManager/Types' @@ -112,6 +119,10 @@ export class SNComponentManager }) } + componentWithIdentifier(identifier: FeatureIdentifier | string): SNComponent | undefined { + return this.components.find((component) => component.identifier === identifier) + } + override deinit(): void { super.deinit() @@ -569,13 +580,6 @@ export class SNComponentManager } allComponentIframes(): HTMLIFrameElement[] { - if (this.isMobile) { - /** - * Retrieving all iframes is typically related to lifecycle management of - * non-editor components. So this function is not useful to mobile. - */ - return [] - } return Array.from(document.getElementsByTagName('iframe')) } @@ -584,23 +588,29 @@ export class SNComponentManager } editorForNote(note: SNNote): SNComponent | undefined { + if (note.editorIdentifier) { + return this.componentWithIdentifier(note.editorIdentifier) + } + + if (note.noteType === NoteType.Plain) { + return undefined + } + + return this.legacyGetEditorForNote(note) + } + + /** + * Uses legacy approach of note/editor association. New method uses note.editorIdentifier and note.noteType directly. + */ + private legacyGetEditorForNote(note: SNNote): SNComponent | undefined { const editors = this.componentsForArea(ComponentArea.Editor) for (const editor of editors) { if (editor.isExplicitlyEnabledForItem(note.uuid)) { return editor } } - let defaultEditor - /* No editor found for note. Use default editor, if note does not prefer system editor */ - if (this.isMobile) { - if (!note.mobilePrefersPlainEditor) { - defaultEditor = this.getDefaultEditor() - } - } else { - if (!note.prefersPlainEditor) { - defaultEditor = this.getDefaultEditor() - } - } + const defaultEditor = this.getDefaultEditor() + if (defaultEditor && !defaultEditor.isExplicitlyDisabledForItem(note.uuid)) { return defaultEditor } else { @@ -608,15 +618,9 @@ export class SNComponentManager } } - getDefaultEditor(): SNComponent { + getDefaultEditor(): SNComponent | undefined { const editors = this.componentsForArea(ComponentArea.Editor) - if (this.isMobile) { - return editors.filter((e) => { - return e.isMobileDefault - })[0] - } else { - return editors.filter((e) => e.isDefaultEditor())[0] - } + return editors.filter((e) => e.isDefaultEditor())[0] } permissionsStringForPermissions(permissions: ComponentPermission[], component: SNComponent): string { diff --git a/packages/snjs/lib/Services/Items/ItemManager.spec.ts b/packages/snjs/lib/Services/Items/ItemManager.spec.ts index b4a413a61..ed5c1b2c8 100644 --- a/packages/snjs/lib/Services/Items/ItemManager.spec.ts +++ b/packages/snjs/lib/Services/Items/ItemManager.spec.ts @@ -14,6 +14,7 @@ import { SmartView, SystemViewId, } from '@standardnotes/models' +import { createNoteWithTitle } from '../../Spec/SpecUtils' const setupRandomUuid = () => { UuidGenerator.SetGenerator(() => String(Math.random())) @@ -83,19 +84,6 @@ describe('itemManager', () => { ) } - const createNote = (title: string) => { - return new Models.SNNote( - new Models.DecryptedPayload({ - uuid: String(Math.random()), - content_type: ContentType.Note, - content: Models.FillItemContent({ - title: title, - }), - ...PayloadTimestampDefaults(), - }), - ) - } - const createFile = (name: string) => { return new Models.FileItem( new Models.DecryptedPayload({ @@ -168,7 +156,7 @@ describe('itemManager', () => { it('viewing notes with tag', async () => { itemManager = createService() const tag = createTag('parent') - const note = createNote('note') + const note = createNoteWithTitle('note') await itemManager.insertItems([tag, note]) await itemManager.addTagToNote(note, tag, false) @@ -185,9 +173,9 @@ describe('itemManager', () => { it('viewing trashed notes smart view should include archived notes', async () => { itemManager = createService() - const archivedNote = createNote('archived') - const trashedNote = createNote('trashed') - const archivedAndTrashedNote = createNote('archived&trashed') + const archivedNote = createNoteWithTitle('archived') + const trashedNote = createNoteWithTitle('trashed') + const archivedAndTrashedNote = createNoteWithTitle('archived&trashed') await itemManager.insertItems([archivedNote, trashedNote, archivedAndTrashedNote]) @@ -389,8 +377,8 @@ describe('itemManager', () => { await itemManager.insertItems([parentTag, childTag]) await itemManager.setTagParent(parentTag, childTag) - const parentNote = createNote('parentNote') - const childNote = createNote('childNote') + const parentNote = createNoteWithTitle('parentNote') + const childNote = createNoteWithTitle('childNote') await itemManager.insertItems([parentNote, childNote]) await itemManager.addTagToNote(parentNote, parentTag, false) @@ -477,7 +465,7 @@ describe('itemManager', () => { const fooDelimiter = await itemManager.createTag('bar.foo') const barFooDelimiter = await itemManager.createTag('baz.bar.foo') const fooAttached = await itemManager.createTag('Foo') - const note = createNote('note') + const note = createNoteWithTitle('note') await itemManager.insertItems([foo, foobar, bar, barfoo, fooDelimiter, barFooDelimiter, fooAttached, note]) await itemManager.addTagToNote(note, fooAttached, false) @@ -501,8 +489,8 @@ describe('itemManager', () => { await itemManager.insertItems([parentTag, childTag]) await itemManager.setTagParent(parentTag, childTag) - const parentNote = createNote('parentNote') - const childNote = createNote('childNote') + const parentNote = createNoteWithTitle('parentNote') + const childNote = createNoteWithTitle('childNote') await itemManager.insertItems([parentNote, childNote]) await itemManager.addTagToNote(parentNote, parentTag, false) @@ -519,8 +507,8 @@ describe('itemManager', () => { const tag1 = createTag('tag 1') await itemManager.insertItems([tag1]) - const note1 = createNote('note 1') - const note2 = createNote('note 2') + const note1 = createNoteWithTitle('note 1') + const note2 = createNoteWithTitle('note 2') await itemManager.insertItems([note1, note2]) await itemManager.addTagToNote(note1, tag1, false) @@ -654,8 +642,8 @@ describe('itemManager', () => { const view = itemManager.untaggedNotesSmartView const tag = createTag('tag') - const untaggedNote = createNote('note') - const taggedNote = createNote('taggedNote') + const untaggedNote = createNoteWithTitle('note') + const taggedNote = createNoteWithTitle('taggedNote') await itemManager.insertItems([tag, untaggedNote, taggedNote]) expect(itemManager.notesMatchingSmartView(view)).toHaveLength(2) @@ -704,7 +692,7 @@ describe('itemManager', () => { itemManager = createService() const parentTag = createTag('parent') const childTag = createTag('child') - const note = createNote('note') + const note = createNoteWithTitle('note') await itemManager.insertItems([parentTag, childTag, note]) await itemManager.setTagParent(parentTag, childTag) @@ -722,7 +710,7 @@ describe('itemManager', () => { itemManager = createService() const parentTag = createTag('parent') const childTag = createTag('child') - const note = createNote('note') + const note = createNoteWithTitle('note') await itemManager.insertItems([parentTag, childTag, note]) await itemManager.setTagParent(parentTag, childTag) @@ -772,7 +760,7 @@ describe('itemManager', () => { it('should link file with note', async () => { itemManager = createService() - const note = createNote('invoices') + const note = createNoteWithTitle('invoices') const file = createFile('invoice_1.pdf') await itemManager.insertItems([note, file]) @@ -785,7 +773,7 @@ describe('itemManager', () => { it('should unlink file from note', async () => { itemManager = createService() - const note = createNote('invoices') + const note = createNoteWithTitle('invoices') const file = createFile('invoice_1.pdf') await itemManager.insertItems([note, file]) @@ -798,7 +786,7 @@ describe('itemManager', () => { it('should get files linked with note', async () => { itemManager = createService() - const note = createNote('invoices') + const note = createNoteWithTitle('invoices') const file = createFile('invoice_1.pdf') const secondFile = createFile('unrelated-file.xlsx') await itemManager.insertItems([note, file, secondFile]) @@ -813,8 +801,8 @@ describe('itemManager', () => { it('should link note to note', async () => { itemManager = createService() - const note = createNote('research') - const note2 = createNote('citation') + const note = createNoteWithTitle('research') + const note2 = createNoteWithTitle('citation') await itemManager.insertItems([note, note2]) const resultingNote = await itemManager.linkNoteToNote(note, note2) @@ -839,9 +827,9 @@ describe('itemManager', () => { it('should get the relationship type for two items', async () => { itemManager = createService() - const firstNote = createNote('First note') - const secondNote = createNote('Second note') - const unlinkedNote = createNote('Unlinked note') + const firstNote = createNoteWithTitle('First note') + const secondNote = createNoteWithTitle('Second note') + const unlinkedNote = createNoteWithTitle('Unlinked note') await itemManager.insertItems([firstNote, secondNote, unlinkedNote]) const firstNoteLinkedToSecond = await itemManager.linkNoteToNote(firstNote, secondNote) @@ -860,8 +848,8 @@ describe('itemManager', () => { it('should unlink itemToUnlink from item', async () => { itemManager = createService() - const note = createNote('Note 1') - const note2 = createNote('Note 2') + const note = createNoteWithTitle('Note 1') + const note2 = createNoteWithTitle('Note 2') await itemManager.insertItems([note, note2]) const linkedItem = await itemManager.linkNoteToNote(note, note2) @@ -909,9 +897,9 @@ describe('itemManager', () => { it('should get all linked notes for item', async () => { itemManager = createService() - const baseNote = createNote('note') - const noteToLink1 = createNote('A1') - const noteToLink2 = createNote('B2') + const baseNote = createNoteWithTitle('note') + const noteToLink1 = createNoteWithTitle('A1') + const noteToLink2 = createNoteWithTitle('B2') await itemManager.insertItems([baseNote, noteToLink1, noteToLink2]) @@ -927,9 +915,9 @@ describe('itemManager', () => { it('should get all notes linking to item', async () => { itemManager = createService() - const baseNote = createNote('note') - const noteToLink1 = createNote('A1') - const noteToLink2 = createNote('B2') + const baseNote = createNoteWithTitle('note') + const noteToLink1 = createNoteWithTitle('A1') + const noteToLink2 = createNoteWithTitle('B2') await itemManager.insertItems([baseNote, noteToLink1, noteToLink2]) diff --git a/packages/snjs/lib/Spec/SpecUtils.ts b/packages/snjs/lib/Spec/SpecUtils.ts new file mode 100644 index 000000000..cdb5083a1 --- /dev/null +++ b/packages/snjs/lib/Spec/SpecUtils.ts @@ -0,0 +1,29 @@ +import { ContentType } from '@standardnotes/common' +import * as Models from '@standardnotes/models' + +export const createNote = (payload?: Partial): Models.SNNote => { + return new Models.SNNote( + new Models.DecryptedPayload( + { + uuid: String(Math.random()), + content_type: ContentType.Note, + content: Models.FillItemContent({ ...payload }), + ...Models.PayloadTimestampDefaults(), + }, + Models.PayloadSource.Constructor, + ), + ) +} + +export const createNoteWithTitle = (title: string) => { + return new Models.SNNote( + new Models.DecryptedPayload({ + uuid: String(Math.random()), + content_type: ContentType.Note, + content: Models.FillItemContent({ + title: title, + }), + ...Models.PayloadTimestampDefaults(), + }), + ) +} diff --git a/packages/snjs/mocha/model_tests/appmodels.test.js b/packages/snjs/mocha/model_tests/appmodels.test.js index ca7e2660f..5b6aca03b 100644 --- a/packages/snjs/mocha/model_tests/appmodels.test.js +++ b/packages/snjs/mocha/model_tests/appmodels.test.js @@ -356,14 +356,13 @@ describe('app models', () => { }) it('maintains editor reference when duplicating note', async function () { - const note = await Factory.createMappedNote(this.application) const editor = await this.application.itemManager.createItem( ContentType.Component, - { area: ComponentArea.Editor }, + { area: ComponentArea.Editor, package_info: { identifier: 'foo-editor' } }, true, ) - await this.application.itemManager.changeComponent(editor, (mutator) => { - mutator.associateWithItem(note.uuid) + const note = await Factory.insertItemWithOverride(this.application, ContentType.Note, { + editorIdentifier: 'foo-editor', }) expect(this.application.componentManager.editorForNote(note).uuid).to.equal(editor.uuid) diff --git a/packages/snjs/mocha/sync_tests/conflicting.test.js b/packages/snjs/mocha/sync_tests/conflicting.test.js index 9da947311..87dd0100c 100644 --- a/packages/snjs/mocha/sync_tests/conflicting.test.js +++ b/packages/snjs/mocha/sync_tests/conflicting.test.js @@ -114,59 +114,6 @@ describe('online conflict handling', function () { await this.sharedFinalAssertions() }) - it('duplicating note should maintain editor ref', async function () { - const note = await Factory.createSyncedNote(this.application) - this.expectedItemCount++ - const basePayload = createDirtyPayload(ContentType.Component) - const payload = basePayload.copy({ - content: { - ...basePayload.content, - area: ComponentArea.Editor, - }, - }) - const editor = await this.application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged) - this.expectedItemCount++ - await this.application.syncService.sync(syncOptions) - - await this.application.mutator.changeAndSaveItem( - editor, - (mutator) => { - mutator.associateWithItem(note.uuid) - }, - undefined, - undefined, - syncOptions, - ) - - expect(this.application.componentManager.editorForNote(note)).to.be.ok - - /** Conflict the note */ - /** First modify the item without saving so that - * our local contents digress from the server's */ - await this.application.mutator.changeItem(note, (mutator) => { - mutator.title = `${Math.random()}` - }) - - await Factory.changePayloadTimeStampAndSync( - this.application, - note.payload, - Factory.dateToMicroseconds(Factory.yesterday()), - { - title: 'zar', - }, - syncOptions, - ) - - this.expectedItemCount++ - - const duplicate = this.application.itemManager.getDisplayableNotes().find((n) => { - return n.uuid !== note.uuid - }) - expect(duplicate).to.be.ok - expect(this.application.componentManager.editorForNote(duplicate)).to.be.ok - await this.sharedFinalAssertions() - }) - it('should create conflicted copy if incoming server item attempts to overwrite local dirty item', async function () { // create an item and sync it const note = await Factory.createMappedNote(this.application) diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx index a2ba480dd..845cc90ff 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorMenu.tsx @@ -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 = ({ } 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], diff --git a/packages/web/src/javascripts/Components/ChangeEditor/createEditorMenuGroups.ts b/packages/web/src/javascripts/Components/ChangeEditor/createEditorMenuGroups.ts index 6d94cd67d..283050caa 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/createEditorMenuGroups.ts +++ b/packages/web/src/javascripts/Components/ChangeEditor/createEditorMenuGroups.ts @@ -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 = { - 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', diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts b/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts index 544200089..95ff7d89c 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.test.ts @@ -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 - notesState = {} as jest.Mocked - notesState.setShowProtectedWarning = jest.fn() + notesController = {} as jest.Mocked + notesController.setShowProtectedWarning = jest.fn() + notesController.getSpellcheckStateForNote = jest.fn() viewControllerManager = { - notesController: notesState, + notesController: notesController, } as jest.Mocked application = {} as jest.Mocked @@ -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 + + const view = createNoteView() + view.reloadEditorComponent = jest.fn() + view.setState = jest.fn() + + const changedItem = { + noteType: NoteType.Plain, + } as jest.Mocked + view.onNoteInnerChange(changedItem, PayloadEmitSource.LocalChanged) + + expect(view.reloadEditorComponent).toHaveBeenCalled() + }) + + it('should reload editor if editorIdentifier changes', async () => { + noteViewController.item = { + editorIdentifier: 'foo', + } as jest.Mocked + + const view = createNoteView() + view.reloadEditorComponent = jest.fn() + view.setState = jest.fn() + + const changedItem = { + editorIdentifier: 'bar', + } as jest.Mocked + 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) }) }) }) diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index f0c7a966f..57886fe1f 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -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 = { @@ -151,6 +155,8 @@ class NoteView extends PureComponent { rightResizerWidth: 0, rightResizerOffset: 0, shouldStickyHeader: false, + editorFeatureIdentifier: this.controller.item.editorIdentifier, + noteType: this.controller.item.noteType, } this.editorContentRef = createRef() @@ -249,7 +255,7 @@ class NoteView extends PureComponent { } } - 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 { }) } + 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 { } } - 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 { 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, diff --git a/packages/web/src/javascripts/__mocks__/@standardnotes/snjs.js b/packages/web/src/javascripts/__mocks__/@standardnotes/snjs.js index e512f92c2..cd10116d4 100644 --- a/packages/web/src/javascripts/__mocks__/@standardnotes/snjs.js +++ b/packages/web/src/javascripts/__mocks__/@standardnotes/snjs.js @@ -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 diff --git a/yarn.lock b/yarn.lock index be57108b9..bdd4a15af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5925,7 +5925,7 @@ __metadata: css-loader: ^6.7.1 eslint: ^8.17.0 eslint-plugin-prettier: ^4.2.1 - husky: ^8.0.0 + husky: ^8.0.1 lint-staged: ^13.0.1 npm-check-updates: ^14.1.1 prettier: ^2.6.2 @@ -20622,7 +20622,7 @@ __metadata: languageName: node linkType: hard -"husky@npm:^8.0.0": +"husky@npm:^8.0.1": version: 8.0.1 resolution: "husky@npm:8.0.1" bin: