diff --git a/eslint.config.js b/eslint.config.js index 092408a..1adb816 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -25,4 +25,10 @@ export default tseslint.config( ], }, }, + { + files: ["**/*.stories.tsx"], + rules: { + "react-hooks/rules-of-hooks": "off" + } + } ) diff --git a/package-lock.json b/package-lock.json index 81a8346..612d68f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@weng-lab/psychscreen-ui-components", - "version": "2.0.4", + "version": "2.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@weng-lab/psychscreen-ui-components", - "version": "2.0.4", + "version": "2.0.5", "license": "MIT", "dependencies": { "cytoscape": "^3.30.2", diff --git a/package.json b/package.json index 14507f3..3e90cf2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@weng-lab/psychscreen-ui-components", "description": "Typescript and Material UI based components used for psychSCREEN", "author": "SCREEN Team @ UMass Chan Medical School", - "version": "2.0.5", + "version": "2.0.6", "license": "MIT", "type": "module", "typings": "dist/index.d.ts", @@ -75,5 +75,6 @@ "extends": [ "plugin:storybook/recommended" ] - } + }, + "packageManager": "yarn@3.5.0+sha512.2dc70be5fce9f66756d25b00a888f3ca66f86b502b76750e72ba54cec89da767b938c54124595e26f868825688e0fe3552c26c76a330673343057acadd5cfcf2" } diff --git a/src/components/DataTable/DataTable.stories.tsx b/src/components/DataTable/DataTable.stories.tsx index 6c614d2..3acd1cd 100644 --- a/src/components/DataTable/DataTable.stories.tsx +++ b/src/components/DataTable/DataTable.stories.tsx @@ -227,11 +227,10 @@ export const FunctionalComponentColumn: Story = { args: { rows: ROWS, columns: FCCOLUMNS, - itemsPerPage: 4, tableTitle: 'Table Title', searchable: true, showMoreColumns: true, - noOfDefaultColumns: 3, + noOfDefaultColumns: 5, } } @@ -295,6 +294,23 @@ export const HeaderColored: Story = { } } +export const ItemsPerPage: Story = { + args: { + rows: ROWS, + columns: COLUMNS, + tableTitle: "Table Title" + }, + render: (args) => + + Default + + itemsPerPage = 5 + + itemsPerPage = [3,5,10] + + +} + export const ConstrainSize: Story = { args: { rows: ROWS, @@ -432,7 +448,7 @@ export const LotsOfCols: Story = { } } -const headeRenderCOLUMNS = (func: any) => { +const headeRenderCOLUMNS = (setX: React.Dispatch>) => { return [ { header: 'Index', @@ -488,7 +504,7 @@ const headeRenderCOLUMNS = (func: any) => { switch (value) { case 0: setDistanceChecked(event.target.checked); - func(event.target.checked); + setX(event.target.checked); break; case 1: setCTCF_ChIAPETChecked(event.target.checked); @@ -570,7 +586,7 @@ export const HeaderRender: Story = { searchable: true }, render: (args) => { - const [x, setX] = React.useState(null); + const [x, setX] = React.useState(null); useEffect(() => console.log(x)); return ( ({ //Styling for "Add Columns" Modal const boxStyle = { - position: 'absolute' as 'absolute', + position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', @@ -104,10 +104,12 @@ const boxStyle = { const DataTable = ( props: DataTableProps ) => { - // Sets default rows to display at 5 if unspecified - const itemsPerPage = props.itemsPerPage || 5; const [page, setPage] = useState(props.page || 0); - const [rowsPerPage, setRowsPerPage] = useState(itemsPerPage); + const [rowsPerPage, setRowsPerPage] = useState(() => { + if (Array.isArray(props.itemsPerPage)) { return props.itemsPerPage[0] } + else if (typeof props.itemsPerPage == "number") { return props.itemsPerPage } + else return 5 + }) const handleChangePage = (page: number) => { setPage(page); @@ -120,8 +122,8 @@ const DataTable = ( setPage(0); }; - function handleEmptyTable(noColumns: number): React.JSX.Element[] { - let cells = []; + const handleSpawnEmptyCells = (noColumns: number): React.JSX.Element[] => { + const cells = []; for (let i = 1; i < noColumns; i++) { cells.push(); } @@ -129,7 +131,7 @@ const DataTable = ( } function highlightCheck(row: T): boolean { - var found = false; + let found = false; if (Array.isArray(props.highlighted)) { props.highlighted.forEach((highlight) => { if (JSON.stringify(row) === JSON.stringify(highlight)) { @@ -169,7 +171,7 @@ const DataTable = ( }); const search = useCallback( - (row: any, value: string): boolean => { + (row: T, value: string): boolean => { /* look for any matching searchable column */ for (const i in state.columns) { /* get column; look for a user-defined search function first */ @@ -207,7 +209,7 @@ const DataTable = ( ); const displayRows = useCallback( - (sortedRows: any[], filterValue: string): any[] => + (sortedRows: T[], filterValue: string): T[] => filterValue === '' ? [...sortedRows] : sortedRows.filter((row) => search(row, filterValue)), @@ -216,21 +218,24 @@ const DataTable = ( const displayedRows = useMemo( () => sort(displayRows(props.rows, state.filter || props.search || '')), - [displayRows, sort, state.filter, props.rows, state.sort, props.search] + [displayRows, sort, state.filter, props.rows, props.search] ); const rowsOnCurrentPage = useMemo(() => { const newRowsOnPage = displayedRows.slice(page * rowsPerPage, (page + 1) * rowsPerPage); props.onDisplayedRowsChange?.(page, newRowsOnPage) return newRowsOnPage - }, [displayedRows, page, rowsPerPage]) + }, [displayedRows, page, rowsPerPage, props.onDisplayedRowsChange]) + + // Avoid a layout jump when reaching the last page with empty rows. + const emptyRows = page > 0 ? Math.max(0, (1 + page) * rowsPerPage - props.rows.length) : 0; const download = useCallback(() => { const data = state.columns.map((col) => col.header).join('\t') + '\n' + displayedRows - .map((row: any) => + .map((row: T) => state.columns.map((col) => col.value(row)).join('\t') ) .join('\n') + @@ -245,7 +250,8 @@ const DataTable = ( a.click(); window.URL.revokeObjectURL(url); a.remove(); - }, [state.columns, displayedRows]); + }, [state.columns, displayedRows, props.downloadFileName]); + //Refs used in tracking overflow const containerRef = useRef(null); @@ -279,14 +285,14 @@ const DataTable = ( monitorOverflow(containerRef, arrowRightRef, arrowLeftRef) ); - new ResizeObserver((entries) => { - for (const _ of entries) { - monitorOverflow(containerRef, arrowRightRef, arrowLeftRef); - } + new ResizeObserver(() => { + monitorOverflow(containerRef, arrowRightRef, arrowLeftRef); }).observe(containerRef.current); } }, [containerRef, arrowLeftRef, arrowRightRef]); + console.log(document.getElementById('row0')?.offsetHeight) + return ( (( sx={i !== state.columns.length - 1 ? { pr: 0 } : {}} key={`${column.header}${i}`} onClick={() => { - !column.unsortable && + if (!column.unsortable) { dispatch({ type: 'sortChanged', sortColumn: i }); + } setPage(0); }} > @@ -425,16 +432,18 @@ const DataTable = ( {props.emptyText || 'No data available.'} {/* Render needed number of empty cells to fill row */} - {handleEmptyTable(props.columns.length)} + {handleSpawnEmptyCells(props.columns.length)} ) : ( - rowsOnCurrentPage + <> + {rowsOnCurrentPage .map((row, i) => ( props.onRowClick && props.onRowClick(row, i + page * rowsPerPage) @@ -463,7 +472,7 @@ const DataTable = ( } > {column.FunctionalRender ? ( - + ) : column.render ? ( column.render(row) ) : ( @@ -473,7 +482,17 @@ const DataTable = ( ); })} - )) + ))} + {emptyRows > 0 && ( + + + + )} + )} @@ -521,7 +540,7 @@ const DataTable = ( `Showing ${displayedRows.length} matching rows of ${props.rows.length} total.`} ( sx={ props.dense ? { - '& .MuiTablePagination-toolbar': { pl: '6px' }, - '& .css-h0cf5v-MuiInputBase-root-MuiTablePagination-select': - { mr: '6px', ml: '0px' }, - '& .MuiTablePagination-actions': { ml: '4px !important' }, - } + '& .MuiTablePagination-toolbar': { pl: '6px' }, + '& .css-h0cf5v-MuiInputBase-root-MuiTablePagination-select': + { mr: '6px', ml: '0px' }, + '& .MuiTablePagination-actions': { ml: '4px !important' }, + } : undefined } /> diff --git a/src/components/DataTable/types.ts b/src/components/DataTable/types.ts index ad8ea5a..261d8b9 100644 --- a/src/components/DataTable/types.ts +++ b/src/components/DataTable/types.ts @@ -3,12 +3,12 @@ import React from "react" export type DataTableColumn = { tooltip?: string header: string - HeaderRender?: React.FC + HeaderRender?: React.FC value: (row: T) => string | number search?: (row: T) => boolean unsearchable?: boolean render?: (row: T) => string | JSX.Element - FunctionalRender?: React.FC + FunctionalRender?: (props: { row: T }) => JSX.Element; sort?: (a: T, b: T) => number unsortable?: boolean } @@ -20,7 +20,18 @@ type HEX = `#${string}` | `# ${string}`; export type DataTableProps = { columns: DataTableColumn[] rows: T[] - itemsPerPage?: number + + /** + * Sets the number of items on each page. + * If one number specified, the rows per page selection is hidden. + * Specify an array to provide user-selectable options + * + * @default + * [5, 10, 25, 100] + * + */ + itemsPerPage?: number | number[] + hidePageMenu?: boolean tableTitle?: string selectable?: boolean