refactor: import modal (#2670)
This commit is contained in:
@@ -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 && (
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user