Skip to content

Commit

Permalink
chore: immediately insert file node and show progress inside super no…
Browse files Browse the repository at this point in the history
…te when uploading a new file (#2876) [skip e2e]
  • Loading branch information
amanharwara authored May 6, 2024
1 parent 5cb781e commit 561e451
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 100 deletions.
1 change: 1 addition & 0 deletions packages/files/src/Domain/Service/FilesClientInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface FilesClientInterface {
finishUpload(
operation: EncryptAndUploadFileOperation,
fileMetadata: FileMetadata,
uuid: string,
): Promise<FileItem | ClientDisplayableError>

downloadFile(
Expand Down
27 changes: 20 additions & 7 deletions packages/services/src/Domain/Files/FileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import {
isEncryptedPayload,
VaultListingInterface,
SharedVaultListingInterface,
DecryptedPayload,
FillItemContent,
PayloadVaultOverrides,
PayloadTimestampDefaults,
CreateItemFromPayload,
DecryptedItemInterface,
} from '@standardnotes/models'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { LoggerInterface, spaceSeparatedStrings, UuidGenerator } from '@standardnotes/utils'
Expand Down Expand Up @@ -246,6 +252,7 @@ export class FileService extends AbstractService implements FilesClientInterface
public async finishUpload(
operation: EncryptAndUploadFileOperation,
fileMetadata: FileMetadata,
uuid: string,
): Promise<FileItem | ClientDisplayableError> {
const uploadSessionClosed = await this.api.closeUploadSession(
operation.getValetToken(),
Expand All @@ -268,16 +275,22 @@ export class FileService extends AbstractService implements FilesClientInterface
remoteIdentifier: result.remoteIdentifier,
}

const file = await this.mutator.createItem<FileItem>(
ContentType.TYPES.File,
FillItemContentSpecialized(fileContent),
true,
operation.vault,
)
const filePayload = new DecryptedPayload<FileContent>({
uuid,
content_type: ContentType.TYPES.File,
content: FillItemContent<FileContent>(FillItemContentSpecialized(fileContent)),
dirty: true,
...PayloadVaultOverrides(operation.vault),
...PayloadTimestampDefaults(),
})

const fileItem = CreateItemFromPayload(filePayload) as DecryptedItemInterface<FileContent>

const insertedItem = await this.mutator.insertItem<FileItem>(fileItem)

await this.sync.sync()

return file
return insertedItem
}

private async decryptCachedEntry(file: FileItem, entry: EncryptedBytes): Promise<DecryptedBytes> {
Expand Down
23 changes: 20 additions & 3 deletions packages/web/src/javascripts/Components/FileDragNDropProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@ import { useMemo, useState, createContext, ReactNode, useRef, useCallback, useEf
import Portal from './Portal/Portal'
import { ElementIds } from '@/Constants/ElementIDs'

type FileDragTargetData = {
type FileDragTargetCommonData = {
tooltipText: string
callback: (files: FileItem) => void
note?: SNNote
}

type FileDragTargetCallbacks =
| {
callback: (files: FileItem) => void
handleFileUpload?: never
}
| {
handleFileUpload: (fileOrHandle: File | FileSystemFileHandle) => void
callback?: never
}
type FileDragTargetData = FileDragTargetCommonData & FileDragTargetCallbacks

type FileDnDContextData = {
isDraggingFiles: boolean
addDragTarget: (target: HTMLElement, data: FileDragTargetData) => void
Expand Down Expand Up @@ -203,6 +213,11 @@ const FileDragNDropProvider = ({ application, children }: Props) => {

const dragTarget = closestDragTarget ? dragTargets.current.get(closestDragTarget) : undefined

if (dragTarget?.handleFileUpload) {
dragTarget.handleFileUpload(fileOrHandle)
return
}

const uploadedFile = await application.filesController.uploadNewFile(fileOrHandle, {
note: dragTarget?.note,
})
Expand All @@ -211,7 +226,9 @@ const FileDragNDropProvider = ({ application, children }: Props) => {
return
}

dragTarget?.callback(uploadedFile)
if (dragTarget?.callback) {
dragTarget.callback(uploadedFile)
}
})

dragCounter.current = 0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FilesController } from '@/Controllers/FilesController'
import { LinkingController } from '@/Controllers/LinkingController'
import { SNNote } from '@standardnotes/snjs'
import { NoteType, SNNote } from '@standardnotes/snjs'
import { useEffect } from 'react'
import { useApplication } from '../ApplicationProvider'
import { useFileDragNDrop } from '../FileDragNDropProvider'
Expand All @@ -20,17 +20,28 @@ const NoteViewFileDropTarget = ({ note, linkingController, noteViewElement, file
const target = noteViewElement

if (target) {
addDragTarget(target, {
tooltipText: 'Drop your files to upload and link them to the current note',
callback: async (uploadedFile) => {
await linkingController.linkItems(note, uploadedFile)
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
mutator.protected = note.protected
})
filesController.notifyObserversOfUploadedFileLinkingToCurrentNote(uploadedFile.uuid)
},
note,
})
const tooltipText = 'Drop your files to upload and link them to the current note'
if (note.noteType === NoteType.Super) {
addDragTarget(target, {
tooltipText,
handleFileUpload: (fileOrHandle) => {
filesController.uploadAndInsertFileToCurrentNote(fileOrHandle)
},
note,
})
} else {
addDragTarget(target, {
tooltipText,
callback: async (uploadedFile) => {
await linkingController.linkItems(note, uploadedFile)
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
mutator.protected = note.protected
})
filesController.notifyObserversOfUploadedFileLinkingToCurrentNote(uploadedFile.uuid)
},
note,
})
}
}

return () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createCommand, LexicalCommand } from 'lexical'

export const INSERT_FILE_COMMAND: LexicalCommand<string> = createCommand('INSERT_FILE_COMMAND')
export const UPLOAD_AND_INSERT_FILE_COMMAND: LexicalCommand<File> = createCommand('UPLOAD_AND_INSERT_FILE_COMMAND')
export const INSERT_BUBBLE_COMMAND: LexicalCommand<string> = createCommand('INSERT_BUBBLE_COMMAND')
export const INSERT_DATETIME_COMMAND: LexicalCommand<'date' | 'time' | 'datetime'> =
createCommand('INSERT_DATETIME_COMMAND')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { INSERT_FILE_COMMAND } from '../Commands'
import { INSERT_FILE_COMMAND, UPLOAD_AND_INSERT_FILE_COMMAND } from '../Commands'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'

import { useEffect, useState } from 'react'
Expand All @@ -19,45 +19,24 @@ import { FilesControllerEvent } from '@/Controllers/FilesController'
import { useLinkingController } from '@/Controllers/LinkingControllerProvider'
import { useApplication } from '@/Components/ApplicationProvider'
import { SNNote } from '@standardnotes/snjs'
import Spinner from '../../../Spinner/Spinner'
import Modal from '../../Lexical/UI/Modal'
import Button from '@/Components/Button/Button'
import { isMobileScreen } from '../../../../Utils'

export const OPEN_FILE_UPLOAD_MODAL_COMMAND = createCommand('OPEN_FILE_UPLOAD_MODAL_COMMAND')

function UploadFileDialog({ currentNote, onClose }: { currentNote: SNNote; onClose: () => void }) {
const application = useApplication()
function UploadFileDialog({ onClose }: { onClose: () => void }) {
const [editor] = useLexicalComposerContext()
const filesController = useFilesController()
const linkingController = useLinkingController()

const [file, setFile] = useState<File>()
const [isUploadingFile, setIsUploadingFile] = useState(false)

const onClick = () => {
if (!file) {
return
}

setIsUploadingFile(true)
filesController
.uploadNewFile(file)
.then((uploadedFile) => {
if (!uploadedFile) {
return
}
editor.dispatchCommand(INSERT_FILE_COMMAND, uploadedFile.uuid)
void linkingController.linkItemToSelectedItem(uploadedFile)
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
mutator.protected = currentNote.protected
})
})
.catch(console.error)
.finally(() => {
setIsUploadingFile(false)
onClose()
})
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, file)
onClose()
}

return (
Expand All @@ -72,13 +51,9 @@ function UploadFileDialog({ currentNote, onClose }: { currentNote: SNNote; onClo
}}
/>
<div className="mt-1.5 flex justify-end">
{isUploadingFile ? (
<Spinner className="h-4 w-4" />
) : (
<Button onClick={onClick} disabled={!file} small={isMobileScreen()}>
Upload
</Button>
)}
<Button onClick={onClick} disabled={!file} small={isMobileScreen()}>
Upload
</Button>
</div>
</>
)
Expand All @@ -99,17 +74,23 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS

const uploadFilesList = (files: FileList) => {
Array.from(files).forEach(async (file) => {
try {
const uploadedFile = await filesController.uploadNewFile(file)
if (uploadedFile) {
editor.dispatchCommand(INSERT_FILE_COMMAND, uploadedFile.uuid)
void linkingController.linkItemToSelectedItem(uploadedFile)
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
mutator.protected = currentNote.protected
})
}
} catch (error) {
console.error(error)
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, file)
})
}

const insertFileNode = (uuid: string, onInsert?: (node: FileNode) => void) => {
editor.update(() => {
const fileNode = $createFileNode(uuid)
$insertNodes([fileNode])
if ($isRootOrShadowRoot(fileNode.getParentOrThrow())) {
$wrapNodeInElement(fileNode, $createParagraphNode).selectEnd()
}
const newLineNode = $createParagraphNode()
fileNode.getParentOrThrow().insertAfter(newLineNode)
newLineNode.selectEnd()
editor.focus()
if (onInsert) {
onInsert(fileNode)
}
})
}
Expand All @@ -118,14 +99,34 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
editor.registerCommand<string>(
INSERT_FILE_COMMAND,
(payload) => {
const fileNode = $createFileNode(payload)
$insertNodes([fileNode])
if ($isRootOrShadowRoot(fileNode.getParentOrThrow())) {
$wrapNodeInElement(fileNode, $createParagraphNode).selectEnd()
}
const newLineNode = $createParagraphNode()
fileNode.getParentOrThrow().insertAfter(newLineNode)

insertFileNode(payload)
return true
},
COMMAND_PRIORITY_EDITOR,
),
editor.registerCommand(
UPLOAD_AND_INSERT_FILE_COMMAND,
(file) => {
const note = currentNote
let fileNode: FileNode | undefined
filesController
.uploadNewFile(file, {
showToast: false,
onUploadStart(fileUuid) {
insertFileNode(fileUuid, (node) => (fileNode = node))
},
})
.then((uploadedFile) => {
if (uploadedFile) {
void linkingController.linkItems(note, uploadedFile)
void application.changeAndSaveItem.execute(uploadedFile, (mutator) => {
mutator.protected = note.protected
})
} else {
editor.update(() => fileNode?.remove())
}
})
.catch(console.error)
return true
},
COMMAND_PRIORITY_EDITOR,
Expand All @@ -150,28 +151,26 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
},
COMMAND_PRIORITY_NORMAL,
),
editor.registerNodeTransform(FileNode, (node) => {
/**
* When adding the node we wrap it with a paragraph to avoid insertion errors,
* however that causes issues with selection. We unwrap the node to fix that.
*/
const parent = node.getParent()
if (!parent) {
return
}
if (parent.getChildrenSize() === 1) {
parent.insertBefore(node)
parent.remove()
}
}),
)
}, [application, currentNote.protected, editor, filesController, linkingController])
}, [application, currentNote, editor, filesController, linkingController])

useEffect(() => {
const disposer = filesController.addEventObserver((event, data) => {
if (event === FilesControllerEvent.FileUploadedToNote) {
if (event === FilesControllerEvent.FileUploadedToNote && data[FilesControllerEvent.FileUploadedToNote]) {
const fileUuid = data[FilesControllerEvent.FileUploadedToNote].uuid
editor.dispatchCommand(INSERT_FILE_COMMAND, fileUuid)
} else if (event === FilesControllerEvent.UploadAndInsertFile && data[FilesControllerEvent.UploadAndInsertFile]) {
const { fileOrHandle } = data[FilesControllerEvent.UploadAndInsertFile]
if (fileOrHandle instanceof FileSystemFileHandle) {
fileOrHandle
.getFile()
.then((file) => {
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, file)
})
.catch(console.error)
} else {
editor.dispatchCommand(UPLOAD_AND_INSERT_FILE_COMMAND, fileOrHandle)
}
}
})

Expand All @@ -181,7 +180,7 @@ export default function FilePlugin({ currentNote }: { currentNote: SNNote }): JS
if (showFileUploadModal) {
return (
<Modal onClose={() => setShowFileUploadModal(false)} title="Upload File">
<UploadFileDialog currentNote={currentNote} onClose={() => setShowFileUploadModal(false)} />
<UploadFileDialog onClose={() => setShowFileUploadModal(false)} />
</Modal>
)
}
Expand Down
Loading

0 comments on commit 561e451

Please sign in to comment.