From 85ecb1092408c5636fbe07131e6be2ac38fb69f0 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Tue, 5 Dec 2023 02:55:32 +0530 Subject: [PATCH] refactor: importer service (#2674) --- .../AegisToAuthenticatorConverter.spec.ts | 65 +----- .../AegisToAuthenticatorConverter.ts | 76 +++---- packages/ui-services/src/Import/Converter.ts | 41 ++++ .../EvernoteConverter.spec.ts | 75 +++---- .../EvernoteConverter/EvernoteConverter.ts | 106 ++++------ .../GoogleKeepConverter.spec.ts | 47 ++-- .../GoogleKeepConverter.ts | 131 ++++++------ .../src/Import/HTMLConverter/HTMLConverter.ts | 57 ++--- packages/ui-services/src/Import/Importer.ts | 200 ++++++++++++------ .../PlaintextConverter/PlaintextConverter.ts | 58 +++-- .../SimplenoteConverter.spec.ts | 27 ++- .../SimplenoteConverter.ts | 72 +++---- .../Import/SuperConverter/SuperConverter.ts | 46 ++-- .../Components/ImportModal/ImportModal.tsx | 3 +- .../ImportModal/ImportModalController.ts | 10 +- .../ImportModal/ImportModalFileItem.tsx | 10 +- .../Components/ImportModal/InitialPage.tsx | 3 +- 17 files changed, 509 insertions(+), 518 deletions(-) create mode 100644 packages/ui-services/src/Import/Converter.ts diff --git a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts index 3f40295cf..e9f656036 100644 --- a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts +++ b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.spec.ts @@ -1,18 +1,9 @@ -import { PureCryptoInterface } from '@standardnotes/sncrypto-common' -import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' import { AegisToAuthenticatorConverter } from './AegisToAuthenticatorConverter' import data from './testData' -import { GenerateUuid } from '@standardnotes/services' describe('AegisConverter', () => { - const crypto = { - generateUUID: () => String(Math.random()), - } as unknown as PureCryptoInterface - - const generateUuid = new GenerateUuid(crypto) - it('should parse entries', () => { - const converter = new AegisToAuthenticatorConverter(generateUuid) + const converter = new AegisToAuthenticatorConverter() const result = converter.parseEntries(data) @@ -31,58 +22,4 @@ describe('AegisConverter', () => { notes: 'Some other service', }) }) - - it('should create note from entries with editor info', () => { - const converter = new AegisToAuthenticatorConverter(generateUuid) - - const parsedEntries = converter.parseEntries(data) - - const result = converter.createNoteFromEntries( - parsedEntries!, - { - lastModified: 123456789, - name: 'test.json', - }, - true, - ) - - expect(result).not.toBeNull() - expect(result.content_type).toBe('Note') - expect(result.created_at).toBeInstanceOf(Date) - expect(result.updated_at).toBeInstanceOf(Date) - expect(result.uuid).not.toBeNull() - expect(result.content.title).toBe('test') - expect(result.content.text).toBe( - '[{"service":"TestMail","account":"test@test.com","secret":"TESTMAILTESTMAILTESTMAILTESTMAIL","notes":"Some note"},{"service":"Some Service","account":"test@test.com","secret":"SOMESERVICESOMESERVICESOMESERVIC","notes":"Some other service"}]', - ) - expect(result.content.noteType).toBe(NoteType.Authentication) - expect(result.content.editorIdentifier).toBe(NativeFeatureIdentifier.TYPES.TokenVaultEditor) - }) - - it('should create note from entries without editor info', () => { - const converter = new AegisToAuthenticatorConverter(generateUuid) - - const parsedEntries = converter.parseEntries(data) - - const result = converter.createNoteFromEntries( - parsedEntries!, - { - lastModified: 123456789, - name: 'test.json', - }, - false, - ) - - expect(result).not.toBeNull() - expect(result.content_type).toBe('Note') - expect(result.created_at).toBeInstanceOf(Date) - expect(result.updated_at).toBeInstanceOf(Date) - expect(result.uuid).not.toBeNull() - expect(result.content.title).toBe('test') - expect(result.content.text).toBe( - '[{"service":"TestMail","account":"test@test.com","secret":"TESTMAILTESTMAILTESTMAILTESTMAIL","notes":"Some note"},{"service":"Some Service","account":"test@test.com","secret":"SOMESERVICESOMESERVICESOMESERVIC","notes":"Some other service"}]', - ) - expect(result.content.noteType).toBeFalsy() - expect(result.content.editorIdentifier).toBeFalsy() - }) }) diff --git a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts index 957c12ef0..519f8c746 100644 --- a/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts +++ b/packages/ui-services/src/Import/AegisConverter/AegisToAuthenticatorConverter.ts @@ -1,8 +1,5 @@ -import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' -import { readFileAsText } from '../Utils' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' -import { ContentType } from '@standardnotes/domain-core' -import { GenerateUuid } from '@standardnotes/services' +import { Converter } from '../Converter' type AegisData = { db: { @@ -26,19 +23,29 @@ type AuthenticatorEntry = { notes: string } -export class AegisToAuthenticatorConverter { - constructor(private _generateUuid: GenerateUuid) {} +export class AegisToAuthenticatorConverter implements Converter { + constructor() {} - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static isValidAegisJson(json: any): boolean { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return json.db && json.db.entries && json.db.entries.every((entry: any) => AegisEntryTypes.includes(entry.type)) + getImportType(): string { + return 'aegis' } - async convertAegisBackupFileToNote( - file: File, - addEditorInfo: boolean, - ): Promise> { + getSupportedFileTypes(): string[] { + return ['application/json'] + } + + isContentValid(content: string): boolean { + try { + const json = JSON.parse(content) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return json.db && json.db.entries && json.db.entries.every((entry: any) => AegisEntryTypes.includes(entry.type)) + } catch (error) { + console.error(error) + } + return false + } + + convert: Converter['convert'] = async (file, { createNote, readFileAsText }) => { const content = await readFileAsText(file) const entries = this.parseEntries(content) @@ -47,34 +54,21 @@ export class AegisToAuthenticatorConverter { throw new Error('Could not parse entries') } - return this.createNoteFromEntries(entries, file, addEditorInfo) - } + const createdAt = file.lastModified ? new Date(file.lastModified) : new Date() + const updatedAt = file.lastModified ? new Date(file.lastModified) : new Date() + const title = file.name.split('.')[0] + const text = JSON.stringify(entries) - createNoteFromEntries( - entries: AuthenticatorEntry[], - file: { - lastModified: number - name: string - }, - addEditorInfo: boolean, - ): DecryptedTransferPayload { - return { - created_at: new Date(file.lastModified), - created_at_timestamp: file.lastModified, - updated_at: new Date(file.lastModified), - updated_at_timestamp: file.lastModified, - uuid: this._generateUuid.execute().getValue(), - content_type: ContentType.TYPES.Note, - content: { - title: file.name.split('.')[0], - text: JSON.stringify(entries), - references: [], - ...(addEditorInfo && { - noteType: NoteType.Authentication, - editorIdentifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor, - }), - }, - } + return [ + createNote({ + createdAt, + updatedAt, + title, + text, + noteType: NoteType.Authentication, + editorIdentifier: NativeFeatureIdentifier.TYPES.TokenVaultEditor, + }), + ] } parseEntries(data: string): AuthenticatorEntry[] | null { diff --git a/packages/ui-services/src/Import/Converter.ts b/packages/ui-services/src/Import/Converter.ts new file mode 100644 index 000000000..d975cfc6a --- /dev/null +++ b/packages/ui-services/src/Import/Converter.ts @@ -0,0 +1,41 @@ +import { NoteType } from '@standardnotes/features' +import { DecryptedTransferPayload, ItemContent, NoteContent, TagContent } from '@standardnotes/models' + +export interface Converter { + getImportType(): string + + getSupportedFileTypes?: () => string[] + getFileExtension?: () => string + + isContentValid: (content: string) => boolean + + convert( + file: File, + dependencies: { + createNote: CreateNoteFn + createTag: CreateTagFn + canUseSuper: boolean + convertHTMLToSuper: (html: string) => string + convertMarkdownToSuper: (markdown: string) => string + readFileAsText: (file: File) => Promise + }, + ): Promise[]> +} + +export type CreateNoteFn = (options: { + createdAt: Date + updatedAt: Date + title: string + text: string + noteType?: NoteType + archived?: boolean + pinned?: boolean + trashed?: boolean + editorIdentifier?: NoteContent['editorIdentifier'] +}) => DecryptedTransferPayload + +export type CreateTagFn = (options: { + createdAt: Date + updatedAt: Date + title: string +}) => DecryptedTransferPayload diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts index c38931ef7..818bbe8a7 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.spec.ts @@ -3,12 +3,12 @@ */ import { ContentType } from '@standardnotes/domain-core' -import { DecryptedTransferPayload, FileItem, NoteContent, TagContent } from '@standardnotes/models' +import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models' import { EvernoteConverter, EvernoteResource } from './EvernoteConverter' import { createTestResourceElement, enex, enexWithNoNoteOrTag } from './testData' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { GenerateUuid } from '@standardnotes/services' -import { SuperConverterServiceInterface } from '@standardnotes/files' +import { Converter } from '../Converter' // Mock dayjs so dayjs.extend() doesn't throw an error in EvernoteConverter.ts jest.mock('dayjs', () => { @@ -28,43 +28,43 @@ describe('EvernoteConverter', () => { generateUUID: () => String(Math.random()), } as unknown as PureCryptoInterface - const superConverterService: SuperConverterServiceInterface = { - isValidSuperString: () => true, - convertOtherFormatToSuperString: (data: string) => data, - convertSuperStringToOtherFormat: async (data: string) => data, - getEmbeddedFileIDsFromSuperString: () => [], - uploadAndReplaceInlineFilesInSuperString: async ( - superString: string, - _uploadFile: (file: File) => Promise, - _linkFile: (file: FileItem) => Promise, - _generateUuid: GenerateUuid, - ) => superString, - } - const generateUuid = new GenerateUuid(crypto) - it('should throw error if DOMParser is not available', () => { - const converter = new EvernoteConverter(superConverterService, generateUuid) + const readFileAsText = async (file: File) => file as unknown as string - const originalDOMParser = window.DOMParser - // @ts-ignore - window.DOMParser = undefined - - expect(() => converter.parseENEXData(enex)).toThrowError() - - window.DOMParser = originalDOMParser - }) + const dependencies: Parameters[1] = { + createNote: ({ text }) => + ({ + content_type: ContentType.TYPES.Note, + content: { + text, + references: [], + }, + }) as unknown as DecryptedTransferPayload, + createTag: ({ title }) => + ({ + content_type: ContentType.TYPES.Tag, + content: { + title, + references: [], + }, + }) as unknown as DecryptedTransferPayload, + convertHTMLToSuper: (data) => data, + convertMarkdownToSuper: jest.fn(), + readFileAsText, + canUseSuper: false, + } it('should throw error if no note or tag in enex', () => { - const converter = new EvernoteConverter(superConverterService, generateUuid) + const converter = new EvernoteConverter(generateUuid) - expect(() => converter.parseENEXData(enexWithNoNoteOrTag)).toThrowError() + expect(converter.convert(enexWithNoNoteOrTag as unknown as File, dependencies)).rejects.toThrowError() }) - it('should parse and strip html', () => { - const converter = new EvernoteConverter(superConverterService, generateUuid) + it('should parse and strip html', async () => { + const converter = new EvernoteConverter(generateUuid) - const result = converter.parseENEXData(enex, false) + const result = await converter.convert(enex as unknown as File, dependencies) expect(result).not.toBeNull() expect(result?.length).toBe(3) @@ -81,10 +81,13 @@ describe('EvernoteConverter', () => { expect((result?.[2] as DecryptedTransferPayload).content.references[1].uuid).toBe(result?.[1].uuid) }) - it('should parse and not strip html', () => { - const converter = new EvernoteConverter(superConverterService, generateUuid) + it('should parse and not strip html', async () => { + const converter = new EvernoteConverter(generateUuid) - const result = converter.parseENEXData(enex, true) + const result = await converter.convert(enex as unknown as File, { + ...dependencies, + canUseSuper: true, + }) expect(result).not.toBeNull() expect(result?.length).toBe(3) @@ -117,7 +120,7 @@ describe('EvernoteConverter', () => { const array = [unorderedList1, unorderedList2] - const converter = new EvernoteConverter(superConverterService, generateUuid) + const converter = new EvernoteConverter(generateUuid) converter.convertListsToSuperFormatIfApplicable(array) expect(unorderedList1.getAttribute('__lexicallisttype')).toBe('check') @@ -148,14 +151,14 @@ describe('EvernoteConverter', () => { const array = [mediaElement1, mediaElement2, mediaElement3] - const converter = new EvernoteConverter(superConverterService, generateUuid) + const converter = new EvernoteConverter(generateUuid) const replacedCount = converter.replaceMediaElementsWithResources(array, resources) expect(replacedCount).toBe(1) }) describe('getResourceFromElement', () => { - const converter = new EvernoteConverter(superConverterService, generateUuid) + const converter = new EvernoteConverter(generateUuid) it('should return undefined if no mime type is present', () => { const resourceElementWithoutMimeType = createTestResourceElement(false) diff --git a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts index 0af064a88..28fcc1104 100644 --- a/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts +++ b/packages/ui-services/src/Import/EvernoteConverter/EvernoteConverter.ts @@ -1,14 +1,12 @@ import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models' -import { readFileAsText } from '../Utils' import dayjs from 'dayjs' import customParseFormat from 'dayjs/plugin/customParseFormat' import utc from 'dayjs/plugin/utc' -import { ContentType } from '@standardnotes/domain-core' import { GenerateUuid } from '@standardnotes/services' -import { SuperConverterServiceInterface } from '@standardnotes/files' -import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' +import { NoteType } from '@standardnotes/features' import MD5 from 'crypto-js/md5' import Base64 from 'crypto-js/enc-base64' +import { Converter } from '../Converter' dayjs.extend(customParseFormat) dayjs.extend(utc) @@ -21,22 +19,28 @@ export type EvernoteResource = { mimeType: string } -export class EvernoteConverter { - constructor( - private superConverterService: SuperConverterServiceInterface, - private _generateUuid: GenerateUuid, - ) {} +export class EvernoteConverter implements Converter { + constructor(private _generateUuid: GenerateUuid) {} - async convertENEXFileToNotesAndTags(file: File, isEntitledToSuper: boolean): Promise { - const content = await readFileAsText(file) - - const notesAndTags = this.parseENEXData(content, isEntitledToSuper) - - return notesAndTags + getImportType(): string { + return 'evernote' } - parseENEXData(data: string, isEntitledToSuper = false) { - const xmlDoc = this.loadXMLString(data, 'xml') + getFileExtension(): string { + return 'enex' + } + + isContentValid(content: string): boolean { + return content.includes('') + } + + convert: Converter['convert'] = async ( + file, + { createNote, createTag, canUseSuper, convertHTMLToSuper, readFileAsText }, + ) => { + const content = await readFileAsText(file) + + const xmlDoc = this.loadXMLString(content, 'xml') const xmlNotes = xmlDoc.getElementsByTagName('note') const notes: DecryptedTransferPayload[] = [] const tags: DecryptedTransferPayload[] = [] @@ -47,10 +51,6 @@ export class EvernoteConverter { })[0] } - function addTag(tag: DecryptedTransferPayload) { - tags.push(tag) - } - for (const [index, xmlNote] of Array.from(xmlNotes).entries()) { const title = xmlNote.getElementsByTagName('title')[0].textContent const created = xmlNote.getElementsByTagName('created')[0]?.textContent @@ -70,7 +70,7 @@ export class EvernoteConverter { const noteElement = contentXml.getElementsByTagName('en-note')[0] const unorderedLists = Array.from(noteElement.getElementsByTagName('ul')) - if (isEntitledToSuper) { + if (canUseSuper) { this.convertListsToSuperFormatIfApplicable(unorderedLists) } @@ -105,37 +105,23 @@ export class EvernoteConverter { } let contentHTML = noteElement.innerHTML - if (!isEntitledToSuper) { + if (!canUseSuper) { contentHTML = contentHTML.replace(/<\/div>/g, '\n') contentHTML = contentHTML.replace(/]*>/g, '\n') contentHTML = contentHTML.trim() } - const text = !isEntitledToSuper - ? this.stripHTML(contentHTML) - : this.superConverterService.convertOtherFormatToSuperString(contentHTML, 'html') + const text = !canUseSuper ? this.stripHTML(contentHTML) : convertHTMLToSuper(contentHTML) const createdAtDate = created ? dayjs.utc(created, dateFormat).toDate() : new Date() const updatedAtDate = updated ? dayjs.utc(updated, dateFormat).toDate() : createdAtDate - const note: DecryptedTransferPayload = { - created_at: createdAtDate, - created_at_timestamp: createdAtDate.getTime(), - updated_at: updatedAtDate, - updated_at_timestamp: updatedAtDate.getTime(), - uuid: this._generateUuid.execute().getValue(), - content_type: ContentType.TYPES.Note, - content: { - title: !title ? `Imported note ${index + 1} from Evernote` : title, - text, - references: [], - ...(isEntitledToSuper - ? { - noteType: NoteType.Super, - editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor, - } - : {}), - }, - } + const note = createNote({ + createdAt: createdAtDate, + updatedAt: updatedAtDate, + title: !title ? `Imported note ${index + 1} from Evernote` : title, + text, + noteType: NoteType.Super, + }) const xmlTags = xmlNote.getElementsByTagName('tag') for (const tagXml of Array.from(xmlTags)) { @@ -143,21 +129,12 @@ export class EvernoteConverter { let tag = findTag(tagName) if (!tag) { const now = new Date() - tag = { - uuid: this._generateUuid.execute().getValue(), - content_type: ContentType.TYPES.Tag, - created_at: now, - created_at_timestamp: now.getTime(), - updated_at: now, - updated_at_timestamp: now.getTime(), - content: { - title: tagName || `Imported tag ${index + 1} from Evernote`, - expanded: false, - iconString: '', - references: [], - }, - } - addTag(tag) + tag = createTag({ + createdAt: now, + updatedAt: now, + title: tagName || `Imported tag ${index + 1} from Evernote`, + }) + tags.push(tag) } note.content.references.push({ content_type: tag.content_type, uuid: tag.uuid }) @@ -291,13 +268,8 @@ export class EvernoteConverter { } loadXMLString(string: string, type: 'html' | 'xml') { - let xmlDoc - if (window.DOMParser) { - const parser = new DOMParser() - xmlDoc = parser.parseFromString(string, `text/${type}`) - } else { - throw new Error('Could not parse XML string') - } + const parser = new DOMParser() + const xmlDoc = parser.parseFromString(string, `text/${type}`) return xmlDoc } diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts index 6bd936368..0fc2a9468 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts @@ -4,33 +4,30 @@ import { jsonTextContentData, htmlTestData, jsonListContentData } from './testData' import { GoogleKeepConverter } from './GoogleKeepConverter' -import { PureCryptoInterface } from '@standardnotes/sncrypto-common' -import { GenerateUuid } from '@standardnotes/services' -import { FileItem, SuperConverterServiceInterface } from '@standardnotes/snjs' +import { ContentType, DecryptedTransferPayload, NoteContent } from '@standardnotes/snjs' +import { CreateNoteFn } from '../Converter' describe('GoogleKeepConverter', () => { - const crypto = { - generateUUID: () => String(Math.random()), - } as unknown as PureCryptoInterface - - const superConverterService: SuperConverterServiceInterface = { - isValidSuperString: () => true, - convertOtherFormatToSuperString: (data: string) => data, - convertSuperStringToOtherFormat: async (data: string) => data, - getEmbeddedFileIDsFromSuperString: () => [], - uploadAndReplaceInlineFilesInSuperString: async ( - superString: string, - _uploadFile: (file: File) => Promise, - _linkFile: (file: FileItem) => Promise, - _generateUuid: GenerateUuid, - ) => superString, - } - const generateUuid = new GenerateUuid(crypto) + const createNote: CreateNoteFn = ({ title, text, createdAt, updatedAt, trashed, archived, pinned }) => + ({ + uuid: Math.random().toString(), + created_at: createdAt, + updated_at: updatedAt, + content_type: ContentType.TYPES.Note, + content: { + title, + text, + trashed, + archived, + pinned, + references: [], + }, + }) as unknown as DecryptedTransferPayload it('should parse json data', () => { - const converter = new GoogleKeepConverter(superConverterService, generateUuid) + const converter = new GoogleKeepConverter() - const textContent = converter.tryParseAsJson(jsonTextContentData, false) + const textContent = converter.tryParseAsJson(jsonTextContentData, createNote, (md) => md) expect(textContent).not.toBeNull() expect(textContent?.created_at).toBeInstanceOf(Date) @@ -43,7 +40,7 @@ describe('GoogleKeepConverter', () => { expect(textContent?.content.archived).toBe(false) expect(textContent?.content.pinned).toBe(false) - const listContent = converter.tryParseAsJson(jsonListContentData, false) + const listContent = converter.tryParseAsJson(jsonListContentData, createNote, (md) => md) expect(listContent).not.toBeNull() expect(listContent?.created_at).toBeInstanceOf(Date) @@ -58,13 +55,15 @@ describe('GoogleKeepConverter', () => { }) it('should parse html data', () => { - const converter = new GoogleKeepConverter(superConverterService, generateUuid) + const converter = new GoogleKeepConverter() const result = converter.tryParseAsHtml( htmlTestData, { name: 'note-2.html', }, + createNote, + (html) => html, false, ) diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts index 3947de55a..17f04788f 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts @@ -1,9 +1,6 @@ -import { ContentType } from '@standardnotes/domain-core' import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' -import { readFileAsText } from '../Utils' -import { GenerateUuid } from '@standardnotes/services' -import { SuperConverterServiceInterface } from '@standardnotes/files' -import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' +import { NoteType } from '@standardnotes/features' +import { Converter, CreateNoteFn } from '../Converter' type Content = | { @@ -25,28 +22,44 @@ type GoogleKeepJsonNote = { userEditedTimestampUsec: number } & Content -export class GoogleKeepConverter { - constructor( - private superConverterService: SuperConverterServiceInterface, - private _generateUuid: GenerateUuid, - ) {} +export class GoogleKeepConverter implements Converter { + constructor() {} - async convertGoogleKeepBackupFileToNote( - file: File, - isEntitledToSuper: boolean, - ): Promise> { - const content = await readFileAsText(file) + getImportType(): string { + return 'google-keep' + } - const possiblePayloadFromJson = this.tryParseAsJson(content, isEntitledToSuper) + getSupportedFileTypes(): string[] { + return ['text/html', 'application/json'] + } - if (possiblePayloadFromJson) { - return possiblePayloadFromJson + isContentValid(content: string): boolean { + try { + const parsed = JSON.parse(content) + return GoogleKeepConverter.isValidGoogleKeepJson(parsed) + } catch (error) { + console.error(error) } - const possiblePayloadFromHtml = this.tryParseAsHtml(content, file, isEntitledToSuper) + return false + } + + convert: Converter['convert'] = async ( + file, + { createNote, canUseSuper, convertHTMLToSuper, convertMarkdownToSuper, readFileAsText }, + ) => { + const content = await readFileAsText(file) + + const possiblePayloadFromJson = this.tryParseAsJson(content, createNote, convertMarkdownToSuper) + + if (possiblePayloadFromJson) { + return [possiblePayloadFromJson] + } + + const possiblePayloadFromHtml = this.tryParseAsHtml(content, file, createNote, convertHTMLToSuper, canUseSuper) if (possiblePayloadFromHtml) { - return possiblePayloadFromHtml + return [possiblePayloadFromHtml] } throw new Error('Could not parse Google Keep backup file') @@ -55,7 +68,9 @@ export class GoogleKeepConverter { tryParseAsHtml( data: string, file: { name: string }, - isEntitledToSuper: boolean, + createNote: CreateNoteFn, + convertHTMLToSuper: (html: string) => string, + canUseSuper: boolean, ): DecryptedTransferPayload { const rootElement = document.createElement('html') rootElement.innerHTML = data @@ -85,18 +100,18 @@ export class GoogleKeepConverter { const checked = item.classList.contains('checked') item.setAttribute('aria-checked', checked ? 'true' : 'false') - if (!isEntitledToSuper) { + if (!canUseSuper) { item.textContent = `- ${checked ? '[x]' : '[ ]'} ${item.textContent?.trim()}\n` } }) }) - if (!isEntitledToSuper) { + if (!canUseSuper) { // Replace
with \n so line breaks get recognised contentElement.innerHTML = contentElement.innerHTML.replace(/
/g, '\n') content = contentElement.textContent } else { - content = this.superConverterService.convertOtherFormatToSuperString(rootElement.innerHTML, 'html') + content = convertHTMLToSuper(rootElement.innerHTML) } if (!content) { @@ -105,25 +120,13 @@ export class GoogleKeepConverter { const title = rootElement.getElementsByClassName('title')[0]?.textContent || file.name - return { - created_at: date, - created_at_timestamp: date.getTime(), - updated_at: date, - updated_at_timestamp: date.getTime(), - uuid: this._generateUuid.execute().getValue(), - content_type: ContentType.TYPES.Note, - content: { - title: title, - text: content, - references: [], - ...(isEntitledToSuper - ? { - noteType: NoteType.Super, - editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor, - } - : {}), - }, - } + return createNote({ + createdAt: date, + updatedAt: date, + title: title, + text: content, + noteType: NoteType.Super, + }) } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -148,7 +151,11 @@ export class GoogleKeepConverter { ) } - tryParseAsJson(data: string, isEntitledToSuper: boolean): DecryptedTransferPayload | null { + tryParseAsJson( + data: string, + createNote: CreateNoteFn, + convertMarkdownToSuper: (md: string) => string, + ): DecryptedTransferPayload | null { try { const parsed = JSON.parse(data) as GoogleKeepJsonNote if (!GoogleKeepConverter.isValidGoogleKeepJson(parsed)) { @@ -165,31 +172,17 @@ export class GoogleKeepConverter { }) .join('\n') } - if (isEntitledToSuper) { - text = this.superConverterService.convertOtherFormatToSuperString(text, 'md') - } - return { - created_at: date, - created_at_timestamp: date.getTime(), - updated_at: date, - updated_at_timestamp: date.getTime(), - uuid: this._generateUuid.execute().getValue(), - content_type: ContentType.TYPES.Note, - content: { - title: parsed.title, - text, - references: [], - archived: Boolean(parsed.isArchived), - trashed: Boolean(parsed.isTrashed), - pinned: Boolean(parsed.isPinned), - ...(isEntitledToSuper - ? { - noteType: NoteType.Super, - editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor, - } - : {}), - }, - } + text = convertMarkdownToSuper(text) + return createNote({ + createdAt: date, + updatedAt: date, + title: parsed.title, + text, + archived: Boolean(parsed.isArchived), + trashed: Boolean(parsed.isTrashed), + pinned: Boolean(parsed.isPinned), + noteType: NoteType.Super, + }) } catch (e) { console.error(e) return null diff --git a/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts b/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts index 044810926..f8002cba0 100644 --- a/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts +++ b/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts @@ -1,22 +1,23 @@ -import { ContentType } from '@standardnotes/domain-core' -import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' +import { NoteType } from '@standardnotes/features' import { parseFileName } from '@standardnotes/filepicker' -import { SuperConverterServiceInterface } from '@standardnotes/files' -import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' -import { GenerateUuid } from '@standardnotes/services' -import { readFileAsText } from '../Utils' +import { Converter } from '../Converter' -export class HTMLConverter { - constructor( - private superConverterService: SuperConverterServiceInterface, - private _generateUuid: GenerateUuid, - ) {} +export class HTMLConverter implements Converter { + constructor() {} - static isHTMLFile(file: File): boolean { - return file.type === 'text/html' + getImportType(): string { + return 'html' } - async convertHTMLFileToNote(file: File, isEntitledToSuper: boolean): Promise> { + getSupportedFileTypes(): string[] { + return ['text/html'] + } + + isContentValid(_content: string): boolean { + return true + } + + convert: Converter['convert'] = async (file, { createNote, convertHTMLToSuper, readFileAsText }) => { const content = await readFileAsText(file) const { name } = parseFileName(file.name) @@ -24,28 +25,16 @@ export class HTMLConverter { const createdAtDate = file.lastModified ? new Date(file.lastModified) : new Date() const updatedAtDate = file.lastModified ? new Date(file.lastModified) : new Date() - const text = isEntitledToSuper - ? this.superConverterService.convertOtherFormatToSuperString(content, 'html') - : content + const text = convertHTMLToSuper(content) - return { - created_at: createdAtDate, - created_at_timestamp: createdAtDate.getTime(), - updated_at: updatedAtDate, - updated_at_timestamp: updatedAtDate.getTime(), - uuid: this._generateUuid.execute().getValue(), - content_type: ContentType.TYPES.Note, - content: { + return [ + createNote({ + createdAt: createdAtDate, + updatedAt: updatedAtDate, title: name, text, - references: [], - ...(isEntitledToSuper - ? { - noteType: NoteType.Super, - editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor, - } - : {}), - }, - } + noteType: NoteType.Super, + }), + ] } } diff --git a/packages/ui-services/src/Import/Importer.ts b/packages/ui-services/src/Import/Importer.ts index b99a90f31..7e359a2cb 100644 --- a/packages/ui-services/src/Import/Importer.ts +++ b/packages/ui-services/src/Import/Importer.ts @@ -24,19 +24,13 @@ import { isNote, } from '@standardnotes/models' import { HTMLConverter } from './HTMLConverter/HTMLConverter' -import { SuperConverterServiceInterface } from '@standardnotes/snjs/dist/@types' import { SuperConverter } from './SuperConverter/SuperConverter' - -export type NoteImportType = 'plaintext' | 'evernote' | 'google-keep' | 'simplenote' | 'aegis' | 'html' | 'super' +import { Converter, CreateNoteFn, CreateTagFn } from './Converter' +import { SuperConverterServiceInterface } from '@standardnotes/files' +import { ContentType } from '@standardnotes/domain-core' export class Importer { - aegisConverter: AegisToAuthenticatorConverter - googleKeepConverter: GoogleKeepConverter - simplenoteConverter: SimplenoteConverter - plaintextConverter: PlaintextConverter - evernoteConverter: EvernoteConverter - htmlConverter: HTMLConverter - superConverter: SuperConverter + converters: Set = new Set() constructor( private features: FeaturesClientInterface, @@ -60,83 +54,155 @@ export class Importer { }, private _generateUuid: GenerateUuid, ) { - this.aegisConverter = new AegisToAuthenticatorConverter(_generateUuid) - this.googleKeepConverter = new GoogleKeepConverter(this.superConverterService, _generateUuid) - this.simplenoteConverter = new SimplenoteConverter(_generateUuid) - this.plaintextConverter = new PlaintextConverter(this.superConverterService, _generateUuid) - this.evernoteConverter = new EvernoteConverter(this.superConverterService, _generateUuid) - this.htmlConverter = new HTMLConverter(this.superConverterService, _generateUuid) - this.superConverter = new SuperConverter(this.superConverterService, _generateUuid) + this.registerNativeConverters() } - detectService = async (file: File): Promise => { + registerNativeConverters() { + this.converters.add(new AegisToAuthenticatorConverter()) + this.converters.add(new GoogleKeepConverter()) + this.converters.add(new SimplenoteConverter()) + this.converters.add(new PlaintextConverter()) + this.converters.add(new EvernoteConverter(this._generateUuid)) + this.converters.add(new HTMLConverter()) + this.converters.add(new SuperConverter(this.superConverterService)) + } + + detectService = async (file: File): Promise => { const content = await readFileAsText(file) const { ext } = parseFileName(file.name) - if (ext === 'enex') { - return 'evernote' - } + for (const converter of this.converters) { + const isCorrectType = converter.getSupportedFileTypes && converter.getSupportedFileTypes().includes(file.type) + const isCorrectExtension = converter.getFileExtension && converter.getFileExtension() === ext - try { - const json = JSON.parse(content) - - if (AegisToAuthenticatorConverter.isValidAegisJson(json)) { - return 'aegis' + if (!isCorrectType && !isCorrectExtension) { + continue } - if (GoogleKeepConverter.isValidGoogleKeepJson(json)) { - return 'google-keep' + if (converter.isContentValid(content)) { + return converter.getImportType() } - - if (SimplenoteConverter.isValidSimplenoteJson(json)) { - return 'simplenote' - } - } catch { - /* empty */ - } - - if (file.type === 'application/json' && this.superConverterService.isValidSuperString(content)) { - return 'super' - } - - if (PlaintextConverter.isValidPlaintextFile(file)) { - return 'plaintext' - } - - if (HTMLConverter.isHTMLFile(file)) { - return 'html' } return null } - async getPayloadsFromFile(file: File, type: NoteImportType): Promise { + createNote: CreateNoteFn = ({ + createdAt, + updatedAt, + title, + text, + noteType, + editorIdentifier, + trashed, + archived, + pinned, + }) => { + if (noteType === NoteType.Super && !this.isEntitledToSuper()) { + noteType = undefined + } + + if ( + editorIdentifier && + this.features.getFeatureStatus(NativeFeatureIdentifier.create(editorIdentifier).getValue()) !== + FeatureStatus.Entitled + ) { + editorIdentifier = undefined + } + + return { + created_at: createdAt, + created_at_timestamp: createdAt.getTime(), + updated_at: updatedAt, + updated_at_timestamp: updatedAt.getTime(), + uuid: this._generateUuid.execute().getValue(), + content_type: ContentType.TYPES.Note, + content: { + title, + text, + references: [], + noteType, + trashed, + archived, + pinned, + editorIdentifier, + }, + } + } + + createTag: CreateTagFn = ({ createdAt, updatedAt, title }) => { + return { + uuid: this._generateUuid.execute().getValue(), + content_type: ContentType.TYPES.Tag, + created_at: createdAt, + created_at_timestamp: createdAt.getTime(), + updated_at: updatedAt, + updated_at_timestamp: updatedAt.getTime(), + content: { + title: title, + expanded: false, + iconString: '', + references: [], + }, + } + } + + isEntitledToSuper = (): boolean => { + return ( + this.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(), + ) === FeatureStatus.Entitled + ) + } + + convertHTMLToSuper = (html: string): string => { + if (!this.isEntitledToSuper()) { + return html + } + + return this.superConverterService.convertOtherFormatToSuperString(html, 'html') + } + + convertMarkdownToSuper = (markdown: string): string => { + if (!this.isEntitledToSuper()) { + return markdown + } + + return this.superConverterService.convertOtherFormatToSuperString(markdown, 'md') + } + + async getPayloadsFromFile(file: File, type: string): Promise { const isEntitledToSuper = this.features.getFeatureStatus( NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(), ) === FeatureStatus.Entitled - if (type === 'super') { - if (!isEntitledToSuper) { - throw new Error('Importing Super notes requires a subscription.') + + if (type === 'super' && !isEntitledToSuper) { + throw new Error('Importing Super notes requires a subscription') + } + + for (const converter of this.converters) { + const isCorrectType = converter.getImportType() === type + + if (!isCorrectType) { + continue } - return [await this.superConverter.convertSuperFileToNote(file)] - } else if (type === 'aegis') { - const isEntitledToAuthenticator = - this.features.getFeatureStatus( - NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(), - ) === FeatureStatus.Entitled - return [await this.aegisConverter.convertAegisBackupFileToNote(file, isEntitledToAuthenticator)] - } else if (type === 'google-keep') { - return [await this.googleKeepConverter.convertGoogleKeepBackupFileToNote(file, isEntitledToSuper)] - } else if (type === 'simplenote') { - return await this.simplenoteConverter.convertSimplenoteBackupFileToNotes(file) - } else if (type === 'evernote') { - return await this.evernoteConverter.convertENEXFileToNotesAndTags(file, isEntitledToSuper) - } else if (type === 'plaintext') { - return [await this.plaintextConverter.convertPlaintextFileToNote(file, isEntitledToSuper)] - } else if (type === 'html') { - return [await this.htmlConverter.convertHTMLFileToNote(file, isEntitledToSuper)] + + const content = await readFileAsText(file) + + if (!converter.isContentValid(content)) { + throw new Error('Content is not valid') + } + + return await converter.convert(file, { + createNote: this.createNote, + createTag: this.createTag, + canUseSuper: isEntitledToSuper, + convertHTMLToSuper: this.convertHTMLToSuper, + convertMarkdownToSuper: this.convertMarkdownToSuper, + readFileAsText, + }) } return [] diff --git a/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts b/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts index 79f698917..1dfe2ff17 100644 --- a/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts +++ b/packages/ui-services/src/Import/PlaintextConverter/PlaintextConverter.ts @@ -1,25 +1,27 @@ -import { ContentType } from '@standardnotes/domain-core' import { parseFileName } from '@standardnotes/filepicker' -import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' -import { readFileAsText } from '../Utils' -import { GenerateUuid } from '@standardnotes/services' -import { SuperConverterServiceInterface } from '@standardnotes/files' -import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' +import { Converter } from '../Converter' +import { NoteType } from '@standardnotes/features' -export class PlaintextConverter { - constructor( - private superConverterService: SuperConverterServiceInterface, - private _generateUuid: GenerateUuid, - ) {} +export class PlaintextConverter implements Converter { + constructor() {} + + getImportType(): string { + return 'plaintext' + } + + getSupportedFileTypes(): string[] { + return ['text/plain', 'text/markdown'] + } + + isContentValid(_content: string): boolean { + return true + } static isValidPlaintextFile(file: File): boolean { return file.type === 'text/plain' || file.type === 'text/markdown' } - async convertPlaintextFileToNote( - file: File, - isEntitledToSuper: boolean, - ): Promise> { + convert: Converter['convert'] = async (file, { createNote, convertMarkdownToSuper, readFileAsText }) => { const content = await readFileAsText(file) const { name } = parseFileName(file.name) @@ -27,24 +29,14 @@ export class PlaintextConverter { const createdAtDate = file.lastModified ? new Date(file.lastModified) : new Date() const updatedAtDate = file.lastModified ? new Date(file.lastModified) : new Date() - return { - created_at: createdAtDate, - created_at_timestamp: createdAtDate.getTime(), - updated_at: updatedAtDate, - updated_at_timestamp: updatedAtDate.getTime(), - uuid: this._generateUuid.execute().getValue(), - content_type: ContentType.TYPES.Note, - content: { + return [ + createNote({ + createdAt: createdAtDate, + updatedAt: updatedAtDate, title: name, - text: isEntitledToSuper ? this.superConverterService.convertOtherFormatToSuperString(content, 'md') : content, - references: [], - ...(isEntitledToSuper - ? { - noteType: NoteType.Super, - editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor, - } - : {}), - }, - } + text: convertMarkdownToSuper(content), + noteType: NoteType.Super, + }), + ] } } diff --git a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts index fb84ba3b6..0d6df8603 100644 --- a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts +++ b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.spec.ts @@ -1,19 +1,28 @@ -import { PureCryptoInterface } from '@standardnotes/sncrypto-common' +import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' import { SimplenoteConverter } from './SimplenoteConverter' import data from './testData' -import { GenerateUuid } from '@standardnotes/services' +import { ContentType } from '@standardnotes/domain-core' +import { CreateNoteFn } from '../Converter' describe('SimplenoteConverter', () => { - const crypto = { - generateUUID: () => String(Math.random()), - } as unknown as PureCryptoInterface - - const generateUuid = new GenerateUuid(crypto) + const createNote: CreateNoteFn = ({ title, text, trashed, createdAt, updatedAt }) => + ({ + uuid: Math.random().toString(), + created_at: createdAt, + updated_at: updatedAt, + content_type: ContentType.TYPES.Note, + content: { + title, + text, + trashed, + references: [], + }, + }) as unknown as DecryptedTransferPayload it('should parse', () => { - const converter = new SimplenoteConverter(generateUuid) + const converter = new SimplenoteConverter() - const result = converter.parse(data) + const result = converter.parse(data, createNote) expect(result).not.toBeNull() expect(result?.length).toBe(3) diff --git a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts index 6f85c003b..1d6b19e38 100644 --- a/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts +++ b/packages/ui-services/src/Import/SimplenoteConverter/SimplenoteConverter.ts @@ -1,7 +1,4 @@ -import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' -import { ContentType } from '@standardnotes/domain-core' -import { readFileAsText } from '../Utils' -import { GenerateUuid } from '@standardnotes/services' +import { Converter, CreateNoteFn } from '../Converter' type SimplenoteItem = { creationDate: string @@ -17,21 +14,34 @@ type SimplenoteData = { // eslint-disable-next-line @typescript-eslint/no-explicit-any const isSimplenoteEntry = (entry: any): boolean => entry.id && entry.content && entry.creationDate && entry.lastModified -export class SimplenoteConverter { - constructor(private _generateUuid: GenerateUuid) {} +export class SimplenoteConverter implements Converter { + constructor() {} - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static isValidSimplenoteJson(json: any): boolean { - return ( - (json.activeNotes && json.activeNotes.every(isSimplenoteEntry)) || - (json.trashedNotes && json.trashedNotes.every(isSimplenoteEntry)) - ) + getImportType(): string { + return 'simplenote' } - async convertSimplenoteBackupFileToNotes(file: File): Promise[]> { + getSupportedFileTypes(): string[] { + return ['application/json'] + } + + isContentValid(content: string): boolean { + try { + const json = JSON.parse(content) + return ( + (json.activeNotes && json.activeNotes.every(isSimplenoteEntry)) || + (json.trashedNotes && json.trashedNotes.every(isSimplenoteEntry)) + ) + } catch (error) { + console.error(error) + } + return false + } + + convert: Converter['convert'] = async (file, { createNote, readFileAsText }) => { const content = await readFileAsText(file) - const notes = this.parse(content) + const notes = this.parse(content, createNote) if (!notes) { throw new Error('Could not parse notes') @@ -40,7 +50,7 @@ export class SimplenoteConverter { return notes } - createNoteFromItem(item: SimplenoteItem, trashed: boolean): DecryptedTransferPayload { + createNoteFromItem(item: SimplenoteItem, trashed: boolean, createNote: CreateNoteFn): ReturnType { const createdAtDate = new Date(item.creationDate) const updatedAtDate = new Date(item.lastModified) @@ -50,32 +60,20 @@ export class SimplenoteConverter { hasTitleAndContent && splitItemContent[0].length ? splitItemContent[0] : createdAtDate.toLocaleString() const content = hasTitleAndContent && splitItemContent[1].length ? splitItemContent[1] : item.content - return { - created_at: createdAtDate, - created_at_timestamp: createdAtDate.getTime(), - updated_at: updatedAtDate, - updated_at_timestamp: updatedAtDate.getTime(), - uuid: this._generateUuid.execute().getValue(), - content_type: ContentType.TYPES.Note, - content: { - title, - text: content, - references: [], - trashed, - appData: { - 'org.standardnotes.sn': { - client_updated_at: updatedAtDate, - }, - }, - }, - } + return createNote({ + createdAt: createdAtDate, + updatedAt: updatedAtDate, + title, + text: content, + trashed, + }) } - parse(data: string) { + parse(data: string, createNote: CreateNoteFn) { try { const parsed = JSON.parse(data) as SimplenoteData - const activeNotes = parsed.activeNotes.reverse().map((item) => this.createNoteFromItem(item, false)) - const trashedNotes = parsed.trashedNotes.reverse().map((item) => this.createNoteFromItem(item, true)) + const activeNotes = parsed.activeNotes.reverse().map((item) => this.createNoteFromItem(item, false, createNote)) + const trashedNotes = parsed.trashedNotes.reverse().map((item) => this.createNoteFromItem(item, true, createNote)) return [...activeNotes, ...trashedNotes] } catch (error) { diff --git a/packages/ui-services/src/Import/SuperConverter/SuperConverter.ts b/packages/ui-services/src/Import/SuperConverter/SuperConverter.ts index b505dc6d1..82d9f53d9 100644 --- a/packages/ui-services/src/Import/SuperConverter/SuperConverter.ts +++ b/packages/ui-services/src/Import/SuperConverter/SuperConverter.ts @@ -1,18 +1,24 @@ import { SuperConverterServiceInterface } from '@standardnotes/files' -import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models' -import { GenerateUuid } from '@standardnotes/services' -import { readFileAsText } from '../Utils' import { parseFileName } from '@standardnotes/filepicker' -import { ContentType } from '@standardnotes/domain-core' -import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' +import { NoteType } from '@standardnotes/features' +import { Converter } from '../Converter' -export class SuperConverter { - constructor( - private converterService: SuperConverterServiceInterface, - private _generateUuid: GenerateUuid, - ) {} +export class SuperConverter implements Converter { + constructor(private converterService: SuperConverterServiceInterface) {} - async convertSuperFileToNote(file: File): Promise> { + getImportType(): string { + return 'super' + } + + getSupportedFileTypes(): string[] { + return ['application/json'] + } + + isContentValid(content: string): boolean { + return this.converterService.isValidSuperString(content) + } + + convert: Converter['convert'] = async (file, { createNote, readFileAsText }) => { const content = await readFileAsText(file) if (!this.converterService.isValidSuperString(content)) { @@ -24,20 +30,14 @@ export class SuperConverter { const createdAtDate = file.lastModified ? new Date(file.lastModified) : new Date() const updatedAtDate = file.lastModified ? new Date(file.lastModified) : new Date() - return { - created_at: createdAtDate, - created_at_timestamp: createdAtDate.getTime(), - updated_at: updatedAtDate, - updated_at_timestamp: updatedAtDate.getTime(), - uuid: this._generateUuid.execute().getValue(), - content_type: ContentType.TYPES.Note, - content: { + return [ + createNote({ + createdAt: createdAtDate, + updatedAt: updatedAtDate, title: name, text: content, - references: [], noteType: NoteType.Super, - editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor, - }, - } + }), + ] } } diff --git a/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx b/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx index 1ddf6e8e6..ed3f56ef3 100644 --- a/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx @@ -13,7 +13,6 @@ import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdow import { ContentType, SNTag } from '@standardnotes/snjs' import Button from '../Button/Button' import { ClassicFileReader } from '@standardnotes/filepicker' -import { NoteImportType } from '@standardnotes/ui-services' const ImportModal = ({ importModalController }: { importModalController: ImportModalController }) => { const application = useApplication() @@ -60,7 +59,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM ) const selectFiles = useCallback( - async (service?: NoteImportType) => { + async (service?: string) => { const files = await ClassicFileReader.selectFiles() addFiles(files, service) diff --git a/packages/web/src/javascripts/Components/ImportModal/ImportModalController.ts b/packages/web/src/javascripts/Components/ImportModal/ImportModalController.ts index 2c084d2a0..3d180281c 100644 --- a/packages/web/src/javascripts/Components/ImportModal/ImportModalController.ts +++ b/packages/web/src/javascripts/Components/ImportModal/ImportModalController.ts @@ -9,7 +9,7 @@ import { PreferencesServiceEvent, UuidGenerator, } from '@standardnotes/snjs' -import { Importer, NoteImportType } from '@standardnotes/ui-services' +import { Importer } from '@standardnotes/ui-services' import { action, makeObservable, observable, runInAction } from 'mobx' import { NavigationController } from '../../Controllers/Navigation/NavigationController' import { LinkingController } from '@/Controllers/LinkingController' @@ -18,7 +18,7 @@ import { AbstractViewController } from '@/Controllers/Abstract/AbstractViewContr type ImportModalFileCommon = { id: string file: File - service: NoteImportType | null | undefined + service: string | null | undefined } export type ImportModalFile = ( @@ -107,7 +107,7 @@ export class ImportModalController extends AbstractViewController { this.preferences.setValue(PrefKey.ExistingTagForImports, tag?.uuid).catch(console.error) } - getImportFromFile = (file: File, service?: NoteImportType) => { + getImportFromFile = (file: File, service?: string) => { return { id: UuidGenerator.GenerateUuid(), file, @@ -116,11 +116,11 @@ export class ImportModalController extends AbstractViewController { } as ImportModalFile } - setFiles = (files: File[], service?: NoteImportType) => { + setFiles = (files: File[], service?: string) => { this.files = files.map((file) => this.getImportFromFile(file, service)) } - addFiles = (files: File[], service?: NoteImportType) => { + addFiles = (files: File[], service?: string) => { this.files = [...this.files, ...files.map((file) => this.getImportFromFile(file, service))] } diff --git a/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx b/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx index 13e810054..58ec31ff3 100644 --- a/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx @@ -1,11 +1,11 @@ import { ImportModalController, ImportModalFile } from '@/Components/ImportModal/ImportModalController' import { classNames, ContentType, pluralize } from '@standardnotes/snjs' -import { Importer, NoteImportType } from '@standardnotes/ui-services' +import { Importer } from '@standardnotes/ui-services' import { observer } from 'mobx-react-lite' import { useCallback, useEffect, useState } from 'react' import Icon from '../Icon/Icon' -const NoteImportTypeColors: Record = { +const NoteImportTypeColors: Record = { evernote: 'bg-[#14cc45] text-[#000]', simplenote: 'bg-[#3360cc] text-default', 'google-keep': 'bg-[#fbbd00] text-[#000]', @@ -15,7 +15,7 @@ const NoteImportTypeColors: Record = { super: 'bg-accessory-tint-1 text-accessory-tint-1', } -const NoteImportTypeIcons: Record = { +const NoteImportTypeIcons: Record = { evernote: 'evernote', simplenote: 'simplenote', 'google-keep': 'gkeep', @@ -39,7 +39,7 @@ const ImportModalFileItem = ({ const [changingService, setChangingService] = useState(false) const setFileService = useCallback( - async (service: NoteImportType | null) => { + async (service: string | null) => { if (!service) { setChangingService(true) } @@ -116,7 +116,7 @@ const ImportModalFileItem = ({ event.preventDefault() const form = event.target as HTMLFormElement const service = form.elements[0] as HTMLSelectElement - void setFileService(service.value as NoteImportType) + void setFileService(service.value) setChangingService(false) }} > diff --git a/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx b/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx index fa32d483e..0816a7240 100644 --- a/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx @@ -5,11 +5,10 @@ import Icon from '../Icon/Icon' import { useApplication } from '../ApplicationProvider' import { FeatureName } from '@/Controllers/FeatureName' import { NativeFeatureIdentifier, FeatureStatus } from '@standardnotes/snjs' -import { NoteImportType } from '@standardnotes/ui-services' type Props = { setFiles: ImportModalController['setFiles'] - selectFiles: (service?: NoteImportType) => Promise + selectFiles: (service?: string) => Promise } const ImportModalInitialPage = ({ setFiles, selectFiles }: Props) => {