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