-
Notifications
You must be signed in to change notification settings - Fork 719
UN-4124 [FIX] Implement antd's expandable API on the shared DataTable so nested cell values expand again #2288
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e59d02a
d197cde
577970e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -413,17 +413,167 @@ | |
| * order-less state. | ||
| */ | ||
| sortDirections, | ||
| /** | ||
| * antd's `expandable={{ expandedRowRender, expandedRowKeys, rowExpandable, | ||
| * showExpandColumn, onExpand, … }}` — a full-width extra row rendered under | ||
| * the record it belongs to. | ||
| * | ||
| * Declared for the same reason as `onRow`, `showHeader`, `scroll`, | ||
| * `bordered`, `locale` and `sortDirections` above, and it is the widest | ||
| * silent drop of the set: undeclared, the whole object fell into `...props` | ||
| * and onto the wrapper <div>, so `expandedRowRender` was never called and | ||
| * the table rendered as if the prop had not been passed. HITL's review | ||
| * editor is the visible casualty — an array or object inside a table cell | ||
| * shows a truncated JSON blob with an expand button beside it, and clicking | ||
| * that button did nothing at all, leaving nested values unreadable in Table | ||
| * view (UN-4124). | ||
| */ | ||
| expandable, | ||
| ...props | ||
| }) { | ||
| const empty = locale?.emptyText ?? emptyText; | ||
| const [sorting, setSorting] = React.useState([]); | ||
| const [selection, setSelection] = React.useState({}); | ||
|
|
||
| const rows = React.useMemo(() => dataSource ?? [], [dataSource]); | ||
| const cols = React.useMemo( | ||
| () => toColumns(columns, rowSelection), | ||
| [columns, rowSelection], | ||
|
|
||
| /* | ||
| * Expansion, in antd's shape. Keys are compared as strings because that is | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: |
||
| * what TanStack's `getRowId` (and so `row.id`) produces from whatever | ||
| * `rowKey` resolves to — a call-site numbering its rows `key: index` passes | ||
| * numbers, and `[0].includes("0")` is false. | ||
| */ | ||
| const expandedRowRender = expandable?.expandedRowRender; | ||
| const canExpand = typeof expandedRowRender === "function"; | ||
| const controlledExpandedKeys = expandable?.expandedRowKeys; | ||
| const [ownExpandedKeys, setOwnExpandedKeys] = React.useState( | ||
| () => expandable?.defaultExpandedRowKeys ?? [], | ||
| ); | ||
| const expandedKeys = React.useMemo( | ||
| () => new Set((controlledExpandedKeys ?? ownExpandedKeys).map(String)), | ||
| [controlledExpandedKeys, ownExpandedKeys], | ||
| ); | ||
|
|
||
| const isRowExpandable = React.useCallback( | ||
| (record) => | ||
| canExpand && | ||
| (typeof expandable?.rowExpandable === "function" | ||
| ? Boolean(expandable.rowExpandable(record)) | ||
| : true), | ||
| [canExpand, expandable?.rowExpandable], | ||
| ); | ||
|
|
||
| const onExpandCb = expandable?.onExpand; | ||
| const onExpandedRowsChange = expandable?.onExpandedRowsChange; | ||
| const toggleExpanded = React.useCallback( | ||
| (key, record, originalKey) => { | ||
| const willExpand = !expandedKeys.has(key); | ||
| /* | ||
| * Report the caller's own key values, never the normalized strings: | ||
| * normalization exists to match TanStack's `row.id` and must not leak | ||
| * out. A controlled caller that passed `[1]` and then tests | ||
| * `next.includes(1)` would never match `["1"]`. | ||
| */ | ||
| const source = controlledExpandedKeys ?? ownExpandedKeys; | ||
| const next = willExpand | ||
| ? [...source, originalKey] | ||
| : source.filter((k) => String(k) !== key); | ||
| // A controlled `expandedRowKeys` belongs to the parent: report, never set. | ||
| if (controlledExpandedKeys === undefined) { | ||
| setOwnExpandedKeys(next); | ||
| } | ||
| onExpandCb?.(willExpand, record); | ||
| onExpandedRowsChange?.(next); | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| }, | ||
| [ | ||
| expandedKeys, | ||
| controlledExpandedKeys, | ||
| ownExpandedKeys, | ||
| onExpandCb, | ||
| onExpandedRowsChange, | ||
| ], | ||
| ); | ||
|
|
||
| /* | ||
| * The untouched key behind `row.id`. `getRowId` stringifies whatever `rowKey` | ||
| * resolves to, so this is the only way back to the value the call-site | ||
| * actually holds — falling back to the row index, which is what `getRowId` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This claim is only true for a string One-liner fix in |
||
| * itself uses when the record carries no key. | ||
| */ | ||
| const originalRowKey = React.useCallback( | ||
| (record, index) => { | ||
| const raw = | ||
| typeof rowKey === "function" ? rowKey(record) : record?.[rowKey]; | ||
| return raw === undefined || raw === null ? index : raw; | ||
| }, | ||
| [rowKey], | ||
| ); | ||
|
|
||
| /* | ||
| * antd hides the toggle column for `showExpandColumn: false` — the idiom for | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: references HITL from OSS again — rot risk since the plugin can change independently. |
||
| * a table driven entirely by its own controls, which is how HITL opens a | ||
| * cell's nested table from a button inside the cell. | ||
| */ | ||
| const showExpandColumn = canExpand && expandable?.showExpandColumn !== false; | ||
| const expandIcon = expandable?.expandIcon; | ||
| const cols = React.useMemo(() => { | ||
| const base = toColumns(columns, rowSelection); | ||
| if (!showExpandColumn) { | ||
| return base; | ||
| } | ||
| return [ | ||
| { | ||
| id: "__expand", | ||
| header: () => null, | ||
| enableSorting: false, | ||
| size: 48, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
meta: { width: expandable?.columnWidth ?? 48 },which also picks up antd's |
||
| cell: ({ row }) => { | ||
|
Check warning on line 530 in frontend/src/components/data-table/DataTable.jsx
|
||
| const record = row.original; | ||
| if (!isRowExpandable(record)) { | ||
| return null; | ||
| } | ||
| const expanded = expandedKeys.has(row.id); | ||
| const onExpand = (event) => { | ||
| // The row itself may carry an onRow click handler. | ||
| event?.stopPropagation?.(); | ||
| toggleExpanded(row.id, record, originalRowKey(record, row.index)); | ||
| }; | ||
| if (typeof expandIcon === "function") { | ||
| return expandIcon({ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. antd hands a custom |
||
| expanded, | ||
| record, | ||
| onExpand: (_record, event) => onExpand(event), | ||
| }); | ||
| } | ||
| return ( | ||
| <button | ||
| type="button" | ||
| aria-expanded={expanded} | ||
| aria-label={expanded ? "Collapse row" : "Expand row"} | ||
| onClick={onExpand} | ||
| className="ant-table-row-expand-icon inline-flex size-5 items-center justify-center rounded border text-muted-foreground hover:text-foreground" | ||
| > | ||
| {expanded ? ( | ||
| <ChevronDown className="size-3" /> | ||
| ) : ( | ||
| <ChevronRight className="size-3" /> | ||
| )} | ||
| </button> | ||
| ); | ||
| }, | ||
| }, | ||
| ...base, | ||
| ]; | ||
| }, [ | ||
| columns, | ||
| rowSelection, | ||
| showExpandColumn, | ||
| expandIcon, | ||
| expandedKeys, | ||
| isRowExpandable, | ||
| toggleExpanded, | ||
| originalRowKey, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Depending on User-visible: Tab to the chevron, press Enter, focus drops to Cheapest fix: hold the four in a ref and read |
||
| ]); | ||
| const leaves = React.useMemo(() => leafColumns(columns), [columns]); | ||
|
|
||
| /* | ||
|
|
@@ -858,35 +1008,65 @@ | |
| </TableCell> | ||
| </TableRow> | ||
| ) : table.getRowModel().rows.length ? ( | ||
| table.getRowModel().rows.map((row) => ( | ||
| <TableRow | ||
| key={row.id} | ||
| data-state={row.getIsSelected() ? "selected" : undefined} | ||
| className={ | ||
| typeof rowClassName === "function" | ||
| ? rowClassName(row.original, row.index) | ||
| : rowClassName | ||
| } | ||
| {...(onRow ? onRow(row.original, row.index) : {})} | ||
| > | ||
| {row.getVisibleCells().map((cell) => ( | ||
| <TableCell | ||
| key={cell.id} | ||
| className={cn( | ||
| cell.column.columnDef.meta?.align === "center" && | ||
| "text-center", | ||
| cell.column.columnDef.meta?.align === "right" && | ||
| "text-right", | ||
| )} | ||
| table.getRowModel().rows.map((row) => { | ||
| const expanded = | ||
| expandedKeys.has(row.id) && isRowExpandable(row.original); | ||
| return ( | ||
| /* | ||
| * The expanded row is a SIBLING <tr>, not a nested one: a | ||
| * table row may only contain cells, so antd's full-width | ||
| * panel has to be its own row spanning every column. | ||
| */ | ||
| <React.Fragment key={row.id}> | ||
| <TableRow | ||
| data-state={row.getIsSelected() ? "selected" : undefined} | ||
| className={ | ||
| typeof rowClassName === "function" | ||
| ? rowClassName(row.original, row.index) | ||
| : rowClassName | ||
| } | ||
| {...(onRow ? onRow(row.original, row.index) : {})} | ||
| > | ||
| {flexRender( | ||
| cell.column.columnDef.cell, | ||
| cell.getContext(), | ||
| )} | ||
| </TableCell> | ||
| ))} | ||
| </TableRow> | ||
| )) | ||
| {row.getVisibleCells().map((cell) => ( | ||
| <TableCell | ||
| key={cell.id} | ||
| className={cn( | ||
| cell.column.columnDef.meta?.align === "center" && | ||
| "text-center", | ||
| cell.column.columnDef.meta?.align === "right" && | ||
| "text-right", | ||
| )} | ||
| > | ||
| {flexRender( | ||
| cell.column.columnDef.cell, | ||
| cell.getContext(), | ||
| )} | ||
| </TableCell> | ||
| ))} | ||
| </TableRow> | ||
| {expanded ? ( | ||
| <TableRow | ||
| className={cn( | ||
| "ant-table-expanded-row hover:bg-transparent", | ||
| typeof expandable?.expandedRowClassName === "function" | ||
| ? expandable.expandedRowClassName( | ||
| row.original, | ||
| row.index, | ||
| ) | ||
| : expandable?.expandedRowClassName, | ||
| )} | ||
| > | ||
| <TableCell | ||
| colSpan={table.getVisibleLeafColumns().length} | ||
| className="p-2" | ||
| > | ||
| {expandedRowRender(row.original, row.index, 0, true)} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low: when |
||
| </TableCell> | ||
| </TableRow> | ||
| ) : null} | ||
| </React.Fragment> | ||
| ); | ||
| }) | ||
| ) : ( | ||
| <TableRow className="hover:bg-transparent"> | ||
| <TableCell | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The trailing
…implies the full antd surface, butexpandRowByClick,childrenColumnName(tree rows),indentSize,columnWidth,fixed,expandIconColumnIndexandexpandedRowOffsetare silently ignored — the same failure mode this PR exists to fix. Given the file's whole theme, please enumerate what IS honoured instead:That also drops the HITL incident narrative — the review editor is a cloud-only plugin that OSS readers can't see, and the ticket ref would be the first
UN-xxxxin OSS frontend source (grep -rln 'UN-[0-9]' frontend/srcis empty today). Ticket belongs in the commit message.