refactor(dev-only): files table view (#2118)
This commit is contained in:
@@ -20,6 +20,7 @@ import LinkedItemBubble from '../LinkedItems/LinkedItemBubble'
|
||||
import LinkedItemsPanel from '../LinkedItems/LinkedItemsPanel'
|
||||
import { LinkingController } from '@/Controllers/LinkingController'
|
||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||
|
||||
const ContextMenuCell = ({ files, filesController }: { files: FileItem[]; filesController: FilesController }) => {
|
||||
const [contextMenuVisible, setContextMenuVisible] = useState(false)
|
||||
@@ -152,6 +153,8 @@ const FilesTableView = ({ application, filesController, featuresController, link
|
||||
const [contextMenuFile, setContextMenuFile] = useState<FileItem | undefined>(undefined)
|
||||
const [contextMenuPosition, setContextMenuPosition] = useState<{ x: number; y: number } | undefined>(undefined)
|
||||
|
||||
const isSmallBreakpoint = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
|
||||
|
||||
const columnDefs: TableColumn<FileItem>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -159,9 +162,9 @@ const FilesTableView = ({ application, filesController, featuresController, link
|
||||
sortBy: 'title',
|
||||
cell: (file) => {
|
||||
return (
|
||||
<div className="flex max-w-[40vw] items-center gap-3 whitespace-normal">
|
||||
<div className="flex 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>
|
||||
<span className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium">{file.title}</span>
|
||||
{file.protected && (
|
||||
<span className="flex items-center" title="File is protected">
|
||||
<Icon
|
||||
@@ -182,15 +185,18 @@ const FilesTableView = ({ application, filesController, featuresController, link
|
||||
cell: (file) => {
|
||||
return formatDateForContextMenu(file.created_at)
|
||||
},
|
||||
hidden: isSmallBreakpoint,
|
||||
},
|
||||
{
|
||||
name: 'Size',
|
||||
cell: (file) => {
|
||||
return formatSizeToReadableString(file.decryptedSize)
|
||||
},
|
||||
hidden: isSmallBreakpoint,
|
||||
},
|
||||
{
|
||||
name: 'Attached to',
|
||||
hidden: isSmallBreakpoint,
|
||||
cell: (file) => {
|
||||
const links = [
|
||||
...naturalSort(application.items.referencesForItem(file), 'title').map((item) =>
|
||||
@@ -223,7 +229,7 @@ const FilesTableView = ({ application, filesController, featuresController, link
|
||||
},
|
||||
},
|
||||
],
|
||||
[application.items],
|
||||
[application.items, isSmallBreakpoint],
|
||||
)
|
||||
|
||||
const getRowId = useCallback((file: FileItem) => file.uuid, [])
|
||||
|
||||
@@ -7,11 +7,18 @@ export type TableColumn<Data> = {
|
||||
name: string
|
||||
sortBy?: TableSortBy
|
||||
cell: (data: Data) => ReactNode
|
||||
hidden?: boolean
|
||||
}
|
||||
|
||||
type TableCell = {
|
||||
render: ReactNode
|
||||
hidden: boolean
|
||||
colIndex: number
|
||||
}
|
||||
|
||||
export type TableRow<Data> = {
|
||||
id: string
|
||||
cells: ReactNode[]
|
||||
cells: TableCell[]
|
||||
isSelected: boolean
|
||||
rowData: Data
|
||||
rowActions?: ReactNode
|
||||
@@ -23,11 +30,15 @@ export type TableHeader = {
|
||||
sortBy?: TableSortBy
|
||||
sortReversed: boolean | undefined
|
||||
onSortChange: () => void
|
||||
hidden: boolean
|
||||
colIndex: number
|
||||
}
|
||||
|
||||
export type Table<Data> = {
|
||||
headers: TableHeader[]
|
||||
rows: TableRow<Data>[]
|
||||
rowCount: number
|
||||
colCount: number
|
||||
handleRowClick: (id: string) => MouseEventHandler<HTMLTableRowElement>
|
||||
handleRowDoubleClick: (id: string) => MouseEventHandler<HTMLTableRowElement>
|
||||
handleRowContextMenu: (id: string) => MouseEventHandler<HTMLTableRowElement>
|
||||
|
||||
@@ -1,26 +1,122 @@
|
||||
import { classNames } from '@standardnotes/snjs'
|
||||
import { useState } from 'react'
|
||||
import Icon from '../Icon/Icon'
|
||||
import { Table } from './CommonTypes'
|
||||
import { Table, TableRow } from './CommonTypes'
|
||||
|
||||
function TableRow<Data>({
|
||||
row,
|
||||
index: rowIndex,
|
||||
canSelectRows,
|
||||
handleRowClick,
|
||||
handleRowContextMenu,
|
||||
handleRowDoubleClick,
|
||||
}: {
|
||||
row: TableRow<Data>
|
||||
index: number
|
||||
canSelectRows: Table<Data>['canSelectRows']
|
||||
handleRowClick: Table<Data>['handleRowClick']
|
||||
handleRowContextMenu: Table<Data>['handleRowContextMenu']
|
||||
handleRowDoubleClick: Table<Data>['handleRowDoubleClick']
|
||||
}) {
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
const visibleCells = row.cells.filter((cell) => !cell.hidden)
|
||||
|
||||
return (
|
||||
<div
|
||||
role="row"
|
||||
aria-rowindex={rowIndex + 2}
|
||||
className="group relative contents"
|
||||
onMouseEnter={() => {
|
||||
setIsHovered(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovered(false)
|
||||
}}
|
||||
onClick={handleRowClick(row.id)}
|
||||
onDoubleClick={handleRowDoubleClick(row.id)}
|
||||
onContextMenu={handleRowContextMenu(row.id)}
|
||||
>
|
||||
{visibleCells.map((cell, index, array) => {
|
||||
return (
|
||||
<div
|
||||
role="gridcell"
|
||||
aria-rowindex={rowIndex + 2}
|
||||
aria-colindex={cell.colIndex + 1}
|
||||
key={index}
|
||||
className={classNames(
|
||||
'relative overflow-hidden border-b border-border py-3 px-3',
|
||||
index === 0 && 'ml-3',
|
||||
index === array.length - 1 && 'mr-3',
|
||||
row.isSelected && 'bg-info-backdrop',
|
||||
canSelectRows && 'cursor-pointer',
|
||||
canSelectRows && isHovered && 'bg-contrast',
|
||||
)}
|
||||
>
|
||||
{cell.render}
|
||||
{row.rowActions && index === array.length - 1 && (
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute right-3 top-1/2 -translate-y-1/2',
|
||||
row.isSelected ? '' : isHovered ? '' : 'invisible',
|
||||
)}
|
||||
>
|
||||
{row.rowActions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Table<Data>({ table }: { table: Table<Data> }) {
|
||||
const {
|
||||
headers,
|
||||
rows,
|
||||
colCount,
|
||||
rowCount,
|
||||
handleRowClick,
|
||||
handleRowContextMenu,
|
||||
handleRowDoubleClick,
|
||||
selectedRows,
|
||||
selectionActions,
|
||||
canSelectRows,
|
||||
showSelectionActions,
|
||||
} = table
|
||||
|
||||
return (
|
||||
<div className="block min-h-0 overflow-auto">
|
||||
{table.showSelectionActions && table.selectedRows.length >= 2 && (
|
||||
{showSelectionActions && 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}
|
||||
<span className="text-info-0 text-sm font-medium">{selectedRows.length} selected</span>
|
||||
{selectedRows.length > 0 && selectionActions}
|
||||
</div>
|
||||
)}
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
{table.headers.map((header, index) => {
|
||||
<div
|
||||
className="relative grid w-full overflow-x-hidden"
|
||||
role="grid"
|
||||
aria-colcount={colCount}
|
||||
aria-rowcount={rowCount}
|
||||
>
|
||||
<div role="row" aria-rowindex={1} className="contents">
|
||||
{headers
|
||||
.filter((header) => !header.hidden)
|
||||
.map((header, index) => {
|
||||
return (
|
||||
<th
|
||||
<div
|
||||
role="columnheader"
|
||||
aria-rowindex={1}
|
||||
aria-colindex={header.colIndex + 1}
|
||||
aria-sort={header.isSorting ? (header.sortReversed ? 'descending' : 'ascending') : 'none'}
|
||||
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',
|
||||
)}
|
||||
style={{
|
||||
gridColumn: index + 1,
|
||||
}}
|
||||
onClick={header.onSortChange}
|
||||
key={index.toString()}
|
||||
>
|
||||
@@ -34,47 +130,24 @@ function Table<Data>({ table }: { table: Table<Data> }) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</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>
|
||||
<div className="contents divide-y divide-border whitespace-nowrap">
|
||||
{rows.map((row, index) => (
|
||||
<TableRow
|
||||
row={row}
|
||||
key={row.id}
|
||||
index={index}
|
||||
canSelectRows={canSelectRows}
|
||||
handleRowClick={handleRowClick}
|
||||
handleRowContextMenu={handleRowContextMenu}
|
||||
handleRowDoubleClick={handleRowDoubleClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MouseEventHandler, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useApplication } from '../ApplicationProvider'
|
||||
import { Table, TableColumn, TableRow, TableSortBy } from './CommonTypes'
|
||||
import { Table, TableColumn, TableHeader, TableRow, TableSortBy } from './CommonTypes'
|
||||
|
||||
type TableSortOptions =
|
||||
| {
|
||||
@@ -78,9 +78,9 @@ export function useTable<Data>({
|
||||
}
|
||||
}, [selectedRows, onRowSelectionChange])
|
||||
|
||||
const headers = useMemo(
|
||||
const headers: TableHeader[] = useMemo(
|
||||
() =>
|
||||
columns.map((column) => {
|
||||
columns.map((column, index) => {
|
||||
return {
|
||||
name: column.name,
|
||||
isSorting: sortBy && sortBy === column.sortBy,
|
||||
@@ -92,6 +92,8 @@ export function useTable<Data>({
|
||||
}
|
||||
onSortChange(column.sortBy, sortBy === column.sortBy ? !sortReversed : false)
|
||||
},
|
||||
hidden: column.hidden || false,
|
||||
colIndex: index,
|
||||
}
|
||||
}),
|
||||
[columns, onSortChange, sortBy, sortReversed],
|
||||
@@ -100,8 +102,12 @@ export function useTable<Data>({
|
||||
const rows: TableRow<Data>[] = useMemo(
|
||||
() =>
|
||||
data.map((rowData, index) => {
|
||||
const cells = columns.map((column) => {
|
||||
return column.cell(rowData)
|
||||
const cells = columns.map((column, index) => {
|
||||
return {
|
||||
render: column.cell(rowData),
|
||||
hidden: column.hidden || false,
|
||||
colIndex: index,
|
||||
}
|
||||
})
|
||||
const id = getRowId ? getRowId(rowData) : index.toString()
|
||||
const row: TableRow<Data> = {
|
||||
@@ -175,10 +181,15 @@ export function useTable<Data>({
|
||||
[onRowContextMenu, rows],
|
||||
)
|
||||
|
||||
const colCount = useMemo(() => columns.length, [columns])
|
||||
const rowCount = useMemo(() => data.length, [data.length])
|
||||
|
||||
const table: Table<Data> = useMemo(
|
||||
() => ({
|
||||
headers,
|
||||
rows,
|
||||
colCount,
|
||||
rowCount,
|
||||
handleRowClick,
|
||||
handleRowDoubleClick,
|
||||
handleRowContextMenu,
|
||||
@@ -188,11 +199,13 @@ export function useTable<Data>({
|
||||
showSelectionActions: showSelectionActions || false,
|
||||
}),
|
||||
[
|
||||
colCount,
|
||||
enableRowSelection,
|
||||
handleRowClick,
|
||||
handleRowContextMenu,
|
||||
handleRowDoubleClick,
|
||||
headers,
|
||||
rowCount,
|
||||
rows,
|
||||
selectedRows,
|
||||
selectionActions,
|
||||
|
||||
Reference in New Issue
Block a user