feat: generic items list (#1035)
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import {
|
||||
PopoverFileItemAction,
|
||||
PopoverFileItemActionType,
|
||||
} from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
||||
import { PopoverTabs } from '@/Components/AttachedFilesPopover/PopoverTabs'
|
||||
import { BYTES_IN_ONE_MEGABYTE } from '@/Constants'
|
||||
import { confirmDialog } from '@/Services/AlertService'
|
||||
import { concatenateUint8Arrays } from '@/Utils/ConcatenateUint8Arrays'
|
||||
import {
|
||||
ClassicFileReader,
|
||||
@@ -7,11 +13,202 @@ import {
|
||||
ClassicFileSaver,
|
||||
parseFileName,
|
||||
} from '@standardnotes/filepicker'
|
||||
import { ClientDisplayableError, FileItem } from '@standardnotes/snjs'
|
||||
import { ChallengeReason, ClientDisplayableError, ContentType, FileItem } from '@standardnotes/snjs'
|
||||
import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/stylekit'
|
||||
import { action, computed, makeObservable, observable, reaction } from 'mobx'
|
||||
import { WebApplication } from '../Application'
|
||||
import { AbstractState } from './AbstractState'
|
||||
import { AppState } from './AppState'
|
||||
|
||||
const UnprotectedFileActions = [PopoverFileItemActionType.ToggleFileProtection]
|
||||
const NonMutatingFileActions = [PopoverFileItemActionType.DownloadFile, PopoverFileItemActionType.PreviewFile]
|
||||
|
||||
type FileContextMenuLocation = { x: number; y: number }
|
||||
|
||||
export class FilesState extends AbstractState {
|
||||
allFiles: FileItem[] = []
|
||||
attachedFiles: FileItem[] = []
|
||||
showFileContextMenu = false
|
||||
fileContextMenuLocation: FileContextMenuLocation = { x: 0, y: 0 }
|
||||
|
||||
constructor(application: WebApplication, override appState: AppState, appObservers: (() => void)[]) {
|
||||
super(application, appState)
|
||||
|
||||
makeObservable(this, {
|
||||
allFiles: observable,
|
||||
attachedFiles: observable,
|
||||
showFileContextMenu: observable,
|
||||
fileContextMenuLocation: observable,
|
||||
|
||||
selectedFiles: computed,
|
||||
|
||||
reloadAllFiles: action,
|
||||
reloadAttachedFiles: action,
|
||||
setShowFileContextMenu: action,
|
||||
setFileContextMenuLocation: action,
|
||||
})
|
||||
|
||||
appObservers.push(
|
||||
application.streamItems(ContentType.File, () => {
|
||||
this.reloadAllFiles()
|
||||
this.reloadAttachedFiles()
|
||||
}),
|
||||
reaction(
|
||||
() => appState.notes.selectedNotes,
|
||||
() => {
|
||||
this.reloadAttachedFiles()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
get selectedFiles() {
|
||||
return this.appState.selectedItems.getSelectedItems<FileItem>(ContentType.File)
|
||||
}
|
||||
|
||||
setShowFileContextMenu = (enabled: boolean) => {
|
||||
this.showFileContextMenu = enabled
|
||||
}
|
||||
|
||||
setFileContextMenuLocation = (location: FileContextMenuLocation) => {
|
||||
this.fileContextMenuLocation = location
|
||||
}
|
||||
|
||||
reloadAllFiles = () => {
|
||||
this.allFiles = this.application.items.getDisplayableFiles()
|
||||
}
|
||||
|
||||
reloadAttachedFiles = () => {
|
||||
const note = this.appState.notes.firstSelectedNote
|
||||
if (note) {
|
||||
this.attachedFiles = this.application.items.getFilesForNote(note)
|
||||
}
|
||||
}
|
||||
|
||||
deleteFile = async (file: FileItem) => {
|
||||
const shouldDelete = await confirmDialog({
|
||||
text: `Are you sure you want to permanently delete "${file.name}"?`,
|
||||
confirmButtonStyle: 'danger',
|
||||
})
|
||||
if (shouldDelete) {
|
||||
const deletingToastId = addToast({
|
||||
type: ToastType.Loading,
|
||||
message: `Deleting file "${file.name}"...`,
|
||||
})
|
||||
await this.application.files.deleteFile(file)
|
||||
addToast({
|
||||
type: ToastType.Success,
|
||||
message: `Deleted file "${file.name}"`,
|
||||
})
|
||||
dismissToast(deletingToastId)
|
||||
}
|
||||
}
|
||||
|
||||
attachFileToNote = async (file: FileItem) => {
|
||||
const note = this.appState.notes.firstSelectedNote
|
||||
if (!note) {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: 'Could not attach file because selected note was deleted',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await this.application.items.associateFileWithNote(file, note)
|
||||
}
|
||||
|
||||
detachFileFromNote = async (file: FileItem) => {
|
||||
const note = this.appState.notes.firstSelectedNote
|
||||
if (!note) {
|
||||
addToast({
|
||||
type: ToastType.Error,
|
||||
message: 'Could not attach file because selected note was deleted',
|
||||
})
|
||||
return
|
||||
}
|
||||
await this.application.items.disassociateFileWithNote(file, note)
|
||||
}
|
||||
|
||||
toggleFileProtection = async (file: FileItem) => {
|
||||
let result: FileItem | undefined
|
||||
if (file.protected) {
|
||||
result = await this.application.mutator.unprotectFile(file)
|
||||
} else {
|
||||
result = await this.application.mutator.protectFile(file)
|
||||
}
|
||||
const isProtected = result ? result.protected : file.protected
|
||||
return isProtected
|
||||
}
|
||||
|
||||
authorizeProtectedActionForFile = async (file: FileItem, challengeReason: ChallengeReason) => {
|
||||
const authorizedFiles = await this.application.protections.authorizeProtectedActionForItems([file], challengeReason)
|
||||
const isAuthorized = authorizedFiles.length > 0 && authorizedFiles.includes(file)
|
||||
return isAuthorized
|
||||
}
|
||||
|
||||
renameFile = async (file: FileItem, fileName: string) => {
|
||||
await this.application.items.renameFile(file, fileName)
|
||||
}
|
||||
|
||||
handleFileAction = async (
|
||||
action: PopoverFileItemAction,
|
||||
currentTab: PopoverTabs,
|
||||
): Promise<{
|
||||
didHandleAction: boolean
|
||||
}> => {
|
||||
const file = action.type !== PopoverFileItemActionType.RenameFile ? action.payload : action.payload.file
|
||||
let isAuthorizedForAction = true
|
||||
|
||||
const requiresAuthorization = file.protected && !UnprotectedFileActions.includes(action.type)
|
||||
|
||||
if (requiresAuthorization) {
|
||||
isAuthorizedForAction = await this.authorizeProtectedActionForFile(file, ChallengeReason.AccessProtectedFile)
|
||||
}
|
||||
|
||||
if (!isAuthorizedForAction) {
|
||||
return {
|
||||
didHandleAction: false,
|
||||
}
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case PopoverFileItemActionType.AttachFileToNote:
|
||||
await this.attachFileToNote(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DetachFileToNote:
|
||||
await this.detachFileFromNote(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DeleteFile:
|
||||
await this.deleteFile(file)
|
||||
break
|
||||
case PopoverFileItemActionType.DownloadFile:
|
||||
await this.downloadFile(file)
|
||||
break
|
||||
case PopoverFileItemActionType.ToggleFileProtection: {
|
||||
const isProtected = await this.toggleFileProtection(file)
|
||||
action.callback(isProtected)
|
||||
break
|
||||
}
|
||||
case PopoverFileItemActionType.RenameFile:
|
||||
await this.renameFile(file, action.payload.name)
|
||||
break
|
||||
case PopoverFileItemActionType.PreviewFile:
|
||||
this.appState.filePreviewModal.activate(
|
||||
file,
|
||||
currentTab === PopoverTabs.AllFiles ? this.allFiles : this.attachedFiles,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
if (!NonMutatingFileActions.includes(action.type)) {
|
||||
this.application.sync.sync().catch(console.error)
|
||||
}
|
||||
|
||||
return {
|
||||
didHandleAction: true,
|
||||
}
|
||||
}
|
||||
|
||||
public async downloadFile(file: FileItem): Promise<void> {
|
||||
let downloadingToastId = ''
|
||||
|
||||
|
||||
Reference in New Issue
Block a user