mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02:00
Feat/frontend changeset list (#2264)
* ChangesetTable initial * ChangesetTable bug fixes * Added tabs * Add Applied and Cancelled badges * fix alignment * cleanup * cleanup * cleanup * cleanup * cleanup * cleanup * cleanup * replace updatedAt with createdAt * bug fix * bug fix
This commit is contained in:
parent
d2324ee91f
commit
d48cfc8585
@ -1,23 +1,25 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Box } from '@mui/material';
|
import { Box, SxProps, Theme } from '@mui/material';
|
||||||
import { useStyles } from './TextCell.styles';
|
import { useStyles } from './TextCell.styles';
|
||||||
|
|
||||||
interface ITextCellProps {
|
interface ITextCellProps {
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
lineClamp?: number;
|
lineClamp?: number;
|
||||||
'data-testid'?: string;
|
'data-testid'?: string;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextCell: FC<ITextCellProps> = ({
|
export const TextCell: FC<ITextCellProps> = ({
|
||||||
value,
|
value,
|
||||||
children,
|
children,
|
||||||
lineClamp,
|
lineClamp,
|
||||||
|
sx,
|
||||||
'data-testid': testid,
|
'data-testid': testid,
|
||||||
}) => {
|
}) => {
|
||||||
const { classes } = useStyles({ lineClamp });
|
const { classes } = useStyles({ lineClamp });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.wrapper}>
|
<Box className={classes.wrapper} sx={sx}>
|
||||||
<span data-loading="true" data-testid={testid}>
|
<span data-loading="true" data-testid={testid}>
|
||||||
{children ?? value}
|
{children ?? value}
|
||||||
</span>
|
</span>
|
||||||
|
@ -27,6 +27,7 @@ import { ProjectLog } from './ProjectLog/ProjectLog';
|
|||||||
import { SuggestedChangeOverview } from 'component/suggestChanges/SuggestedChangeOverview/SuggestedChangeOverview';
|
import { SuggestedChangeOverview } from 'component/suggestChanges/SuggestedChangeOverview/SuggestedChangeOverview';
|
||||||
import { DraftBanner } from 'component/suggestChanges/DraftBanner/DraftBanner';
|
import { DraftBanner } from 'component/suggestChanges/DraftBanner/DraftBanner';
|
||||||
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
|
import { MainLayout } from 'component/layout/MainLayout/MainLayout';
|
||||||
|
import { ProjectSuggestedChanges } from '../../suggest-changes/ProjectSuggestions/ProjectSuggestedChanges';
|
||||||
|
|
||||||
const StyledDiv = styled('div')(() => ({
|
const StyledDiv = styled('div')(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -88,6 +89,11 @@ const Project = () => {
|
|||||||
path: `${basePath}/archive`,
|
path: `${basePath}/archive`,
|
||||||
name: 'archive',
|
name: 'archive',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Change requests',
|
||||||
|
path: `${basePath}/suggest-changes`,
|
||||||
|
name: 'suggest-changes' + '',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Event log',
|
title: 'Event log',
|
||||||
path: `${basePath}/logs`,
|
path: `${basePath}/logs`,
|
||||||
@ -228,6 +234,15 @@ const Project = () => {
|
|||||||
<Route path="environments" element={<ProjectEnvironment />} />
|
<Route path="environments" element={<ProjectEnvironment />} />
|
||||||
<Route path="archive" element={<ProjectFeaturesArchive />} />
|
<Route path="archive" element={<ProjectFeaturesArchive />} />
|
||||||
<Route path="logs" element={<ProjectLog />} />
|
<Route path="logs" element={<ProjectLog />} />
|
||||||
|
<Route
|
||||||
|
path="suggest-changes"
|
||||||
|
element={
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={Boolean(uiConfig?.flags?.suggestChanges)}
|
||||||
|
show={<ProjectSuggestedChanges />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="suggest-changes/:id"
|
path="suggest-changes/:id"
|
||||||
element={
|
element={
|
||||||
@ -237,7 +252,6 @@ const Project = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="*" element={<ProjectOverview />} />
|
<Route path="*" element={<ProjectOverview />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
import { usePageTitle } from 'hooks/usePageTitle';
|
||||||
|
import { createLocalStorage } from 'utils/createLocalStorage';
|
||||||
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
|
import { useProjectNameOrId } from 'hooks/api/getters/useProject/useProject';
|
||||||
|
import { SuggestionsTabs } from './SuggestionsTabs/SuggestionsTabs';
|
||||||
|
import { SortingRule } from 'react-table';
|
||||||
|
import { useProjectSuggestedChanges } from 'hooks/api/getters/useProjectSuggestedChanges/useProjectSuggestedChanges';
|
||||||
|
|
||||||
|
const defaultSort: SortingRule<string> = { id: 'updatedAt', desc: true };
|
||||||
|
|
||||||
|
export const ProjectSuggestedChanges = () => {
|
||||||
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
const projectName = useProjectNameOrId(projectId);
|
||||||
|
|
||||||
|
usePageTitle(`Change requests – ${projectName}`);
|
||||||
|
|
||||||
|
const { changesets, loading } = useProjectSuggestedChanges(projectId);
|
||||||
|
|
||||||
|
const { value, setValue } = createLocalStorage(
|
||||||
|
`${projectId}:ProjectSuggestedChanges`,
|
||||||
|
defaultSort
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SuggestionsTabs
|
||||||
|
changesets={changesets}
|
||||||
|
storedParams={value}
|
||||||
|
setStoredParams={setValue}
|
||||||
|
projectId={projectId}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,13 @@
|
|||||||
|
import { UserAvatar } from '../../../../common/UserAvatar/UserAvatar';
|
||||||
|
import { TextCell } from '../../../../common/Table/cells/TextCell/TextCell';
|
||||||
|
|
||||||
|
export const AvatarCell = ({ value }: any) => {
|
||||||
|
return (
|
||||||
|
<TextCell>
|
||||||
|
<UserAvatar
|
||||||
|
user={value}
|
||||||
|
sx={{ maxWidth: '30px', maxHeight: '30px', alignSelf: 'left' }}
|
||||||
|
/>
|
||||||
|
</TextCell>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,12 @@
|
|||||||
|
import { ArrowRight } from '@mui/icons-material';
|
||||||
|
import { useTheme } from '@mui/system';
|
||||||
|
import { TextCell } from '../../../../common/Table/cells/TextCell/TextCell';
|
||||||
|
|
||||||
|
export const ChangesetActionCell = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<TextCell sx={{ textAlign: 'right' }}>
|
||||||
|
<ArrowRight sx={{ color: theme.palette.secondary.main }} />{' '}
|
||||||
|
</TextCell>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,111 @@
|
|||||||
|
import { Chip, styled } from '@mui/material';
|
||||||
|
import { colors } from '../../../../../themes/colors';
|
||||||
|
import { TextCell } from '../../../../common/Table/cells/TextCell/TextCell';
|
||||||
|
import { Check, CircleOutlined, Close } from '@mui/icons-material';
|
||||||
|
|
||||||
|
interface IChangesetStatusCellProps {
|
||||||
|
value?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SuggestChangesetState {
|
||||||
|
DRAFT = 'Draft',
|
||||||
|
APPROVED = 'Approved',
|
||||||
|
IN_REVIEW = 'In review',
|
||||||
|
APPLIED = 'Applied',
|
||||||
|
CANCELLED = 'Cancelled',
|
||||||
|
REJECTED = 'Rejected',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StyledChip = styled(Chip)(({ theme, icon }) => ({
|
||||||
|
padding: theme.spacing(0, 1),
|
||||||
|
height: 30,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
fontWeight: theme.typography.fontWeightMedium,
|
||||||
|
gap: theme.spacing(1, 1),
|
||||||
|
['& .MuiChip-label']: {
|
||||||
|
padding: 0,
|
||||||
|
paddingLeft: Boolean(icon) ? theme.spacing(0.5) : 0,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledRejectedChip = styled(StyledChip)(({ theme }) => ({
|
||||||
|
border: `1px solid ${theme.palette.error.main}`,
|
||||||
|
backgroundColor: colors.red['100'],
|
||||||
|
['& .MuiChip-label']: {
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
},
|
||||||
|
['& .MuiChip-icon']: {
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledApprovedChip = styled(StyledChip)(({ theme }) => ({
|
||||||
|
border: `1px solid ${theme.palette.success.main}`,
|
||||||
|
backgroundColor: colors.green['100'],
|
||||||
|
['& .MuiChip-label']: {
|
||||||
|
color: theme.palette.success.main,
|
||||||
|
},
|
||||||
|
['& .MuiChip-icon']: {
|
||||||
|
color: theme.palette.success.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledReviewChip = styled(StyledChip)(({ theme }) => ({
|
||||||
|
border: `1px solid ${theme.palette.primary.main}`,
|
||||||
|
backgroundColor: colors.purple['100'],
|
||||||
|
['& .MuiChip-label']: {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
['& .MuiChip-icon']: {
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
export const ChangesetStatusCell = ({ value }: IChangesetStatusCellProps) => {
|
||||||
|
const renderState = (state: string) => {
|
||||||
|
switch (state) {
|
||||||
|
case SuggestChangesetState.IN_REVIEW:
|
||||||
|
return (
|
||||||
|
<StyledReviewChip
|
||||||
|
label={'Review required'}
|
||||||
|
icon={<CircleOutlined fontSize={'small'} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case SuggestChangesetState.APPROVED:
|
||||||
|
return (
|
||||||
|
<StyledApprovedChip
|
||||||
|
label={'Approved'}
|
||||||
|
icon={<Check fontSize={'small'} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case SuggestChangesetState.APPLIED:
|
||||||
|
return (
|
||||||
|
<StyledApprovedChip
|
||||||
|
label={'Applied'}
|
||||||
|
icon={<Check fontSize={'small'} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case SuggestChangesetState.CANCELLED:
|
||||||
|
return (
|
||||||
|
<StyledRejectedChip
|
||||||
|
label={'Cancelled'}
|
||||||
|
icon={<Close fontSize={'small'} sx={{ mr: 8 }} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case SuggestChangesetState.REJECTED:
|
||||||
|
return (
|
||||||
|
<StyledRejectedChip
|
||||||
|
label={'Rejected'}
|
||||||
|
icon={<Close fontSize={'small'} sx={{ mr: 8 }} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return <TextCell />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TextCell>{renderState(value)}</TextCell>;
|
||||||
|
};
|
@ -0,0 +1,60 @@
|
|||||||
|
import { TextCell } from '../../../../common/Table/cells/TextCell/TextCell';
|
||||||
|
import { Link, styled, Typography } from '@mui/material';
|
||||||
|
import { Link as RouterLink } from 'react-router-dom';
|
||||||
|
import { useTheme } from '@mui/system';
|
||||||
|
|
||||||
|
interface IChangesetTitleCellProps {
|
||||||
|
value?: any;
|
||||||
|
row: { original: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StyledLink = styled('div')(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
margin: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ChangesetTitleCell = ({
|
||||||
|
value,
|
||||||
|
row: { original },
|
||||||
|
}: IChangesetTitleCellProps) => {
|
||||||
|
const { id, features: changes, project } = original;
|
||||||
|
const theme = useTheme();
|
||||||
|
const path = `projects/${project}/suggest-changes/${id}`;
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return <TextCell />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextCell>
|
||||||
|
<StyledLink>
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
underline={'hover'}
|
||||||
|
to={path}
|
||||||
|
sx={{ pt: 0.2 }}
|
||||||
|
>
|
||||||
|
Suggestion
|
||||||
|
</Link>
|
||||||
|
<Typography
|
||||||
|
component={'span'}
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
sx={{ margin: theme.spacing(0, 1), pt: 0 }}
|
||||||
|
>
|
||||||
|
{`#${id}`}
|
||||||
|
</Typography>
|
||||||
|
</StyledLink>
|
||||||
|
<StyledLink>
|
||||||
|
<Link
|
||||||
|
component={RouterLink}
|
||||||
|
underline={'hover'}
|
||||||
|
to={path}
|
||||||
|
>{`${changes?.length}`}</Link>
|
||||||
|
<span style={{ margin: 'auto 8px' }}>
|
||||||
|
{changes.length < 1 ? `update` : 'updates'}
|
||||||
|
</span>
|
||||||
|
</StyledLink>
|
||||||
|
</TextCell>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
import { makeStyles } from 'tss-react/mui';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles()(theme => ({
|
||||||
|
tabContainer: {
|
||||||
|
paddingLeft: 0,
|
||||||
|
},
|
||||||
|
tabButton: {
|
||||||
|
textTransform: 'none',
|
||||||
|
width: 'auto',
|
||||||
|
fontSize: '1rem',
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
minWidth: 160,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,291 @@
|
|||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import {
|
||||||
|
SortableTableHeader,
|
||||||
|
Table,
|
||||||
|
TableCell,
|
||||||
|
TablePlaceholder,
|
||||||
|
} from 'component/common/Table';
|
||||||
|
import { SortingRule, useSortBy, useTable } from 'react-table';
|
||||||
|
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
|
||||||
|
import { Tab, Tabs, useMediaQuery } from '@mui/material';
|
||||||
|
import { sortTypes } from 'utils/sortTypes';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { Search } from 'component/common/Search/Search';
|
||||||
|
import { featuresPlaceholder } from 'component/feature/FeatureToggleList/FeatureToggleListTable';
|
||||||
|
import theme from 'themes/theme';
|
||||||
|
import { useSearch } from 'hooks/useSearch';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { TimeAgoCell } from '../../../common/Table/cells/TimeAgoCell/TimeAgoCell';
|
||||||
|
import { TextCell } from '../../../common/Table/cells/TextCell/TextCell';
|
||||||
|
import { ChangesetStatusCell } from './ChangesetStatusCell/ChangesetStatusCell';
|
||||||
|
import { ChangesetActionCell } from './ChangesetActionCell/ChangesetActionCell';
|
||||||
|
import { AvatarCell } from './AvatarCell/AvatarCell';
|
||||||
|
import { ChangesetTitleCell } from './ChangesetTitleCell/ChangesetTitleCell';
|
||||||
|
import { TableBody, TableRow } from '../../../common/Table';
|
||||||
|
import { useStyles } from './SuggestionsTabs.styles';
|
||||||
|
|
||||||
|
export interface IChangeSetTableProps {
|
||||||
|
changesets: any[];
|
||||||
|
loading: boolean;
|
||||||
|
storedParams: SortingRule<string>;
|
||||||
|
setStoredParams: (
|
||||||
|
newValue:
|
||||||
|
| SortingRule<string>
|
||||||
|
| ((prev: SortingRule<string>) => SortingRule<string>)
|
||||||
|
) => SortingRule<string>;
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SuggestionsTabs = ({
|
||||||
|
changesets = [],
|
||||||
|
loading,
|
||||||
|
storedParams,
|
||||||
|
setStoredParams,
|
||||||
|
projectId,
|
||||||
|
}: IChangeSetTableProps) => {
|
||||||
|
const { classes } = useStyles();
|
||||||
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const [searchValue, setSearchValue] = useState(
|
||||||
|
searchParams.get('search') || ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const [openChangesets, closedChangesets] = useMemo(() => {
|
||||||
|
const open = changesets.filter(
|
||||||
|
changeset =>
|
||||||
|
changeset.state !== 'Cancelled' && changeset.state !== 'Applied'
|
||||||
|
);
|
||||||
|
const closed = changesets.filter(
|
||||||
|
changeset =>
|
||||||
|
changeset.state === 'Cancelled' || changeset.state === 'Applied'
|
||||||
|
);
|
||||||
|
|
||||||
|
return [open, closed];
|
||||||
|
}, [changesets]);
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
title: 'Suggestions',
|
||||||
|
data: openChangesets,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Closed',
|
||||||
|
data: closedChangesets,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState(tabs[0]);
|
||||||
|
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
id: 'Title',
|
||||||
|
Header: 'Title',
|
||||||
|
width: 100,
|
||||||
|
canSort: true,
|
||||||
|
accessor: 'id',
|
||||||
|
Cell: ChangesetTitleCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'By',
|
||||||
|
accessor: 'createdBy',
|
||||||
|
maxWidth: 50,
|
||||||
|
canSort: false,
|
||||||
|
Cell: AvatarCell,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Submitted',
|
||||||
|
accessor: 'createdAt',
|
||||||
|
searchable: true,
|
||||||
|
maxWidth: 100,
|
||||||
|
Cell: TimeAgoCell,
|
||||||
|
sortType: 'alphanumeric',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Environment',
|
||||||
|
accessor: 'environment',
|
||||||
|
maxWidth: 100,
|
||||||
|
Cell: TextCell,
|
||||||
|
sortType: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: 'Status',
|
||||||
|
accessor: 'state',
|
||||||
|
minWidth: 150,
|
||||||
|
width: 150,
|
||||||
|
Cell: ChangesetStatusCell,
|
||||||
|
sortType: 'text',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: '',
|
||||||
|
id: 'Actions',
|
||||||
|
minWidth: 50,
|
||||||
|
width: 50,
|
||||||
|
canSort: false,
|
||||||
|
Cell: ChangesetActionCell,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
//eslint-disable-next-line
|
||||||
|
[projectId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: searchedData,
|
||||||
|
getSearchText,
|
||||||
|
getSearchContext,
|
||||||
|
} = useSearch(columns, searchValue, activeTab.data);
|
||||||
|
|
||||||
|
const data = useMemo(
|
||||||
|
() => (loading ? featuresPlaceholder : searchedData),
|
||||||
|
[searchedData, loading]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [initialState] = useState(() => ({
|
||||||
|
sortBy: [
|
||||||
|
{
|
||||||
|
id: searchParams.get('sort') || storedParams.id,
|
||||||
|
desc: searchParams.has('order')
|
||||||
|
? searchParams.get('order') === 'desc'
|
||||||
|
: storedParams.desc,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
hiddenColumns: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
headerGroups,
|
||||||
|
rows,
|
||||||
|
state: { sortBy },
|
||||||
|
prepareRow,
|
||||||
|
setHiddenColumns,
|
||||||
|
getTableProps,
|
||||||
|
getTableBodyProps,
|
||||||
|
} = useTable(
|
||||||
|
{
|
||||||
|
columns: columns as any[], // TODO: fix after `react-table` v8 update
|
||||||
|
data,
|
||||||
|
initialState,
|
||||||
|
sortTypes,
|
||||||
|
disableSortRemove: true,
|
||||||
|
autoResetSortBy: false,
|
||||||
|
defaultColumn: {
|
||||||
|
Cell: TextCell,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
useSortBy
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hiddenColumns = [''];
|
||||||
|
if (isSmallScreen) {
|
||||||
|
hiddenColumns.push('createdBy', 'updatedAt');
|
||||||
|
}
|
||||||
|
setHiddenColumns(hiddenColumns);
|
||||||
|
}, [setHiddenColumns, isSmallScreen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tableState: Record<string, string> = {};
|
||||||
|
tableState.sort = sortBy[0].id;
|
||||||
|
if (sortBy[0].desc) {
|
||||||
|
tableState.order = 'desc';
|
||||||
|
}
|
||||||
|
if (searchValue) {
|
||||||
|
tableState.search = searchValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchParams(tableState, {
|
||||||
|
replace: true,
|
||||||
|
});
|
||||||
|
setStoredParams({ id: sortBy[0].id, desc: sortBy[0].desc || false });
|
||||||
|
}, [loading, sortBy, searchValue]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const renderTabs = () => {
|
||||||
|
return (
|
||||||
|
<div className={classes.tabContainer}>
|
||||||
|
<Tabs
|
||||||
|
value={activeTab?.title}
|
||||||
|
indicatorColor="primary"
|
||||||
|
textColor="primary"
|
||||||
|
>
|
||||||
|
{tabs.map(tab => (
|
||||||
|
<Tab
|
||||||
|
key={tab.title}
|
||||||
|
label={`${tab.title} (${tab.data.length})`}
|
||||||
|
value={tab.title}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
className={classes.tabButton}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent
|
||||||
|
isLoading={loading}
|
||||||
|
header={
|
||||||
|
<PageHeader
|
||||||
|
titleElement={renderTabs()}
|
||||||
|
actions={
|
||||||
|
<Search
|
||||||
|
initialValue={searchValue}
|
||||||
|
onChange={setSearchValue}
|
||||||
|
hasFilters
|
||||||
|
getSearchContext={getSearchContext}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SearchHighlightProvider value={getSearchText(searchValue)}>
|
||||||
|
<Table {...getTableProps()}>
|
||||||
|
<SortableTableHeader headerGroups={headerGroups} />
|
||||||
|
<TableBody {...getTableBodyProps()}>
|
||||||
|
{rows.map(row => {
|
||||||
|
prepareRow(row);
|
||||||
|
return (
|
||||||
|
<TableRow hover {...row.getRowProps()}>
|
||||||
|
{row.cells.map(cell => (
|
||||||
|
<TableCell
|
||||||
|
{...cell.getCellProps()}
|
||||||
|
padding="none"
|
||||||
|
>
|
||||||
|
{cell.render('Cell')}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</SearchHighlightProvider>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={rows.length === 0}
|
||||||
|
show={() => (
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={searchValue?.length > 0}
|
||||||
|
show={
|
||||||
|
<TablePlaceholder>
|
||||||
|
No changes found matching “
|
||||||
|
{searchValue}”
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
elseShow={
|
||||||
|
<TablePlaceholder>
|
||||||
|
None of the changes where submitted yet.
|
||||||
|
</TablePlaceholder>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,27 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
|
||||||
|
const fetcher = (path: string) => {
|
||||||
|
return fetch(path)
|
||||||
|
.then(handleErrorResponses('SuggestedChanges'))
|
||||||
|
.then(res => res.json());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useProjectSuggestedChanges = (project: string) => {
|
||||||
|
const { data, error, mutate } = useSWR(
|
||||||
|
formatApiPath(`api/admin/projects/${project}/suggest-changes`),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
changesets: data,
|
||||||
|
loading: !error && !data,
|
||||||
|
refetch: () => mutate(),
|
||||||
|
error,
|
||||||
|
}),
|
||||||
|
[data, error, mutate]
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user