feat-dev(wip): files table view (#2100)
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user