feat-dev(wip): files table view (#2100)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { FileItem } from '@standardnotes/snjs'
|
import { FileItem } from '@standardnotes/snjs'
|
||||||
|
|
||||||
export enum PopoverFileItemActionType {
|
export enum FileItemActionType {
|
||||||
AttachFileToNote,
|
AttachFileToNote,
|
||||||
DetachFileToNote,
|
DetachFileToNote,
|
||||||
DeleteFile,
|
DeleteFile,
|
||||||
@@ -10,34 +10,32 @@ export enum PopoverFileItemActionType {
|
|||||||
PreviewFile,
|
PreviewFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PopoverFileItemAction =
|
export type FileItemAction =
|
||||||
| {
|
| {
|
||||||
type: Exclude<
|
type: Exclude<
|
||||||
PopoverFileItemActionType,
|
FileItemActionType,
|
||||||
| PopoverFileItemActionType.RenameFile
|
FileItemActionType.RenameFile | FileItemActionType.ToggleFileProtection | FileItemActionType.PreviewFile
|
||||||
| PopoverFileItemActionType.ToggleFileProtection
|
|
||||||
| PopoverFileItemActionType.PreviewFile
|
|
||||||
>
|
>
|
||||||
payload: {
|
payload: {
|
||||||
file: FileItem
|
file: FileItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: PopoverFileItemActionType.ToggleFileProtection
|
type: FileItemActionType.ToggleFileProtection
|
||||||
payload: {
|
payload: {
|
||||||
file: FileItem
|
file: FileItem
|
||||||
}
|
}
|
||||||
callback: (isProtected: boolean) => void
|
callback: (isProtected: boolean) => void
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: PopoverFileItemActionType.RenameFile
|
type: FileItemActionType.RenameFile
|
||||||
payload: {
|
payload: {
|
||||||
file: FileItem
|
file: FileItem
|
||||||
name: string
|
name: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: PopoverFileItemActionType.PreviewFile
|
type: FileItemActionType.PreviewFile
|
||||||
payload: {
|
payload: {
|
||||||
file: FileItem
|
file: FileItem
|
||||||
otherFiles?: FileItem[]
|
otherFiles?: FileItem[]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from '@standardnotes/ui-services'
|
} from '@standardnotes/ui-services'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { PANEL_NAME_NOTES } from '@/Constants/Constants'
|
import { PANEL_NAME_NOTES } from '@/Constants/Constants'
|
||||||
import { FileItem, PrefKey, WebAppEvent } from '@standardnotes/snjs'
|
import { FileItem, PrefKey, SystemViewId, WebAppEvent } from '@standardnotes/snjs'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { forwardRef, useCallback, useEffect, useMemo } from 'react'
|
import { forwardRef, useCallback, useEffect, useMemo } from 'react'
|
||||||
import ContentList from '@/Components/ContentListView/ContentList'
|
import ContentList from '@/Components/ContentListView/ContentList'
|
||||||
@@ -37,6 +37,9 @@ import { PanelResizedData } from '@/Types/PanelResizedData'
|
|||||||
import { useForwardedRef } from '@/Hooks/useForwardedRef'
|
import { useForwardedRef } from '@/Hooks/useForwardedRef'
|
||||||
import { isMobileScreen } from '@/Utils'
|
import { isMobileScreen } from '@/Utils'
|
||||||
import FloatingAddButton from './FloatingAddButton'
|
import FloatingAddButton from './FloatingAddButton'
|
||||||
|
import FilesTableView from '../FilesTableView/FilesTableView'
|
||||||
|
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||||
|
import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
accountMenuController: AccountMenuController
|
accountMenuController: AccountMenuController
|
||||||
@@ -49,6 +52,7 @@ type Props = {
|
|||||||
selectionController: SelectedItemsController
|
selectionController: SelectedItemsController
|
||||||
searchOptionsController: SearchOptionsController
|
searchOptionsController: SearchOptionsController
|
||||||
linkingController: LinkingController
|
linkingController: LinkingController
|
||||||
|
featuresController: FeaturesController
|
||||||
className?: string
|
className?: string
|
||||||
id: string
|
id: string
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode
|
||||||
@@ -68,6 +72,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
|||||||
selectionController,
|
selectionController,
|
||||||
searchOptionsController,
|
searchOptionsController,
|
||||||
linkingController,
|
linkingController,
|
||||||
|
featuresController,
|
||||||
className,
|
className,
|
||||||
id,
|
id,
|
||||||
children,
|
children,
|
||||||
@@ -280,6 +285,9 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
|||||||
}
|
}
|
||||||
}, [selectedUuids, innerRef, isCurrentNoteTemplate, renderedItems, panes])
|
}, [selectedUuids, innerRef, isCurrentNoteTemplate, renderedItems, panes])
|
||||||
|
|
||||||
|
const isFilesTableViewEnabled = featureTrunkEnabled(FeatureTrunkName.FilesTableView)
|
||||||
|
const shouldShowFilesTableView = isFilesTableViewEnabled && selectedTag?.uuid === SystemViewId.Files
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id={id}
|
id={id}
|
||||||
@@ -300,12 +308,16 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
|||||||
addButtonLabel={addButtonLabel}
|
addButtonLabel={addButtonLabel}
|
||||||
addNewItem={addNewItem}
|
addNewItem={addNewItem}
|
||||||
isFilesSmartView={isFilesSmartView}
|
isFilesSmartView={isFilesSmartView}
|
||||||
|
isFilesTableViewEnabled={isFilesTableViewEnabled}
|
||||||
optionsSubtitle={optionsSubtitle}
|
optionsSubtitle={optionsSubtitle}
|
||||||
selectedTag={selectedTag}
|
selectedTag={selectedTag}
|
||||||
filesController={filesController}
|
filesController={filesController}
|
||||||
|
itemListController={itemListController}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SearchBar itemListController={itemListController} searchOptionsController={searchOptionsController} />
|
{!isFilesTableViewEnabled && (
|
||||||
|
<SearchBar itemListController={itemListController} searchOptionsController={searchOptionsController} />
|
||||||
|
)}
|
||||||
<NoAccountWarning
|
<NoAccountWarning
|
||||||
accountMenuController={accountMenuController}
|
accountMenuController={accountMenuController}
|
||||||
noAccountWarningController={noAccountWarningController}
|
noAccountWarningController={noAccountWarningController}
|
||||||
@@ -324,13 +336,18 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
|||||||
{!dailyMode && completedFullSync && !renderedItems.length ? (
|
{!dailyMode && completedFullSync && !renderedItems.length ? (
|
||||||
<p className="empty-items-list opacity-50">No items.</p>
|
<p className="empty-items-list opacity-50">No items.</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!dailyMode && !completedFullSync && !renderedItems.length ? (
|
{!dailyMode && !completedFullSync && !renderedItems.length ? (
|
||||||
<p className="empty-items-list opacity-50">Loading...</p>
|
<p className="empty-items-list opacity-50">Loading...</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!dailyMode && renderedItems.length ? (
|
{!dailyMode && renderedItems.length ? (
|
||||||
<>
|
shouldShowFilesTableView ? (
|
||||||
|
<FilesTableView
|
||||||
|
application={application}
|
||||||
|
filesController={filesController}
|
||||||
|
featuresController={featuresController}
|
||||||
|
linkingController={linkingController}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<ContentList
|
<ContentList
|
||||||
items={renderedItems}
|
items={renderedItems}
|
||||||
selectedUuids={selectedUuids}
|
selectedUuids={selectedUuids}
|
||||||
@@ -342,7 +359,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
|||||||
notesController={notesController}
|
notesController={notesController}
|
||||||
selectionController={selectionController}
|
selectionController={selectionController}
|
||||||
/>
|
/>
|
||||||
</>
|
)
|
||||||
) : null}
|
) : null}
|
||||||
<div className="absolute bottom-0 h-safe-bottom w-full" />
|
<div className="absolute bottom-0 h-safe-bottom w-full" />
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -44,12 +44,8 @@ const AddItemMenuButton = ({
|
|||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'hidden md:flex',
|
'z-editor-title-bar hidden h-8 w-8 cursor-pointer items-center justify-center rounded-full border border-solid border-transparent hover:brightness-125 md:flex',
|
||||||
'h-8 w-8 hover:brightness-125',
|
isDailyEntry ? 'bg-danger text-danger-contrast' : 'bg-info text-info-contrast',
|
||||||
'z-editor-title-bar ml-3 cursor-pointer items-center',
|
|
||||||
`justify-center rounded-full border border-solid border-transparent ${
|
|
||||||
isDailyEntry ? 'bg-danger text-danger-contrast' : 'bg-info text-info-contrast'
|
|
||||||
}`,
|
|
||||||
)}
|
)}
|
||||||
title={addButtonLabel}
|
title={addButtonLabel}
|
||||||
aria-label={addButtonLabel}
|
aria-label={addButtonLabel}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
|
|||||||
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
import { MediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||||
import AddItemMenuButton from './AddItemMenuButton'
|
import AddItemMenuButton from './AddItemMenuButton'
|
||||||
import { FilesController } from '@/Controllers/FilesController'
|
import { FilesController } from '@/Controllers/FilesController'
|
||||||
|
import SearchButton from './SearchButton'
|
||||||
|
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -19,9 +21,11 @@ type Props = {
|
|||||||
addButtonLabel: string
|
addButtonLabel: string
|
||||||
addNewItem: () => void
|
addNewItem: () => void
|
||||||
isFilesSmartView: boolean
|
isFilesSmartView: boolean
|
||||||
|
isFilesTableViewEnabled: boolean
|
||||||
optionsSubtitle?: string
|
optionsSubtitle?: string
|
||||||
selectedTag: AnyTag
|
selectedTag: AnyTag
|
||||||
filesController: FilesController
|
filesController: FilesController
|
||||||
|
itemListController: ItemListController
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContentListHeader = ({
|
const ContentListHeader = ({
|
||||||
@@ -31,9 +35,11 @@ const ContentListHeader = ({
|
|||||||
addButtonLabel,
|
addButtonLabel,
|
||||||
addNewItem,
|
addNewItem,
|
||||||
isFilesSmartView,
|
isFilesSmartView,
|
||||||
|
isFilesTableViewEnabled,
|
||||||
optionsSubtitle,
|
optionsSubtitle,
|
||||||
selectedTag,
|
selectedTag,
|
||||||
filesController,
|
filesController,
|
||||||
|
itemListController,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
|
const displayOptionsContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
|
const displayOptionsButtonRef = useRef<HTMLButtonElement>(null)
|
||||||
@@ -98,6 +104,14 @@ const ContentListHeader = ({
|
|||||||
)
|
)
|
||||||
}, [addButtonLabel, addNewItem, filesController, isDailyEntry, isFilesSmartView])
|
}, [addButtonLabel, addNewItem, filesController, isDailyEntry, isFilesSmartView])
|
||||||
|
|
||||||
|
const SearchBarButton = useMemo(() => {
|
||||||
|
if (!isFilesSmartView || !isFilesTableViewEnabled) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SearchButton itemListController={itemListController} />
|
||||||
|
}, [isFilesSmartView, isFilesTableViewEnabled, itemListController])
|
||||||
|
|
||||||
const FolderName = useMemo(() => {
|
const FolderName = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 flex-grow flex-col break-words pt-1 lg:pt-0">
|
<div className="flex min-w-0 flex-grow flex-col break-words pt-1 lg:pt-0">
|
||||||
@@ -125,13 +139,14 @@ const ContentListHeader = ({
|
|||||||
<div className={'flex w-full justify-between md:flex'}>
|
<div className={'flex w-full justify-between md:flex'}>
|
||||||
<NavigationMenuButton />
|
<NavigationMenuButton />
|
||||||
{FolderName}
|
{FolderName}
|
||||||
<div className="flex">
|
<div className="flex items-center gap-3">
|
||||||
|
{SearchBarButton}
|
||||||
{OptionsMenu}
|
{OptionsMenu}
|
||||||
{AddButton}
|
{AddButton}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [OptionsMenu, AddButton, FolderName])
|
}, [FolderName, SearchBarButton, OptionsMenu, AddButton])
|
||||||
|
|
||||||
const TabletLayout = useMemo(() => {
|
const TabletLayout = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import RoundIconButton from '@/Components/Button/RoundIconButton'
|
||||||
|
import ClearInputButton from '@/Components/ClearInputButton/ClearInputButton'
|
||||||
|
import Icon from '@/Components/Icon/Icon'
|
||||||
|
import DecoratedInput from '@/Components/Input/DecoratedInput'
|
||||||
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
|
import { ItemListController } from '@/Controllers/ItemList/ItemListController'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
itemListController: ItemListController
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchButton = ({ itemListController }: Props) => {
|
||||||
|
const { noteFilterText, setNoteFilterText, clearFilterText } = itemListController
|
||||||
|
|
||||||
|
const [isSearchBarVisible, setIsSearchBarVisible] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isSearchBarVisible && (
|
||||||
|
<DecoratedInput
|
||||||
|
autocomplete={false}
|
||||||
|
id={ElementIds.SearchBar}
|
||||||
|
className={{
|
||||||
|
container: 'px-1',
|
||||||
|
input: 'text-base placeholder:text-passive-0 lg:text-sm',
|
||||||
|
}}
|
||||||
|
placeholder={'Search...'}
|
||||||
|
value={noteFilterText}
|
||||||
|
ref={(node) => {
|
||||||
|
if (node && document.activeElement !== node) {
|
||||||
|
node.focus()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onChange={(query) => setNoteFilterText(query)}
|
||||||
|
left={[<Icon type="search" className="mr-1 h-4.5 w-4.5 flex-shrink-0 text-passive-1" />]}
|
||||||
|
right={[noteFilterText && <ClearInputButton onClick={clearFilterText} />]}
|
||||||
|
roundedFull
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<RoundIconButton
|
||||||
|
onClick={() => {
|
||||||
|
setIsSearchBarVisible(!isSearchBarVisible)
|
||||||
|
}}
|
||||||
|
icon={isSearchBarVisible ? 'close' : 'search'}
|
||||||
|
label="Display options menu"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(SearchButton)
|
||||||
@@ -13,6 +13,7 @@ type Props = {
|
|||||||
|
|
||||||
const FileContextMenu: FunctionComponent<Props> = observer(({ filesController, selectionController }) => {
|
const FileContextMenu: FunctionComponent<Props> = observer(({ filesController, selectionController }) => {
|
||||||
const { showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = filesController
|
const { showFileContextMenu, setShowFileContextMenu, fileContextMenuLocation } = filesController
|
||||||
|
const { selectedFiles } = selectionController
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
@@ -25,7 +26,7 @@ const FileContextMenu: FunctionComponent<Props> = observer(({ filesController, s
|
|||||||
<Menu a11yLabel="File context menu" isOpen={showFileContextMenu}>
|
<Menu a11yLabel="File context menu" isOpen={showFileContextMenu}>
|
||||||
<FileMenuOptions
|
<FileMenuOptions
|
||||||
filesController={filesController}
|
filesController={filesController}
|
||||||
selectionController={selectionController}
|
selectedFiles={selectedFiles}
|
||||||
closeMenu={() => setShowFileContextMenu(false)}
|
closeMenu={() => setShowFileContextMenu(false)}
|
||||||
shouldShowRenameOption={false}
|
shouldShowRenameOption={false}
|
||||||
shouldShowAttachOption={false}
|
shouldShowAttachOption={false}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { FunctionComponent, useCallback, useMemo } from 'react'
|
import { FunctionComponent, useCallback, useMemo } from 'react'
|
||||||
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
import { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FilesController } from '@/Controllers/FilesController'
|
import { FilesController } from '@/Controllers/FilesController'
|
||||||
import { SelectedItemsController } from '@/Controllers/SelectedItemsController'
|
|
||||||
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
import HorizontalSeparator from '../Shared/HorizontalSeparator'
|
||||||
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||||
@@ -11,27 +10,27 @@ import { AppPaneId } from '../Panes/AppPaneMetadata'
|
|||||||
import MenuItem from '../Menu/MenuItem'
|
import MenuItem from '../Menu/MenuItem'
|
||||||
import { FileContextMenuBackupOption } from './FileContextMenuBackupOption'
|
import { FileContextMenuBackupOption } from './FileContextMenuBackupOption'
|
||||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||||
|
import { FileItem } from '@standardnotes/snjs'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
closeMenu: () => void
|
closeMenu: () => void
|
||||||
filesController: FilesController
|
filesController: FilesController
|
||||||
selectionController: SelectedItemsController
|
|
||||||
isFileAttachedToNote?: boolean
|
isFileAttachedToNote?: boolean
|
||||||
renameToggleCallback?: (isRenamingFile: boolean) => void
|
renameToggleCallback?: (isRenamingFile: boolean) => void
|
||||||
shouldShowRenameOption: boolean
|
shouldShowRenameOption: boolean
|
||||||
shouldShowAttachOption: boolean
|
shouldShowAttachOption: boolean
|
||||||
|
selectedFiles: FileItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileMenuOptions: FunctionComponent<Props> = ({
|
const FileMenuOptions: FunctionComponent<Props> = ({
|
||||||
closeMenu,
|
closeMenu,
|
||||||
filesController,
|
filesController,
|
||||||
selectionController,
|
|
||||||
isFileAttachedToNote,
|
isFileAttachedToNote,
|
||||||
renameToggleCallback,
|
renameToggleCallback,
|
||||||
shouldShowRenameOption,
|
shouldShowRenameOption,
|
||||||
shouldShowAttachOption,
|
shouldShowAttachOption,
|
||||||
|
selectedFiles,
|
||||||
}) => {
|
}) => {
|
||||||
const { selectedFiles } = selectionController
|
|
||||||
const { handleFileAction } = filesController
|
const { handleFileAction } = filesController
|
||||||
const { toggleAppPane } = useResponsiveAppPane()
|
const { toggleAppPane } = useResponsiveAppPane()
|
||||||
|
|
||||||
@@ -46,7 +45,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
const onDetach = useCallback(() => {
|
const onDetach = useCallback(() => {
|
||||||
const file = selectedFiles[0]
|
const file = selectedFiles[0]
|
||||||
void handleFileAction({
|
void handleFileAction({
|
||||||
type: PopoverFileItemActionType.DetachFileToNote,
|
type: FileItemActionType.DetachFileToNote,
|
||||||
payload: { file },
|
payload: { file },
|
||||||
})
|
})
|
||||||
closeMenu()
|
closeMenu()
|
||||||
@@ -55,7 +54,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
const onAttach = useCallback(() => {
|
const onAttach = useCallback(() => {
|
||||||
const file = selectedFiles[0]
|
const file = selectedFiles[0]
|
||||||
void handleFileAction({
|
void handleFileAction({
|
||||||
type: PopoverFileItemActionType.AttachFileToNote,
|
type: FileItemActionType.AttachFileToNote,
|
||||||
payload: { file },
|
payload: { file },
|
||||||
})
|
})
|
||||||
closeMenu()
|
closeMenu()
|
||||||
@@ -86,7 +85,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
<MenuSwitchButtonItem
|
<MenuSwitchButtonItem
|
||||||
checked={hasProtectedFiles}
|
checked={hasProtectedFiles}
|
||||||
onChange={(hasProtectedFiles) => {
|
onChange={(hasProtectedFiles) => {
|
||||||
void filesController.setProtectionForFiles(hasProtectedFiles, selectionController.selectedFiles)
|
void filesController.setProtectionForFiles(hasProtectedFiles, selectedFiles)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="lock" className="mr-2 text-neutral" />
|
<Icon type="lock" className="mr-2 text-neutral" />
|
||||||
@@ -95,7 +94,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
<HorizontalSeparator classes="my-1" />
|
<HorizontalSeparator classes="my-1" />
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void filesController.downloadFiles(selectionController.selectedFiles)
|
void filesController.downloadFiles(selectedFiles)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="download" className="mr-2 text-neutral" />
|
<Icon type="download" className="mr-2 text-neutral" />
|
||||||
@@ -114,7 +113,7 @@ const FileMenuOptions: FunctionComponent<Props> = ({
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
closeMenuAndToggleFilesList()
|
closeMenuAndToggleFilesList()
|
||||||
void filesController.deleteFilesPermanently(selectionController.selectedFiles)
|
void filesController.deleteFilesPermanently(selectedFiles)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon type="trash" className="mr-2 text-danger" />
|
<Icon type="trash" className="mr-2 text-danger" />
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const FilesOptionsPanel = ({ filesController, selectionController }: Props) => {
|
|||||||
<Menu a11yLabel="File options panel" isOpen={isOpen}>
|
<Menu a11yLabel="File options panel" isOpen={isOpen}>
|
||||||
<FileMenuOptions
|
<FileMenuOptions
|
||||||
filesController={filesController}
|
filesController={filesController}
|
||||||
selectionController={selectionController}
|
selectedFiles={selectionController.selectedFiles}
|
||||||
closeMenu={() => {
|
closeMenu={() => {
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||||
|
import { FilesController } from '@/Controllers/FilesController'
|
||||||
|
import { formatDateForContextMenu } from '@/Utils/DateUtils'
|
||||||
|
import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType'
|
||||||
|
import { formatSizeToReadableString } from '@standardnotes/filepicker'
|
||||||
|
import { ContentType, FileItem, SortableItem, PrefKey, ApplicationEvent, naturalSort } from '@standardnotes/snjs'
|
||||||
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||||
|
import { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||||
|
import { getFileIconComponent } from '../FilePreview/getFileIconComponent'
|
||||||
|
import Popover from '../Popover/Popover'
|
||||||
|
import Table from '../Table/Table'
|
||||||
|
import { TableColumn } from '../Table/CommonTypes'
|
||||||
|
import { useTable } from '../Table/useTable'
|
||||||
|
import Menu from '../Menu/Menu'
|
||||||
|
import FileMenuOptions from '../FileContextMenu/FileMenuOptions'
|
||||||
|
import Icon from '../Icon/Icon'
|
||||||
|
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
|
||||||
|
import LinkedItemBubble from '../LinkedItems/LinkedItemBubble'
|
||||||
|
import LinkedItemsPanel from '../LinkedItems/LinkedItemsPanel'
|
||||||
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
|
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||||
|
|
||||||
|
const ContextMenuCell = ({ files, filesController }: { files: FileItem[]; filesController: FilesController }) => {
|
||||||
|
const [contextMenuVisible, setContextMenuVisible] = useState(false)
|
||||||
|
const anchorElementRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="rounded-full border border-border bg-default p-1"
|
||||||
|
ref={anchorElementRef}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
setContextMenuVisible((visible) => !visible)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="more" />
|
||||||
|
</button>
|
||||||
|
<Popover
|
||||||
|
open={contextMenuVisible}
|
||||||
|
anchorElement={anchorElementRef.current}
|
||||||
|
togglePopover={() => {
|
||||||
|
setContextMenuVisible(false)
|
||||||
|
}}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
className="py-2"
|
||||||
|
>
|
||||||
|
<Menu a11yLabel="File context menu" isOpen={contextMenuVisible}>
|
||||||
|
<FileMenuOptions
|
||||||
|
closeMenu={() => {
|
||||||
|
setContextMenuVisible(false)
|
||||||
|
}}
|
||||||
|
filesController={filesController}
|
||||||
|
shouldShowRenameOption={false}
|
||||||
|
shouldShowAttachOption={false}
|
||||||
|
selectedFiles={files}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileLinksCell = ({
|
||||||
|
file,
|
||||||
|
filesController,
|
||||||
|
linkingController,
|
||||||
|
featuresController,
|
||||||
|
}: {
|
||||||
|
file: FileItem
|
||||||
|
filesController: FilesController
|
||||||
|
linkingController: LinkingController
|
||||||
|
featuresController: FeaturesController
|
||||||
|
}) => {
|
||||||
|
const [contextMenuVisible, setContextMenuVisible] = useState(false)
|
||||||
|
const anchorElementRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="rounded-full border border-border bg-default p-1"
|
||||||
|
ref={anchorElementRef}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
setContextMenuVisible((visible) => !visible)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="link" />
|
||||||
|
</button>
|
||||||
|
<Popover
|
||||||
|
open={contextMenuVisible}
|
||||||
|
anchorElement={anchorElementRef.current}
|
||||||
|
togglePopover={() => {
|
||||||
|
setContextMenuVisible(false)
|
||||||
|
}}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
className="py-2"
|
||||||
|
>
|
||||||
|
<LinkedItemsPanel
|
||||||
|
linkingController={linkingController}
|
||||||
|
filesController={filesController}
|
||||||
|
featuresController={featuresController}
|
||||||
|
isOpen={contextMenuVisible}
|
||||||
|
item={file}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication
|
||||||
|
filesController: FilesController
|
||||||
|
featuresController: FeaturesController
|
||||||
|
linkingController: LinkingController
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilesTableView = ({ application, filesController, featuresController, linkingController }: Props) => {
|
||||||
|
const files = application.items
|
||||||
|
.getDisplayableNotesAndFiles()
|
||||||
|
.filter((item) => item.content_type === ContentType.File) as FileItem[]
|
||||||
|
|
||||||
|
const [sortBy, setSortBy] = useState<keyof SortableItem>(
|
||||||
|
application.getPreference(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy]),
|
||||||
|
)
|
||||||
|
const [sortReversed, setSortReversed] = useState(
|
||||||
|
application.getPreference(PrefKey.SortNotesReverse, PrefDefaults[PrefKey.SortNotesReverse]),
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return application.addEventObserver(async (event) => {
|
||||||
|
if (event === ApplicationEvent.PreferencesChanged) {
|
||||||
|
setSortBy(application.getPreference(PrefKey.SortNotesBy, PrefDefaults[PrefKey.SortNotesBy]))
|
||||||
|
setSortReversed(application.getPreference(PrefKey.SortNotesReverse, PrefDefaults[PrefKey.SortNotesReverse]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [application])
|
||||||
|
|
||||||
|
const onSortChange = useCallback(
|
||||||
|
async (sortBy: keyof SortableItem, sortReversed: boolean) => {
|
||||||
|
await application.setPreference(PrefKey.SortNotesBy, sortBy)
|
||||||
|
await application.setPreference(PrefKey.SortNotesReverse, sortReversed)
|
||||||
|
},
|
||||||
|
[application],
|
||||||
|
)
|
||||||
|
|
||||||
|
const [contextMenuFile, setContextMenuFile] = useState<FileItem | undefined>(undefined)
|
||||||
|
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | undefined>(undefined)
|
||||||
|
|
||||||
|
const columnDefs: TableColumn<FileItem>[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
name: 'Name',
|
||||||
|
sortBy: 'title',
|
||||||
|
cell: (file) => {
|
||||||
|
return (
|
||||||
|
<div className="flex max-w-[40vw] items-center gap-3 whitespace-normal">
|
||||||
|
{getFileIconComponent(getIconForFileType(file.mimeType), 'w-6 h-6 flex-shrink-0')}
|
||||||
|
<span className="text-sm font-medium">{file.title}</span>
|
||||||
|
{file.protected && (
|
||||||
|
<span className="flex items-center" title="File is protected">
|
||||||
|
<Icon
|
||||||
|
ariaLabel="File is protected"
|
||||||
|
type="lock-filled"
|
||||||
|
className="h-3.5 w-3.5 text-passive-1"
|
||||||
|
size="custom"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Upload date',
|
||||||
|
sortBy: 'created_at',
|
||||||
|
cell: (file) => {
|
||||||
|
return formatDateForContextMenu(file.created_at)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Size',
|
||||||
|
cell: (file) => {
|
||||||
|
return formatSizeToReadableString(file.decryptedSize)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Attached to',
|
||||||
|
cell: (file) => {
|
||||||
|
const links = [
|
||||||
|
...naturalSort(application.items.referencesForItem(file), 'title').map((item) =>
|
||||||
|
createLinkFromItem(item, 'linked'),
|
||||||
|
),
|
||||||
|
...naturalSort(application.items.itemsReferencingItem(file), 'title').map((item) =>
|
||||||
|
createLinkFromItem(item, 'linked-by'),
|
||||||
|
),
|
||||||
|
...application.items.getSortedTagsForItem(file).map((item) => createLinkFromItem(item, 'linked')),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!links.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex max-w-76 flex-wrap gap-2">
|
||||||
|
<LinkedItemBubble
|
||||||
|
className="overflow-hidden"
|
||||||
|
link={links[0]}
|
||||||
|
key={links[0].id}
|
||||||
|
unlinkItem={async (itemToUnlink) => {
|
||||||
|
void application.items.unlinkItems(file, itemToUnlink)
|
||||||
|
}}
|
||||||
|
isBidirectional={false}
|
||||||
|
/>
|
||||||
|
{links.length > 1 && <span>and {links.length - 1} more...</span>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[application.items],
|
||||||
|
)
|
||||||
|
|
||||||
|
const getRowId = useCallback((file: FileItem) => file.uuid, [])
|
||||||
|
|
||||||
|
const table = useTable({
|
||||||
|
data: files,
|
||||||
|
sortBy,
|
||||||
|
sortReversed,
|
||||||
|
onSortChange,
|
||||||
|
getRowId,
|
||||||
|
columns: columnDefs,
|
||||||
|
enableRowSelection: true,
|
||||||
|
enableMultipleRowSelection: true,
|
||||||
|
onRowDoubleClick(file) {
|
||||||
|
void filesController.handleFileAction({
|
||||||
|
type: FileItemActionType.PreviewFile,
|
||||||
|
payload: {
|
||||||
|
file,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onRowContextMenu(x, y, file) {
|
||||||
|
setContextMenuPosition({ x, y })
|
||||||
|
setContextMenuFile(file)
|
||||||
|
},
|
||||||
|
rowActions: (file) => {
|
||||||
|
const links = [
|
||||||
|
...naturalSort(application.items.referencesForItem(file), 'title').map((item) =>
|
||||||
|
createLinkFromItem(item, 'linked'),
|
||||||
|
),
|
||||||
|
...naturalSort(application.items.itemsReferencingItem(file), 'title').map((item) =>
|
||||||
|
createLinkFromItem(item, 'linked-by'),
|
||||||
|
),
|
||||||
|
...application.items.getSortedTagsForItem(file).map((item) => createLinkFromItem(item, 'linked')),
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{links.length > 0 && (
|
||||||
|
<FileLinksCell
|
||||||
|
file={file}
|
||||||
|
filesController={filesController}
|
||||||
|
featuresController={featuresController}
|
||||||
|
linkingController={linkingController}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ContextMenuCell files={[file]} filesController={filesController} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selectionActions: (fileIds) => (
|
||||||
|
<ContextMenuCell files={files.filter((file) => fileIds.includes(file.uuid))} filesController={filesController} />
|
||||||
|
),
|
||||||
|
showSelectionActions: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table table={table} />
|
||||||
|
{contextMenuPosition && contextMenuFile && (
|
||||||
|
<Popover
|
||||||
|
open={true}
|
||||||
|
anchorPoint={contextMenuPosition}
|
||||||
|
togglePopover={() => {
|
||||||
|
setContextMenuPosition(undefined)
|
||||||
|
setContextMenuFile(undefined)
|
||||||
|
}}
|
||||||
|
side="bottom"
|
||||||
|
align="start"
|
||||||
|
className="py-2"
|
||||||
|
>
|
||||||
|
<Menu a11yLabel="File context menu" isOpen={true}>
|
||||||
|
<FileMenuOptions
|
||||||
|
closeMenu={() => {
|
||||||
|
setContextMenuPosition(undefined)
|
||||||
|
setContextMenuFile(undefined)
|
||||||
|
}}
|
||||||
|
filesController={filesController}
|
||||||
|
shouldShowRenameOption={false}
|
||||||
|
shouldShowAttachOption={false}
|
||||||
|
selectedFiles={[contextMenuFile]}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
export default FilesTableView
|
||||||
@@ -2,6 +2,8 @@ import * as icons from '@standardnotes/icons'
|
|||||||
|
|
||||||
export const IconNameToSvgMapping = {
|
export const IconNameToSvgMapping = {
|
||||||
'account-circle': icons.AccountCircleIcon,
|
'account-circle': icons.AccountCircleIcon,
|
||||||
|
'arrow-up': icons.ArrowUpIcon,
|
||||||
|
'arrow-down': icons.ArrowDownIcon,
|
||||||
'arrow-left': icons.ArrowLeftIcon,
|
'arrow-left': icons.ArrowLeftIcon,
|
||||||
'arrow-right': icons.ArrowRightIcon,
|
'arrow-right': icons.ArrowRightIcon,
|
||||||
'arrows-sort-down': icons.ArrowsSortDownIcon,
|
'arrows-sort-down': icons.ArrowsSortDownIcon,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants'
|
|||||||
import { FilesController } from '@/Controllers/FilesController'
|
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 { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||||
import { FileContextMenuBackupOption } from '../FileContextMenu/FileContextMenuBackupOption'
|
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'
|
||||||
@@ -24,7 +24,7 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
|
|||||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void handleFileAction({
|
void handleFileAction({
|
||||||
type: PopoverFileItemActionType.PreviewFile,
|
type: FileItemActionType.PreviewFile,
|
||||||
payload: {
|
payload: {
|
||||||
file,
|
file,
|
||||||
otherFiles: [],
|
otherFiles: [],
|
||||||
@@ -41,7 +41,7 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
|
|||||||
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
className="flex w-full cursor-pointer items-center justify-between border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleFileAction({
|
handleFileAction({
|
||||||
type: PopoverFileItemActionType.ToggleFileProtection,
|
type: FileItemActionType.ToggleFileProtection,
|
||||||
payload: { file },
|
payload: { file },
|
||||||
callback: (isProtected: boolean) => {
|
callback: (isProtected: boolean) => {
|
||||||
setIsFileProtected(isProtected)
|
setIsFileProtected(isProtected)
|
||||||
@@ -60,7 +60,7 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
|
|||||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleFileAction({
|
handleFileAction({
|
||||||
type: PopoverFileItemActionType.DownloadFile,
|
type: FileItemActionType.DownloadFile,
|
||||||
payload: { file },
|
payload: { file },
|
||||||
}).catch(console.error)
|
}).catch(console.error)
|
||||||
closeMenu()
|
closeMenu()
|
||||||
@@ -83,7 +83,7 @@ const LinkedFileMenuOptions = ({ file, closeMenu, handleFileAction, setIsRenamin
|
|||||||
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
className="flex w-full cursor-pointer items-center border-0 bg-transparent px-3 py-1.5 text-left text-sm text-text hover:bg-contrast hover:text-foreground focus:bg-info-backdrop focus:shadow-none"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleFileAction({
|
handleFileAction({
|
||||||
type: PopoverFileItemActionType.DeleteFile,
|
type: FileItemActionType.DeleteFile,
|
||||||
payload: { file },
|
payload: { file },
|
||||||
}).catch(console.error)
|
}).catch(console.error)
|
||||||
closeMenu()
|
closeMenu()
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
|||||||
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
|
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
|
||||||
import { FOCUS_TAGS_INPUT_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
|
import { FOCUS_TAGS_INPUT_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
|
||||||
import { useCommandService } from '../CommandProvider'
|
import { useCommandService } from '../CommandProvider'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
|
||||||
import { useItemLinks } from '@/Hooks/useItemLinks'
|
import { useItemLinks } from '@/Hooks/useItemLinks'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -19,14 +18,11 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
|
const LinkedItemBubblesContainer = ({ linkingController }: Props) => {
|
||||||
const application = useApplication()
|
|
||||||
const activeItem = application.itemControllerGroup.activeItemViewController?.item
|
|
||||||
|
|
||||||
const { toggleAppPane } = useResponsiveAppPane()
|
const { toggleAppPane } = useResponsiveAppPane()
|
||||||
|
|
||||||
const commandService = useCommandService()
|
const commandService = useCommandService()
|
||||||
|
|
||||||
const { unlinkItemFromSelectedItem: unlinkItem, activateItem } = linkingController
|
const { activeItem, unlinkItemFromSelectedItem: unlinkItem, activateItem } = linkingController
|
||||||
|
|
||||||
const { notesLinkedToItem, filesLinkedToItem, tagsLinkedToItem, notesLinkingToItem, filesLinkingToItem } =
|
const { notesLinkedToItem, filesLinkedToItem, tagsLinkedToItem, notesLinkingToItem, filesLinkingToItem } =
|
||||||
useItemLinks(activeItem)
|
useItemLinks(activeItem)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { FilesController } from '@/Controllers/FilesController'
|
|||||||
import { LinkingController } from '@/Controllers/LinkingController'
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { useRef, useCallback } from 'react'
|
import { useRef, useCallback } from 'react'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
|
||||||
import RoundIconButton from '../Button/RoundIconButton'
|
import RoundIconButton from '../Button/RoundIconButton'
|
||||||
import Popover from '../Popover/Popover'
|
import Popover from '../Popover/Popover'
|
||||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||||
@@ -17,10 +16,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LinkedItemsButton = ({ linkingController, filesController, onClickPreprocessing, featuresController }: Props) => {
|
const LinkedItemsButton = ({ linkingController, filesController, onClickPreprocessing, featuresController }: Props) => {
|
||||||
const application = useApplication()
|
const { activeItem, isLinkingPanelOpen, setIsLinkingPanelOpen } = linkingController
|
||||||
const activeItem = application.itemControllerGroup.activeItemViewController?.item
|
|
||||||
|
|
||||||
const { isLinkingPanelOpen, setIsLinkingPanelOpen } = linkingController
|
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
const toggleMenu = useCallback(async () => {
|
const toggleMenu = useCallback(async () => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import DecoratedInput from '../Input/DecoratedInput'
|
|||||||
import LinkedItemSearchResults from './LinkedItemSearchResults'
|
import LinkedItemSearchResults from './LinkedItemSearchResults'
|
||||||
import { LinkedItemsSectionItem } from './LinkedItemsSectionItem'
|
import { LinkedItemsSectionItem } from './LinkedItemsSectionItem'
|
||||||
import { DecryptedItem } from '@standardnotes/snjs'
|
import { DecryptedItem } from '@standardnotes/snjs'
|
||||||
|
import { useItemLinks } from '@/Hooks/useItemLinks'
|
||||||
|
|
||||||
const LinkedItemsPanel = ({
|
const LinkedItemsPanel = ({
|
||||||
linkingController,
|
linkingController,
|
||||||
@@ -27,22 +28,10 @@ const LinkedItemsPanel = ({
|
|||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
item: DecryptedItem
|
item: DecryptedItem
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const { linkItems, unlinkItems, activateItem, createAndAddNewTag, isEntitledToNoteLinking } = linkingController
|
||||||
getLinkedTagsForItem,
|
|
||||||
getFilesLinksForItem,
|
|
||||||
getLinkedNotesForItem,
|
|
||||||
getNotesLinkingToItem,
|
|
||||||
linkItems,
|
|
||||||
unlinkItemFromSelectedItem,
|
|
||||||
activateItem,
|
|
||||||
createAndAddNewTag,
|
|
||||||
isEntitledToNoteLinking,
|
|
||||||
} = linkingController
|
|
||||||
|
|
||||||
const tagsLinkedToItem = getLinkedTagsForItem(item) || []
|
const { notesLinkedToItem, notesLinkingToItem, filesLinkedToItem, filesLinkingToItem, tagsLinkedToItem } =
|
||||||
const { filesLinkedToItem, filesLinkingToItem } = getFilesLinksForItem(item)
|
useItemLinks(item)
|
||||||
const notesLinkedToItem = getLinkedNotesForItem(item) || []
|
|
||||||
const notesLinkingToItem = getNotesLinkingToItem(item) || []
|
|
||||||
|
|
||||||
const { entitledToFiles } = featuresController
|
const { entitledToFiles } = featuresController
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
@@ -128,7 +117,7 @@ const LinkedItemsPanel = ({
|
|||||||
key={link.id}
|
key={link.id}
|
||||||
item={link.item}
|
item={link.item}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
|
unlinkItem={() => unlinkItems(item, link.item)}
|
||||||
activateItem={activateItem}
|
activateItem={activateItem}
|
||||||
handleFileAction={filesController.handleFileAction}
|
handleFileAction={filesController.handleFileAction}
|
||||||
/>
|
/>
|
||||||
@@ -148,7 +137,7 @@ const LinkedItemsPanel = ({
|
|||||||
key={link.id}
|
key={link.id}
|
||||||
item={link.item}
|
item={link.item}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
|
unlinkItem={() => unlinkItems(item, link.item)}
|
||||||
activateItem={activateItem}
|
activateItem={activateItem}
|
||||||
handleFileAction={filesController.handleFileAction}
|
handleFileAction={filesController.handleFileAction}
|
||||||
/>
|
/>
|
||||||
@@ -172,7 +161,7 @@ const LinkedItemsPanel = ({
|
|||||||
key={link.id}
|
key={link.id}
|
||||||
item={link.item}
|
item={link.item}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
|
unlinkItem={() => unlinkItems(item, link.item)}
|
||||||
activateItem={activateItem}
|
activateItem={activateItem}
|
||||||
handleFileAction={filesController.handleFileAction}
|
handleFileAction={filesController.handleFileAction}
|
||||||
/>
|
/>
|
||||||
@@ -191,7 +180,7 @@ const LinkedItemsPanel = ({
|
|||||||
key={link.id}
|
key={link.id}
|
||||||
item={link.item}
|
item={link.item}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
|
unlinkItem={() => unlinkItems(item, link.item)}
|
||||||
activateItem={activateItem}
|
activateItem={activateItem}
|
||||||
handleFileAction={filesController.handleFileAction}
|
handleFileAction={filesController.handleFileAction}
|
||||||
/>
|
/>
|
||||||
@@ -208,7 +197,7 @@ const LinkedItemsPanel = ({
|
|||||||
key={link.id}
|
key={link.id}
|
||||||
item={link.item}
|
item={link.item}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
|
unlinkItem={() => unlinkItems(item, link.item)}
|
||||||
activateItem={activateItem}
|
activateItem={activateItem}
|
||||||
handleFileAction={filesController.handleFileAction}
|
handleFileAction={filesController.handleFileAction}
|
||||||
/>
|
/>
|
||||||
@@ -227,7 +216,7 @@ const LinkedItemsPanel = ({
|
|||||||
key={link.id}
|
key={link.id}
|
||||||
item={link.item}
|
item={link.item}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
unlinkItem={() => unlinkItemFromSelectedItem(link.item)}
|
unlinkItem={() => unlinkItems(item, link.item)}
|
||||||
activateItem={activateItem}
|
activateItem={activateItem}
|
||||||
handleFileAction={filesController.handleFileAction}
|
handleFileAction={filesController.handleFileAction}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { FileItem } from '@standardnotes/snjs'
|
|||||||
import { KeyboardKey } from '@standardnotes/ui-services'
|
import { KeyboardKey } from '@standardnotes/ui-services'
|
||||||
import { useRef, useState } from 'react'
|
import { useRef, useState } from 'react'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
import { PopoverFileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
import { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import MenuItem from '../Menu/MenuItem'
|
import MenuItem from '../Menu/MenuItem'
|
||||||
import Popover from '../Popover/Popover'
|
import Popover from '../Popover/Popover'
|
||||||
@@ -46,7 +46,7 @@ export const LinkedItemsSectionItem = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
await handleFileAction({
|
await handleFileAction({
|
||||||
type: PopoverFileItemActionType.RenameFile,
|
type: FileItemActionType.RenameFile,
|
||||||
payload: {
|
payload: {
|
||||||
file: item,
|
file: item,
|
||||||
name: name,
|
name: name,
|
||||||
|
|||||||
@@ -303,6 +303,7 @@ const PanesSystemComponent = () => {
|
|||||||
selectionController={viewControllerManager.selectionController}
|
selectionController={viewControllerManager.selectionController}
|
||||||
searchOptionsController={viewControllerManager.searchOptionsController}
|
searchOptionsController={viewControllerManager.searchOptionsController}
|
||||||
linkingController={viewControllerManager.linkingController}
|
linkingController={viewControllerManager.linkingController}
|
||||||
|
featuresController={viewControllerManager.featuresController}
|
||||||
>
|
>
|
||||||
{showPanelResizers && listRef && (
|
{showPanelResizers && listRef && (
|
||||||
<PanelResizer
|
<PanelResizer
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ type ExperimentalFeatureItem = {
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: {
|
application: {
|
||||||
|
setValue: WebApplication['setValue']
|
||||||
|
getValue: WebApplication['getValue']
|
||||||
features: WebApplication['features']
|
features: WebApplication['features']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
packages/web/src/javascripts/Components/Table/CommonTypes.ts
Normal file
38
packages/web/src/javascripts/Components/Table/CommonTypes.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { SortableItem } from '@standardnotes/snjs'
|
||||||
|
import { MouseEventHandler, ReactNode } from 'react'
|
||||||
|
|
||||||
|
export type TableSortBy = keyof SortableItem
|
||||||
|
|
||||||
|
export type TableColumn<Data> = {
|
||||||
|
name: string
|
||||||
|
sortBy?: TableSortBy
|
||||||
|
cell: (data: Data) => ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TableRow<Data> = {
|
||||||
|
id: string
|
||||||
|
cells: ReactNode[]
|
||||||
|
isSelected: boolean
|
||||||
|
rowData: Data
|
||||||
|
rowActions?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TableHeader = {
|
||||||
|
name: string
|
||||||
|
isSorting: boolean | undefined
|
||||||
|
sortBy?: TableSortBy
|
||||||
|
sortReversed: boolean | undefined
|
||||||
|
onSortChange: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Table<Data> = {
|
||||||
|
headers: TableHeader[]
|
||||||
|
rows: TableRow<Data>[]
|
||||||
|
handleRowClick: (id: string) => MouseEventHandler<HTMLTableRowElement>
|
||||||
|
handleRowDoubleClick: (id: string) => MouseEventHandler<HTMLTableRowElement>
|
||||||
|
handleRowContextMenu: (id: string) => MouseEventHandler<HTMLTableRowElement>
|
||||||
|
canSelectRows: boolean
|
||||||
|
selectedRows: string[]
|
||||||
|
selectionActions: ReactNode | undefined
|
||||||
|
showSelectionActions: boolean
|
||||||
|
}
|
||||||
82
packages/web/src/javascripts/Components/Table/Table.tsx
Normal file
82
packages/web/src/javascripts/Components/Table/Table.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { classNames } from '@standardnotes/snjs'
|
||||||
|
import Icon from '../Icon/Icon'
|
||||||
|
import { Table } from './CommonTypes'
|
||||||
|
|
||||||
|
function Table<Data>({ table }: { table: Table<Data> }) {
|
||||||
|
return (
|
||||||
|
<div className="block min-h-0 overflow-auto">
|
||||||
|
{table.showSelectionActions && table.selectedRows.length >= 2 && (
|
||||||
|
<div className="flex items-center justify-between border-b border-border px-3 py-2">
|
||||||
|
<span className="text-info-0 text-sm font-medium">{table.selectedRows.length} selected</span>
|
||||||
|
{table.selectedRows.length > 0 && table.selectionActions}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{table.headers.map((header, index) => {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
className={classNames(
|
||||||
|
'border-b border-border px-3 pt-3 pb-2 text-left text-sm font-medium text-passive-0',
|
||||||
|
header.sortBy && 'cursor-pointer hover:bg-info-backdrop hover:underline',
|
||||||
|
)}
|
||||||
|
onClick={header.onSortChange}
|
||||||
|
key={index.toString()}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{header.name}
|
||||||
|
{header.isSorting && (
|
||||||
|
<Icon
|
||||||
|
type={header.sortReversed ? 'arrow-up' : 'arrow-down'}
|
||||||
|
size="custom"
|
||||||
|
className="h-4.5 w-4.5 text-passive-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-border whitespace-nowrap">
|
||||||
|
{table.rows.map((row) => {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={row.id}
|
||||||
|
className={classNames(
|
||||||
|
'group relative',
|
||||||
|
row.isSelected && 'bg-info-backdrop',
|
||||||
|
table.canSelectRows && 'cursor-pointer hover:bg-contrast',
|
||||||
|
)}
|
||||||
|
onClick={table.handleRowClick(row.id)}
|
||||||
|
onDoubleClick={table.handleRowDoubleClick(row.id)}
|
||||||
|
onContextMenu={table.handleRowContextMenu(row.id)}
|
||||||
|
>
|
||||||
|
{row.cells.map((cell, index) => {
|
||||||
|
return (
|
||||||
|
<td key={index} className="py-3 px-3">
|
||||||
|
{cell}
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{row.rowActions ? (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'absolute right-3 top-1/2 -translate-y-1/2',
|
||||||
|
row.isSelected ? '' : 'invisible group-hover:visible',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{row.rowActions}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Table
|
||||||
204
packages/web/src/javascripts/Components/Table/useTable.tsx
Normal file
204
packages/web/src/javascripts/Components/Table/useTable.tsx
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { MouseEventHandler, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useApplication } from '../ApplicationProvider'
|
||||||
|
import { Table, TableColumn, TableRow, TableSortBy } from './CommonTypes'
|
||||||
|
|
||||||
|
type TableSortOptions =
|
||||||
|
| {
|
||||||
|
sortBy: TableSortBy
|
||||||
|
sortReversed: boolean
|
||||||
|
onSortChange: (sortBy: TableSortBy, reversed: boolean) => void
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
sortBy?: never
|
||||||
|
sortReversed?: never
|
||||||
|
onSortChange?: never
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableSelectionOptions =
|
||||||
|
| {
|
||||||
|
enableRowSelection: boolean
|
||||||
|
enableMultipleRowSelection?: boolean
|
||||||
|
selectedRowIds?: string[]
|
||||||
|
onRowSelectionChange?: (rowIds: string[]) => void
|
||||||
|
selectionActions?: (selected: string[]) => ReactNode
|
||||||
|
showSelectionActions?: boolean
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
enableRowSelection?: never
|
||||||
|
enableMultipleRowSelection?: never
|
||||||
|
selectedRowIds?: never
|
||||||
|
onRowSelectionChange?: never
|
||||||
|
selectionActions?: never
|
||||||
|
showSelectionActions?: never
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableRowOptions<Data> = {
|
||||||
|
getRowId?: (data: Data) => string
|
||||||
|
onRowDoubleClick?: (data: Data) => void
|
||||||
|
onRowContextMenu?: (x: number, y: number, data: Data) => void
|
||||||
|
rowActions?: (data: Data) => ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UseTableOptions<Data> = {
|
||||||
|
data: Data[]
|
||||||
|
columns: TableColumn<Data>[]
|
||||||
|
} & TableRowOptions<Data> &
|
||||||
|
TableSortOptions &
|
||||||
|
TableSelectionOptions
|
||||||
|
|
||||||
|
export function useTable<Data>({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
sortBy,
|
||||||
|
sortReversed,
|
||||||
|
onSortChange,
|
||||||
|
getRowId,
|
||||||
|
enableRowSelection,
|
||||||
|
enableMultipleRowSelection,
|
||||||
|
selectedRowIds,
|
||||||
|
onRowSelectionChange,
|
||||||
|
onRowDoubleClick,
|
||||||
|
onRowContextMenu,
|
||||||
|
rowActions,
|
||||||
|
selectionActions,
|
||||||
|
showSelectionActions,
|
||||||
|
}: UseTableOptions<Data>): Table<Data> {
|
||||||
|
const application = useApplication()
|
||||||
|
const [selectedRows, setSelectedRows] = useState<string[]>(selectedRowIds || [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedRowIds) {
|
||||||
|
setSelectedRows(selectedRowIds)
|
||||||
|
}
|
||||||
|
}, [selectedRowIds])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onRowSelectionChange) {
|
||||||
|
onRowSelectionChange(selectedRows)
|
||||||
|
}
|
||||||
|
}, [selectedRows, onRowSelectionChange])
|
||||||
|
|
||||||
|
const headers = useMemo(
|
||||||
|
() =>
|
||||||
|
columns.map((column) => {
|
||||||
|
return {
|
||||||
|
name: column.name,
|
||||||
|
isSorting: sortBy && sortBy === column.sortBy,
|
||||||
|
sortBy: column.sortBy,
|
||||||
|
sortReversed: sortReversed,
|
||||||
|
onSortChange: () => {
|
||||||
|
if (!onSortChange || !column.sortBy) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onSortChange(column.sortBy, sortBy === column.sortBy ? !sortReversed : false)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[columns, onSortChange, sortBy, sortReversed],
|
||||||
|
)
|
||||||
|
|
||||||
|
const rows: TableRow<Data>[] = useMemo(
|
||||||
|
() =>
|
||||||
|
data.map((rowData, index) => {
|
||||||
|
const cells = columns.map((column) => {
|
||||||
|
return column.cell(rowData)
|
||||||
|
})
|
||||||
|
const id = getRowId ? getRowId(rowData) : index.toString()
|
||||||
|
const row: TableRow<Data> = {
|
||||||
|
id,
|
||||||
|
isSelected: enableRowSelection ? selectedRows.includes(id) : false,
|
||||||
|
cells,
|
||||||
|
rowData,
|
||||||
|
rowActions: rowActions ? rowActions(rowData) : undefined,
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
}),
|
||||||
|
[columns, data, enableRowSelection, getRowId, rowActions, selectedRows],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleRowClick = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const handler: MouseEventHandler<HTMLTableRowElement> = (event) => {
|
||||||
|
if (!enableRowSelection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const isCmdOrCtrlPressed = application.keyboardService.isMac ? event.metaKey : event.ctrlKey
|
||||||
|
if (isCmdOrCtrlPressed && enableMultipleRowSelection) {
|
||||||
|
setSelectedRows((prev) => (prev.includes(id) ? prev.filter((rowId) => rowId !== id) : [...prev, id]))
|
||||||
|
} else if (event.shiftKey && enableMultipleRowSelection) {
|
||||||
|
const lastSelectedIndex = rows.findIndex((row) => row.id === selectedRows[selectedRows.length - 1])
|
||||||
|
const currentIndex = rows.findIndex((row) => row.id === id)
|
||||||
|
const start = Math.min(lastSelectedIndex, currentIndex)
|
||||||
|
const end = Math.max(lastSelectedIndex, currentIndex)
|
||||||
|
const newSelectedRows = rows.slice(start, end + 1).map((row) => row.id)
|
||||||
|
setSelectedRows(newSelectedRows)
|
||||||
|
} else {
|
||||||
|
setSelectedRows([id])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handler
|
||||||
|
},
|
||||||
|
[application.keyboardService.isMac, enableMultipleRowSelection, enableRowSelection, rows, selectedRows],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleRowDoubleClick = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const handler: MouseEventHandler<HTMLTableRowElement> = () => {
|
||||||
|
if (!onRowDoubleClick) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const rowData = rows.find((row) => row.id === id)?.rowData
|
||||||
|
if (rowData) {
|
||||||
|
onRowDoubleClick(rowData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handler
|
||||||
|
},
|
||||||
|
[onRowDoubleClick, rows],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleRowContextMenu = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
const handler: MouseEventHandler<HTMLTableRowElement> = (event) => {
|
||||||
|
if (!onRowContextMenu) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
const rowData = rows.find((row) => row.id === id)?.rowData
|
||||||
|
if (rowData) {
|
||||||
|
setSelectedRows([id])
|
||||||
|
onRowContextMenu(event.clientX, event.clientY, rowData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return handler
|
||||||
|
},
|
||||||
|
[onRowContextMenu, rows],
|
||||||
|
)
|
||||||
|
|
||||||
|
const table: Table<Data> = useMemo(
|
||||||
|
() => ({
|
||||||
|
headers,
|
||||||
|
rows,
|
||||||
|
handleRowClick,
|
||||||
|
handleRowDoubleClick,
|
||||||
|
handleRowContextMenu,
|
||||||
|
selectedRows,
|
||||||
|
canSelectRows: enableRowSelection || false,
|
||||||
|
selectionActions: selectionActions ? selectionActions(selectedRows) : undefined,
|
||||||
|
showSelectionActions: showSelectionActions || false,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
enableRowSelection,
|
||||||
|
handleRowClick,
|
||||||
|
handleRowContextMenu,
|
||||||
|
handleRowDoubleClick,
|
||||||
|
headers,
|
||||||
|
rows,
|
||||||
|
selectedRows,
|
||||||
|
selectionActions,
|
||||||
|
showSelectionActions,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return table
|
||||||
|
}
|
||||||
@@ -4,10 +4,7 @@ import {
|
|||||||
OnChunkCallbackNoProgress,
|
OnChunkCallbackNoProgress,
|
||||||
} from '@standardnotes/files'
|
} from '@standardnotes/files'
|
||||||
import { FilePreviewModalController } from './FilePreviewModalController'
|
import { FilePreviewModalController } from './FilePreviewModalController'
|
||||||
import {
|
import { FileItemAction, FileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
||||||
PopoverFileItemAction,
|
|
||||||
PopoverFileItemActionType,
|
|
||||||
} from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
|
||||||
import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants'
|
import { BYTES_IN_ONE_MEGABYTE } from '@/Constants/Constants'
|
||||||
import { confirmDialog } from '@standardnotes/ui-services'
|
import { confirmDialog } from '@standardnotes/ui-services'
|
||||||
import { Strings, StringUtils } from '@/Constants/Strings'
|
import { Strings, StringUtils } from '@/Constants/Strings'
|
||||||
@@ -34,8 +31,8 @@ import { AbstractViewController } from './Abstract/AbstractViewController'
|
|||||||
import { NotesController } from './NotesController/NotesController'
|
import { NotesController } from './NotesController/NotesController'
|
||||||
import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform'
|
import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform'
|
||||||
|
|
||||||
const UnprotectedFileActions = [PopoverFileItemActionType.ToggleFileProtection]
|
const UnprotectedFileActions = [FileItemActionType.ToggleFileProtection]
|
||||||
const NonMutatingFileActions = [PopoverFileItemActionType.DownloadFile, PopoverFileItemActionType.PreviewFile]
|
const NonMutatingFileActions = [FileItemActionType.DownloadFile, FileItemActionType.PreviewFile]
|
||||||
|
|
||||||
type FileContextMenuLocation = { x: number; y: number }
|
type FileContextMenuLocation = { x: number; y: number }
|
||||||
|
|
||||||
@@ -195,7 +192,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleFileAction = async (
|
handleFileAction = async (
|
||||||
action: PopoverFileItemAction,
|
action: FileItemAction,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
didHandleAction: boolean
|
didHandleAction: boolean
|
||||||
}> => {
|
}> => {
|
||||||
@@ -215,27 +212,27 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case PopoverFileItemActionType.AttachFileToNote:
|
case FileItemActionType.AttachFileToNote:
|
||||||
await this.attachFileToSelectedNote(file)
|
await this.attachFileToSelectedNote(file)
|
||||||
break
|
break
|
||||||
case PopoverFileItemActionType.DetachFileToNote:
|
case FileItemActionType.DetachFileToNote:
|
||||||
await this.detachFileFromNote(file)
|
await this.detachFileFromNote(file)
|
||||||
break
|
break
|
||||||
case PopoverFileItemActionType.DeleteFile:
|
case FileItemActionType.DeleteFile:
|
||||||
await this.deleteFile(file)
|
await this.deleteFile(file)
|
||||||
break
|
break
|
||||||
case PopoverFileItemActionType.DownloadFile:
|
case FileItemActionType.DownloadFile:
|
||||||
await this.downloadFile(file)
|
await this.downloadFile(file)
|
||||||
break
|
break
|
||||||
case PopoverFileItemActionType.ToggleFileProtection: {
|
case FileItemActionType.ToggleFileProtection: {
|
||||||
const isProtected = await this.toggleFileProtection(file)
|
const isProtected = await this.toggleFileProtection(file)
|
||||||
action.callback(isProtected)
|
action.callback(isProtected)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case PopoverFileItemActionType.RenameFile:
|
case FileItemActionType.RenameFile:
|
||||||
await this.renameFile(file, action.payload.name)
|
await this.renameFile(file, action.payload.name)
|
||||||
break
|
break
|
||||||
case PopoverFileItemActionType.PreviewFile:
|
case FileItemActionType.PreviewFile:
|
||||||
this.filePreviewModalController.activate(file, action.payload.otherFiles)
|
this.filePreviewModalController.activate(file, action.payload.otherFiles)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -426,7 +423,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
label: 'Open',
|
label: 'Open',
|
||||||
handler: (toastId) => {
|
handler: (toastId) => {
|
||||||
void this.handleFileAction({
|
void this.handleFileAction({
|
||||||
type: PopoverFileItemActionType.PreviewFile,
|
type: FileItemActionType.PreviewFile,
|
||||||
payload: { file: uploadedFile },
|
payload: { file: uploadedFile },
|
||||||
})
|
})
|
||||||
dismissToast(toastId)
|
dismissToast(toastId)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { PopoverFileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
import { FileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
||||||
import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewController'
|
import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewController'
|
||||||
import { AppPaneId } from '@/Components/Panes/AppPaneMetadata'
|
import { AppPaneId } from '@/Components/Panes/AppPaneMetadata'
|
||||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||||
@@ -153,7 +153,7 @@ export class LinkingController extends AbstractViewController {
|
|||||||
}
|
}
|
||||||
} else if (item instanceof FileItem) {
|
} else if (item instanceof FileItem) {
|
||||||
await this.filesController.handleFileAction({
|
await this.filesController.handleFileAction({
|
||||||
type: PopoverFileItemActionType.PreviewFile,
|
type: FileItemActionType.PreviewFile,
|
||||||
payload: {
|
payload: {
|
||||||
file: item,
|
file: item,
|
||||||
otherFiles: [],
|
otherFiles: [],
|
||||||
@@ -164,6 +164,12 @@ export class LinkingController extends AbstractViewController {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unlinkItems = async (item: LinkableItem, itemToUnlink: LinkableItem) => {
|
||||||
|
await this.application.items.unlinkItems(item, itemToUnlink)
|
||||||
|
|
||||||
|
void this.application.sync.sync()
|
||||||
|
}
|
||||||
|
|
||||||
unlinkItemFromSelectedItem = async (itemToUnlink: LinkableItem) => {
|
unlinkItemFromSelectedItem = async (itemToUnlink: LinkableItem) => {
|
||||||
const selectedItem = this.selectionController.firstSelectedItem
|
const selectedItem = this.selectionController.firstSelectedItem
|
||||||
|
|
||||||
@@ -171,9 +177,7 @@ export class LinkingController extends AbstractViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.application.items.unlinkItems(selectedItem, itemToUnlink)
|
void this.unlinkItems(selectedItem, itemToUnlink)
|
||||||
|
|
||||||
void this.application.sync.sync()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureActiveItemIsInserted = async () => {
|
ensureActiveItemIsInserted = async () => {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { isDev } from '@/Utils'
|
|||||||
|
|
||||||
export enum FeatureTrunkName {
|
export enum FeatureTrunkName {
|
||||||
Super,
|
Super,
|
||||||
|
FilesTableView,
|
||||||
}
|
}
|
||||||
|
|
||||||
const FeatureTrunkStatus: Record<FeatureTrunkName, boolean> = {
|
const FeatureTrunkStatus: Record<FeatureTrunkName, boolean> = {
|
||||||
[FeatureTrunkName.Super]: isDev && true,
|
[FeatureTrunkName.Super]: isDev && true,
|
||||||
|
[FeatureTrunkName.FilesTableView]: isDev && true,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function featureTrunkEnabled(trunk: FeatureTrunkName): boolean {
|
export function featureTrunkEnabled(trunk: FeatureTrunkName): boolean {
|
||||||
|
|||||||
Reference in New Issue
Block a user