From 4207348f9cb6087ea259c535a3772a40e4c4b657 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 18 Aug 2023 00:34:15 +0530 Subject: [PATCH 01/16] feat: super note import option in import modal --- .../Service/SuperConverterServiceInterface.ts | 1 + .../src/Import/HTMLConverter/HTMLConverter.ts | 9 ++++ packages/ui-services/src/Import/Importer.ts | 31 +++++++++++-- .../Import/SuperConverter/SuperConverter.ts | 43 +++++++++++++++++ .../Application/Dependencies/Types.ts | 1 + .../Dependencies/WebDependencies.ts | 13 +++++- .../ImportModal/ImportModalFileItem.tsx | 8 +++- .../Components/ImportModal/InitialPage.tsx | 46 ++++++++----------- .../Tools/HeadlessSuperConverter.tsx | 9 ++++ 9 files changed, 128 insertions(+), 33 deletions(-) create mode 100644 packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts create mode 100644 packages/ui-services/src/Import/SuperConverter/SuperConverter.ts diff --git a/packages/files/src/Domain/Service/SuperConverterServiceInterface.ts b/packages/files/src/Domain/Service/SuperConverterServiceInterface.ts index 2f1fa91f595..727b608ba61 100644 --- a/packages/files/src/Domain/Service/SuperConverterServiceInterface.ts +++ b/packages/files/src/Domain/Service/SuperConverterServiceInterface.ts @@ -1,3 +1,4 @@ export interface SuperConverterServiceInterface { + isValidSuperString(superString: string): boolean convertString: (superString: string, toFormat: 'txt' | 'md' | 'html' | 'json') => string } diff --git a/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts b/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts new file mode 100644 index 00000000000..5a422a9a1b8 --- /dev/null +++ b/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts @@ -0,0 +1,9 @@ +import { GenerateUuid } from '@standardnotes/services' + +export class HTMLConverter { + constructor(_generateUuid: GenerateUuid) {} + + static isHTMLFile(file: File): boolean { + return file.type === 'text/html' + } +} diff --git a/packages/ui-services/src/Import/Importer.ts b/packages/ui-services/src/Import/Importer.ts index 0f825eb9773..a7c985cf276 100644 --- a/packages/ui-services/src/Import/Importer.ts +++ b/packages/ui-services/src/Import/Importer.ts @@ -14,8 +14,11 @@ import { PlaintextConverter } from './PlaintextConverter/PlaintextConverter' import { SimplenoteConverter } from './SimplenoteConverter/SimplenoteConverter' import { readFileAsText } from './Utils' import { DecryptedTransferPayload, NoteContent } 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' +export type NoteImportType = 'plaintext' | 'evernote' | 'google-keep' | 'simplenote' | 'aegis' | 'html' | 'super' export class Importer { aegisConverter: AegisToAuthenticatorConverter @@ -23,11 +26,14 @@ export class Importer { simplenoteConverter: SimplenoteConverter plaintextConverter: PlaintextConverter evernoteConverter: EvernoteConverter + htmlConverter: HTMLConverter + superConverter: SuperConverter constructor( private features: FeaturesClientInterface, private mutator: MutatorClientInterface, private items: ItemManagerInterface, + private superConverterService: SuperConverterServiceInterface, _generateUuid: GenerateUuid, ) { this.aegisConverter = new AegisToAuthenticatorConverter(_generateUuid) @@ -35,9 +41,11 @@ export class Importer { this.simplenoteConverter = new SimplenoteConverter(_generateUuid) this.plaintextConverter = new PlaintextConverter(_generateUuid) this.evernoteConverter = new EvernoteConverter(_generateUuid) + this.htmlConverter = new HTMLConverter(_generateUuid) + this.superConverter = new SuperConverter(this.superConverterService, _generateUuid) } - static detectService = async (file: File): Promise => { + detectService = async (file: File): Promise => { const content = await readFileAsText(file) const { ext } = parseFileName(file.name) @@ -46,6 +54,10 @@ export class Importer { return 'evernote' } + if (file.type === 'application/json' && this.superConverterService.isValidSuperString(content)) { + return 'super' + } + try { const json = JSON.parse(content) @@ -68,11 +80,24 @@ export class Importer { return 'plaintext' } + if (HTMLConverter.isHTMLFile(file)) { + return 'html' + } + return null } async getPayloadsFromFile(file: File, type: NoteImportType): Promise { - if (type === 'aegis') { + if (type === 'super') { + const isEntitledToSuper = + this.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(), + ) === FeatureStatus.Entitled + if (!isEntitledToSuper) { + throw new Error('Importing Super notes requires a subscription.') + } + return [await this.superConverter.convertSuperFileToNote(file)] + } else if (type === 'aegis') { const isEntitledToAuthenticator = this.features.getFeatureStatus( NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.TokenVaultEditor).getValue(), diff --git a/packages/ui-services/src/Import/SuperConverter/SuperConverter.ts b/packages/ui-services/src/Import/SuperConverter/SuperConverter.ts new file mode 100644 index 00000000000..b505dc6d1ae --- /dev/null +++ b/packages/ui-services/src/Import/SuperConverter/SuperConverter.ts @@ -0,0 +1,43 @@ +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' + +export class SuperConverter { + constructor( + private converterService: SuperConverterServiceInterface, + private _generateUuid: GenerateUuid, + ) {} + + async convertSuperFileToNote(file: File): Promise> { + const content = await readFileAsText(file) + + if (!this.converterService.isValidSuperString(content)) { + throw new Error('Content is not valid Super JSON') + } + + const { name } = parseFileName(file.name) + + 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: { + title: name, + text: content, + references: [], + noteType: NoteType.Super, + editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor, + }, + } + } +} diff --git a/packages/web/src/javascripts/Application/Dependencies/Types.ts b/packages/web/src/javascripts/Application/Dependencies/Types.ts index 915398a43f1..7463595617e 100644 --- a/packages/web/src/javascripts/Application/Dependencies/Types.ts +++ b/packages/web/src/javascripts/Application/Dependencies/Types.ts @@ -7,6 +7,7 @@ export const Web_TYPES = { AutolockService: Symbol.for('AutolockService'), ChangelogService: Symbol.for('ChangelogService'), DesktopManager: Symbol.for('DesktopManager'), + SuperConverter: Symbol.for('SuperConverter'), Importer: Symbol.for('Importer'), ItemGroupController: Symbol.for('ItemGroupController'), KeyboardService: Symbol.for('KeyboardService'), diff --git a/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts b/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts index 28774417b64..da93e90f777 100644 --- a/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts +++ b/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts @@ -48,13 +48,24 @@ import { PanesForLayout } from '../UseCase/PanesForLayout' import { LoadPurchaseFlowUrl } from '../UseCase/LoadPurchaseFlowUrl' import { GetPurchaseFlowUrl } from '../UseCase/GetPurchaseFlowUrl' import { OpenSubscriptionDashboard } from '../UseCase/OpenSubscriptionDashboard' +import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter' export class WebDependencies extends DependencyContainer { constructor(private application: WebApplicationInterface) { super() + this.bind(Web_TYPES.SuperConverter, () => { + return new HeadlessSuperConverter() + }) + this.bind(Web_TYPES.Importer, () => { - return new Importer(application.features, application.mutator, application.items, application.generateUuid) + return new Importer( + application.features, + application.mutator, + application.items, + this.get(Web_TYPES.SuperConverter), + application.generateUuid, + ) }) this.bind(Web_TYPES.IsNativeIOS, () => { diff --git a/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx b/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx index ba0878a20bf..e696bed8cf2 100644 --- a/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/ImportModalFileItem.tsx @@ -11,6 +11,8 @@ const NoteImportTypeColors: Record = { 'google-keep': 'bg-[#fbbd00] text-[#000]', aegis: 'bg-[#0d47a1] text-default', plaintext: 'bg-default border border-border', + html: 'bg-accessory-tint-2', + super: 'bg-accessory-tint-1', } const NoteImportTypeIcons: Record = { @@ -19,6 +21,8 @@ const NoteImportTypeIcons: Record = { 'google-keep': 'gkeep', aegis: 'aegis', plaintext: 'plain-text', + html: 'rich-text', + super: 'file-doc', } const ImportModalFileItem = ({ @@ -53,13 +57,13 @@ const ImportModalFileItem = ({ useEffect(() => { const detect = async () => { - const detectedService = await Importer.detectService(file.file) + const detectedService = await importer.detectService(file.file) void setFileService(detectedService) } if (file.service === undefined) { void detect() } - }, [file, setFileService]) + }, [file, importer, setFileService]) const notePayloads = file.status === 'ready' && file.payloads diff --git a/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx b/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx index cb822bf69f7..5cfde488747 100644 --- a/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx @@ -38,41 +38,33 @@ const ImportModalInitialPage = ({ setFiles }: Props) => {
or import from:
- - - - - - */} +
diff --git a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx index b3cd5befc78..a58d6f77430 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx @@ -20,6 +20,15 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface { }) } + isValidSuperString(superString: string): boolean { + try { + this.editor.parseEditorState(superString) + return true + } catch (error) { + return false + } + } + convertString(superString: string, format: 'txt' | 'md' | 'html' | 'json'): string { if (superString.length === 0) { return superString From 96996e72b35fe59b9d6b00a5cbbb8d2655544fab Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 18 Aug 2023 13:13:10 +0530 Subject: [PATCH 02/16] feat: html importing --- .../Service/SuperConverterServiceInterface.ts | 3 +- .../src/Import/HTMLConverter/HTMLConverter.ts | 44 ++++++++++++- packages/ui-services/src/Import/Importer.ts | 12 ++-- .../Components/ImportModal/InitialPage.tsx | 4 +- .../Plugins/ImportPlugin/ImportPlugin.tsx | 5 +- .../Tools/HeadlessSuperConverter.tsx | 65 +++++++++++++++++-- 6 files changed, 117 insertions(+), 16 deletions(-) diff --git a/packages/files/src/Domain/Service/SuperConverterServiceInterface.ts b/packages/files/src/Domain/Service/SuperConverterServiceInterface.ts index 727b608ba61..9e826f67b10 100644 --- a/packages/files/src/Domain/Service/SuperConverterServiceInterface.ts +++ b/packages/files/src/Domain/Service/SuperConverterServiceInterface.ts @@ -1,4 +1,5 @@ export interface SuperConverterServiceInterface { isValidSuperString(superString: string): boolean - convertString: (superString: string, toFormat: 'txt' | 'md' | 'html' | 'json') => string + convertSuperStringToOtherFormat: (superString: string, toFormat: 'txt' | 'md' | 'html' | 'json') => string + convertOtherFormatToSuperString: (otherFormatString: string, fromFormat: 'txt' | 'md' | 'html' | 'json') => string } diff --git a/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts b/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts index 5a422a9a1b8..04481092668 100644 --- a/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts +++ b/packages/ui-services/src/Import/HTMLConverter/HTMLConverter.ts @@ -1,9 +1,51 @@ +import { ContentType } from '@standardnotes/domain-core' +import { NativeFeatureIdentifier, 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' export class HTMLConverter { - constructor(_generateUuid: GenerateUuid) {} + constructor( + private superConverterService: SuperConverterServiceInterface, + private _generateUuid: GenerateUuid, + ) {} static isHTMLFile(file: File): boolean { return file.type === 'text/html' } + + async convertHTMLFileToNote(file: File, isEntitledToSuper: boolean): Promise> { + const content = await readFileAsText(file) + + const { name } = parseFileName(file.name) + + 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 + + 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: name, + text, + references: [], + ...(isEntitledToSuper + ? { + noteType: NoteType.Super, + editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor, + } + : {}), + }, + } + } } diff --git a/packages/ui-services/src/Import/Importer.ts b/packages/ui-services/src/Import/Importer.ts index a7c985cf276..1db2e184085 100644 --- a/packages/ui-services/src/Import/Importer.ts +++ b/packages/ui-services/src/Import/Importer.ts @@ -41,7 +41,7 @@ export class Importer { this.simplenoteConverter = new SimplenoteConverter(_generateUuid) this.plaintextConverter = new PlaintextConverter(_generateUuid) this.evernoteConverter = new EvernoteConverter(_generateUuid) - this.htmlConverter = new HTMLConverter(_generateUuid) + this.htmlConverter = new HTMLConverter(this.superConverterService, _generateUuid) this.superConverter = new SuperConverter(this.superConverterService, _generateUuid) } @@ -88,11 +88,11 @@ export class Importer { } async getPayloadsFromFile(file: File, type: NoteImportType): Promise { + const isEntitledToSuper = + this.features.getFeatureStatus( + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(), + ) === FeatureStatus.Entitled if (type === 'super') { - const isEntitledToSuper = - this.features.getFeatureStatus( - NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(), - ) === FeatureStatus.Entitled if (!isEntitledToSuper) { throw new Error('Importing Super notes requires a subscription.') } @@ -111,6 +111,8 @@ export class Importer { return await this.evernoteConverter.convertENEXFileToNotesAndTags(file, false) } else if (type === 'plaintext') { return [await this.plaintextConverter.convertPlaintextFileToNote(file)] + } else if (type === 'html') { + return [await this.htmlConverter.convertHTMLFileToNote(file, isEntitledToSuper)] } return [] diff --git a/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx b/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx index 5cfde488747..13087c29fd9 100644 --- a/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx @@ -58,10 +58,10 @@ const ImportModalInitialPage = ({ setFiles }: Props) => { Plaintext / Markdown - {/* */} + - diff --git a/packages/web/src/javascripts/Controllers/FeatureName.ts b/packages/web/src/javascripts/Controllers/FeatureName.ts index fef41c61c00..26c257285ba 100644 --- a/packages/web/src/javascripts/Controllers/FeatureName.ts +++ b/packages/web/src/javascripts/Controllers/FeatureName.ts @@ -1,3 +1,4 @@ export enum FeatureName { Files = 'Encrypted File Storage', + Super = 'Super notes', } From cf080132e144a5c59070fc440733d7b5aca23ca4 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 18 Aug 2023 15:06:05 +0530 Subject: [PATCH 12/16] chore: fix feature identifier --- .../web/src/javascripts/Components/ImportModal/InitialPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx b/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx index 28504e9f53c..1cfa3e26cac 100644 --- a/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/InitialPage.tsx @@ -72,7 +72,7 @@ const ImportModalInitialPage = ({ setFiles }: Props) => { onClick={() => { const isEntitledToSuper = application.features.getFeatureStatus( - NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.UniversalSecondFactor).getValue(), + NativeFeatureIdentifier.create(NativeFeatureIdentifier.TYPES.SuperEditor).getValue(), ) === FeatureStatus.Entitled if (!isEntitledToSuper) { application.showPremiumModal(FeatureName.Super) From b0e8164b85404a43f001cf4eca9d6676c9ab10f2 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 18 Aug 2023 15:23:41 +0530 Subject: [PATCH 13/16] fix: don't create tag if no notes are imported --- .../web/src/javascripts/Controllers/ImportModalController.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/web/src/javascripts/Controllers/ImportModalController.ts b/packages/web/src/javascripts/Controllers/ImportModalController.ts index 8c92acd643e..71646df596a 100644 --- a/packages/web/src/javascripts/Controllers/ImportModalController.ts +++ b/packages/web/src/javascripts/Controllers/ImportModalController.ts @@ -155,6 +155,9 @@ export class ImportModalController { console.error(error) } } + if (!importedPayloads.length) { + return + } const currentDate = new Date() const importTagItem = this.items.createTemplateItem(ContentType.TYPES.Tag, { title: `Imported on ${currentDate.toLocaleString()}`, From 32b28ef10801ee6cfd4f7e99122e3e181224e912 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 18 Aug 2023 15:43:37 +0530 Subject: [PATCH 14/16] feat: correctly parse list content in google keep json note --- .../GoogleKeepConverter.spec.ts | 37 +++++++++++++------ .../GoogleKeepConverter.ts | 37 +++++++++++++++++-- .../Import/GoogleKeepConverter/testData.ts | 25 ++++++++++++- 3 files changed, 81 insertions(+), 18 deletions(-) diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts index a621ac85e2c..124c6d1013b 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts @@ -2,7 +2,7 @@ * @jest-environment jsdom */ -import { jsonTestData, htmlTestData } from './testData' +import { jsonTextContentData, htmlTestData, jsonListContentData } from './testData' import { GoogleKeepConverter } from './GoogleKeepConverter' import { PureCryptoInterface } from '@standardnotes/sncrypto-common' import { GenerateUuid } from '@standardnotes/services' @@ -23,18 +23,31 @@ describe('GoogleKeepConverter', () => { it('should parse json data', () => { const converter = new GoogleKeepConverter(superConverterService, generateUuid) - const result = converter.tryParseAsJson(jsonTestData) + const textContent = converter.tryParseAsJson(jsonTextContentData) - expect(result).not.toBeNull() - expect(result?.created_at).toBeInstanceOf(Date) - expect(result?.updated_at).toBeInstanceOf(Date) - expect(result?.uuid).not.toBeNull() - expect(result?.content_type).toBe('Note') - expect(result?.content.title).toBe('Testing 1') - expect(result?.content.text).toBe('This is a test.') - expect(result?.content.trashed).toBe(false) - expect(result?.content.archived).toBe(false) - expect(result?.content.pinned).toBe(false) + expect(textContent).not.toBeNull() + expect(textContent?.created_at).toBeInstanceOf(Date) + expect(textContent?.updated_at).toBeInstanceOf(Date) + expect(textContent?.uuid).not.toBeNull() + expect(textContent?.content_type).toBe('Note') + expect(textContent?.content.title).toBe('Testing 1') + expect(textContent?.content.text).toBe('This is a test.') + expect(textContent?.content.trashed).toBe(false) + expect(textContent?.content.archived).toBe(false) + expect(textContent?.content.pinned).toBe(false) + + const listContent = converter.tryParseAsJson(jsonListContentData) + + expect(listContent).not.toBeNull() + expect(listContent?.created_at).toBeInstanceOf(Date) + expect(listContent?.updated_at).toBeInstanceOf(Date) + expect(listContent?.uuid).not.toBeNull() + expect(listContent?.content_type).toBe('Note') + expect(listContent?.content.title).toBe('Testing 1') + expect(listContent?.content.text).toBe('- [ ] Test 1\n- [x] Test 2') + expect(textContent?.content.trashed).toBe(false) + expect(textContent?.content.archived).toBe(false) + expect(textContent?.content.pinned).toBe(false) }) it('should parse html data', () => { diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts index a8719575966..b6b5250fd19 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts @@ -5,15 +5,25 @@ import { GenerateUuid } from '@standardnotes/services' import { SuperConverterServiceInterface } from '@standardnotes/files' import { NativeFeatureIdentifier, NoteType } from '@standardnotes/features' +type Content = + | { + textContent: string + } + | { + listContent: { + text: string + isChecked: boolean + }[] + } + type GoogleKeepJsonNote = { color: string isTrashed: boolean isPinned: boolean isArchived: boolean - textContent: string title: string userEditedTimestampUsec: number -} +} & Content export class GoogleKeepConverter { constructor( @@ -94,9 +104,18 @@ export class GoogleKeepConverter { // eslint-disable-next-line @typescript-eslint/no-explicit-any static isValidGoogleKeepJson(json: any): boolean { + if (typeof json.textContent !== 'string') { + if (typeof json.listContent === 'object' && Array.isArray(json.listContent)) { + return json.listContent.every( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (item: any) => typeof item.text === 'string' && typeof item.isChecked === 'boolean', + ) + } + return false + } + return ( typeof json.title === 'string' && - typeof json.textContent === 'string' && typeof json.userEditedTimestampUsec === 'number' && typeof json.isArchived === 'boolean' && typeof json.isTrashed === 'boolean' && @@ -112,6 +131,16 @@ export class GoogleKeepConverter { return null } const date = new Date(parsed.userEditedTimestampUsec / 1000) + let text: string + if ('textContent' in parsed) { + text = parsed.textContent + } else { + text = parsed.listContent + .map((item) => { + return item.isChecked ? `- [x] ${item.text}` : `- [ ] ${item.text}` + }) + .join('\n') + } return { created_at: date, created_at_timestamp: date.getTime(), @@ -121,7 +150,7 @@ export class GoogleKeepConverter { content_type: ContentType.TYPES.Note, content: { title: parsed.title, - text: parsed.textContent, + text, references: [], archived: Boolean(parsed.isArchived), trashed: Boolean(parsed.isTrashed), diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/testData.ts b/packages/ui-services/src/Import/GoogleKeepConverter/testData.ts index 37cb2006c5d..4ed4fb461c1 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/testData.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/testData.ts @@ -1,4 +1,4 @@ -const json = { +const jsonWithTextContent = { color: 'DEFAULT', isTrashed: false, isPinned: false, @@ -8,7 +8,28 @@ const json = { userEditedTimestampUsec: 1618528050144000, } -export const jsonTestData = JSON.stringify(json) +export const jsonTextContentData = JSON.stringify(jsonWithTextContent) + +const jsonWithListContent = { + color: 'DEFAULT', + isTrashed: false, + isPinned: false, + isArchived: false, + listContent: [ + { + text: 'Test 1', + isChecked: false, + }, + { + text: 'Test 2', + isChecked: true, + }, + ], + title: 'Testing 1', + userEditedTimestampUsec: 1618528050144000, +} + +export const jsonListContentData = JSON.stringify(jsonWithListContent) export const htmlTestData = ` From dcf5c4989c20cdab7393494ae9bc4c4d09203e81 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 18 Aug 2023 15:51:21 +0530 Subject: [PATCH 15/16] feat: convert json google keep note to super --- .../GoogleKeepConverter.spec.ts | 4 ++-- .../GoogleKeepConverter/GoogleKeepConverter.ts | 14 ++++++++++++-- .../SuperEditor/Tools/HeadlessSuperConverter.tsx | 9 ++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts index 124c6d1013b..4cc57aa1e71 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.spec.ts @@ -23,7 +23,7 @@ describe('GoogleKeepConverter', () => { it('should parse json data', () => { const converter = new GoogleKeepConverter(superConverterService, generateUuid) - const textContent = converter.tryParseAsJson(jsonTextContentData) + const textContent = converter.tryParseAsJson(jsonTextContentData, false) expect(textContent).not.toBeNull() expect(textContent?.created_at).toBeInstanceOf(Date) @@ -36,7 +36,7 @@ describe('GoogleKeepConverter', () => { expect(textContent?.content.archived).toBe(false) expect(textContent?.content.pinned).toBe(false) - const listContent = converter.tryParseAsJson(jsonListContentData) + const listContent = converter.tryParseAsJson(jsonListContentData, false) expect(listContent).not.toBeNull() expect(listContent?.created_at).toBeInstanceOf(Date) diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts index b6b5250fd19..1b2f4c15911 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts @@ -37,7 +37,7 @@ export class GoogleKeepConverter { ): Promise> { const content = await readFileAsText(file) - const possiblePayloadFromJson = this.tryParseAsJson(content) + const possiblePayloadFromJson = this.tryParseAsJson(content, isEntitledToSuper) if (possiblePayloadFromJson) { return possiblePayloadFromJson @@ -124,7 +124,7 @@ export class GoogleKeepConverter { ) } - tryParseAsJson(data: string): DecryptedTransferPayload | null { + tryParseAsJson(data: string, isEntitledToSuper: boolean): DecryptedTransferPayload | null { try { const parsed = JSON.parse(data) as GoogleKeepJsonNote if (!GoogleKeepConverter.isValidGoogleKeepJson(parsed)) { @@ -141,6 +141,9 @@ export class GoogleKeepConverter { }) .join('\n') } + if (isEntitledToSuper) { + text = this.superConverterService.convertOtherFormatToSuperString(text, 'md') + } return { created_at: date, created_at_timestamp: date.getTime(), @@ -155,9 +158,16 @@ export class GoogleKeepConverter { archived: Boolean(parsed.isArchived), trashed: Boolean(parsed.isTrashed), pinned: Boolean(parsed.isPinned), + ...(isEntitledToSuper + ? { + noteType: NoteType.Super, + editorIdentifier: NativeFeatureIdentifier.TYPES.SuperEditor, + } + : {}), }, } } catch (e) { + console.error(e) return null } } diff --git a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx index 654cb6481a3..91b416dfb57 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx +++ b/packages/web/src/javascripts/Components/SuperEditor/Tools/HeadlessSuperConverter.tsx @@ -120,7 +120,14 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface { { discrete: true }, ) } else { - $convertFromMarkdownString(otherFormatString, MarkdownTransformers) + this.editor.update( + () => { + $convertFromMarkdownString(otherFormatString, MarkdownTransformers) + }, + { + discrete: true, + }, + ) } return JSON.stringify(this.editor.getEditorState()) From b88a2025795eb548a8e05b3e8ac3792b4d735f54 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 18 Aug 2023 16:19:29 +0530 Subject: [PATCH 16/16] feat: improve list imports --- .../GoogleKeepConverter.ts | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts index 1b2f4c15911..3947de55a1f 100644 --- a/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts +++ b/packages/ui-services/src/Import/GoogleKeepConverter/GoogleKeepConverter.ts @@ -65,8 +65,32 @@ export class GoogleKeepConverter { headingElement?.remove() const contentElement = rootElement.getElementsByClassName('content')[0] + if (!contentElement) { + throw new Error('Could not parse content. Content element not found.') + } + let content: string | null + // Convert lists to readable plaintext format + // or Super-convertable format + const lists = contentElement.getElementsByTagName('ul') + Array.from(lists).forEach((list) => { + list.setAttribute('__lexicallisttype', 'check') + + const items = list.getElementsByTagName('li') + Array.from(items).forEach((item) => { + const bulletSpan = item.getElementsByClassName('bullet')[0] + bulletSpan?.remove() + + const checked = item.classList.contains('checked') + item.setAttribute('aria-checked', checked ? 'true' : 'false') + + if (!isEntitledToSuper) { + item.textContent = `- ${checked ? '[x]' : '[ ]'} ${item.textContent?.trim()}\n` + } + }) + }) + if (!isEntitledToSuper) { // Replace
with \n so line breaks get recognised contentElement.innerHTML = contentElement.innerHTML.replace(/
/g, '\n')