refactor: importer service (#2674)

This commit is contained in:
Aman Harwara
2023-12-05 02:55:32 +05:30
committed by GitHub
parent 9265e7afe9
commit 85ecb10924
17 changed files with 509 additions and 518 deletions

View File

@@ -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()
})
})

View File

@@ -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<DecryptedTransferPayload<NoteContent>> {
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<NoteContent> {
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 {

View File

@@ -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<string>
},
): Promise<DecryptedTransferPayload<ItemContent>[]>
}
export type CreateNoteFn = (options: {
createdAt: Date
updatedAt: Date
title: string
text: string
noteType?: NoteType
archived?: boolean
pinned?: boolean
trashed?: boolean
editorIdentifier?: NoteContent['editorIdentifier']
}) => DecryptedTransferPayload<NoteContent>
export type CreateTagFn = (options: {
createdAt: Date
updatedAt: Date
title: string
}) => DecryptedTransferPayload<TagContent>

View File

@@ -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<FileItem | undefined>,
_linkFile: (file: FileItem) => Promise<void>,
_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<Converter['convert']>[1] = {
createNote: ({ text }) =>
({
content_type: ContentType.TYPES.Note,
content: {
text,
references: [],
},
}) as unknown as DecryptedTransferPayload<NoteContent>,
createTag: ({ title }) =>
({
content_type: ContentType.TYPES.Tag,
content: {
title,
references: [],
},
}) as unknown as DecryptedTransferPayload<TagContent>,
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<TagContent>).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)

View File

@@ -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<DecryptedTransferPayload[]> {
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('<en-export') && content.includes('</en-export>')
}
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<NoteContent>[] = []
const tags: DecryptedTransferPayload<TagContent>[] = []
@@ -47,10 +51,6 @@ export class EvernoteConverter {
})[0]
}
function addTag(tag: DecryptedTransferPayload<TagContent>) {
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, '</div>\n')
contentHTML = contentHTML.replace(/<li[^>]*>/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<NoteContent> = {
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
}

View File

@@ -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<FileItem | undefined>,
_linkFile: (file: FileItem) => Promise<void>,
_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<NoteContent>
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,
)

View File

@@ -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<DecryptedTransferPayload<NoteContent>> {
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<NoteContent> {
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 <br> with \n so line breaks get recognised
contentElement.innerHTML = contentElement.innerHTML.replace(/<br>/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<NoteContent> | null {
tryParseAsJson(
data: string,
createNote: CreateNoteFn,
convertMarkdownToSuper: (md: string) => string,
): DecryptedTransferPayload<NoteContent> | 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

View File

@@ -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<DecryptedTransferPayload<NoteContent>> {
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,
}),
]
}
}

View File

@@ -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<Converter> = 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<NoteImportType | null> => {
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<string | null> => {
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<DecryptedTransferPayload[]> {
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<DecryptedTransferPayload[]> {
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 []

View File

@@ -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<DecryptedTransferPayload<NoteContent>> {
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,
}),
]
}
}

View File

@@ -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<NoteContent>
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)

View File

@@ -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<DecryptedTransferPayload<NoteContent>[]> {
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<NoteContent> {
createNoteFromItem(item: SimplenoteItem, trashed: boolean, createNote: CreateNoteFn): ReturnType<CreateNoteFn> {
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) {

View File

@@ -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<DecryptedTransferPayload<NoteContent>> {
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,
},
}
}),
]
}
}

View File

@@ -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)

View File

@@ -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))]
}

View File

@@ -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<NoteImportType, string> = {
const NoteImportTypeColors: Record<string, string> = {
evernote: 'bg-[#14cc45] text-[#000]',
simplenote: 'bg-[#3360cc] text-default',
'google-keep': 'bg-[#fbbd00] text-[#000]',
@@ -15,7 +15,7 @@ const NoteImportTypeColors: Record<NoteImportType, string> = {
super: 'bg-accessory-tint-1 text-accessory-tint-1',
}
const NoteImportTypeIcons: Record<NoteImportType, string> = {
const NoteImportTypeIcons: Record<string, string> = {
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)
}}
>

View File

@@ -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<void>
selectFiles: (service?: string) => Promise<void>
}
const ImportModalInitialPage = ({ setFiles, selectFiles }: Props) => {