diff --git a/packages/webapp/src/components/Table/Header/TableHead.jsx b/packages/webapp/src/components/Table/Header/TableHead.jsx index 5489231083..4ba23254a6 100644 --- a/packages/webapp/src/components/Table/Header/TableHead.jsx +++ b/packages/webapp/src/components/Table/Header/TableHead.jsx @@ -13,6 +13,7 @@ * GNU General Public License for more details, see . */ import PropTypes from 'prop-types'; +import { Checkbox } from '@mui/material'; import TableCell from '@mui/material/TableCell'; import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; @@ -22,7 +23,17 @@ import { ReactComponent as UnfoldCircle } from '../../../assets/images/unfold-ci import { ReactComponent as ArrowDownCircle } from '../../../assets/images/arrow-down-circle.svg'; import styles from '../styles.module.scss'; -export default function EnhancedTableHead({ columns, order, orderBy, onRequestSort, dense }) { +export default function EnhancedTableHead({ + columns, + order, + orderBy, + onRequestSort, + dense, + shouldShowCheckbox, + onSelectAllClick, + numSelected, + rowCount, +}) { const createSortHandler = (property) => (event) => { onRequestSort(event, property); }; @@ -30,6 +41,16 @@ export default function EnhancedTableHead({ columns, order, orderBy, onRequestSo return ( + {shouldShowCheckbox && ( + + 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={onSelectAllClick} + /> + + )} {columns.map(({ id, align, columnProps, label, sortable = true }) => { if (!id) { return null; @@ -80,4 +101,8 @@ EnhancedTableHead.propTypes = { order: PropTypes.oneOf(['asc', 'desc']).isRequired, orderBy: PropTypes.string.isRequired, dense: PropTypes.bool, + shouldShowCheckbox: PropTypes.bool, + onSelectAllClick: PropTypes.func, + numSelected: PropTypes.number, + rowCount: PropTypes.number, }; diff --git a/packages/webapp/src/components/Table/TableV2.jsx b/packages/webapp/src/components/Table/TableV2.jsx index 1017c19c79..2ab8308e1e 100644 --- a/packages/webapp/src/components/Table/TableV2.jsx +++ b/packages/webapp/src/components/Table/TableV2.jsx @@ -12,9 +12,10 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details, see . */ -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useTranslation } from 'react-i18next'; +import { Checkbox } from '@mui/material'; import Box from '@mui/material/Box'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; @@ -93,6 +94,11 @@ export default function TableV2(props) { defaultOrderBy, alternatingRowColor, showHeader, + onCheck, + handleSelectAllClick, + selectedIds, + stickyHeader, + maxHeight, } = props; const [order, setOrder] = useState('asc'); @@ -101,6 +107,7 @@ export default function TableV2(props) { const [rowsPerPage, setRowsPerPage] = useState(minRows); const fullColSpan = columns.reduce((total, column) => total + (column.id ? 1 : 0), 0); + const shouldShowCheckbox = onCheck && handleSelectAllClick && selectedIds; const handleRequestSort = (event, property) => { const isAsc = orderBy === property && order === 'asc'; @@ -108,12 +115,20 @@ export default function TableV2(props) { setOrderBy(property); }; - const handleRowClick = (event, index) => { + const handleRowClick = (event, row) => { if (onRowClick) { - onRowClick(event, index); + onRowClick(event, row); } }; + const handleCheckboxClick = (event, row) => { + if (onCheck) { + onCheck(event, row); + } + // prevent handleRowClick from being called + event.stopPropagation(); + }; + const handleChangePage = (event, newPage) => { setPage(newPage); }; @@ -142,10 +157,11 @@ export default function TableV2(props) { return ( - + {showHeader && ( )} {visibleRows.map((row, index) => { + const isItemSelected = selectedIds?.includes(row.id); + return ( handleRowClick(event, row)} + isItemSelected={isItemSelected} + aria-checked={isItemSelected} className={clsx( styles.tableRow, styles.itemRow, @@ -169,6 +193,16 @@ export default function TableV2(props) { alternatingRowColor ? styles.alternatingRowColor : styles.plainRowColor, )} > + {shouldShowCheckbox && ( + + handleCheckboxClick(event, row)} + checked={isItemSelected} + className={styles.checkbox} + /> + + )} {columns.map(({ id, format, align, columnProps }) => { if (!id) { return null; @@ -204,19 +238,22 @@ export default function TableV2(props) { )} {columns.some((column) => column.id && column.Footer) && ( - {columns.map(({ id, align, columnProps, Footer }) => { + {columns.map(({ id, align, columnProps, Footer }, index) => { if (!id) { return null; } return ( - - {Footer} - + <> + {!index && shouldShowCheckbox && } + + {Footer} + + ); })} @@ -271,6 +308,11 @@ TableV2.propTypes = { defaultOrderBy: PropTypes.string, alternatingRowColor: PropTypes.bool, showHeader: PropTypes.bool, + onCheck: PropTypes.func, + handleSelectAllClick: PropTypes.func, + selectedIds: PropTypes.array, + stickyHeader: PropTypes.bool, + maxHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), }; TableV2.defaultProps = { diff --git a/packages/webapp/src/components/Table/styles.module.scss b/packages/webapp/src/components/Table/styles.module.scss index 603058cb42..4635adc654 100644 --- a/packages/webapp/src/components/Table/styles.module.scss +++ b/packages/webapp/src/components/Table/styles.module.scss @@ -14,25 +14,57 @@ */ .table { - padding: 0.5px; + border-collapse: separate; + &.fixed { table-layout: fixed; } } .headerRow { - border-top: 1px solid var(--Colors-Neutral-Neutral-50); - border-bottom: 1px solid var(--Colors-Neutral-Neutral-50); margin-bottom: 16px; + &.clickable { cursor: pointer; } + + th { + border-top: 1px solid var(--Colors-Neutral-Neutral-50); + border-bottom: 1px solid var(--Colors-Neutral-Neutral-50); + } } .tableRow { &.clickable { cursor: pointer; } + + &[aria-checked="true"] { + border: solid 1px var(--Colors-Accent-Accent-yellow-400); + background-color: var(--Colors-Accent-Accent-yellow-50); + + & > td { + border-top: solid 1px var(--Colors-Accent-Accent-yellow-400); + border-bottom: solid 1px var(--Colors-Accent-Accent-yellow-400); + + &:first-child { + border-radius: 4px 0 0 4px; + border: solid 1px var(--Colors-Accent-Accent-yellow-400); + border-right: none; + } + &:last-child { + border-radius: 0 4px 4px 0; + border: solid 1px var(--Colors-Accent-Accent-yellow-400); + border-left: none; + } + }; + } + + // override mui's style + td:first-child { + border-top: solid 1px transparent; + border-bottom: solid 1px var(--grey200); + } } .itemRow { @@ -74,12 +106,9 @@ .tableCell { padding: 0 12px; height: 56px; + border-top: solid 1px transparent; border-bottom: solid 1px var(--grey200); - &.tableHead { - border-bottom: none; - } - &.dense { padding: 0 12px; height: 40px; @@ -172,3 +201,17 @@ line-height: 16px; color: transparent; } + +// checkbox +.checkboxCell { + width: 32px; + text-align: center; + + span { + padding: 0; + + &:hover { + background-color: transparent; + } + } +} diff --git a/packages/webapp/src/components/Table/types.ts b/packages/webapp/src/components/Table/types.ts index bd42a2a19a..809b15d1e2 100644 --- a/packages/webapp/src/components/Table/types.ts +++ b/packages/webapp/src/components/Table/types.ts @@ -75,4 +75,9 @@ export type TableV2Props = { shouldFixTableLayout?: boolean; defaultOrderBy?: string; showHeader?: boolean; + onCheck?: (id: string | number) => void; + handleSelectAllClick?: () => void; + selectedIds?: (string | number)[]; + stickyHeader?: boolean; + maxHeight?: number; }; diff --git a/packages/webapp/src/stories/Table/TableV2.stories.jsx b/packages/webapp/src/stories/Table/TableV2.stories.jsx index 008e995f7a..65f6db6bfa 100644 --- a/packages/webapp/src/stories/Table/TableV2.stories.jsx +++ b/packages/webapp/src/stories/Table/TableV2.stories.jsx @@ -12,10 +12,12 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details, see . */ -import React from 'react'; + import { v2TableDecorator } from '../Pages/config/Decorators'; import Table from '../../components/Table'; import { TableKind } from '../../components/Table/types'; +import { useState } from 'react'; +import TextButton from '../../components/Form/Button/TextButton'; export default { title: 'Components/Tables/V2', @@ -74,16 +76,16 @@ const getCropSalesColumns = (mobileView = true) => { const getCropSalesData = (length) => { return [ - { crop: 'White corn, Corn', quantity: 2124, revenue: 8796.0 }, - { crop: 'Koto, Buckwheat', quantity: 724, revenue: 692.5 }, - { crop: 'Lutz green leaf, Beetroot', quantity: 58, revenue: 210.0 }, - { crop: 'Cox’s orange pippin, Apple', quantity: 48, revenue: 340.0 }, - { crop: 'Macoun, Apples', quantity: 124, revenue: 1234.0 }, - { crop: 'Butter Boy Hybrid, Butternut ', quantity: 24, revenue: 785.5 }, - { crop: 'King Edward, Potato', quantity: 58, revenue: 237.0 }, - { crop: 'Blanco Veneto, Celeriac', quantity: 56, revenue: 895.0 }, - { crop: 'Hollow Crown, Parsnips ', quantity: 23, revenue: 354.0 }, - { crop: 'Early White Hybrid, Cauliflower', quantity: 87, revenue: 789.5 }, + { id: 1, crop: 'White corn, Corn', quantity: 2124, revenue: 8796.0 }, + { id: 2, crop: 'Koto, Buckwheat', quantity: 724, revenue: 692.5 }, + { id: 3, crop: 'Lutz green leaf, Beetroot', quantity: 58, revenue: 210.0 }, + { id: 4, crop: 'Cox’s orange pippin, Apple', quantity: 48, revenue: 340.0 }, + { id: 5, crop: 'Macoun, Apples', quantity: 124, revenue: 1234.0 }, + { id: 6, crop: 'Butter Boy Hybrid, Butternut ', quantity: 24, revenue: 785.5 }, + { id: 7, crop: 'King Edward, Potato', quantity: 58, revenue: 237.0 }, + { id: 8, crop: 'Blanco Veneto, Celeriac', quantity: 56, revenue: 895.0 }, + { id: 9, crop: 'Hollow Crown, Parsnips ', quantity: 23, revenue: 354.0 }, + { id: 10, crop: 'Early White Hybrid, Cauliflower', quantity: 87, revenue: 789.5 }, ].slice(0, length); }; @@ -110,7 +112,7 @@ const FooterCell = () => ( ); -export const CropSalesMobileView = { +export const WithCustomFooterCell = { args: { kind: TableKind.V2, columns: getCropSalesColumns(), @@ -121,7 +123,7 @@ export const CropSalesMobileView = { }, }; -export const CropSalesDesktopView = { +export const WithNormalFooter = { args: { kind: TableKind.V2, columns: getCropSalesColumns(false), @@ -142,57 +144,6 @@ export const AlternatingRowColor = { }, }; -const getEmployeesLabourColumns = () => { - return [ - { - id: 'employee', - label: 'Employee', - Footer: 'DAILY TOTAL', - }, - { - id: 'time', - label: 'Time', - format: (d) => `${d.time} h`, - align: 'right', - Footer: 89 h, - }, - { - id: 'labourCost', - label: 'Labour cost', - format: (d) => { - const sign = '$'; - return ( - - {sign} - {Math.abs(d.labourCost).toFixed(2)} - - ); - }, - align: 'right', - Footer: $3732.50, - }, - ]; -}; - -const getEmployeesLabourData = (length) => { - return [ - { employee: 'Sue D.', time: 1.25, labourCost: 0.0 }, - { employee: 'L.F. C.', time: 77.5, labourCost: 3692.5 }, - { employee: 'Joey.', time: 2.75, labourCost: 0.0 }, - { employee: 'Farmie.', time: 7.5, labourCost: 40.0 }, - ].slice(0, length); -}; - -export const EmployeesLabour = { - args: { - kind: TableKind.V2, - columns: getEmployeesLabourColumns(), - data: getEmployeesLabourData(10), - minRows: 10, - onClickMore: () => console.log('Go to labour page'), - }, -}; - const getTasksLabourColumns = () => { return [ { @@ -246,7 +197,7 @@ const getTasksLabourData = (length) => { ].slice(0, length); }; -export const TasksLabour = { +export const WithOnClickMore = { args: { kind: TableKind.V2, columns: getTasksLabourColumns(), @@ -256,7 +207,7 @@ export const TasksLabour = { }, }; -export const TasksLabourWithPagination = { +export const WithPagination = { args: { kind: TableKind.V2, columns: getTasksLabourColumns(), @@ -266,3 +217,60 @@ export const TasksLabourWithPagination = { showPagination: true, }, }; + +export const withCheckboxes = { + args: { + kind: TableKind.V2, + columns: getCropSalesColumns(false), + minRows: 10, + shouldFixTableLayout: true, + handleSelectAllClick: () => console.log('all checked!'), + }, + render: (props) => { + const [selectedIds, setSelectedIds] = useState([]); + const data = getCropSalesData(10); + const onCheck = (e, { id }) => { + setSelectedIds((prevSelectedTypeIds) => { + const isSelected = prevSelectedTypeIds.includes(id); + const newSelectedIds = isSelected + ? selectedIds.filter((selectedId) => id !== selectedId) + : [...prevSelectedTypeIds, id]; + + return newSelectedIds; + }); + }; + const handleSelectAllClick = (e) => { + if (e.target.checked) { + setSelectedIds(data.map(({ id }) => id)); + } else { + setSelectedIds([]); + } + }; + + return ( +
{ + console.log(`Row id ${rowData.id} is clicked!`); + }} + /> + ); + }, +}; + +export const stickyHeader = { + args: { + kind: TableKind.V2, + columns: getCropSalesColumns(false), + data: getCropSalesData(10), + minRows: 10, + shouldFixTableLayout: true, + handleSelectAllClick: () => console.log('all checked!'), + stickyHeader: true, + maxHeight: 200, + }, +};