refactor: import modal
This commit is contained in:
@@ -165,7 +165,12 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={() => viewControllerManager.isImportModalVisible.set(true)}>
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
viewControllerManager.importModalController.setIsVisible(true)
|
||||||
|
viewControllerManager.accountMenuController.closeAccountMenu()
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Icon type="archive" className={iconClassName} />
|
<Icon type="archive" className={iconClassName} />
|
||||||
Import
|
Import
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
application={application}
|
application={application}
|
||||||
viewControllerManager={viewControllerManager}
|
viewControllerManager={viewControllerManager}
|
||||||
/>
|
/>
|
||||||
<ImportModal viewControllerManager={viewControllerManager} />
|
<ImportModal importModalController={viewControllerManager.importModalController} />
|
||||||
</>
|
</>
|
||||||
{application.routeService.isDotOrg && <DotOrgNotice />}
|
{application.routeService.isDotOrg && <DotOrgNotice />}
|
||||||
{isIOS() && <IosKeyboardClose />}
|
{isIOS() && <IosKeyboardClose />}
|
||||||
|
|||||||
@@ -1,191 +1,13 @@
|
|||||||
import { ViewControllerManager } from '@/Controllers/ViewControllerManager'
|
|
||||||
import { ContentType, DecryptedTransferPayload, pluralize, SNTag, TagContent, UuidGenerator } from '@standardnotes/snjs'
|
|
||||||
import { Importer } from '@standardnotes/ui-services'
|
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { useCallback, useMemo, useReducer, useState } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import ImportModalFileItem from './ImportModalFileItem'
|
||||||
import { useStateRef } from '@/Hooks/useStateRef'
|
|
||||||
import { ImportModalFileItem } from './ImportModalFileItem'
|
|
||||||
import ImportModalInitialPage from './InitialPage'
|
import ImportModalInitialPage from './InitialPage'
|
||||||
import { ImportModalAction, ImportModalFile, ImportModalState } from './Types'
|
|
||||||
import Modal, { ModalAction } from '../Modal/Modal'
|
import Modal, { ModalAction } from '../Modal/Modal'
|
||||||
import ModalOverlay from '../Modal/ModalOverlay'
|
import ModalOverlay from '../Modal/ModalOverlay'
|
||||||
|
import { ImportModalController } from '@/Controllers/ImportModalController'
|
||||||
|
|
||||||
const reducer = (state: ImportModalState, action: ImportModalAction): ImportModalState => {
|
const ImportModal = ({ importModalController }: { importModalController: ImportModalController }) => {
|
||||||
switch (action.type) {
|
const { files, setFiles, updateFile, removeFile, importer, parseAndImport, isVisible, close } = importModalController
|
||||||
case 'setFiles':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
files: action.files.map((file) => ({
|
|
||||||
id: UuidGenerator.GenerateUuid(),
|
|
||||||
file,
|
|
||||||
status: action.service ? 'ready' : 'pending',
|
|
||||||
service: action.service,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
case 'updateFile':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
files: state.files.map((file) => {
|
|
||||||
if (file.file.name === action.file.file.name) {
|
|
||||||
return action.file
|
|
||||||
}
|
|
||||||
return file
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
case 'removeFile':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
files: state.files.filter((file) => file.id !== action.id),
|
|
||||||
}
|
|
||||||
case 'setImportTag':
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
importTag: action.tag,
|
|
||||||
}
|
|
||||||
case 'clearState':
|
|
||||||
return {
|
|
||||||
files: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: ImportModalState = {
|
|
||||||
files: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImportModal = ({ viewControllerManager }: { viewControllerManager: ViewControllerManager }) => {
|
|
||||||
const application = useApplication()
|
|
||||||
const [importer] = useState(() => new Importer(application))
|
|
||||||
const [state, dispatch] = useReducer(reducer, initialState)
|
|
||||||
const { files } = state
|
|
||||||
const filesRef = useStateRef(files)
|
|
||||||
|
|
||||||
const importFromPayloads = useCallback(
|
|
||||||
async (file: ImportModalFile, payloads: DecryptedTransferPayload[]) => {
|
|
||||||
dispatch({
|
|
||||||
type: 'updateFile',
|
|
||||||
file: {
|
|
||||||
...file,
|
|
||||||
status: 'importing',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
await importer.importFromTransferPayloads(payloads)
|
|
||||||
|
|
||||||
const notesImported = payloads.filter((payload) => payload.content_type === ContentType.Note)
|
|
||||||
const tagsImported = payloads.filter((payload) => payload.content_type === ContentType.Tag)
|
|
||||||
|
|
||||||
const successMessage =
|
|
||||||
`Successfully imported ${notesImported.length} ` +
|
|
||||||
pluralize(notesImported.length, 'note', 'notes') +
|
|
||||||
(tagsImported.length > 0
|
|
||||||
? ` and ${tagsImported.length} ${pluralize(tagsImported.length, 'tag', 'tags')}`
|
|
||||||
: '')
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: 'updateFile',
|
|
||||||
file: {
|
|
||||||
...file,
|
|
||||||
status: 'success',
|
|
||||||
successMessage,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: 'updateFile',
|
|
||||||
file: {
|
|
||||||
...file,
|
|
||||||
status: 'error',
|
|
||||||
error: error instanceof Error ? error : new Error('Could not import file'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[importer],
|
|
||||||
)
|
|
||||||
|
|
||||||
const parseAndImport = useCallback(async () => {
|
|
||||||
const files = filesRef.current
|
|
||||||
if (files.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const importedPayloads: DecryptedTransferPayload[] = []
|
|
||||||
for (const file of files) {
|
|
||||||
if (!file.service) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file.status === 'ready' && file.payloads) {
|
|
||||||
await importFromPayloads(file, file.payloads)
|
|
||||||
importedPayloads.push(...file.payloads)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({
|
|
||||||
type: 'updateFile',
|
|
||||||
file: {
|
|
||||||
...file,
|
|
||||||
status: 'parsing',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payloads = await importer.getPayloadsFromFile(file.file, file.service)
|
|
||||||
await importFromPayloads(file, payloads)
|
|
||||||
importedPayloads.push(...payloads)
|
|
||||||
} catch (error) {
|
|
||||||
dispatch({
|
|
||||||
type: 'updateFile',
|
|
||||||
file: {
|
|
||||||
...file,
|
|
||||||
status: 'error',
|
|
||||||
error: error instanceof Error ? error : new Error('Could not import file'),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const currentDate = new Date()
|
|
||||||
const importTagPayload: DecryptedTransferPayload<TagContent> = {
|
|
||||||
uuid: UuidGenerator.GenerateUuid(),
|
|
||||||
created_at: currentDate,
|
|
||||||
created_at_timestamp: currentDate.getTime(),
|
|
||||||
updated_at: currentDate,
|
|
||||||
updated_at_timestamp: currentDate.getTime(),
|
|
||||||
content_type: ContentType.Tag,
|
|
||||||
content: {
|
|
||||||
title: `Imported on ${currentDate.toLocaleString()}`,
|
|
||||||
expanded: false,
|
|
||||||
iconString: '',
|
|
||||||
references: importedPayloads
|
|
||||||
.filter((payload) => payload.content_type === ContentType.Note)
|
|
||||||
.map((payload) => ({
|
|
||||||
content_type: ContentType.Note,
|
|
||||||
uuid: payload.uuid,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const [importTag] = await importer.importFromTransferPayloads([importTagPayload])
|
|
||||||
if (importTag) {
|
|
||||||
dispatch({
|
|
||||||
type: 'setImportTag',
|
|
||||||
tag: importTag as SNTag,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [filesRef, importFromPayloads, importer])
|
|
||||||
|
|
||||||
const closeDialog = useCallback(() => {
|
|
||||||
viewControllerManager.isImportModalVisible.set(false)
|
|
||||||
if (state.importTag) {
|
|
||||||
void viewControllerManager.navigationController.setSelectedTag(state.importTag, 'all', {
|
|
||||||
userTriggered: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
dispatch({
|
|
||||||
type: 'clearState',
|
|
||||||
})
|
|
||||||
}, [state.importTag, viewControllerManager.isImportModalVisible, viewControllerManager.navigationController])
|
|
||||||
|
|
||||||
const isReadyToImport = files.length > 0 && files.every((file) => file.status === 'ready')
|
const isReadyToImport = files.length > 0 && files.every((file) => file.status === 'ready')
|
||||||
const importSuccessOrError =
|
const importSuccessOrError =
|
||||||
@@ -203,22 +25,28 @@ const ImportModal = ({ viewControllerManager }: { viewControllerManager: ViewCon
|
|||||||
{
|
{
|
||||||
label: importSuccessOrError ? 'Close' : 'Cancel',
|
label: importSuccessOrError ? 'Close' : 'Cancel',
|
||||||
type: 'cancel',
|
type: 'cancel',
|
||||||
onClick: closeDialog,
|
onClick: close,
|
||||||
mobileSlot: 'left',
|
mobileSlot: 'left',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[closeDialog, importSuccessOrError, isReadyToImport, parseAndImport],
|
[close, importSuccessOrError, isReadyToImport, parseAndImport],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalOverlay isOpen={viewControllerManager.isImportModalVisible.get()} onDismiss={closeDialog}>
|
<ModalOverlay isOpen={isVisible} onDismiss={close}>
|
||||||
<Modal title="Import" close={closeDialog} actions={modalActions}>
|
<Modal title="Import" close={close} actions={modalActions}>
|
||||||
<div className="px-4 py-4">
|
<div className="px-4 py-4">
|
||||||
{!files.length && <ImportModalInitialPage dispatch={dispatch} />}
|
{!files.length && <ImportModalInitialPage setFiles={setFiles} />}
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<div className="divide-y divide-border">
|
<div className="divide-y divide-border">
|
||||||
{files.map((file) => (
|
{files.map((file) => (
|
||||||
<ImportModalFileItem file={file} key={file.id} dispatch={dispatch} importer={importer} />
|
<ImportModalFileItem
|
||||||
|
file={file}
|
||||||
|
key={file.id}
|
||||||
|
updateFile={updateFile}
|
||||||
|
removeFile={removeFile}
|
||||||
|
importer={importer}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { ImportModalController, ImportModalFile } from '@/Controllers/ImportModalController'
|
||||||
import { classNames, ContentType, DecryptedTransferPayload, pluralize } from '@standardnotes/snjs'
|
import { classNames, ContentType, DecryptedTransferPayload, pluralize } from '@standardnotes/snjs'
|
||||||
import { Importer, NoteImportType } from '@standardnotes/ui-services'
|
import { Importer, NoteImportType } from '@standardnotes/ui-services'
|
||||||
import { Dispatch, useCallback, useEffect } from 'react'
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import { useCallback, useEffect } from 'react'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import { ImportModalAction, ImportModalFile } from './Types'
|
|
||||||
|
|
||||||
const NoteImportTypeColors: Record<NoteImportType, string> = {
|
const NoteImportTypeColors: Record<NoteImportType, string> = {
|
||||||
evernote: 'bg-[#14cc45] text-[#000]',
|
evernote: 'bg-[#14cc45] text-[#000]',
|
||||||
@@ -20,13 +21,15 @@ const NoteImportTypeIcons: Record<NoteImportType, string> = {
|
|||||||
plaintext: 'plain-text',
|
plaintext: 'plain-text',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImportModalFileItem = ({
|
const ImportModalFileItem = ({
|
||||||
file,
|
file,
|
||||||
dispatch,
|
updateFile,
|
||||||
|
removeFile,
|
||||||
importer,
|
importer,
|
||||||
}: {
|
}: {
|
||||||
file: ImportModalFile
|
file: ImportModalFile
|
||||||
dispatch: Dispatch<ImportModalAction>
|
updateFile: ImportModalController['updateFile']
|
||||||
|
removeFile: ImportModalController['removeFile']
|
||||||
importer: Importer
|
importer: Importer
|
||||||
}) => {
|
}) => {
|
||||||
const setFileService = useCallback(
|
const setFileService = useCallback(
|
||||||
@@ -34,21 +37,18 @@ export const ImportModalFileItem = ({
|
|||||||
let payloads: DecryptedTransferPayload[] | undefined
|
let payloads: DecryptedTransferPayload[] | undefined
|
||||||
try {
|
try {
|
||||||
payloads = service ? await importer.getPayloadsFromFile(file.file, service) : undefined
|
payloads = service ? await importer.getPayloadsFromFile(file.file, service) : undefined
|
||||||
} catch {
|
} catch (error) {
|
||||||
//
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
updateFile({
|
||||||
type: 'updateFile',
|
...file,
|
||||||
file: {
|
service,
|
||||||
...file,
|
status: service ? 'ready' : 'pending',
|
||||||
service,
|
payloads,
|
||||||
status: service ? 'ready' : 'pending',
|
|
||||||
payloads,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[dispatch, file, importer],
|
[file, importer, updateFile],
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -59,7 +59,7 @@ export const ImportModalFileItem = ({
|
|||||||
if (file.service === undefined) {
|
if (file.service === undefined) {
|
||||||
void detect()
|
void detect()
|
||||||
}
|
}
|
||||||
}, [dispatch, file, setFileService])
|
}, [file, setFileService])
|
||||||
|
|
||||||
const notePayloads =
|
const notePayloads =
|
||||||
file.status === 'ready' && file.payloads
|
file.status === 'ready' && file.payloads
|
||||||
@@ -129,10 +129,7 @@ export const ImportModalFileItem = ({
|
|||||||
<button
|
<button
|
||||||
className="ml-2 rounded border border-border bg-default p-1.5 hover:bg-contrast"
|
className="ml-2 rounded border border-border bg-default p-1.5 hover:bg-contrast"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch({
|
removeFile(file.id)
|
||||||
type: 'removeFile',
|
|
||||||
id: file.id,
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="close" size="medium" />
|
<Icon type="close" size="medium" />
|
||||||
@@ -144,3 +141,5 @@ export const ImportModalFileItem = ({
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default observer(ImportModalFileItem)
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
|
import { ImportModalController } from '@/Controllers/ImportModalController'
|
||||||
import { ClassicFileReader } from '@standardnotes/filepicker'
|
import { ClassicFileReader } from '@standardnotes/filepicker'
|
||||||
import { NoteImportType } from '@standardnotes/ui-services'
|
import { NoteImportType } from '@standardnotes/ui-services'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import Button from '../Button/Button'
|
import Button from '../Button/Button'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import { ImportModalAction } from './Types'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
dispatch: React.Dispatch<ImportModalAction>
|
setFiles: ImportModalController['setFiles']
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImportModalInitialPage = ({ dispatch }: Props) => {
|
const ImportModalInitialPage = ({ setFiles }: Props) => {
|
||||||
const selectFiles = useCallback(
|
const selectFiles = useCallback(
|
||||||
async (service?: NoteImportType) => {
|
async (service?: NoteImportType) => {
|
||||||
const files = await ClassicFileReader.selectFiles()
|
const files = await ClassicFileReader.selectFiles()
|
||||||
|
|
||||||
dispatch({
|
setFiles(files, service)
|
||||||
type: 'setFiles',
|
|
||||||
files,
|
|
||||||
service,
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
[dispatch],
|
[setFiles],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,10 +30,7 @@ const ImportModalInitialPage = ({ dispatch }: Props) => {
|
|||||||
onDrop={(e) => {
|
onDrop={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const files = Array.from(e.dataTransfer.files)
|
const files = Array.from(e.dataTransfer.files)
|
||||||
dispatch({
|
setFiles(files)
|
||||||
type: 'setFiles',
|
|
||||||
files,
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-lg font-semibold">Drag and drop files to auto-detect and import</div>
|
<div className="text-lg font-semibold">Drag and drop files to auto-detect and import</div>
|
||||||
@@ -90,4 +84,4 @@ const ImportModalInitialPage = ({ dispatch }: Props) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ImportModalInitialPage
|
export default observer(ImportModalInitialPage)
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { DecryptedTransferPayload, SNTag } from '@standardnotes/models'
|
|
||||||
import { NoteImportType } from '@standardnotes/ui-services'
|
|
||||||
|
|
||||||
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 type ImportModalState = {
|
|
||||||
files: ImportModalFile[]
|
|
||||||
importTag?: SNTag
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ImportModalAction =
|
|
||||||
| { type: 'setFiles'; files: File[]; service?: NoteImportType }
|
|
||||||
| { type: 'updateFile'; file: ImportModalFile }
|
|
||||||
| { type: 'removeFile'; id: ImportModalFile['id'] }
|
|
||||||
| { type: 'setImportTag'; tag: SNTag }
|
|
||||||
| { type: 'clearState' }
|
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { DecryptedTransferPayload, SNTag, TagContent } from '@standardnotes/models'
|
||||||
|
import { ContentType, pluralize, UuidGenerator } from '@standardnotes/snjs'
|
||||||
|
import { Importer, NoteImportType } from '@standardnotes/ui-services'
|
||||||
|
import { action, makeObservable, observable } from 'mobx'
|
||||||
|
import { NavigationController } from './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
|
||||||
|
importer: Importer
|
||||||
|
files: ImportModalFile[] = []
|
||||||
|
importTag: SNTag | undefined = undefined
|
||||||
|
|
||||||
|
constructor(application: WebApplication, private navigationController: NavigationController) {
|
||||||
|
makeObservable(this, {
|
||||||
|
isVisible: observable,
|
||||||
|
setIsVisible: action,
|
||||||
|
|
||||||
|
files: observable,
|
||||||
|
setFiles: action,
|
||||||
|
updateFile: action,
|
||||||
|
removeFile: action,
|
||||||
|
|
||||||
|
importTag: observable,
|
||||||
|
setImportTag: action,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.importer = new Importer(application)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsVisible = (isVisible: boolean) => {
|
||||||
|
this.isVisible = isVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
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.Note)
|
||||||
|
const tagsImported = payloads.filter((payload) => payload.content_type === ContentType.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const currentDate = new Date()
|
||||||
|
const importTagPayload: DecryptedTransferPayload<TagContent> = {
|
||||||
|
uuid: UuidGenerator.GenerateUuid(),
|
||||||
|
created_at: currentDate,
|
||||||
|
created_at_timestamp: currentDate.getTime(),
|
||||||
|
updated_at: currentDate,
|
||||||
|
updated_at_timestamp: currentDate.getTime(),
|
||||||
|
content_type: ContentType.Tag,
|
||||||
|
content: {
|
||||||
|
title: `Imported on ${currentDate.toLocaleString()}`,
|
||||||
|
expanded: false,
|
||||||
|
iconString: '',
|
||||||
|
references: importedPayloads
|
||||||
|
.filter((payload) => payload.content_type === ContentType.Note)
|
||||||
|
.map((payload) => ({
|
||||||
|
content_type: ContentType.Note,
|
||||||
|
uuid: payload.uuid,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const [importTag] = await this.importer.importFromTransferPayloads([importTagPayload])
|
||||||
|
if (importTag) {
|
||||||
|
this.setImportTag(importTag as SNTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,7 @@ import { PersistenceService } from './Abstract/PersistenceService'
|
|||||||
import { CrossControllerEvent } from './CrossControllerEvent'
|
import { CrossControllerEvent } from './CrossControllerEvent'
|
||||||
import { EventObserverInterface } from '@/Event/EventObserverInterface'
|
import { EventObserverInterface } from '@/Event/EventObserverInterface'
|
||||||
import { ApplicationEventObserver } from '@/Event/ApplicationEventObserver'
|
import { ApplicationEventObserver } from '@/Event/ApplicationEventObserver'
|
||||||
|
import { ImportModalController } from './ImportModalController'
|
||||||
|
|
||||||
export class ViewControllerManager implements InternalEventHandlerInterface {
|
export class ViewControllerManager implements InternalEventHandlerInterface {
|
||||||
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
|
readonly enableUnfinishedFeatures: boolean = window?.enabledUnfinishedFeatures
|
||||||
@@ -67,9 +68,9 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
|
|||||||
readonly historyModalController: HistoryModalController
|
readonly historyModalController: HistoryModalController
|
||||||
readonly linkingController: LinkingController
|
readonly linkingController: LinkingController
|
||||||
readonly paneController: PaneController
|
readonly paneController: PaneController
|
||||||
|
readonly importModalController: ImportModalController
|
||||||
|
|
||||||
public isSessionsModalVisible = false
|
public isSessionsModalVisible = false
|
||||||
public isImportModalVisible = observable.box(false)
|
|
||||||
|
|
||||||
private appEventObserverRemovers: (() => void)[] = []
|
private appEventObserverRemovers: (() => void)[] = []
|
||||||
private eventBus: InternalEventBus
|
private eventBus: InternalEventBus
|
||||||
@@ -154,6 +155,8 @@ export class ViewControllerManager implements InternalEventHandlerInterface {
|
|||||||
|
|
||||||
this.historyModalController = new HistoryModalController(this.application, this.eventBus, this.notesController)
|
this.historyModalController = new HistoryModalController(this.application, this.eventBus, this.notesController)
|
||||||
|
|
||||||
|
this.importModalController = new ImportModalController(this.application, this.navigationController)
|
||||||
|
|
||||||
this.toastService = new ToastService()
|
this.toastService = new ToastService()
|
||||||
|
|
||||||
this.applicationEventObserver = new ApplicationEventObserver(
|
this.applicationEventObserver = new ApplicationEventObserver(
|
||||||
|
|||||||
Reference in New Issue
Block a user