feat-dev(wip): files table view (#2100)

This commit is contained in:
Aman Harwara
2022-12-20 19:01:24 +05:30
committed by GitHub
parent 343c39e873
commit c94035c1d6
23 changed files with 800 additions and 93 deletions

View 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
}

View 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

View 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
}