refactor: evernote imports (#2619) [skip e2e]
This commit is contained in:
@@ -4,7 +4,7 @@ import ImportModalFileItem from './ImportModalFileItem'
|
||||
import ImportModalInitialPage from './InitialPage'
|
||||
import Modal, { ModalAction } from '../Modal/Modal'
|
||||
import ModalOverlay from '../Modal/ModalOverlay'
|
||||
import { ImportModalController } from '@/Controllers/ImportModalController'
|
||||
import { ImportModalController } from '@/Components/ImportModal/ImportModalController'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import Switch from '../Switch/Switch'
|
||||
|
||||
@@ -66,7 +66,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
|
||||
)}
|
||||
</div>
|
||||
{files.length > 0 && (
|
||||
<label className="py-2 px-4 flex items-center gap-2 border-t border-border">
|
||||
<label className="flex items-center gap-2 border-t border-border px-4 py-2">
|
||||
<Switch checked={shouldCreateTag} onChange={setShouldCreateTag} />
|
||||
<span className="text-sm">Create tag with all imported notes</span>
|
||||
</label>
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import { DecryptedTransferPayload, SNTag, TagContent } from '@standardnotes/models'
|
||||
import {
|
||||
ContentType,
|
||||
ItemManagerInterface,
|
||||
MutatorClientInterface,
|
||||
pluralize,
|
||||
UuidGenerator,
|
||||
} from '@standardnotes/snjs'
|
||||
import { Importer, NoteImportType } from '@standardnotes/ui-services'
|
||||
import { action, makeObservable, observable } from 'mobx'
|
||||
import { NavigationController } from '../../Controllers/Navigation/NavigationController'
|
||||
|
||||
type ImportModalFileCommon = {
|
||||
id: string
|
||||
file: File
|
||||
service: NoteImportType | null | undefined
|
||||
}
|
||||
|
||||
export type ImportModalFile = (
|
||||
| { status: 'pending' }
|
||||
| { status: 'ready'; payloads?: DecryptedTransferPayload[] }
|
||||
| { status: 'parsing' }
|
||||
| { status: 'importing' }
|
||||
| { status: 'success'; successMessage: string }
|
||||
| { status: 'error'; error: Error }
|
||||
) &
|
||||
ImportModalFileCommon
|
||||
|
||||
export class ImportModalController {
|
||||
isVisible = false
|
||||
shouldCreateTag = false
|
||||
files: ImportModalFile[] = []
|
||||
importTag: SNTag | undefined = undefined
|
||||
|
||||
constructor(
|
||||
private importer: Importer,
|
||||
private navigationController: NavigationController,
|
||||
private items: ItemManagerInterface,
|
||||
private mutator: MutatorClientInterface,
|
||||
) {
|
||||
makeObservable(this, {
|
||||
isVisible: observable,
|
||||
setIsVisible: action,
|
||||
|
||||
shouldCreateTag: observable,
|
||||
setShouldCreateTag: action,
|
||||
|
||||
files: observable,
|
||||
setFiles: action,
|
||||
updateFile: action,
|
||||
removeFile: action,
|
||||
|
||||
importTag: observable,
|
||||
setImportTag: action,
|
||||
})
|
||||
}
|
||||
|
||||
setIsVisible = (isVisible: boolean) => {
|
||||
this.isVisible = isVisible
|
||||
}
|
||||
|
||||
setShouldCreateTag = (shouldCreateTag: boolean) => {
|
||||
this.shouldCreateTag = shouldCreateTag
|
||||
}
|
||||
|
||||
setFiles = (files: File[], service?: NoteImportType) => {
|
||||
this.files = files.map((file) => ({
|
||||
id: UuidGenerator.GenerateUuid(),
|
||||
file,
|
||||
service,
|
||||
status: service ? 'ready' : 'pending',
|
||||
}))
|
||||
}
|
||||
|
||||
updateFile = (file: ImportModalFile) => {
|
||||
this.files = this.files.map((f) => (f.id === file.id ? file : f))
|
||||
}
|
||||
|
||||
removeFile = (id: ImportModalFile['id']) => {
|
||||
this.files = this.files.filter((f) => f.id !== id)
|
||||
}
|
||||
|
||||
setImportTag = (tag: SNTag | undefined) => {
|
||||
this.importTag = tag
|
||||
}
|
||||
|
||||
close = () => {
|
||||
this.setIsVisible(false)
|
||||
this.setShouldCreateTag(false)
|
||||
if (this.importTag) {
|
||||
this.navigationController
|
||||
.setSelectedTag(this.importTag, 'all', {
|
||||
userTriggered: true,
|
||||
})
|
||||
.catch(console.error)
|
||||
}
|
||||
this.setFiles([])
|
||||
this.setImportTag(undefined)
|
||||
}
|
||||
|
||||
importFromPayloads = async (file: ImportModalFile, payloads: DecryptedTransferPayload[]) => {
|
||||
this.updateFile({
|
||||
...file,
|
||||
status: 'importing',
|
||||
})
|
||||
|
||||
try {
|
||||
await this.importer.importFromTransferPayloads(payloads)
|
||||
|
||||
const notesImported = payloads.filter((payload) => payload.content_type === ContentType.TYPES.Note)
|
||||
const tagsImported = payloads.filter((payload) => payload.content_type === ContentType.TYPES.Tag)
|
||||
|
||||
const successMessage =
|
||||
`Successfully imported ${notesImported.length} ` +
|
||||
pluralize(notesImported.length, 'note', 'notes') +
|
||||
(tagsImported.length > 0 ? ` and ${tagsImported.length} ${pluralize(tagsImported.length, 'tag', 'tags')}` : '')
|
||||
|
||||
this.updateFile({
|
||||
...file,
|
||||
status: 'success',
|
||||
successMessage,
|
||||
})
|
||||
} catch (error) {
|
||||
this.updateFile({
|
||||
...file,
|
||||
status: 'error',
|
||||
error: error instanceof Error ? error : new Error('Could not import file'),
|
||||
})
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
parseAndImport = async () => {
|
||||
if (this.files.length === 0) {
|
||||
return
|
||||
}
|
||||
const importedPayloads: DecryptedTransferPayload[] = []
|
||||
for (const file of this.files) {
|
||||
if (!file.service) {
|
||||
return
|
||||
}
|
||||
|
||||
if (file.status === 'ready' && file.payloads) {
|
||||
await this.importFromPayloads(file, file.payloads)
|
||||
importedPayloads.push(...file.payloads)
|
||||
continue
|
||||
}
|
||||
|
||||
this.updateFile({
|
||||
...file,
|
||||
status: 'parsing',
|
||||
})
|
||||
|
||||
try {
|
||||
const payloads = await this.importer.getPayloadsFromFile(file.file, file.service)
|
||||
await this.importFromPayloads(file, payloads)
|
||||
importedPayloads.push(...payloads)
|
||||
} catch (error) {
|
||||
this.updateFile({
|
||||
...file,
|
||||
status: 'error',
|
||||
error: error instanceof Error ? error : new Error('Could not import file'),
|
||||
})
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
if (!importedPayloads.length) {
|
||||
return
|
||||
}
|
||||
if (this.shouldCreateTag) {
|
||||
const currentDate = new Date()
|
||||
const importTagItem = this.items.createTemplateItem<TagContent, SNTag>(ContentType.TYPES.Tag, {
|
||||
title: `Imported on ${currentDate.toLocaleString()}`,
|
||||
expanded: false,
|
||||
iconString: '',
|
||||
references: importedPayloads
|
||||
.filter((payload) => payload.content_type === ContentType.TYPES.Note)
|
||||
.map((payload) => ({
|
||||
content_type: ContentType.TYPES.Note,
|
||||
uuid: payload.uuid,
|
||||
})),
|
||||
})
|
||||
const importTag = await this.mutator.insertItem(importTagItem)
|
||||
if (importTag) {
|
||||
this.setImportTag(importTag as SNTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ImportModalController, ImportModalFile } from '@/Controllers/ImportModalController'
|
||||
import { ImportModalController, ImportModalFile } from '@/Components/ImportModal/ImportModalController'
|
||||
import { classNames, ContentType, DecryptedTransferPayload, pluralize } from '@standardnotes/snjs'
|
||||
import { Importer, NoteImportType } from '@standardnotes/ui-services'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ImportModalController } from '@/Controllers/ImportModalController'
|
||||
import { ImportModalController } from '@/Components/ImportModal/ImportModalController'
|
||||
import { ClassicFileReader } from '@standardnotes/filepicker'
|
||||
import { NoteImportType } from '@standardnotes/ui-services'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
@@ -41,30 +41,30 @@ const ImportModalInitialPage = ({ setFiles }: Props) => {
|
||||
<div className="text-lg font-semibold">Drag and drop files to auto-detect and import</div>
|
||||
<div className="text-sm">Or click to open file picker</div>
|
||||
</button>
|
||||
<div className="text-center my-4 w-full">or import from:</div>
|
||||
<div className="my-4 w-full text-center">or import from:</div>
|
||||
<div className="flex flex-wrap items-center justify-center gap-4">
|
||||
<Button className="flex items-center !py-2" onClick={() => selectFiles('evernote')}>
|
||||
<Icon type="evernote" className="text-[#14cc45] mr-2" />
|
||||
<Icon type="evernote" className="mr-2 text-[#14cc45]" />
|
||||
Evernote
|
||||
</Button>
|
||||
<Button className="flex items-center !py-2" onClick={() => selectFiles('google-keep')}>
|
||||
<Icon type="gkeep" className="text-[#fbbd00] mr-2" />
|
||||
<Icon type="gkeep" className="mr-2 text-[#fbbd00]" />
|
||||
Google Keep
|
||||
</Button>
|
||||
<Button className="flex items-center !py-2" onClick={() => selectFiles('simplenote')}>
|
||||
<Icon type="simplenote" className="text-[#3360cc] mr-2" />
|
||||
<Icon type="simplenote" className="mr-2 text-[#3360cc]" />
|
||||
Simplenote
|
||||
</Button>
|
||||
<Button className="flex items-center !py-2" onClick={() => selectFiles('aegis')}>
|
||||
<Icon type="aegis" className="bg-[#0d47a1] text-[#fff] rounded mr-2 p-1" size="normal" />
|
||||
<Icon type="aegis" className="mr-2 rounded bg-[#0d47a1] p-1 text-[#fff]" size="normal" />
|
||||
Aegis
|
||||
</Button>
|
||||
<Button className="flex items-center !py-2" onClick={() => selectFiles('plaintext')}>
|
||||
<Icon type="plain-text" className="text-info mr-2" />
|
||||
<Icon type="plain-text" className="mr-2 text-info" />
|
||||
Plaintext / Markdown
|
||||
</Button>
|
||||
<Button className="flex items-center !py-2" onClick={() => selectFiles('html')}>
|
||||
<Icon type="rich-text" className="text-accessory-tint-2 mr-2" />
|
||||
<Icon type="rich-text" className="mr-2 text-accessory-tint-2" />
|
||||
HTML
|
||||
</Button>
|
||||
<Button
|
||||
@@ -81,7 +81,7 @@ const ImportModalInitialPage = ({ setFiles }: Props) => {
|
||||
selectFiles('super').catch(console.error)
|
||||
}}
|
||||
>
|
||||
<Icon type="file-doc" className="text-accessory-tint-1 mr-2" />
|
||||
<Icon type="file-doc" className="mr-2 text-accessory-tint-1" />
|
||||
Super (JSON)
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user