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:
@@ -30,6 +30,7 @@ import {
|
||||
ArchiveManager,
|
||||
AutolockService,
|
||||
KeyboardService,
|
||||
PreferenceId,
|
||||
RouteService,
|
||||
RouteServiceInterface,
|
||||
ThemeManager,
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ export {
|
||||
DesktopClientRequiresWebMethods,
|
||||
FileBackupsMapping,
|
||||
FileBackupsDevice,
|
||||
FileBackupRecord,
|
||||
} 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 { 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'
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -55,18 +64,18 @@ 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)
|
||||
|
||||
@@ -74,7 +83,7 @@ const FileListItem: FunctionComponent<DisplayableListItemProps<FileItem>> = ({
|
||||
<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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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 MenuItem from '../Menu/MenuItem'
|
||||
import { MenuItemType } from '../Menu/MenuItemType'
|
||||
import { FileContextMenuBackupOption } from './FileContextMenuBackupOption'
|
||||
|
||||
type Props = {
|
||||
closeMenu: () => void
|
||||
@@ -120,6 +121,9 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
||||
<Icon type="trash" className="mr-2 text-danger" />
|
||||
<span className="text-danger">Delete permanently</span>
|
||||
</MenuItem>
|
||||
|
||||
<FileContextMenuBackupOption file={selectedFiles[0]} />
|
||||
|
||||
<HorizontalSeparator classes="my-2" />
|
||||
<div className="px-3 pt-1 pb-0.5 text-xs font-medium text-neutral">
|
||||
{!hasSelectedMultipleFiles && (
|
||||
|
||||
@@ -46,6 +46,7 @@ export const IconNameToSvgMapping = {
|
||||
'star-circle-filled': icons.StarCircleFilled,
|
||||
'star-filled': icons.StarFilledIcon,
|
||||
'star-variant-filled': icons.StarVariantFilledIcon,
|
||||
'safe-square': icons.SafeSquareIcon,
|
||||
'trash-filled': icons.TrashFilledIcon,
|
||||
'trash-sweep': icons.TrashSweepIcon,
|
||||
'user-add': icons.UserAddIcon,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FilesController } from '@/Controllers/FilesController'
|
||||
import { FileItem } from '@standardnotes/snjs'
|
||||
import { useState } from 'react'
|
||||
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||
import { FileContextMenuBackupOption } from '../FileContextMenu/FileContextMenuBackupOption'
|
||||
import Icon from '../Icon/Icon'
|
||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||
import Switch from '../Switch/Switch'
|
||||
@@ -91,6 +92,8 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
|
||||
<Icon type="trash" className="mr-2 text-danger" />
|
||||
<span className="text-danger">Delete permanently</span>
|
||||
</button>
|
||||
|
||||
<FileContextMenuBackupOption file={file} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,17 +76,20 @@ const MenuItem = forwardRef(
|
||||
role={type === MenuItemType.RadioButton ? 'menuitemradio' : 'menuitem'}
|
||||
tabIndex={typeof tabIndex === 'number' ? tabIndex : FOCUSABLE_BUT_NOT_TABBABLE}
|
||||
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',
|
||||
'focus:bg-info-backdrop focus:shadow-none md:text-tablet-menu-item lg:text-menu-item',
|
||||
className,
|
||||
className.includes('items-') ? '' : 'items-center',
|
||||
)}
|
||||
onClick={onClick}
|
||||
onBlur={onBlur}
|
||||
{...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})}
|
||||
>
|
||||
{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' ? (
|
||||
<RadioIndicator disabled={disabled} checked={checked} className="flex-shrink-0" />
|
||||
) : null}
|
||||
|
||||
@@ -21,7 +21,7 @@ const FileBackupsCrossPlatform = ({ application }: Props) => {
|
||||
<PreferencesGroup>
|
||||
<PreferencesSegment>
|
||||
<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>
|
||||
</PreferencesSegment>
|
||||
<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) => {
|
||||
return date.toLocaleTimeString(undefined, {
|
||||
timeStyle: 'short',
|
||||
|
||||
Reference in New Issue
Block a user