diff --git a/frontend/package.json b/frontend/package.json index aeb675ec89..98e885612c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,6 +55,8 @@ "@types/node": "17.0.18", "@types/react": "17.0.44", "@types/react-dom": "17.0.16", + "@types/react-router-dom": "5.3.3", + "@types/react-table": "^7.7.11", "@types/react-test-renderer": "17.0.2", "@types/react-timeago": "4.1.3", "@types/semver": "^7.3.9", @@ -82,6 +84,7 @@ "react-hooks-global-state": "1.0.2", "react-router-dom": "6.3.0", "react-scripts": "5.0.1", + "react-table": "^7.7.0", "react-test-renderer": "17.0.2", "react-timeago": "6.2.1", "sass": "1.51.0", diff --git a/frontend/src/component/admin/users/UsersAdmin.tsx b/frontend/src/component/admin/users/UsersAdmin.tsx index f0b12a0908..44fe37b09a 100644 --- a/frontend/src/component/admin/users/UsersAdmin.tsx +++ b/frontend/src/component/admin/users/UsersAdmin.tsx @@ -32,7 +32,7 @@ const UsersAdmin = () => { show={
setSearch(search) } diff --git a/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.styles.ts b/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.styles.ts index df1a3ca9d3..9dd4166c03 100644 --- a/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.styles.ts +++ b/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.styles.ts @@ -1,19 +1,18 @@ import { makeStyles } from 'tss-react/mui'; -import { unleashGrey } from 'themes/themeColors'; export const useStyles = makeStyles()(theme => ({ tableRow: { '& > td': { padding: '4px 16px', - borderColor: unleashGrey[300], + borderColor: theme.palette.grey[300], }, '&:hover': { - backgroundColor: unleashGrey[100], + backgroundColor: theme.palette.grey[100], }, }, tableCellHeader: { '& > th': { - backgroundColor: unleashGrey[200], + backgroundColor: theme.palette.grey[200], fontWeight: 'normal', border: 0, '&:first-of-type': { diff --git a/frontend/src/component/archive/ArchiveListContainer.tsx b/frontend/src/component/archive/ArchiveListContainer.tsx index 139021b564..fa0dd84c9b 100644 --- a/frontend/src/component/archive/ArchiveListContainer.tsx +++ b/frontend/src/component/archive/ArchiveListContainer.tsx @@ -1,5 +1,5 @@ import { useFeaturesArchive } from 'hooks/api/getters/useFeaturesArchive/useFeaturesArchive'; -import { FeatureToggleList } from '../feature/FeatureToggleList/FeatureToggleList'; +import { FeatureToggleList } from '../feature/FeatureToggleList/FeatureToggleArchiveList'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { useFeaturesFilter } from 'hooks/useFeaturesFilter'; import { useFeatureArchiveApi } from 'hooks/api/actions/useFeatureArchiveApi/useReviveFeatureApi'; diff --git a/frontend/src/component/archive/ProjectFeaturesArchiveList.tsx b/frontend/src/component/archive/ProjectFeaturesArchiveList.tsx index b4e43181df..e39fa883bf 100644 --- a/frontend/src/component/archive/ProjectFeaturesArchiveList.tsx +++ b/frontend/src/component/archive/ProjectFeaturesArchiveList.tsx @@ -1,6 +1,6 @@ import { FC } from 'react'; import { useProjectFeaturesArchive } from 'hooks/api/getters/useProjectFeaturesArchive/useProjectFeaturesArchive'; -import { FeatureToggleList } from '../feature/FeatureToggleList/FeatureToggleList'; +import { FeatureToggleList } from '../feature/FeatureToggleList/FeatureToggleArchiveList'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { useFeaturesFilter } from 'hooks/useFeaturesFilter'; import { useFeatureArchiveApi } from 'hooks/api/actions/useFeatureArchiveApi/useReviveFeatureApi'; diff --git a/frontend/src/component/common/Highlighter/Highlighter.tsx b/frontend/src/component/common/Highlighter/Highlighter.tsx index 52984475f7..f27046de2e 100644 --- a/frontend/src/component/common/Highlighter/Highlighter.tsx +++ b/frontend/src/component/common/Highlighter/Highlighter.tsx @@ -1,16 +1,26 @@ +import { VFC } from 'react'; + interface IHighlighterProps { - search: string; - children: string; + search?: string; + children?: string; caseSensitive?: boolean; } const escapeRegex = (str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -export const Highlighter = ({ +export const Highlighter: VFC = ({ search, children, caseSensitive, -}: IHighlighterProps) => { +}) => { + if (!children) { + return null; + } + + if (!search) { + return <>{children}; + } + const regex = new RegExp(escapeRegex(search), caseSensitive ? 'g' : 'gi'); return ( diff --git a/frontend/src/component/common/ListPlaceholder/ListPlaceholder.tsx b/frontend/src/component/common/ListPlaceholder/ListPlaceholder.tsx deleted file mode 100644 index 4a8ab62063..0000000000 --- a/frontend/src/component/common/ListPlaceholder/ListPlaceholder.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { ListItem } from '@mui/material'; -import { Link } from 'react-router-dom'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { useStyles } from 'component/common/ListPlaceholder/ListPlaceholder.styles'; - -interface IListPlaceholderProps { - text: string; - link?: string; - linkText?: string; -} - -const ListPlaceholder = ({ text, link, linkText }: IListPlaceholderProps) => { - const { classes: styles } = useStyles(); - - return ( - - {text} - Add your first toggle} - /> - - ); -}; - -export default ListPlaceholder; diff --git a/frontend/src/component/common/PaginateUI/PaginateUI.tsx b/frontend/src/component/common/PaginateUI/PaginateUI.tsx index 204006f2f5..26718b0ca1 100644 --- a/frontend/src/component/common/PaginateUI/PaginateUI.tsx +++ b/frontend/src/component/common/PaginateUI/PaginateUI.tsx @@ -18,6 +18,9 @@ interface IPaginateUIProps { style?: React.CSSProperties; } +/** + * @deprecated + */ const PaginateUI = ({ pages, pageIndex, diff --git a/frontend/src/component/common/Table/SearchHighlightContext/SearchHighlightContext.tsx b/frontend/src/component/common/Table/SearchHighlightContext/SearchHighlightContext.tsx new file mode 100644 index 0000000000..abd83f1866 --- /dev/null +++ b/frontend/src/component/common/Table/SearchHighlightContext/SearchHighlightContext.tsx @@ -0,0 +1,8 @@ +import { createContext, useContext } from 'react'; + +const SearchHighlightContext = createContext(''); + +export const SearchHighlightProvider = SearchHighlightContext.Provider; + +export const useSearchHighlightContext = () => + useContext(SearchHighlightContext); diff --git a/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.styles.ts b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.styles.ts new file mode 100644 index 0000000000..b9c1085855 --- /dev/null +++ b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.styles.ts @@ -0,0 +1,27 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + tableCellHeaderSortable: { + padding: 0, + position: 'relative', + }, + sortButton: { + all: 'unset', + padding: theme.spacing(2), + fontWeight: theme.fontWeight.medium, + width: '100%', + '&:focus-visible, &:active': { + outline: 'revert', + }, + display: 'flex', + alignItems: 'center', + '&:hover': { + backgroundColor: theme.palette.grey[400], + }, + boxSizing: 'inherit', + cursor: 'pointer', + }, + sorted: { + fontWeight: theme.fontWeight.bold, + }, +})); diff --git a/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.tsx b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.tsx new file mode 100644 index 0000000000..cbffe16e63 --- /dev/null +++ b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/CellSortable.tsx @@ -0,0 +1,67 @@ +import React, { FC, MouseEventHandler, useContext } from 'react'; +import { TableCell } from '@mui/material'; +import classnames from 'classnames'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useStyles } from './CellSortable.styles'; +import { AnnouncerContext } from 'component/common/Announcer/AnnouncerContext/AnnouncerContext'; +import { SortArrow } from './SortArrow/SortArrow'; + +interface ICellSortableProps { + isSortable?: boolean; + isSorted?: boolean; + isDescending?: boolean; + ariaTitle?: string; + onClick?: MouseEventHandler; +} + +export const CellSortable: FC = ({ + children, + isSortable = true, + isSorted = false, + isDescending, + ariaTitle, + onClick = () => {}, +}) => { + const { setAnnouncement } = useContext(AnnouncerContext); + const { classes: styles } = useStyles(); + + const ariaSort = isSorted + ? isDescending + ? 'descending' + : 'ascending' + : undefined; + + const onSortClick: MouseEventHandler = event => { + onClick(event); + setAnnouncement( + `Sorted${ariaTitle ? ` by ${ariaTitle} ` : ''}, ${ + isDescending ? 'ascending' : 'descending' + }` + ); + }; + + return ( + + + {children} + + + } + elseShow={children} + /> + + ); +}; diff --git a/frontend/src/component/common/Table/SortableTableHeader/CellSortable/SortArrow/SortArrow.styles.ts b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/SortArrow/SortArrow.styles.ts new file mode 100644 index 0000000000..ebb0a4d279 --- /dev/null +++ b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/SortArrow/SortArrow.styles.ts @@ -0,0 +1,13 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + icon: { + marginLeft: theme.spacing(0.5), + color: theme.palette.grey[700], + fontSize: theme.fontSizes.mainHeader, + verticalAlign: 'middle', + }, + sorted: { + color: theme.palette.grey[900], + }, +})); diff --git a/frontend/src/component/common/Table/SortableTableHeader/CellSortable/SortArrow/SortArrow.tsx b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/SortArrow/SortArrow.tsx new file mode 100644 index 0000000000..f8c8220e4a --- /dev/null +++ b/frontend/src/component/common/Table/SortableTableHeader/CellSortable/SortArrow/SortArrow.tsx @@ -0,0 +1,50 @@ +import { VFC } from 'react'; +import { + KeyboardArrowDown, + KeyboardArrowUp, + UnfoldMoreOutlined, +} from '@mui/icons-material'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useStyles } from './SortArrow.styles'; +import classnames from 'classnames'; + +interface ISortArrowProps { + isSorted?: boolean; + isDesc?: boolean; +} + +export const SortArrow: VFC = ({ + isSorted: sorted, + isDesc: desc = false, +}) => { + const { classes: styles } = useStyles(); + + return ( + + } + elseShow={ + + } + /> + } + elseShow={ + + } + /> + ); +}; diff --git a/frontend/src/component/common/Table/SortableTableHeader/SortableTableHeader.styles.ts b/frontend/src/component/common/Table/SortableTableHeader/SortableTableHeader.styles.ts new file mode 100644 index 0000000000..91fc94ab7f --- /dev/null +++ b/frontend/src/component/common/Table/SortableTableHeader/SortableTableHeader.styles.ts @@ -0,0 +1,21 @@ +import { makeStyles } from 'tss-react/mui'; + +export const useStyles = makeStyles()(theme => ({ + tableHeader: { + '& > th': { + border: 0, + '&:first-of-type': { + borderTopLeftRadius: '8px', + borderBottomLeftRadius: '8px', + }, + '&:last-of-type': { + borderTopRightRadius: '8px', + borderBottomRightRadius: '8px', + }, + }, + }, + icon: { + marginLeft: theme.spacing(0.5), + fontSize: 18, + }, +})); diff --git a/frontend/src/component/common/Table/SortableTableHeader/SortableTableHeader.tsx b/frontend/src/component/common/Table/SortableTableHeader/SortableTableHeader.tsx new file mode 100644 index 0000000000..8da89fd53c --- /dev/null +++ b/frontend/src/component/common/Table/SortableTableHeader/SortableTableHeader.tsx @@ -0,0 +1,47 @@ +import { VFC } from 'react'; +import { TableHead, TableRow } from '@mui/material'; +import { HeaderGroup } from 'react-table'; +import { useStyles } from './SortableTableHeader.styles'; +import { CellSortable } from './CellSortable/CellSortable'; + +interface ISortableTableHeaderProps { + headerGroups: HeaderGroup[]; +} + +export const SortableTableHeader: VFC = ({ + headerGroups, +}) => { + const { classes: styles } = useStyles(); + return ( + + {headerGroups.map(headerGroup => ( + + {headerGroup.headers.map(column => { + const content = column.render('Header'); + + return ( + + {content} + + ); + })} + + ))} + + ); +}; diff --git a/frontend/src/component/common/Table/TableActions/TableActions.styles.ts b/frontend/src/component/common/Table/TableActions/TableActions.styles.ts index 3952cff0f8..65133e3555 100644 --- a/frontend/src/component/common/Table/TableActions/TableActions.styles.ts +++ b/frontend/src/component/common/Table/TableActions/TableActions.styles.ts @@ -1,9 +1,18 @@ import { makeStyles } from 'tss-react/mui'; -import { unleashGrey } from 'themes/themeColors'; export const useStyles = makeStyles()(theme => ({ + tableActions: { + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + '&>button': { + padding: theme.spacing(1), + flexShrink: 0, + }, + paddingRight: theme.spacing(1), + }, fieldWidth: { - width: '49px', + width: '45px', '& .search-icon': { marginRight: 0, }, @@ -19,7 +28,7 @@ export const useStyles = makeStyles()(theme => ({ }, }, fieldWidthEnter: { - width: '100%', + width: '250px', transition: 'width 0.6s', '& .search-icon': { marginRight: '8px', @@ -37,7 +46,7 @@ export const useStyles = makeStyles()(theme => ({ }, }, fieldWidthLeave: { - width: '49px', + width: '45px', transition: 'width 0.6s', '& .search-icon': { marginRight: 0, @@ -53,11 +62,11 @@ export const useStyles = makeStyles()(theme => ({ }, verticalSeparator: { height: '100%', - backgroundColor: unleashGrey[500], + backgroundColor: theme.palette.grey[500], width: '1px', display: 'inline-block', - marginLeft: '16px', - marginRight: '32px', + marginLeft: theme.spacing(2), + marginRight: theme.spacing(4), padding: '10px 0', verticalAlign: 'middle', }, diff --git a/frontend/src/component/common/Table/TableActions/TableActions.tsx b/frontend/src/component/common/Table/TableActions/TableActions.tsx index 05994fe36c..9cf8ea444a 100644 --- a/frontend/src/component/common/Table/TableActions/TableActions.tsx +++ b/frontend/src/component/common/Table/TableActions/TableActions.tsx @@ -1,60 +1,87 @@ -import { useState } from 'react'; +import { FC, useState } from 'react'; import { IconButton, Tooltip } from '@mui/material'; import { Search } from '@mui/icons-material'; +import { useAsyncDebounce } from 'react-table'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import AnimateOnMount from 'component/common/AnimateOnMount/AnimateOnMount'; -import { TableSearchField } from 'component/common/Table/TableActions/TableSearchField/TableSearchField'; -import { useStyles } from 'component/common/Table/TableActions/TableActions.styles'; +import { TableSearchField } from './TableSearchField/TableSearchField'; +import { useStyles } from './TableActions.styles'; interface ITableActionsProps { - search: string; - onSearch: (value: string) => void; + initialSearchValue?: string; + onSearch?: (value: string) => void; + searchTip?: string; + isSeparated?: boolean; } -export const TableActions = ({ search, onSearch }: ITableActionsProps) => { - const [searchExpanded, setSearchExpanded] = useState(false); +export const TableActions: FC = ({ + initialSearchValue: search, + onSearch = () => {}, + searchTip = 'Search', + children, + isSeparated, +}) => { + const [searchExpanded, setSearchExpanded] = useState(Boolean(search)); + const [searchInputState, setSearchInputState] = useState(search); const [animating, setAnimating] = useState(false); + const debouncedOnSearch = useAsyncDebounce(onSearch, 200); const { classes: styles } = useStyles(); const onBlur = (clear = false) => { - if (!search || clear) { + if (!searchInputState || clear) { setSearchExpanded(false); } }; + const onSearchChange = (value: string) => { + debouncedOnSearch(value); + setSearchInputState(value); + }; + return ( - <> - setAnimating(true)} - onEnd={() => setAnimating(false)} - > - - +
- setSearchExpanded(true)} - size="large" + <> + setAnimating(true)} + onEnd={() => setAnimating(false)} > - - - + + + + setSearchExpanded(true)} + size="large" + > + + + + } + /> + } /> -
- + } + /> + {children} +
); }; diff --git a/frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.tsx b/frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.tsx index 3ad5ba308c..b6412d9b41 100644 --- a/frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.tsx +++ b/frontend/src/component/common/Table/TableActions/TableSearchField/TableSearchField.tsx @@ -1,11 +1,11 @@ import { IconButton, InputBase, Tooltip } from '@mui/material'; import { Search, Close } from '@mui/icons-material'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { useStyles } from 'component/common/Table/TableActions/TableSearchField/TableSearchField.styles'; import classnames from 'classnames'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { useStyles } from './TableSearchField.styles'; interface ITableSearchFieldProps { - value: string; + value?: string; onChange: (value: string) => void; className?: string; placeholder?: string; @@ -13,7 +13,7 @@ interface ITableSearchFieldProps { } export const TableSearchField = ({ - value, + value = '', onChange, className, placeholder, diff --git a/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.styles.ts b/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.styles.ts index 789bd48e69..4971b723b0 100644 --- a/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.styles.ts +++ b/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.styles.ts @@ -1,5 +1,4 @@ import { makeStyles } from 'tss-react/mui'; -import { unleashGrey } from 'themes/themeColors'; export const useStyles = makeStyles()(theme => ({ tableCellHeaderSortable: { @@ -9,13 +8,13 @@ export const useStyles = makeStyles()(theme => ({ '& > svg': { fontSize: 18, verticalAlign: 'middle', - color: unleashGrey[700], + color: theme.palette.grey[700], marginLeft: '4px', }, '&.sorted': { fontWeight: 'bold', '& > svg': { - color: unleashGrey[900], + color: theme.palette.grey[900], }, }, }, @@ -29,9 +28,9 @@ export const useStyles = makeStyles()(theme => ({ display: 'flex', alignItems: 'center', '&:hover': { - backgroundColor: unleashGrey[400], + backgroundColor: theme.palette.grey[400], '& > svg': { - color: unleashGrey[900], + color: theme.palette.grey[900], }, }, }, diff --git a/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.tsx b/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.tsx index 8a9c7bb4ee..4cc3f1cd58 100644 --- a/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.tsx +++ b/frontend/src/component/common/Table/TableCellSortable/TableCellSortable.tsx @@ -23,6 +23,9 @@ interface ITableCellSortableProps { children: ReactNode; } +/** + * @deprecated No longer in use. See `SortableTableHeader`. Remove when Users table is refactored. + */ export const TableCellSortable = ({ className, name, @@ -40,12 +43,6 @@ export const TableCellSortable = ({ : 'ascending' : undefined; - const cellClassName = classnames( - className, - styles.tableCellHeaderSortable, - sort.type === name && 'sorted' - ); - const onSortClick = () => { setSort(prev => ({ desc: !Boolean(prev.desc), @@ -57,7 +54,14 @@ export const TableCellSortable = ({ }; return ( - +