refactor: note editor relationships (#1821)
This commit is contained in:
33
packages/snjs/lib/Client/NoteViewController.spec.ts
Normal file
33
packages/snjs/lib/Client/NoteViewController.spec.ts
Normal file
@@ -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<SNApplication>
|
||||
application.streamItems = jest.fn()
|
||||
|
||||
const componentManager = {} as jest.Mocked<SNComponentManager>
|
||||
componentManager.getDefaultEditor = jest.fn()
|
||||
Object.defineProperty(application, 'componentManager', { value: componentManager })
|
||||
|
||||
const mutator = {} as jest.Mocked<MutatorService>
|
||||
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 }),
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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<void> {
|
||||
if (!this.item) {
|
||||
const editor = this.application.componentManager.getDefaultEditor()
|
||||
const note = this.application.mutator.createTemplateItem<NoteContent, SNNote>(ContentType.Note, {
|
||||
text: '',
|
||||
title: this.defaultTitle || '',
|
||||
noteType: editor?.noteType || NoteType.Plain,
|
||||
editorIdentifier: editor?.identifier,
|
||||
references: [],
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Models.NoteContent>({
|
||||
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])
|
||||
|
||||
|
||||
29
packages/snjs/lib/Spec/SpecUtils.ts
Normal file
29
packages/snjs/lib/Spec/SpecUtils.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import * as Models from '@standardnotes/models'
|
||||
|
||||
export const createNote = (payload?: Partial<Models.NoteContent>): 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<Models.NoteContent>({
|
||||
title: title,
|
||||
}),
|
||||
...Models.PayloadTimestampDefaults(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user