Skip to content

Commit

Permalink
feat: display file backup status in file context menu (#2044) (skip e2e)
Browse files Browse the repository at this point in the history
* feat: show file backup status in context menu

* feat: show backup status in list cell

* feat: mapping cache

* feat: add to linking menu + date format

* fix: types
  • Loading branch information
moughxyz committed Nov 23, 2022
1 parent 096d82f commit 7c2e832
Show file tree
Hide file tree
Showing 22 changed files with 196 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
import { FileBackupRecord, FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
import { AppState } from 'app/AppState'
import { shell } from 'electron'
import { StoreKeys } from '../Store/StoreKeys'
Expand Down Expand Up @@ -120,6 +120,10 @@ export class FilesBackupManager implements FileBackupsDevice {
return this.defaultMappingFileValue()
}

for (const entry of Object.values(data.files)) {
entry.backedUpOn = new Date(entry.backedUpOn)
}

return data
}

Expand All @@ -129,6 +133,10 @@ export class FilesBackupManager implements FileBackupsDevice {
void shell.openPath(location)
}

async openFileBackup(record: FileBackupRecord): Promise<void> {
void shell.openPath(record.absolutePath)
}

async saveFilesBackupsMappingFile(file: FileBackupsMapping): Promise<'success' | 'failed'> {
await writeJSONFile(this.getMappingFileLocation(), file)

Expand Down
7 changes: 6 additions & 1 deletion packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { StoreKeys } from '../Store/StoreKeys'
const path = require('path')
const rendererPath = path.join('file://', __dirname, '/renderer.js')

import { FileBackupsDevice, FileBackupsMapping } from '@web/Application/Device/DesktopSnjsExports'
import { FileBackupsDevice, FileBackupsMapping, FileBackupRecord } from '@web/Application/Device/DesktopSnjsExports'
import { app, BrowserWindow } from 'electron'
import { BackupsManagerInterface } from '../Backups/BackupsManagerInterface'
import { KeychainInterface } from '../Keychain/KeychainInterface'
Expand Down Expand Up @@ -64,6 +64,7 @@ export class RemoteBridge implements CrossProcessBridge {
changeFilesBackupsLocation: this.changeFilesBackupsLocation.bind(this),
getFilesBackupsLocation: this.getFilesBackupsLocation.bind(this),
openFilesBackupsLocation: this.openFilesBackupsLocation.bind(this),
openFileBackup: this.openFileBackup.bind(this),
}
}

Expand Down Expand Up @@ -202,4 +203,8 @@ export class RemoteBridge implements CrossProcessBridge {
public openFilesBackupsLocation(): Promise<void> {
return this.fileBackups.openFilesBackupsLocation()
}

public openFileBackup(record: FileBackupRecord): Promise<void> {
return this.fileBackups.openFileBackup(record)
}
}
5 changes: 5 additions & 0 deletions packages/desktop/app/javascripts/Renderer/DesktopDevice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Environment,
FileBackupsMapping,
RawKeychainValue,
FileBackupRecord,
} from '@web/Application/Device/DesktopSnjsExports'
import { WebOrDesktopDevice } from '@web/Application/Device/WebOrDesktopDevice'
import { Component } from '../Main/Packages/PackageManagerInterface'
Expand Down Expand Up @@ -132,6 +133,10 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
return this.remoteBridge.openFilesBackupsLocation()
}

openFileBackup(record: FileBackupRecord): Promise<void> {
return this.remoteBridge.openFileBackup(record)
}

async saveFilesBackupsFile(
uuid: string,
metaFile: string,
Expand Down
13 changes: 3 additions & 10 deletions packages/filepicker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,13 @@
"node": ">=16.0.0 <17.0.0"
},
"description": "Web filepicker for Standard Notes projects",
"main": "dist/index.js",
"main": "./src/index.ts",
"author": "Standard Notes",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"types": "./src/index.ts",
"private": true,
"license": "AGPL-3.0-or-later",
"scripts": {
"clean": "rm -fr dist",
"prestart": "yarn clean",
"start": "tsc -p tsconfig.json --watch",
"prebuild": "yarn clean",
"build": "tsc -p tsconfig.json",
"build": "echo 'Empty build script required for yarn topological install'",
"lint": "eslint src --ext .ts",
"test": "jest"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/filepicker/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "../../node_modules/@standardnotes/config/src/tsconfig.json",
"extends": "../../UILib.tsconfig.json",
"compilerOptions": {
"skipLibCheck": true,
"rootDir": "./src",
Expand Down
12 changes: 4 additions & 8 deletions packages/files/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,18 @@
"node": ">=16.0.0 <17.0.0"
},
"description": "Client-side files library",
"main": "dist/index.js",
"main": "./src/index.ts",
"author": "Standard Notes",
"types": "dist/index.d.ts",
"types": "./src/index.ts",
"files": [
"dist"
],
"private": true,
"license": "AGPL-3.0-or-later",
"scripts": {
"clean": "rm -fr dist",
"prestart": "yarn clean",
"start": "tsc -p tsconfig.json --watch",
"prebuild": "yarn clean",
"build": "tsc -p tsconfig.json",
"lint": "eslint src --ext .ts",
"test": "jest"
"test": "jest",
"build": "echo 'Empty build script required for yarn topological install'"
},
"devDependencies": {
"@types/jest": "^29.2.3",
Expand Down
3 changes: 2 additions & 1 deletion packages/files/src/Domain/Device/FileBackupsDevice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Uuid } from '@standardnotes/common'
import { FileBackupsMapping } from './FileBackupsMapping'
import { FileBackupRecord, FileBackupsMapping } from './FileBackupsMapping'

export interface FileBackupsDevice {
getFilesBackupsMappingFile(): Promise<FileBackupsMapping>
Expand All @@ -18,4 +18,5 @@ export interface FileBackupsDevice {
changeFilesBackupsLocation(): Promise<string | undefined>
getFilesBackupsLocation(): Promise<string>
openFilesBackupsLocation(): Promise<void>
openFileBackup(record: FileBackupRecord): Promise<void>
}
21 changes: 10 additions & 11 deletions packages/files/src/Domain/Device/FileBackupsMapping.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { Uuid } from '@standardnotes/common'
import { FileBackupsConstantsV1 } from './FileBackupsConstantsV1'

export type FileBackupRecord = {
backedUpOn: Date
absolutePath: string
relativePath: string
metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName
binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName
version: typeof FileBackupsConstantsV1.Version
}

export interface FileBackupsMapping {
version: typeof FileBackupsConstantsV1.Version
files: Record<
Uuid,
{
backedUpOn: Date
absolutePath: string
relativePath: string
metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName
binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName
version: typeof FileBackupsConstantsV1.Version
}
>
files: Record<Uuid, FileBackupRecord>
}
2 changes: 1 addition & 1 deletion packages/files/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": "../../node_modules/@standardnotes/config/src/tsconfig.json",
"extends": "../../UILib.tsconfig.json",
"compilerOptions": {
"skipLibCheck": true,
"rootDir": "./src",
Expand Down
39 changes: 35 additions & 4 deletions packages/services/src/Domain/Backups/BackupService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { ContentType, Uuid } from '@standardnotes/common'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { FilesApiInterface, FileBackupMetadataFile, FileBackupsDevice, FileBackupsMapping } from '@standardnotes/files'
import {
FilesApiInterface,
FileBackupMetadataFile,
FileBackupsDevice,
FileBackupsMapping,
FileBackupRecord,
} from '@standardnotes/files'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
import { AbstractService } from '../Service/AbstractService'
Expand All @@ -11,6 +17,7 @@ import { StatusServiceInterface } from '../Status/StatusServiceInterface'
export class FilesBackupService extends AbstractService {
private itemsObserverDisposer: () => void
private pendingFiles = new Set<Uuid>()
private mappingCache?: FileBackupsMapping['files']

constructor(
private items: ItemManagerInterface,
Expand Down Expand Up @@ -75,8 +82,30 @@ export class FilesBackupService extends AbstractService {
return this.device.openFilesBackupsLocation()
}

private async getBackupsMapping(): Promise<FileBackupsMapping['files']> {
return (await this.device.getFilesBackupsMappingFile()).files
private async getBackupsMappingFromDisk(): Promise<FileBackupsMapping['files']> {
const result = (await this.device.getFilesBackupsMappingFile()).files

this.mappingCache = result

return result
}

private invalidateMappingCache(): void {
this.mappingCache = undefined
}

private async getBackupsMappingFromCache(): Promise<FileBackupsMapping['files']> {
return this.mappingCache ?? (await this.getBackupsMappingFromDisk())
}

public async getFileBackupInfo(file: FileItem): Promise<FileBackupRecord | undefined> {
const mapping = await this.getBackupsMappingFromCache()
const record = mapping[file.uuid]
return record
}

public async openFileBackup(record: FileBackupRecord): Promise<void> {
await this.device.openFileBackup(record)
}

private async handleChangedFiles(files: FileItem[]): Promise<void> {
Expand All @@ -88,7 +117,7 @@ export class FilesBackupService extends AbstractService {
return
}

const mapping = await this.getBackupsMapping()
const mapping = await this.getBackupsMappingFromDisk()

for (const file of files) {
if (this.pendingFiles.has(file.uuid)) {
Expand All @@ -105,6 +134,8 @@ export class FilesBackupService extends AbstractService {
this.pendingFiles.delete(file.uuid)
}
}

this.invalidateMappingCache()
}

private async performBackupOperation(file: FileItem): Promise<'success' | 'failed' | 'aborted'> {
Expand Down
3 changes: 2 additions & 1 deletion packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"eslint-plugin-prettier": "*",
"jest": "^29.3.1",
"jsdom": "^20.0.2",
"ts-jest": "^29.0.3"
"ts-jest": "^29.0.3",
"typescript": "*"
}
}
8 changes: 8 additions & 0 deletions packages/web/src/javascripts/Application/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
ArchiveManager,
AutolockService,
KeyboardService,
PreferenceId,
RouteService,
RouteServiceInterface,
ThemeManager,
Expand Down Expand Up @@ -387,4 +388,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter
FeatureIdentifier.PlainEditor
)
}

openPreferences(pane?: PreferenceId): void {
this.getViewControllerManager().preferencesController.openPreferences()
if (pane) {
this.getViewControllerManager().preferencesController.setCurrentPane(pane)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export {
DesktopClientRequiresWebMethods,
FileBackupsMapping,
FileBackupsDevice,
FileBackupRecord,
} from '@standardnotes/snjs'
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FileItem } from '@standardnotes/snjs'
import { FileItem, FileBackupRecord } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite'
import { FunctionComponent, useCallback, useRef } from 'react'
import { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react'
import { getFileIconComponent } from '../FilePreview/getFileIconComponent'
import ListItemConflictIndicator from './ListItemConflictIndicator'
import ListItemTags from './ListItemTags'
Expand All @@ -12,19 +12,28 @@ import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { formatSizeToReadableString } from '@standardnotes/filepicker'
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
import { useApplication } from '../ApplicationView/ApplicationProvider'
import Icon from '../Icon/Icon'

const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
filesController,
hideDate,
hideIcon,
hideTags,
item,
item: file,
onSelect,
selected,
sortBy,
tags,
}) => {
const { toggleAppPane } = useResponsiveAppPane()
const application = useApplication()

const [backupInfo, setBackupInfo] = useState<FileBackupRecord | undefined>(undefined)

useEffect(() => {
void application.fileBackups?.getFileBackupInfo(file).then(setBackupInfo)
}, [application, file])

const listItemRef = useRef<HTMLDivElement>(null)

Expand All @@ -45,7 +54,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
let shouldOpenContextMenu = selected

if (!selected) {
const { didSelect } = await onSelect(item)
const { didSelect } = await onSelect(file)
if (didSelect) {
shouldOpenContextMenu = true
}
Expand All @@ -55,26 +64,26 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
openFileContextMenu(posX, posY)
}
},
[selected, onSelect, item, openFileContextMenu],
[selected, onSelect, file, openFileContextMenu],
)

const onClick = useCallback(async () => {
const { didSelect } = await onSelect(item, true)
const { didSelect } = await onSelect(file, true)
if (didSelect) {
toggleAppPane(AppPaneId.Editor)
}
}, [item, onSelect, toggleAppPane])
}, [file, onSelect, toggleAppPane])

const IconComponent = () =>
getFileIconComponent(getIconForFileType((item as FileItem).mimeType), 'w-10 h-10 flex-shrink-0')
getFileIconComponent(getIconForFileType((file as FileItem).mimeType), 'w-10 h-10 flex-shrink-0')

useContextMenuEvent(listItemRef, openContextMenu)

return (
<div
ref={listItemRef}
className={classNames('flex max-h-[300px] w-[190px] cursor-pointer px-1 pt-2 text-text md:w-[200px]')}
id={item.uuid}
id={file.uuid}
onClick={onClick}
>
<div
Expand All @@ -92,11 +101,11 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
)}
<div className="min-w-0 flex-grow py-4 px-0">
<div className="line-clamp-2 overflow-hidden text-editor font-semibold">
<div className="break-word line-clamp-2 mr-2 overflow-hidden">{item.title}</div>
<div className="break-word line-clamp-2 mr-2 overflow-hidden">{file.title}</div>
</div>
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
<ListItemMetadata item={file} hideDate={hideDate} sortBy={sortBy} />
<ListItemTags hideTags={hideTags} tags={tags} />
<ListItemConflictIndicator item={item} />
<ListItemConflictIndicator item={file} />
</div>
</div>
<div
Expand All @@ -105,7 +114,14 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
selected ? 'bg-info text-info-contrast' : 'bg-passive-4 text-neutral',
)}
>
{formatSizeToReadableString(item.decryptedSize)}
<div className="flex justify-between">
{formatSizeToReadableString(file.decryptedSize)}
{backupInfo && (
<div title="File is backed up locally">
<Icon type="check-circle" />
</div>
)}
</div>
</div>
</div>
</div>
Expand Down

0 comments on commit 7c2e832

Please sign in to comment.