Describe the tables as TanStack columns
Add ui/DataTable, which builds ReUI's DataGrid from `columns` and `data`, and move all 23 tables onto it. What the grid does not take (a class on a row or cell, `aria-sort`, `style`, `colSpan`, `role`, `hidden`) is put on the elements it drew after every render. The column definitions keep their identity between renders, so cells are not mounted again, and the grid's own paging is off. The stacked rows on a phone, the column labels and the sort order of the original work as before. In Blocking > Lists a group heading is a row whose first cell spans the four columns. docs/reui-migration-plan.md records it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f3531c64f9
commit
4c0a5de7e7
24 files changed
+1611
-1175
No files matched your search
+166
-112
@@ -5,7 +5,8 @@ import { loadQuickList, type QuickEntry } from '../../lib/quick-lists'
|
||||
import { nextUpdateText } from '../settings/panes/Blocking'
|
||||
import { SectionHeader } from '../../ui/SectionHeader'
|
||||
import { Panel, Body } from '../../ui/Panel'
|
||||
import { RowAction, Table } from '../../ui/Table'
|
||||
import { DataTable } from '../../ui/DataTable'
|
||||
import { RowAction } from '../../ui/Table'
|
||||
import { Field, Input, Select } from '../../ui/Field'
|
||||
import { Button } from '../../ui/Button'
|
||||
import { PermissionButton } from '../../ui/PermissionButton'
|
||||
@@ -478,128 +479,96 @@ export function BlockLists({
|
||||
/* The lists of each group, to say how many it holds. */
|
||||
const groupSize = (g: number) => (heads[g + 1] ?? lines.length) - (g < 0 ? 0 : heads[g] + 1)
|
||||
|
||||
function headingRow(key: string, name: string, count: number, controls: ReactNode) {
|
||||
return (
|
||||
<tr key={key} className={styles.groupRow}>
|
||||
<th colSpan={4} scope="colgroup">
|
||||
<div className={styles.groupHead}>
|
||||
<span className={styles.groupName}>{name}</span>
|
||||
<span className={styles.groupCount}>{plural(count, 'list', 'lists')}</span>
|
||||
{controls != null && <span className={styles.spacer} />}
|
||||
{controls}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
)
|
||||
/* What the table draws, one item per row: a group heading (its content spans the
|
||||
whole row) or a line. The grid draws cells, so a heading is a row whose first cell
|
||||
takes the width of all four and whose other cells are not drawn. */
|
||||
type Entry = ListLine & { kind: 'block' | 'allow' }
|
||||
type Item =
|
||||
| { key: string; kind: 'heading'; node: ReactNode }
|
||||
| { key: string; kind: 'line'; l: Entry; i: number }
|
||||
|
||||
function heading(key: string, name: string, count: number, controls: ReactNode): Item {
|
||||
return {
|
||||
key,
|
||||
kind: 'heading',
|
||||
node: (
|
||||
<div className={styles.groupHead}>
|
||||
<span className={styles.groupName}>{name}</span>
|
||||
<span className={styles.groupCount}>{plural(count, 'list', 'lists')}</span>
|
||||
{controls != null && <span className={styles.spacer} />}
|
||||
{controls}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/* One row per line. A key that follows the line and not its place, so a list that
|
||||
moves or switches keeps its row, and the focus stays on the button pressed. */
|
||||
const seen = new Map<string, number>()
|
||||
const rows: ReactNode[] = []
|
||||
if (heads.length > 0 && heads[0] > 0) rows.push(headingRow('none', 'No group', groupSize(-1), null))
|
||||
const items: Item[] = []
|
||||
if (heads.length > 0 && heads[0] > 0) items.push(heading('none', 'No group', groupSize(-1), null))
|
||||
lines.forEach((l, i) => {
|
||||
if (l.kind === 'comment') {
|
||||
const g = heads.indexOf(i)
|
||||
const name = nameOf(l)
|
||||
rows.push(
|
||||
edit != null && edit.group === g ? (
|
||||
<tr key={`H:${g}`} className={styles.groupRow}>
|
||||
<th colSpan={4} scope="colgroup">
|
||||
<HeadingEditor
|
||||
editing={edit}
|
||||
verb="Rename"
|
||||
onChange={(value) => setEditing({ ...edit, value, problem: null })}
|
||||
onSubmit={submitHeading}
|
||||
onCancel={cancelEditing}
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
) : (
|
||||
headingRow(`H:${g}`, name, groupSize(g), (
|
||||
<>
|
||||
<PermissionButton size="sm" permission={modifyNeed} aria-label={`Rename group ${name}`}
|
||||
onClick={() => setEditing({ group: g, value: headingName(l), problem: null })}>
|
||||
Rename
|
||||
</PermissionButton>
|
||||
<PermissionButton size="sm" permission={modifyNeed} aria-label={`Ungroup ${name}`}
|
||||
onClick={() => ungroup(g)}>
|
||||
Ungroup
|
||||
</PermissionButton>
|
||||
</>
|
||||
))
|
||||
),
|
||||
items.push(
|
||||
edit != null && edit.group === g
|
||||
? {
|
||||
key: `H:${g}`,
|
||||
kind: 'heading',
|
||||
node: (
|
||||
<HeadingEditor
|
||||
editing={edit}
|
||||
verb="Rename"
|
||||
onChange={(value) => setEditing({ ...edit, value, problem: null })}
|
||||
onSubmit={submitHeading}
|
||||
onCancel={cancelEditing}
|
||||
/>
|
||||
),
|
||||
}
|
||||
: heading(`H:${g}`, name, groupSize(g), (
|
||||
<>
|
||||
<PermissionButton size="sm" permission={modifyNeed} aria-label={`Rename group ${name}`}
|
||||
onClick={() => setEditing({ group: g, value: headingName(l), problem: null })}>
|
||||
Rename
|
||||
</PermissionButton>
|
||||
<PermissionButton size="sm" permission={modifyNeed} aria-label={`Ungroup ${name}`}
|
||||
onClick={() => ungroup(g)}>
|
||||
Ungroup
|
||||
</PermissionButton>
|
||||
</>
|
||||
)),
|
||||
)
|
||||
return
|
||||
}
|
||||
const id = `${l.kind}:${l.url}`
|
||||
const n = seen.get(id) ?? 0
|
||||
seen.set(id, n + 1)
|
||||
const name = l.url == null ? null : listName(l.url, named)
|
||||
const faded = !l.enabled ? styles.off : undefined
|
||||
const what = `${l.kind} list ${l.url ?? ''}`
|
||||
rows.push(
|
||||
<tr key={`L:${id}:${n}`}>
|
||||
<td>
|
||||
{canToggle(l) && (
|
||||
<label className={styles.switch}>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`Enabled ${what}`}
|
||||
checked={l.enabled}
|
||||
disabled={off}
|
||||
onChange={() => setLines((all) => all.map((x, j) => (j === i ? toggleLine(x) : x)))}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</td>
|
||||
<td className={faded}>
|
||||
{name != null && <div className={styles.name}>{name}</div>}
|
||||
<div className={styles.url}><UrlText url={l.url ?? l.raw} /></div>
|
||||
{/* The kind under the URL where the Type column is folded away. */}
|
||||
<span className={`${styles.kind} ${styles.kindInline} ${KIND_CLASS[l.kind]}`}>
|
||||
<Icon name={KIND_ICON[l.kind]} size={14} />
|
||||
{KIND_LABEL[l.kind]}
|
||||
</span>
|
||||
</td>
|
||||
<td className={`${faded ?? ''} ${styles.kindCell}`}>
|
||||
<span className={`${styles.kind} ${KIND_CLASS[l.kind]}`}>
|
||||
<Icon name={KIND_ICON[l.kind]} size={14} />
|
||||
{KIND_LABEL[l.kind]}
|
||||
</span>
|
||||
</td>
|
||||
<td className={styles.actions}>
|
||||
<div className={styles.rowActions}>
|
||||
<RowAction icon="moveUp" name="Move up" aria-label={`Move up ${what}`}
|
||||
disabled={off || !canMove(lines, i, -1)} onClick={() => setLines((all) => moveLine(all, i, -1))} />
|
||||
<RowAction icon="moveDown" name="Move down" aria-label={`Move down ${what}`}
|
||||
disabled={off || !canMove(lines, i, 1)} onClick={() => setLines((all) => moveLine(all, i, 1))} />
|
||||
<PermissionButton size="sm" variant="danger" permission={modifyNeed}
|
||||
aria-label={`Remove ${l.url ?? l.raw}`}
|
||||
onClick={() => setLines((all) => all.filter((_, j) => j !== i))}>
|
||||
Remove
|
||||
</PermissionButton>
|
||||
</div>
|
||||
</td>
|
||||
</tr>,
|
||||
)
|
||||
items.push({ key: `L:${id}:${n}`, kind: 'line', l: l as Entry, i })
|
||||
})
|
||||
if (edit != null && edit.group === 'new') {
|
||||
rows.push(
|
||||
<tr key="H:new" className={styles.groupRow}>
|
||||
<th colSpan={4} scope="colgroup">
|
||||
<HeadingEditor
|
||||
editing={edit}
|
||||
verb="Add group"
|
||||
onChange={(value) => setEditing({ ...edit, value, problem: null })}
|
||||
onSubmit={submitHeading}
|
||||
onCancel={cancelEditing}
|
||||
/>
|
||||
</th>
|
||||
</tr>,
|
||||
)
|
||||
items.push({
|
||||
key: 'H:new',
|
||||
kind: 'heading',
|
||||
node: (
|
||||
<HeadingEditor
|
||||
editing={edit}
|
||||
verb="Add group"
|
||||
onChange={(value) => setEditing({ ...edit, value, problem: null })}
|
||||
onSubmit={submitHeading}
|
||||
onCancel={cancelEditing}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
/* What a line's cells share. */
|
||||
const lineCells = (l: Entry) => ({
|
||||
name: l.url == null ? null : listName(l.url, named),
|
||||
faded: !l.enabled ? styles.off : undefined,
|
||||
what: `${l.kind} list ${l.url ?? ''}`,
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<SectionHeader section="Blocking" title="Lists" subtitle={LISTS_SUBTITLE} tabs={tabs} />
|
||||
@@ -778,15 +747,100 @@ export function BlockLists({
|
||||
) : saved == null ? (
|
||||
<Body><Loading /></Body>
|
||||
) : (
|
||||
<Table
|
||||
header={<><th>Enabled</th><th>List</th><th className={styles.kindCell}>Type</th><th /></>}
|
||||
isEmpty={lines.length === 0 && edit?.group !== 'new'}
|
||||
emptyText="No lists"
|
||||
columns={4}
|
||||
<DataTable<Item>
|
||||
data={items}
|
||||
getRowId={(item) => item.key}
|
||||
rowProps={(item) => (item.kind === 'heading' ? { className: styles.groupRow } : undefined)}
|
||||
emptyText={lines.length === 0 && edit?.group !== 'new' ? 'No lists' : undefined}
|
||||
className={shared.inPanel}
|
||||
>
|
||||
{rows}
|
||||
</Table>
|
||||
columns={[
|
||||
{
|
||||
id: 'enabled',
|
||||
header: 'Enabled',
|
||||
td: (item) =>
|
||||
item.kind === 'heading'
|
||||
? { colSpan: 4, scope: 'colgroup', role: 'columnheader' }
|
||||
: undefined,
|
||||
cell: (item) => {
|
||||
if (item.kind === 'heading') return item.node
|
||||
const { l, i } = item
|
||||
const { what } = lineCells(l)
|
||||
return (
|
||||
canToggle(l) && (
|
||||
<label className={styles.switch}>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`Enabled ${what}`}
|
||||
checked={l.enabled}
|
||||
disabled={off}
|
||||
onChange={() => setLines((all) => all.map((x, j) => (j === i ? toggleLine(x) : x)))}
|
||||
/>
|
||||
</label>
|
||||
)
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
header: 'List',
|
||||
td: (item) => (item.kind === 'heading' ? { hidden: true } : { className: lineCells(item.l).faded }),
|
||||
cell: (item) => {
|
||||
if (item.kind === 'heading') return null
|
||||
const { l } = item
|
||||
const { name } = lineCells(l)
|
||||
return (
|
||||
<>
|
||||
{name != null && <div className={styles.name}>{name}</div>}
|
||||
<div className={styles.url}><UrlText url={l.url ?? l.raw} /></div>
|
||||
{/* The kind under the URL where the Type column is folded away. */}
|
||||
<span className={`${styles.kind} ${styles.kindInline} ${KIND_CLASS[l.kind]}`}>
|
||||
<Icon name={KIND_ICON[l.kind]} size={14} />
|
||||
{KIND_LABEL[l.kind]}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'kind',
|
||||
th: { className: styles.kindCell },
|
||||
header: 'Type',
|
||||
td: (item) =>
|
||||
item.kind === 'heading'
|
||||
? { hidden: true }
|
||||
: { className: `${lineCells(item.l).faded ?? ''} ${styles.kindCell}` },
|
||||
cell: (item) =>
|
||||
item.kind === 'heading' ? null : (
|
||||
<span className={`${styles.kind} ${KIND_CLASS[item.l.kind]}`}>
|
||||
<Icon name={KIND_ICON[item.l.kind]} size={14} />
|
||||
{KIND_LABEL[item.l.kind]}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
td: (item) => (item.kind === 'heading' ? { hidden: true } : { className: styles.actions }),
|
||||
cell: (item) => {
|
||||
if (item.kind === 'heading') return null
|
||||
const { l, i } = item
|
||||
const { what } = lineCells(l)
|
||||
return (
|
||||
<div className={styles.rowActions}>
|
||||
<RowAction icon="moveUp" name="Move up" aria-label={`Move up ${what}`}
|
||||
disabled={off || !canMove(lines, i, -1)} onClick={() => setLines((all) => moveLine(all, i, -1))} />
|
||||
<RowAction icon="moveDown" name="Move down" aria-label={`Move down ${what}`}
|
||||
disabled={off || !canMove(lines, i, 1)} onClick={() => setLines((all) => moveLine(all, i, 1))} />
|
||||
<PermissionButton size="sm" variant="danger" permission={modifyNeed}
|
||||
aria-label={`Remove ${l.url ?? l.raw}`}
|
||||
onClick={() => setLines((all) => all.filter((_, j) => j !== i))}>
|
||||
Remove
|
||||
</PermissionButton>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.foot}>
|
||||
<PermissionButton size="sm" permission={modifyNeed} disabled={loading || edit?.group === 'new'}
|
||||
|
||||
Reference in new issue
Block a user