feat: display file backup status in file context menu (#2044) (skip e2e)
* 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
This commit is contained in:
@@ -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 { AppState } from 'app/AppState'
|
||||||
import { shell } from 'electron'
|
import { shell } from 'electron'
|
||||||
import { StoreKeys } from '../Store/StoreKeys'
|
import { StoreKeys } from '../Store/StoreKeys'
|
||||||
@@ -120,6 +120,10 @@ export class FilesBackupManager implements FileBackupsDevice {
|
|||||||
return this.defaultMappingFileValue()
|
return this.defaultMappingFileValue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const entry of Object.values(data.files)) {
|
||||||
|
entry.backedUpOn = new Date(entry.backedUpOn)
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +133,10 @@ export class FilesBackupManager implements FileBackupsDevice {
|
|||||||
void shell.openPath(location)
|
void shell.openPath(location)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openFileBackup(record: FileBackupRecord): Promise<void> {
|
||||||
|
void shell.openPath(record.absolutePath)
|
||||||
|
}
|
||||||
|
|
||||||
async saveFilesBackupsMappingFile(file: FileBackupsMapping): Promise<'success' | 'failed'> {
|
async saveFilesBackupsMappingFile(file: FileBackupsMapping): Promise<'success' | 'failed'> {
|
||||||
await writeJSONFile(this.getMappingFileLocation(), file)
|
await writeJSONFile(this.getMappingFileLocation(), file)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { StoreKeys } from '../Store/StoreKeys'
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const rendererPath = path.join('file://', __dirname, '/renderer.js')
|
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 { app, BrowserWindow } from 'electron'
|
||||||
import { BackupsManagerInterface } from '../Backups/BackupsManagerInterface'
|
import { BackupsManagerInterface } from '../Backups/BackupsManagerInterface'
|
||||||
import { KeychainInterface } from '../Keychain/KeychainInterface'
|
import { KeychainInterface } from '../Keychain/KeychainInterface'
|
||||||
@@ -64,6 +64,7 @@ export class RemoteBridge implements CrossProcessBridge {
|
|||||||
changeFilesBackupsLocation: this.changeFilesBackupsLocation.bind(this),
|
changeFilesBackupsLocation: this.changeFilesBackupsLocation.bind(this),
|
||||||
getFilesBackupsLocation: this.getFilesBackupsLocation.bind(this),
|
getFilesBackupsLocation: this.getFilesBackupsLocation.bind(this),
|
||||||
openFilesBackupsLocation: this.openFilesBackupsLocation.bind(this),
|
openFilesBackupsLocation: this.openFilesBackupsLocation.bind(this),
|
||||||
|
openFileBackup: this.openFileBackup.bind(this),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,4 +203,8 @@ export class RemoteBridge implements CrossProcessBridge {
|
|||||||
public openFilesBackupsLocation(): Promise<void> {
|
public openFilesBackupsLocation(): Promise<void> {
|
||||||
return this.fileBackups.openFilesBackupsLocation()
|
return this.fileBackups.openFilesBackupsLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public openFileBackup(record: FileBackupRecord): Promise<void> {
|
||||||
|
return this.fileBackups.openFileBackup(record)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Environment,
|
Environment,
|
||||||
FileBackupsMapping,
|
FileBackupsMapping,
|
||||||
RawKeychainValue,
|
RawKeychainValue,
|
||||||
|
FileBackupRecord,
|
||||||
} from '@web/Application/Device/DesktopSnjsExports'
|
} from '@web/Application/Device/DesktopSnjsExports'
|
||||||
import { WebOrDesktopDevice } from '@web/Application/Device/WebOrDesktopDevice'
|
import { WebOrDesktopDevice } from '@web/Application/Device/WebOrDesktopDevice'
|
||||||
import { Component } from '../Main/Packages/PackageManagerInterface'
|
import { Component } from '../Main/Packages/PackageManagerInterface'
|
||||||
@@ -132,6 +133,10 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn
|
|||||||
return this.remoteBridge.openFilesBackupsLocation()
|
return this.remoteBridge.openFilesBackupsLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openFileBackup(record: FileBackupRecord): Promise<void> {
|
||||||
|
return this.remoteBridge.openFileBackup(record)
|
||||||
|
}
|
||||||
|
|
||||||
async saveFilesBackupsFile(
|
async saveFilesBackupsFile(
|
||||||
uuid: string,
|
uuid: string,
|
||||||
metaFile: string,
|
metaFile: string,
|
||||||
|
|||||||
@@ -5,20 +5,13 @@
|
|||||||
"node": ">=16.0.0 <17.0.0"
|
"node": ">=16.0.0 <17.0.0"
|
||||||
},
|
},
|
||||||
"description": "Web filepicker for Standard Notes projects",
|
"description": "Web filepicker for Standard Notes projects",
|
||||||
"main": "dist/index.js",
|
"main": "./src/index.ts",
|
||||||
"author": "Standard Notes",
|
"author": "Standard Notes",
|
||||||
"types": "dist/index.d.ts",
|
"types": "./src/index.ts",
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -fr dist",
|
"build": "echo 'Empty build script required for yarn topological install'",
|
||||||
"prestart": "yarn clean",
|
|
||||||
"start": "tsc -p tsconfig.json --watch",
|
|
||||||
"prebuild": "yarn clean",
|
|
||||||
"build": "tsc -p tsconfig.json",
|
|
||||||
"lint": "eslint src --ext .ts",
|
"lint": "eslint src --ext .ts",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../node_modules/@standardnotes/config/src/tsconfig.json",
|
"extends": "../../UILib.tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
|
|||||||
@@ -5,22 +5,18 @@
|
|||||||
"node": ">=16.0.0 <17.0.0"
|
"node": ">=16.0.0 <17.0.0"
|
||||||
},
|
},
|
||||||
"description": "Client-side files library",
|
"description": "Client-side files library",
|
||||||
"main": "dist/index.js",
|
"main": "./src/index.ts",
|
||||||
"author": "Standard Notes",
|
"author": "Standard Notes",
|
||||||
"types": "dist/index.d.ts",
|
"types": "./src/index.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"scripts": {
|
"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",
|
"lint": "eslint src --ext .ts",
|
||||||
"test": "jest"
|
"test": "jest",
|
||||||
|
"build": "echo 'Empty build script required for yarn topological install'"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.2.3",
|
"@types/jest": "^29.2.3",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Uuid } from '@standardnotes/common'
|
import { Uuid } from '@standardnotes/common'
|
||||||
import { FileBackupsMapping } from './FileBackupsMapping'
|
import { FileBackupRecord, FileBackupsMapping } from './FileBackupsMapping'
|
||||||
|
|
||||||
export interface FileBackupsDevice {
|
export interface FileBackupsDevice {
|
||||||
getFilesBackupsMappingFile(): Promise<FileBackupsMapping>
|
getFilesBackupsMappingFile(): Promise<FileBackupsMapping>
|
||||||
@@ -18,4 +18,5 @@ export interface FileBackupsDevice {
|
|||||||
changeFilesBackupsLocation(): Promise<string | undefined>
|
changeFilesBackupsLocation(): Promise<string | undefined>
|
||||||
getFilesBackupsLocation(): Promise<string>
|
getFilesBackupsLocation(): Promise<string>
|
||||||
openFilesBackupsLocation(): Promise<void>
|
openFilesBackupsLocation(): Promise<void>
|
||||||
|
openFileBackup(record: FileBackupRecord): Promise<void>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { Uuid } from '@standardnotes/common'
|
import { Uuid } from '@standardnotes/common'
|
||||||
import { FileBackupsConstantsV1 } from './FileBackupsConstantsV1'
|
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 {
|
export interface FileBackupsMapping {
|
||||||
version: typeof FileBackupsConstantsV1.Version
|
version: typeof FileBackupsConstantsV1.Version
|
||||||
files: Record<
|
files: Record<Uuid, FileBackupRecord>
|
||||||
Uuid,
|
|
||||||
{
|
|
||||||
backedUpOn: Date
|
|
||||||
absolutePath: string
|
|
||||||
relativePath: string
|
|
||||||
metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName
|
|
||||||
binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName
|
|
||||||
version: typeof FileBackupsConstantsV1.Version
|
|
||||||
}
|
|
||||||
>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../node_modules/@standardnotes/config/src/tsconfig.json",
|
"extends": "../../UILib.tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ import { ContentType, Uuid } from '@standardnotes/common'
|
|||||||
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
import { EncryptionProviderInterface } from '@standardnotes/encryption'
|
||||||
import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models'
|
import { PayloadEmitSource, FileItem, CreateEncryptedBackupFileContextPayload } from '@standardnotes/models'
|
||||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
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 { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
|
||||||
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
import { ItemManagerInterface } from '../Item/ItemManagerInterface'
|
||||||
import { AbstractService } from '../Service/AbstractService'
|
import { AbstractService } from '../Service/AbstractService'
|
||||||
@@ -11,6 +17,7 @@ import { StatusServiceInterface } from '../Status/StatusServiceInterface'
|
|||||||
export class FilesBackupService extends AbstractService {
|
export class FilesBackupService extends AbstractService {
|
||||||
private itemsObserverDisposer: () => void
|
private itemsObserverDisposer: () => void
|
||||||
private pendingFiles = new Set<Uuid>()
|
private pendingFiles = new Set<Uuid>()
|
||||||
|
private mappingCache?: FileBackupsMapping['files']
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private items: ItemManagerInterface,
|
private items: ItemManagerInterface,
|
||||||
@@ -75,8 +82,30 @@ export class FilesBackupService extends AbstractService {
|
|||||||
return this.device.openFilesBackupsLocation()
|
return this.device.openFilesBackupsLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getBackupsMapping(): Promise<FileBackupsMapping['files']> {
|
private async getBackupsMappingFromDisk(): Promise<FileBackupsMapping['files']> {
|
||||||
return (await this.device.getFilesBackupsMappingFile()).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> {
|
private async handleChangedFiles(files: FileItem[]): Promise<void> {
|
||||||
@@ -88,7 +117,7 @@ export class FilesBackupService extends AbstractService {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapping = await this.getBackupsMapping()
|
const mapping = await this.getBackupsMappingFromDisk()
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (this.pendingFiles.has(file.uuid)) {
|
if (this.pendingFiles.has(file.uuid)) {
|
||||||
@@ -105,6 +134,8 @@ export class FilesBackupService extends AbstractService {
|
|||||||
this.pendingFiles.delete(file.uuid)
|
this.pendingFiles.delete(file.uuid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.invalidateMappingCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async performBackupOperation(file: FileItem): Promise<'success' | 'failed' | 'aborted'> {
|
private async performBackupOperation(file: FileItem): Promise<'success' | 'failed' | 'aborted'> {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"eslint-plugin-prettier": "*",
|
"eslint-plugin-prettier": "*",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
"jsdom": "^20.0.2",
|
"jsdom": "^20.0.2",
|
||||||
"ts-jest": "^29.0.3"
|
"ts-jest": "^29.0.3",
|
||||||
|
"typescript": "*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
ArchiveManager,
|
ArchiveManager,
|
||||||
AutolockService,
|
AutolockService,
|
||||||
KeyboardService,
|
KeyboardService,
|
||||||
|
PreferenceId,
|
||||||
RouteService,
|
RouteService,
|
||||||
RouteServiceInterface,
|
RouteServiceInterface,
|
||||||
ThemeManager,
|
ThemeManager,
|
||||||
@@ -387,4 +388,11 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
FeatureIdentifier.PlainEditor
|
FeatureIdentifier.PlainEditor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openPreferences(pane?: PreferenceId): void {
|
||||||
|
this.getViewControllerManager().preferencesController.openPreferences()
|
||||||
|
if (pane) {
|
||||||
|
this.getViewControllerManager().preferencesController.setCurrentPane(pane)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export {
|
|||||||
DesktopClientRequiresWebMethods,
|
DesktopClientRequiresWebMethods,
|
||||||
FileBackupsMapping,
|
FileBackupsMapping,
|
||||||
FileBackupsDevice,
|
FileBackupsDevice,
|
||||||
|
FileBackupRecord,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FileItem } from '@standardnotes/snjs'
|
import { FileItem, FileBackupRecord } from '@standardnotes/snjs'
|
||||||
import { observer } from 'mobx-react-lite'
|
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 { getFileIconComponent } from '../FilePreview/getFileIconComponent'
|
||||||
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
import ListItemConflictIndicator from './ListItemConflictIndicator'
|
||||||
import ListItemTags from './ListItemTags'
|
import ListItemTags from './ListItemTags'
|
||||||
@@ -12,19 +12,28 @@ import { useContextMenuEvent } from '@/Hooks/useContextMenuEvent'
|
|||||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||||
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
|
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
|
||||||
|
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||||
|
import Icon from '../Icon/Icon'
|
||||||
|
|
||||||
const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
||||||
filesController,
|
filesController,
|
||||||
hideDate,
|
hideDate,
|
||||||
hideIcon,
|
hideIcon,
|
||||||
hideTags,
|
hideTags,
|
||||||
item,
|
item: file,
|
||||||
onSelect,
|
onSelect,
|
||||||
selected,
|
selected,
|
||||||
sortBy,
|
sortBy,
|
||||||
tags,
|
tags,
|
||||||
}) => {
|
}) => {
|
||||||
const { toggleAppPane } = useResponsiveAppPane()
|
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)
|
const listItemRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@@ -45,7 +54,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
|||||||
let shouldOpenContextMenu = selected
|
let shouldOpenContextMenu = selected
|
||||||
|
|
||||||
if (!selected) {
|
if (!selected) {
|
||||||
const { didSelect } = await onSelect(item)
|
const { didSelect } = await onSelect(file)
|
||||||
if (didSelect) {
|
if (didSelect) {
|
||||||
shouldOpenContextMenu = true
|
shouldOpenContextMenu = true
|
||||||
}
|
}
|
||||||
@@ -55,18 +64,18 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
|||||||
openFileContextMenu(posX, posY)
|
openFileContextMenu(posX, posY)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[selected, onSelect, item, openFileContextMenu],
|
[selected, onSelect, file, openFileContextMenu],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onClick = useCallback(async () => {
|
const onClick = useCallback(async () => {
|
||||||
const { didSelect } = await onSelect(item, true)
|
const { didSelect } = await onSelect(file, true)
|
||||||
if (didSelect) {
|
if (didSelect) {
|
||||||
toggleAppPane(AppPaneId.Editor)
|
toggleAppPane(AppPaneId.Editor)
|
||||||
}
|
}
|
||||||
}, [item, onSelect, toggleAppPane])
|
}, [file, onSelect, toggleAppPane])
|
||||||
|
|
||||||
const IconComponent = () =>
|
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)
|
useContextMenuEvent(listItemRef, openContextMenu)
|
||||||
|
|
||||||
@@ -74,7 +83,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
|||||||
<div
|
<div
|
||||||
ref={listItemRef}
|
ref={listItemRef}
|
||||||
className={classNames('flex max-h-[300px] w-[190px] cursor-pointer px-1 pt-2 text-text md:w-[200px]')}
|
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}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -92,11 +101,11 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
|||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-grow py-4 px-0">
|
<div className="min-w-0 flex-grow py-4 px-0">
|
||||||
<div className="line-clamp-2 overflow-hidden text-editor font-semibold">
|
<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>
|
</div>
|
||||||
<ListItemMetadata item={item} hideDate={hideDate} sortBy={sortBy} />
|
<ListItemMetadata item={file} hideDate={hideDate} sortBy={sortBy} />
|
||||||
<ListItemTags hideTags={hideTags} tags={tags} />
|
<ListItemTags hideTags={hideTags} tags={tags} />
|
||||||
<ListItemConflictIndicator item={item} />
|
<ListItemConflictIndicator item={file} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -105,7 +114,14 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
|||||||
selected ? 'bg-info text-info-contrast' : 'bg-passive-4 text-neutral',
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { FunctionComponent, useCallback, useEffect, useState } from 'react'
|
||||||
|
import MenuItem from '../Menu/MenuItem'
|
||||||
|
import { useApplication } from '../ApplicationView/ApplicationProvider'
|
||||||
|
import { FileBackupRecord, FileItem } from '@standardnotes/snjs'
|
||||||
|
import { dateToStringStyle1 } from '@/Utils/DateUtils'
|
||||||
|
|
||||||
|
export const FileContextMenuBackupOption: FunctionComponent<{ file: FileItem }> = ({ file }) => {
|
||||||
|
const application = useApplication()
|
||||||
|
|
||||||
|
const [backupInfo, setBackupInfo] = useState<FileBackupRecord | undefined>(undefined)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void application.fileBackups?.getFileBackupInfo(file).then(setBackupInfo)
|
||||||
|
}, [application, file])
|
||||||
|
|
||||||
|
const openFileBackup = useCallback(() => {
|
||||||
|
if (backupInfo) {
|
||||||
|
void application.fileBackups?.openFileBackup(backupInfo)
|
||||||
|
}
|
||||||
|
}, [backupInfo, application])
|
||||||
|
|
||||||
|
const configureFileBackups = useCallback(() => {
|
||||||
|
application.openPreferences('backups')
|
||||||
|
}, [application])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{backupInfo && (
|
||||||
|
<MenuItem
|
||||||
|
icon={'check-circle'}
|
||||||
|
iconClassName={'text-success mt-1'}
|
||||||
|
className={'items-start'}
|
||||||
|
onClick={openFileBackup}
|
||||||
|
>
|
||||||
|
<div className="ml-2">
|
||||||
|
<div className="font-semibold text-success">Backed up on {dateToStringStyle1(backupInfo.backedUpOn)}</div>
|
||||||
|
<div className="text-xs text-neutral">{backupInfo.absolutePath}</div>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!backupInfo && application.fileBackups && (
|
||||||
|
<MenuItem
|
||||||
|
icon={'safe-square'}
|
||||||
|
className={'items-start'}
|
||||||
|
iconClassName={'text-neutral mt-1'}
|
||||||
|
onClick={configureFileBackups}
|
||||||
|
>
|
||||||
|
<div className="ml-2">
|
||||||
|
<div>Configure file backups</div>
|
||||||
|
<div className="text-xs text-neutral">File not backed up locally</div>
|
||||||
|
</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider'
|
|||||||
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
import { AppPaneId } from '../ResponsivePane/AppPaneMetadata'
|
||||||
import MenuItem from '../Menu/MenuItem'
|
import MenuItem from '../Menu/MenuItem'
|
||||||
import { MenuItemType } from '../Menu/MenuItemType'
|
import { MenuItemType } from '../Menu/MenuItemType'
|
||||||
|
import { FileContextMenuBackupOption } from './FileContextMenuBackupOption'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
closeMenu: () => void
|
closeMenu: () => void
|
||||||
@@ -120,6 +121,9 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
<Icon type="trash" className="mr-2 text-danger" />
|
<Icon type="trash" className="mr-2 text-danger" />
|
||||||
<span className="text-danger">Delete permanently</span>
|
<span className="text-danger">Delete permanently</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
|
<FileContextMenuBackupOption file={selectedFiles[0]} />
|
||||||
|
|
||||||
<HorizontalSeparator classes="my-2" />
|
<HorizontalSeparator classes="my-2" />
|
||||||
<div className="px-3 pt-1 pb-0.5 text-xs font-medium text-neutral">
|
<div className="px-3 pt-1 pb-0.5 text-xs font-medium text-neutral">
|
||||||
{!hasSelectedMultipleFiles && (
|
{!hasSelectedMultipleFiles && (
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const IconNameToSvgMapping = {
|
|||||||
'star-circle-filled': icons.StarCircleFilled,
|
'star-circle-filled': icons.StarCircleFilled,
|
||||||
'star-filled': icons.StarFilledIcon,
|
'star-filled': icons.StarFilledIcon,
|
||||||
'star-variant-filled': icons.StarVariantFilledIcon,
|
'star-variant-filled': icons.StarVariantFilledIcon,
|
||||||
|
'safe-square': icons.SafeSquareIcon,
|
||||||
'trash-filled': icons.TrashFilledIcon,
|
'trash-filled': icons.TrashFilledIcon,
|
||||||
'trash-sweep': icons.TrashSweepIcon,
|
'trash-sweep': icons.TrashSweepIcon,
|
||||||
'user-add': icons.UserAddIcon,
|
'user-add': icons.UserAddIcon,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FilesController } from '@/Controllers/FilesController'
|
|||||||
import { FileItem } from '@standardnotes/snjs'
|
import { FileItem } from '@standardnotes/snjs'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||||
|
import { FileContextMenuBackupOption } from '../FileContextMenu/FileContextMenuBackupOption'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||||
import Switch from '../Switch/Switch'
|
import Switch from '../Switch/Switch'
|
||||||
@@ -91,6 +92,8 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
|
|||||||
<Icon type="trash" className="mr-2 text-danger" />
|
<Icon type="trash" className="mr-2 text-danger" />
|
||||||
<span className="text-danger">Delete permanently</span>
|
<span className="text-danger">Delete permanently</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<FileContextMenuBackupOption file={file} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,17 +76,20 @@ const MenuItem = forwardRef(
|
|||||||
role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'}
|
role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'}
|
||||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-2 text-left md:py-1.5',
|
'flex w-full cursor-pointer border-0 bg-transparent px-3 py-2 text-left md:py-1.5',
|
||||||
'text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground',
|
'text-mobile-menu-item text-text hover:bg-contrast hover:text-foreground',
|
||||||
'focus:bg-info-backdrop focus:shadow-none md:text-tablet-menu-item lg:text-menu-item',
|
'focus:bg-info-backdrop focus:shadow-none md:text-tablet-menu-item lg:text-menu-item',
|
||||||
className,
|
className,
|
||||||
|
className.includes('items-') ? '' : 'items-center',
|
||||||
)}
|
)}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
{...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})}
|
{...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})}
|
||||||
>
|
>
|
||||||
{shortcut && <KeyboardShortcutIndicator className="mr-2" shortcut={shortcut} />}
|
{shortcut && <KeyboardShortcutIndicator className="mr-2" shortcut={shortcut} />}
|
||||||
{type === MenuItemType.IconButton && icon ? <Icon type={icon} className={iconClassName} /> : null}
|
{type === MenuItemType.IconButton && icon ? (
|
||||||
|
<Icon type={icon} className={`${iconClassName} flex-shrink-0`} />
|
||||||
|
) : null}
|
||||||
{type === MenuItemType.RadioButton && typeof checked === 'boolean' ? (
|
{type === MenuItemType.RadioButton && typeof checked === 'boolean' ? (
|
||||||
<RadioIndicator disabled={disabled} checked={checked} className="flex-shrink-0" />
|
<RadioIndicator disabled={disabled} checked={checked} className="flex-shrink-0" />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const FileBackupsCrossPlatform = ({ application }: Props) => {
|
|||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<Title>File Backups</Title>
|
<Title>File Backups</Title>
|
||||||
<Subtitle>Automatically save encrypted backups of files uploaded to any device to this computer.</Subtitle>
|
<Subtitle>Automatically save encrypted backups of files uploaded on any device to this computer.</Subtitle>
|
||||||
<Text className="mt-3">To enable file backups, use the Standard Notes desktop application.</Text>
|
<Text className="mt-3">To enable file backups, use the Standard Notes desktop application.</Text>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
<HorizontalSeparator classes="my-4" />
|
<HorizontalSeparator classes="my-4" />
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ export const formatDateAndTimeForNote = (date: Date, includeTime = true) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const dateToStringStyle1 = (date: Date) => {
|
||||||
|
const dateString = `${date.toLocaleDateString()}`
|
||||||
|
|
||||||
|
return `${dateString} at ${date.toLocaleTimeString(undefined, {
|
||||||
|
timeStyle: 'short',
|
||||||
|
})}`
|
||||||
|
}
|
||||||
|
|
||||||
export const dateToHoursAndMinutesTimeString = (date: Date) => {
|
export const dateToHoursAndMinutesTimeString = (date: Date) => {
|
||||||
return date.toLocaleTimeString(undefined, {
|
return date.toLocaleTimeString(undefined, {
|
||||||
timeStyle: 'short',
|
timeStyle: 'short',
|
||||||
|
|||||||
Reference in New Issue
Block a user