import { WebApplication } from '@/Application/WebApplication' import { formatDateForContextMenu } from '@/Utils/DateUtils' import { getIconForFileType } from '@/Utils/Items/Icons/getIconForFileType' import { formatSizeToReadableString } from '@standardnotes/filepicker' import { FileItem, SortableItem, PrefKey, FileBackupRecord, SystemViewId, DecryptedItemInterface, SNNote, TagMutator, isSystemView, isSmartView, isNote, } 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 LinkedItemBubble from '../LinkedItems/LinkedItemBubble' import LinkedItemsPanel from '../LinkedItems/LinkedItemsPanel' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' import { useApplication } from '../ApplicationProvider' import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType' import NotesOptions from '../NotesOptions/NotesOptions' import { useItemLinks } from '@/Hooks/useItemLinks' import { ItemLink } from '@/Utils/Items/Search/ItemLink' import ListItemVaultInfo from '../ContentListView/ListItemVaultInfo' const ContextMenuCell = ({ items }: { items: DecryptedItemInterface[] }) => { const [contextMenuVisible, setContextMenuVisible] = useState(false) const anchorElementRef = useRef(null) const allItemsAreNotes = useMemo(() => { return items.every((item) => item instanceof SNNote) }, [items]) const allItemsAreFiles = useMemo(() => { return items.every((item) => item instanceof FileItem) }, [items]) if (!allItemsAreNotes && !allItemsAreFiles) { return null } return ( <> { setContextMenuVisible(false) }} side="bottom" align="start" className="py-2" > {allItemsAreFiles && ( { setContextMenuVisible(false) }} shouldShowRenameOption={false} shouldShowAttachOption={false} selectedFiles={items as FileItem[]} /> )} {allItemsAreNotes && ( { setContextMenuVisible(false) }} /> )} ) } const ItemLinksCell = ({ item }: { item: DecryptedItemInterface }) => { const [contextMenuVisible, setContextMenuVisible] = useState(false) const anchorElementRef = useRef(null) return ( <> { setContextMenuVisible(false) }} side="bottom" align="start" className="py-2" > ) } const ItemNameCell = ({ item, hideIcon }: { item: DecryptedItemInterface; hideIcon: boolean }) => { const application = useApplication() const [backupInfo, setBackupInfo] = useState(undefined) const isItemFile = item instanceof FileItem const editor = isNote(item) ? application.componentManager.editorForNote(item) : undefined const noteType = isNote(item) ? item.noteType : editor ? editor.noteType : undefined const [noteIcon, noteIconTint] = getIconAndTintForNoteType(noteType) useEffect(() => { if (isItemFile) { void application.fileBackups?.getFileBackupInfo(item).then(setBackupInfo) } }, [application, isItemFile, item]) return (
{!hideIcon ? ( isItemFile ? ( getFileIconComponent(getIconForFileType(item.mimeType), 'w-6 h-6 flex-shrink-0') ) : ( ) ) : null} {backupInfo && (
)}
{item.title} {item.protected && ( )}
) } const AttachedToCell = ({ item }: { item: DecryptedItemInterface }) => { const { notesLinkedToItem, notesLinkingToItem, filesLinkedToItem, filesLinkingToItem, tagsLinkedToItem } = useItemLinks(item) const application = useApplication() const allLinks: ItemLink[] = (notesLinkedToItem as ItemLink[]).concat( notesLinkingToItem, filesLinkedToItem, filesLinkingToItem, tagsLinkedToItem, ) if (!allLinks.length) { return null } return (
{ void application.mutator.unlinkItems(item, itemToUnlink) }} isBidirectional={false} /> {allLinks.length - 1 >= 1 && and {allLinks.length - 1} more...}
) } type Props = { application: WebApplication items: DecryptedItemInterface[] } const ContentTableView = ({ application, items }: Props) => { const listHasFiles = items.some((item) => item instanceof FileItem) const { sortBy, sortDirection } = application.itemListController.displayOptions const sortReversed = sortDirection === 'asc' const { hideDate, hideEditorIcon: hideIcon, hideTags } = application.itemListController.webDisplayOptions const onSortChange = useCallback( async (sortBy: keyof SortableItem, sortReversed: boolean) => { const selectedTag = application.navigationController.selected if (!selectedTag) { return } if (selectedTag.uuid === SystemViewId.Files) { const systemViewPreferences = application.getPreference(PrefKey.SystemViewPreferences) || {} const filesViewPreferences = systemViewPreferences[SystemViewId.Files] || {} await application.setPreference(PrefKey.SystemViewPreferences, { ...systemViewPreferences, [SystemViewId.Files]: { ...filesViewPreferences, sortBy, sortReverse: sortReversed, }, }) return } const isNonFilesSystemView = isSmartView(selectedTag) && isSystemView(selectedTag) if (isNonFilesSystemView) { return } await application.changeAndSaveItem.execute(selectedTag, (mutator) => { mutator.preferences = { ...mutator.preferences, sortBy, sortReverse: sortReversed, } }) }, [application], ) const [contextMenuItem, setContextMenuItem] = useState(undefined) const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | undefined>(undefined) const isSmallBreakpoint = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) const isMediumBreakpoint = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.md) const isLargeBreakpoint = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.lg) const columnDefs: TableColumn[] = useMemo( () => [ { name: 'Name', sortBy: 'title', cell: (item) => , }, { name: 'Upload date', sortBy: 'created_at', cell: (item) => { return formatDateForContextMenu(item.created_at) }, hidden: isSmallBreakpoint || hideDate, }, { name: 'Size', sortBy: 'decryptedSize', cell: (item) => { return item instanceof FileItem ? formatSizeToReadableString(item.decryptedSize) : null }, hidden: isSmallBreakpoint || !listHasFiles, }, { name: 'Attached to', hidden: isSmallBreakpoint || isMediumBreakpoint || isLargeBreakpoint || hideTags, cell: (item) => , }, ], [hideDate, hideIcon, hideTags, isLargeBreakpoint, isMediumBreakpoint, isSmallBreakpoint, listHasFiles], ) const getRowId = useCallback((item: DecryptedItemInterface) => item.uuid, []) const table = useTable({ data: items, sortBy, sortReversed, onSortChange, getRowId, columns: columnDefs, enableRowSelection: true, enableMultipleRowSelection: true, onRowActivate(item) { if (item instanceof FileItem) { void application.filesController.handleFileAction({ type: FileItemActionType.PreviewFile, payload: { file: item, otherFiles: items.filter((i) => i instanceof FileItem) as FileItem[], }, }) } }, onRowContextMenu(x, y, file) { setContextMenuPosition({ x, y }) setContextMenuItem(file) }, rowActions: (item) => { return (
) }, selectionActions: (itemIds) => itemIds.includes(item.uuid))} />, showSelectionActions: true, }) const closeContextMenu = () => { setContextMenuPosition(undefined) setContextMenuItem(undefined) } return ( <> {contextMenuPosition && contextMenuItem && ( { setContextMenuPosition(undefined) setContextMenuItem(undefined) }} side="bottom" align="start" className="py-2" > {contextMenuItem instanceof FileItem && ( )} {contextMenuItem instanceof SNNote && ( )} )} ) } export default ContentTableView