From 4207348f9cb6087ea259c535a3772a40e4c4b657 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 18 Aug 2023 00:34:15 +0530 Subject: [PATCH] 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