From 00b238a0afbca43e4541ba9847b748fbb3f25adc Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 14 Mar 2023 13:50:11 -0500 Subject: [PATCH] docs(DataTable): add reference stories for row actions (#2978) * docs(DataTable): add reference stories for row actions * chore: clean-up ts types * fix: add guards for optional id and field * docs: add story ids to DataTable docs json * Update generated/components.json --------- Co-authored-by: Josh Black --- generated/components.json | 12 + src/DataTable/DataTable.docs.json | 9 + src/DataTable/DataTable.features.stories.tsx | 273 ++++++++++++++++++- src/DataTable/DataTable.tsx | 8 +- src/DataTable/column.ts | 4 +- src/DataTable/useTable.ts | 28 +- 6 files changed, 326 insertions(+), 8 deletions(-) diff --git a/generated/components.json b/generated/components.json index 8539728cf9c..d88d2c62563 100644 --- a/generated/components.json +++ b/generated/components.json @@ -1868,6 +1868,18 @@ "id": "components-datatable-features--with-action", "code": "() => (\n \n \n Repositories\n \n \n \n \n \n \n A subtitle could appear here to give extra context to the data.\n \n {\n return \n },\n },\n {\n header: 'Updated',\n field: 'updatedAt',\n renderCell: (row) => {\n return \n },\n },\n {\n header: 'Dependabot',\n field: 'securityFeatures.dependabot',\n renderCell: (row) => {\n return row.securityFeatures.dependabot.length > 0 ? (\n \n {row.securityFeatures.dependabot.map((feature) => {\n return \n })}\n \n ) : null\n },\n },\n {\n header: 'Code scanning',\n field: 'securityFeatures.codeScanning',\n renderCell: (row) => {\n return row.securityFeatures.codeScanning.length > 0 ? (\n \n {row.securityFeatures.codeScanning.map((feature) => {\n return \n })}\n \n ) : null\n },\n },\n ]}\n />\n \n)" }, + { + "id": "components-datatable-features--with-row-action", + "code": "() => (\n \n \n Repositories\n \n \n A subtitle could appear here to give extra context to the data.\n \n {\n return \n },\n },\n {\n header: 'Updated',\n field: 'updatedAt',\n renderCell: (row) => {\n return \n },\n },\n {\n header: 'Dependabot',\n field: 'securityFeatures.dependabot',\n renderCell: (row) => {\n return row.securityFeatures.dependabot.length > 0 ? (\n \n {row.securityFeatures.dependabot.map((feature) => {\n return \n })}\n \n ) : null\n },\n },\n {\n header: 'Code scanning',\n field: 'securityFeatures.codeScanning',\n renderCell: (row) => {\n return row.securityFeatures.codeScanning.length > 0 ? (\n \n {row.securityFeatures.codeScanning.map((feature) => {\n return \n })}\n \n ) : null\n },\n },\n {\n id: 'actions',\n header: () => Actions,\n renderCell: (row) => {\n return (\n {\n action('Download')(row)\n }}\n />\n )\n },\n },\n ]}\n />\n \n)" + }, + { + "id": "components-datatable-features--with-row-actions", + "code": "() => (\n \n \n Repositories\n \n \n A subtitle could appear here to give extra context to the data.\n \n {\n return \n },\n },\n {\n header: 'Updated',\n field: 'updatedAt',\n renderCell: (row) => {\n return \n },\n },\n {\n header: 'Dependabot',\n field: 'securityFeatures.dependabot',\n renderCell: (row) => {\n return row.securityFeatures.dependabot.length > 0 ? (\n \n {row.securityFeatures.dependabot.map((feature) => {\n return \n })}\n \n ) : null\n },\n },\n {\n header: 'Code scanning',\n field: 'securityFeatures.codeScanning',\n renderCell: (row) => {\n return row.securityFeatures.codeScanning.length > 0 ? (\n \n {row.securityFeatures.codeScanning.map((feature) => {\n return \n })}\n \n ) : null\n },\n },\n {\n id: 'actions',\n header: () => Actions,\n renderCell: (row) => {\n return (\n <>\n {\n action('Edit')(row)\n }}\n />\n {\n action('Delete')(row)\n }}\n />\n \n )\n },\n },\n ]}\n />\n \n)" + }, + { + "id": "components-datatable-features--with-row-action-menu", + "code": "() => (\n \n \n Repositories\n \n \n A subtitle could appear here to give extra context to the data.\n \n {\n return \n },\n },\n {\n header: 'Updated',\n field: 'updatedAt',\n renderCell: (row) => {\n return \n },\n },\n {\n header: 'Dependabot',\n field: 'securityFeatures.dependabot',\n renderCell: (row) => {\n return row.securityFeatures.dependabot.length > 0 ? (\n \n {row.securityFeatures.dependabot.map((feature) => {\n return \n })}\n \n ) : null\n },\n },\n {\n header: 'Code scanning',\n field: 'securityFeatures.codeScanning',\n renderCell: (row) => {\n return row.securityFeatures.codeScanning.length > 0 ? (\n \n {row.securityFeatures.codeScanning.map((feature) => {\n return \n })}\n \n ) : null\n },\n },\n {\n id: 'actions',\n header: () => Actions,\n renderCell: (row) => {\n return (\n \n \n \n \n \n \n {\n action('Copy')(row)\n }}\n >\n Copy row\n \n Edit row\n Export row as CSV\n \n \n Delete row\n \n \n \n \n )\n },\n },\n ]}\n />\n \n)" + }, { "id": "components-datatable-features--with-custom-heading", "code": "() => (\n <>\n \n Security coverage\n \n

\n Organization members can only see data for the most recently-updated\n repositories. To see all repositories, talk to your organization\n administrator about becoming a security manager.\n

\n \n {\n return \n },\n },\n {\n header: 'Updated',\n field: 'updatedAt',\n renderCell: (row) => {\n return \n },\n },\n {\n header: 'Dependabot',\n field: 'securityFeatures.dependabot',\n renderCell: (row) => {\n return row.securityFeatures.dependabot.length > 0 ? (\n \n {row.securityFeatures.dependabot.map((feature) => {\n return \n })}\n \n ) : null\n },\n },\n {\n header: 'Code scanning',\n field: 'securityFeatures.codeScanning',\n renderCell: (row) => {\n return row.securityFeatures.codeScanning.length > 0 ? (\n \n {row.securityFeatures.codeScanning.map((feature) => {\n return \n })}\n \n ) : null\n },\n },\n ]}\n />\n \n \n)" diff --git a/src/DataTable/DataTable.docs.json b/src/DataTable/DataTable.docs.json index 88a45f4466d..2cfaf0b1194 100644 --- a/src/DataTable/DataTable.docs.json +++ b/src/DataTable/DataTable.docs.json @@ -19,6 +19,15 @@ { "id": "components-datatable-features--with-action" }, + { + "id": "components-datatable-features--with-row-action" + }, + { + "id": "components-datatable-features--with-row-actions" + }, + { + "id": "components-datatable-features--with-row-action-menu" + }, { "id": "components-datatable-features--with-custom-heading" } diff --git a/src/DataTable/DataTable.features.stories.tsx b/src/DataTable/DataTable.features.stories.tsx index 259984e47af..738d65cc0ef 100644 --- a/src/DataTable/DataTable.features.stories.tsx +++ b/src/DataTable/DataTable.features.stories.tsx @@ -1,12 +1,16 @@ -import {DownloadIcon, PlusIcon} from '@primer/octicons-react' +import {DownloadIcon, KebabHorizontalIcon, PencilIcon, PlusIcon, TrashIcon} from '@primer/octicons-react' +import {action} from '@storybook/addon-actions' import {Meta} from '@storybook/react' import React from 'react' +import {ActionList} from '../ActionList' +import {ActionMenu} from '../ActionMenu' import {Button, IconButton} from '../Button' import {DataTable, Table} from '../DataTable' import Heading from '../Heading' import Label from '../Label' import LabelGroup from '../LabelGroup' import RelativeTime from '../RelativeTime' +import VisuallyHidden from '../_VisuallyHidden' export default { title: 'Components/DataTable/Features', @@ -578,6 +582,273 @@ export const WithActionsOnly = () => ( ) +export const WithRowAction = () => ( + + + Repositories + + + A subtitle could appear here to give extra context to the data. + + { + return + }, + }, + { + header: 'Updated', + field: 'updatedAt', + renderCell: row => { + return + }, + }, + { + header: 'Dependabot', + field: 'securityFeatures.dependabot', + renderCell: row => { + return row.securityFeatures.dependabot.length > 0 ? ( + + {row.securityFeatures.dependabot.map(feature => { + return + })} + + ) : null + }, + }, + { + header: 'Code scanning', + field: 'securityFeatures.codeScanning', + renderCell: row => { + return row.securityFeatures.codeScanning.length > 0 ? ( + + {row.securityFeatures.codeScanning.map(feature => { + return + })} + + ) : null + }, + }, + { + id: 'actions', + header: () => Actions, + renderCell: row => { + return ( + { + action('Download')(row) + }} + /> + ) + }, + }, + ]} + /> + +) + +export const WithRowActions = () => ( + + + Repositories + + + A subtitle could appear here to give extra context to the data. + + { + return + }, + }, + { + header: 'Updated', + field: 'updatedAt', + renderCell: row => { + return + }, + }, + { + header: 'Dependabot', + field: 'securityFeatures.dependabot', + renderCell: row => { + return row.securityFeatures.dependabot.length > 0 ? ( + + {row.securityFeatures.dependabot.map(feature => { + return + })} + + ) : null + }, + }, + { + header: 'Code scanning', + field: 'securityFeatures.codeScanning', + renderCell: row => { + return row.securityFeatures.codeScanning.length > 0 ? ( + + {row.securityFeatures.codeScanning.map(feature => { + return + })} + + ) : null + }, + }, + { + id: 'actions', + header: () => Actions, + renderCell: row => { + return ( + <> + { + action('Edit')(row) + }} + /> + { + action('Delete')(row) + }} + /> + + ) + }, + }, + ]} + /> + +) + +export const WithRowActionMenu = () => ( + + + Repositories + + + A subtitle could appear here to give extra context to the data. + + { + return + }, + }, + { + header: 'Updated', + field: 'updatedAt', + renderCell: row => { + return + }, + }, + { + header: 'Dependabot', + field: 'securityFeatures.dependabot', + renderCell: row => { + return row.securityFeatures.dependabot.length > 0 ? ( + + {row.securityFeatures.dependabot.map(feature => { + return + })} + + ) : null + }, + }, + { + header: 'Code scanning', + field: 'securityFeatures.codeScanning', + renderCell: row => { + return row.securityFeatures.codeScanning.length > 0 ? ( + + {row.securityFeatures.codeScanning.map(feature => { + return + })} + + ) : null + }, + }, + { + id: 'actions', + header: () => Actions, + renderCell: row => { + return ( + + + + + + + { + action('Copy')(row) + }} + > + Copy row + + Edit row + Export row as CSV + + Delete row + + + + ) + }, + }, + ]} + /> + +) + export const WithCustomHeading = () => ( <> diff --git a/src/DataTable/DataTable.tsx b/src/DataTable/DataTable.tsx index e9cf399b0e0..7b6ccb20de3 100644 --- a/src/DataTable/DataTable.tsx +++ b/src/DataTable/DataTable.tsx @@ -81,11 +81,15 @@ function DataTable({ actions.sortBy(header) }} > - {header.column.header} + {typeof header.column.header === 'string' ? header.column.header : header.column.header()} ) } - return {header.column.header} + return ( + + {typeof header.column.header === 'string' ? header.column.header : header.column.header()} + + ) })} diff --git a/src/DataTable/column.ts b/src/DataTable/column.ts index 7a97202ae27..ba4e0bef4e8 100644 --- a/src/DataTable/column.ts +++ b/src/DataTable/column.ts @@ -9,7 +9,7 @@ export interface Column { * Provide the name of the column. This will be rendered as a table header * within the table itself */ - header: string + header: string | (() => React.ReactNode) /** * Optionally provide a field to render for this column. This may be the key @@ -19,7 +19,7 @@ export interface Column { * Alternatively, you may provide a `renderCell` for this column to render the * field in a row */ - field: ObjectPaths + field?: ObjectPaths /** * Provide a custom component or render prop to render the data for this diff --git a/src/DataTable/useTable.ts b/src/DataTable/useTable.ts index 35dba41f34c..ec68b5f4640 100644 --- a/src/DataTable/useTable.ts +++ b/src/DataTable/useTable.ts @@ -80,6 +80,10 @@ export function useTable({ const headers = columns.map(column => { const id = column.id ?? column.field + if (id === undefined) { + throw new Error(`Expected either an \`id\` or \`field\` to be defined for a Column`) + } + const sortable = column.sortBy !== undefined && column.sortBy !== false return { id, @@ -133,6 +137,10 @@ export function useTable({ setRowOrder(rowOrder => { return rowOrder.slice().sort((a, b) => { + if (header.column.field === undefined) { + return 0 + } + const valueA = get(a, header.column.field) const valueB = get(b, header.column.field) @@ -159,7 +167,10 @@ export function useTable({ column: header.column, rowHeader: header.column.rowHeader ?? false, getValue() { - return get(row, header.column.field) + if (header.column.field !== undefined) { + return get(row, header.column.field) + } + throw new Error(`Unable to get value for column header ${header.id}`) }, } }) @@ -203,7 +214,7 @@ function getInitialSortState( } return { - id: initialSortColumn, + id: `${initialSortColumn}`, direction: initialSortDirection ?? DEFAULT_SORT_DIRECTION, } } @@ -223,8 +234,19 @@ function getInitialSortState( return null } + const id = column.id ?? column.field + if (id === undefined) { + if (__DEV__) { + // eslint-disable-next-line no-console + console.warn( + `Warning: Unable to find an \`id\` or \`field\` for the column: ${column}. Please set one of these properties on the column.`, + ) + } + return null + } + return { - id: column.id ?? column.field, + id, direction: initialSortDirection, } }