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:
Mo
2022-11-22 19:58:48 -06:00
committed by GitHub
parent 096d82f7af
commit 7c2e832065
22 changed files with 196 additions and 55 deletions

View File

@@ -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)

View 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)
}
} }

View File

@@ -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,

View File

@@ -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"
}, },

View File

@@ -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",

View File

@@ -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",

View File

@@ -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>
} }

View File

@@ -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
}
>
} }

View File

@@ -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",

View File

@@ -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'> {

View File

@@ -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": "*"
} }
} }

View File

@@ -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)
}
}
} }

View File

@@ -6,4 +6,5 @@ export {
DesktopClientRequiresWebMethods, DesktopClientRequiresWebMethods,
FileBackupsMapping, FileBackupsMapping,
FileBackupsDevice, FileBackupsDevice,
FileBackupRecord,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'

View File

@@ -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>

View File

@@ -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>
)}
</>
)
}

View File

@@ -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 && (

View File

@@ -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,

View File

@@ -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} />
</> </>
) )
} }

View 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}

View File

@@ -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" />

View File

@@ -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',

View File

@@ -6264,6 +6264,7 @@ __metadata:
lodash: ^4.17.21 lodash: ^4.17.21
reflect-metadata: ^0.1.13 reflect-metadata: ^0.1.13
ts-jest: ^29.0.3 ts-jest: ^29.0.3
typescript: "*"
languageName: unknown languageName: unknown
linkType: soft linkType: soft