refactor: import modal (#2670)

This commit is contained in:
Aman Harwara
2023-12-01 18:18:22 +05:30
committed by GitHub
parent deee2cbc4d
commit e040291b11
4 changed files with 119 additions and 63 deletions

View File

@@ -1,5 +1,5 @@
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useMemo } from 'react' import { useCallback, useMemo } from 'react'
import ImportModalFileItem from './ImportModalFileItem' import ImportModalFileItem from './ImportModalFileItem'
import ImportModalInitialPage from './InitialPage' import ImportModalInitialPage from './InitialPage'
import Modal, { ModalAction } from '../Modal/Modal' import Modal, { ModalAction } from '../Modal/Modal'
@@ -11,6 +11,9 @@ import LinkedItemBubble from '../LinkedItems/LinkedItemBubble'
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem' import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdown' import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdown'
import { ContentType, SNTag } from '@standardnotes/snjs' 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 ImportModal = ({ importModalController }: { importModalController: ImportModalController }) => {
const application = useApplication() const application = useApplication()
@@ -18,6 +21,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
const { const {
files, files,
setFiles, setFiles,
addFiles,
addImportsToTag, addImportsToTag,
setAddImportsToTag, setAddImportsToTag,
shouldCreateTag, shouldCreateTag,
@@ -55,23 +59,45 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
[close, existingTagForImports, importSuccessOrError, isReadyToImport, parseAndImport, shouldCreateTag], [close, existingTagForImports, importSuccessOrError, isReadyToImport, parseAndImport, shouldCreateTag],
) )
const selectFiles = useCallback(
async (service?: NoteImportType) => {
const files = await ClassicFileReader.selectFiles()
addFiles(files, service)
},
[addFiles],
)
return ( return (
<ModalOverlay isOpen={isVisible} close={close}> <ModalOverlay isOpen={isVisible} close={close}>
<Modal title="Import" close={close} actions={modalActions} className="flex flex-col"> <Modal title="Import" close={close} actions={modalActions} className="flex flex-col">
<div className="min-h-0 flex-grow px-4 py-4"> <div className="min-h-0 flex-grow px-4 py-4">
{!files.length && <ImportModalInitialPage setFiles={setFiles} />} {!files.length && <ImportModalInitialPage setFiles={setFiles} selectFiles={selectFiles} />}
{files.length > 0 && ( {files.length > 0 && (
<div className="divide-y divide-border"> <>
{files.map((file) => ( <div className="divide-y divide-border">
<ImportModalFileItem {files.map((file) => (
file={file} <ImportModalFileItem
key={file.id} file={file}
updateFile={updateFile} key={file.id}
removeFile={removeFile} updateFile={updateFile}
importer={application.importer} removeFile={removeFile}
/> importer={application.importer}
))} />
</div> ))}
</div>
{!importSuccessOrError && (
<Button
className="mt-4"
onClick={() => {
selectFiles().catch(console.error)
}}
small
>
Add files
</Button>
)}
</>
)} )}
</div> </div>
{files.length > 0 && ( {files.length > 0 && (

View File

@@ -66,6 +66,7 @@ export class ImportModalController extends AbstractViewController {
files: observable, files: observable,
setFiles: action, setFiles: action,
addFiles: action,
updateFile: action, updateFile: action,
removeFile: action, removeFile: action,
@@ -106,13 +107,21 @@ export class ImportModalController extends AbstractViewController {
this.preferences.setValue(PrefKey.ExistingTagForImports, tag?.uuid).catch(console.error) this.preferences.setValue(PrefKey.ExistingTagForImports, tag?.uuid).catch(console.error)
} }
setFiles = (files: File[], service?: NoteImportType) => { getImportFromFile = (file: File, service?: NoteImportType) => {
this.files = files.map((file) => ({ return {
id: UuidGenerator.GenerateUuid(), id: UuidGenerator.GenerateUuid(),
file, file,
service, service,
status: service ? 'ready' : 'pending', status: service ? 'ready' : 'pending',
})) } as ImportModalFile
}
setFiles = (files: File[], service?: NoteImportType) => {
this.files = files.map((file) => this.getImportFromFile(file, service))
}
addFiles = (files: File[], service?: NoteImportType) => {
this.files = [...this.files, ...files.map((file) => this.getImportFromFile(file, service))]
} }
updateFile = (file: ImportModalFile) => { updateFile = (file: ImportModalFile) => {

View File

@@ -1,8 +1,8 @@
import { ImportModalController, ImportModalFile } from '@/Components/ImportModal/ImportModalController' import { ImportModalController, ImportModalFile } from '@/Components/ImportModal/ImportModalController'
import { classNames, ContentType, DecryptedTransferPayload, pluralize } from '@standardnotes/snjs' import { classNames, ContentType, pluralize } from '@standardnotes/snjs'
import { Importer, NoteImportType } from '@standardnotes/ui-services' import { Importer, NoteImportType } from '@standardnotes/ui-services'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { useCallback, useEffect } from 'react' import { useCallback, useEffect, useState } from 'react'
import Icon from '../Icon/Icon' import Icon from '../Icon/Icon'
const NoteImportTypeColors: Record<NoteImportType, string> = { const NoteImportTypeColors: Record<NoteImportType, string> = {
@@ -36,23 +36,20 @@ const ImportModalFileItem = ({
removeFile: ImportModalController['removeFile'] removeFile: ImportModalController['removeFile']
importer: Importer importer: Importer
}) => { }) => {
const [changingService, setChangingService] = useState(false)
const setFileService = useCallback( const setFileService = useCallback(
async (service: NoteImportType | null) => { async (service: NoteImportType | null) => {
let payloads: DecryptedTransferPayload[] | undefined if (!service) {
try { setChangingService(true)
payloads = service ? await importer.getPayloadsFromFile(file.file, service) : undefined
} catch (error) {
console.error(error)
} }
updateFile({ updateFile({
...file, ...file,
service, service,
status: service ? 'ready' : 'pending', status: service ? 'ready' : 'pending',
payloads,
}) })
}, },
[file, importer, updateFile], [file, updateFile],
) )
useEffect(() => { useEffect(() => {
@@ -82,7 +79,7 @@ const ImportModalFileItem = ({
return ( return (
<div <div
className={classNames( className={classNames(
'flex gap-2 px-2 py-2', 'flex gap-2 px-2 py-2.5',
file.service == null ? 'flex-col items-start md:flex-row md:items-center' : 'items-center', file.service == null ? 'flex-col items-start md:flex-row md:items-center' : 'items-center',
)} )}
> >
@@ -100,7 +97,7 @@ const ImportModalFileItem = ({
? payloadsImportMessage ? payloadsImportMessage
: 'Ready to import' : 'Ready to import'
: null} : null}
{file.status === 'pending' && 'Could not auto-detect service. Please select manually.'} {file.status === 'pending' && !file.service && 'Could not auto-detect service. Please select manually.'}
{file.status === 'parsing' && 'Parsing...'} {file.status === 'parsing' && 'Parsing...'}
{file.status === 'importing' && 'Importing...'} {file.status === 'importing' && 'Importing...'}
{file.status === 'uploading-files' && 'Uploading and embedding files...'} {file.status === 'uploading-files' && 'Uploading and embedding files...'}
@@ -109,35 +106,69 @@ const ImportModalFileItem = ({
</div> </div>
</div> </div>
</div> </div>
{file.service == null && ( {(file.status === 'ready' || file.status === 'pending') && (
<div className="flex items-center"> <div className="flex items-center">
<form {changingService ? (
className="flex items-center" <>
onSubmit={(event) => { <form
event.preventDefault() className="flex items-center"
const form = event.target as HTMLFormElement onSubmit={(event) => {
const service = form.elements[0] as HTMLSelectElement event.preventDefault()
void setFileService(service.value as NoteImportType) const form = event.target as HTMLFormElement
}} const service = form.elements[0] as HTMLSelectElement
> void setFileService(service.value as NoteImportType)
<select className="mr-2 rounded border border-border bg-default px-2 py-1 text-sm"> setChangingService(false)
<option value="evernote">Evernote</option> }}
<option value="simplenote">Simplenote</option> >
<option value="google-keep">Google Keep</option> <select
<option value="aegis">Aegis</option> className="mr-2 rounded border border-border bg-default px-2 py-1 text-sm"
<option value="plaintext">Plaintext</option> defaultValue={file.service ? file.service : undefined}
</select> >
<button type="submit" className="rounded border border-border bg-default p-1.5 hover:bg-contrast"> <option value="evernote">Evernote</option>
<Icon type="check" size="medium" /> <option value="simplenote">Simplenote</option>
<option value="google-keep">Google Keep</option>
<option value="aegis">Aegis</option>
<option value="plaintext">Plaintext</option>
<option value="html">HTML</option>
<option value="super">Super</option>
</select>
<button
aria-label="Choose service"
type="submit"
className="rounded border border-border bg-default p-1.5 hover:bg-contrast"
>
<Icon type="check" size="medium" />
</button>
</form>
<button
aria-label="Cancel"
className="ml-2 rounded border border-border bg-default p-1.5 hover:bg-contrast"
onClick={() => {
setChangingService(false)
}}
>
<Icon type="close" size="medium" />
</button>
</>
) : (
<button
aria-label="Change service"
className="rounded border border-border bg-default p-1.5 hover:bg-contrast"
onClick={() => {
setChangingService(true)
}}
>
<Icon type="settings" size="medium" />
</button> </button>
</form> )}
<button <button
aria-label="Remove"
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={() => {
removeFile(file.id) removeFile(file.id)
}} }}
> >
<Icon type="close" size="medium" /> <Icon type="trash" size="medium" />
</button> </button>
</div> </div>
)} )}

View File

@@ -1,30 +1,20 @@
import { ImportModalController } from '@/Components/ImportModal/ImportModalController' import { ImportModalController } from '@/Components/ImportModal/ImportModalController'
import { ClassicFileReader } from '@standardnotes/filepicker'
import { NoteImportType } from '@standardnotes/ui-services'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
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 { useApplication } from '../ApplicationProvider' import { useApplication } from '../ApplicationProvider'
import { FeatureStatus, NativeFeatureIdentifier } from '@standardnotes/snjs'
import { FeatureName } from '@/Controllers/FeatureName' import { FeatureName } from '@/Controllers/FeatureName'
import { NativeFeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
import { NoteImportType } from '@standardnotes/ui-services'
type Props = { type Props = {
setFiles: ImportModalController['setFiles'] setFiles: ImportModalController['setFiles']
selectFiles: (service?: NoteImportType) => Promise<void>
} }
const ImportModalInitialPage = ({ setFiles }: Props) => { const ImportModalInitialPage = ({ setFiles, selectFiles }: Props) => {
const application = useApplication() const application = useApplication()
const selectFiles = useCallback(
async (service?: NoteImportType) => {
const files = await ClassicFileReader.selectFiles()
setFiles(files, service)
},
[setFiles],
)
return ( return (
<> <>
<button <button