From 33e000115abc0951c866ea030590888cade09651 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Fri, 3 Mar 2023 12:07:04 -0600 Subject: [PATCH 1/5] docs(DataTable): add reference stories for row actions --- src/DataTable/DataTable.features.stories.tsx | 273 ++++++++++++++++++- src/DataTable/DataTable.tsx | 8 +- src/DataTable/column.ts | 4 +- src/DataTable/useTable.ts | 4 + 4 files changed, 284 insertions(+), 5 deletions(-) diff --git a/src/DataTable/DataTable.features.stories.tsx b/src/DataTable/DataTable.features.stories.tsx index 1da0ced058f..c602a44a947 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: 'Drafts/Components/DataTable/Features', @@ -577,3 +581,270 @@ 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 + + + + ) + }, + }, + ]} + /> + +) diff --git a/src/DataTable/DataTable.tsx b/src/DataTable/DataTable.tsx index 0ae159cc9a7..4eeb4025e68 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 1fe540b865d..d99b65f3faf 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 | undefined /** * 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 295c5b15f11..ec6de30f993 100644 --- a/src/DataTable/useTable.ts +++ b/src/DataTable/useTable.ts @@ -168,6 +168,10 @@ export function useTable({ setRowOrder(rowOrder => { return rowOrder.slice().sort((a, b) => { + if (header.column.field === null || header.column.field === undefined) { + return 0 + } + const valueA = get(a, header.column.field) const valueB = get(b, header.column.field) From a5db29c254d5e4e05717c0f26669fee78facc62b Mon Sep 17 00:00:00 2001 From: Josh Black Date: Wed, 8 Mar 2023 16:31:12 -0600 Subject: [PATCH 2/5] chore: clean-up ts types --- src/DataTable/column.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataTable/column.ts b/src/DataTable/column.ts index cee902e7ab7..ba4e0bef4e8 100644 --- a/src/DataTable/column.ts +++ b/src/DataTable/column.ts @@ -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 | undefined + field?: ObjectPaths /** * Provide a custom component or render prop to render the data for this From 89d8e1978b06a901c01c7f78ed17bfd1fc9b056c Mon Sep 17 00:00:00 2001 From: Josh Black Date: Wed, 8 Mar 2023 16:40:41 -0600 Subject: [PATCH 3/5] fix: add guards for optional id and field --- src/DataTable/useTable.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/DataTable/useTable.ts b/src/DataTable/useTable.ts index ebcef581650..cf9b998f76e 100644 --- a/src/DataTable/useTable.ts +++ b/src/DataTable/useTable.ts @@ -69,8 +69,12 @@ export function useTable({ return column.sortBy }) if (defaultSortColumn) { + const id = defaultSortColumn.id ?? defaultSortColumn.field + if (!id) { + throw new Error(`Expected either an \`id\` or \`field\` to be defined for a Column`) + } return { - id: defaultSortColumn.id ?? defaultSortColumn.field, + id, direction: initialSortDirection, } } @@ -80,8 +84,12 @@ export function useTable({ return column.sortBy }) if (sortableColumn) { + const id = sortableColumn.id ?? sortableColumn.field + if (!id) { + throw new Error(`Expected either an \`id\` or \`field\` to be defined for a Column`) + } return { - id: sortableColumn.id ?? sortableColumn.field, + id, direction: DEFAULT_SORT_DIRECTION, } } @@ -115,6 +123,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, @@ -168,7 +180,7 @@ export function useTable({ setRowOrder(rowOrder => { return rowOrder.slice().sort((a, b) => { - if (header.column.field === null || header.column.field === undefined) { + if (header.column.field === undefined) { return 0 } @@ -198,7 +210,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}`) }, } }) From 11e7393ce446053fc2c60b6d46a689fe7f89aba5 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 14 Mar 2023 11:12:12 -0500 Subject: [PATCH 4/5] docs: add story ids to DataTable docs json --- src/DataTable/DataTable.docs.json | 9 +++++++++ 1 file changed, 9 insertions(+) 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" } From f0ca0436b9dcdd19a497465f0e8c555427889656 Mon Sep 17 00:00:00 2001 From: joshblack Date: Tue, 14 Mar 2023 16:14:42 +0000 Subject: [PATCH 5/5] Update generated/components.json --- generated/components.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/generated/components.json b/generated/components.json index 01641f79520..8441d2825e9 100644 --- a/generated/components.json +++ b/generated/components.json @@ -1876,6 +1876,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)"