Skip to content

Commit

Permalink
[96] Improve the way change proposals are managed
Browse files Browse the repository at this point in the history
Bug: #96
Signed-off-by: Stéphane Bégaudeau <[email protected]>
  • Loading branch information
sbegaudeau committed Jul 16, 2023
1 parent c3045b2 commit 7517902
Show file tree
Hide file tree
Showing 12 changed files with 123 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import com.svalyn.studio.application.controllers.history.dto.UpdateChangeProposalStatusInput;
import com.svalyn.studio.application.controllers.project.dto.ProjectDTO;
import com.svalyn.studio.application.services.history.api.IChangeProposalService;
import com.svalyn.studio.domain.history.ChangeProposalStatus;
import graphql.relay.Connection;
import graphql.relay.DefaultConnection;
import graphql.relay.DefaultConnectionCursor;
Expand All @@ -44,6 +45,7 @@
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;

import java.util.List;
import java.util.Objects;
import java.util.UUID;

Expand All @@ -62,8 +64,8 @@ public ChangeProposalController(IChangeProposalService changeProposalService) {
}

@SchemaMapping(typeName = "Project")
public Connection<ChangeProposalDTO> changeProposals(ProjectDTO project, @Argument int page, @Argument int rowsPerPage) {
var pageData = this.changeProposalService.findAllByProjectId(project.id(), page, rowsPerPage);
public Connection<ChangeProposalDTO> changeProposals(ProjectDTO project, @Argument List<ChangeProposalStatus> status, @Argument int page, @Argument int rowsPerPage) {
var pageData = this.changeProposalService.findAllByProjectIdAndStatus(project.id(), status, page, rowsPerPage);
var edges = pageData.stream().map(changeProposal -> {
var value = new Relay().toGlobalId("ChangeProposal", changeProposal.id().toString());
var cursor = new DefaultConnectionCursor(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.svalyn.studio.domain.Success;
import com.svalyn.studio.domain.account.repositories.IAccountRepository;
import com.svalyn.studio.domain.history.ChangeProposal;
import com.svalyn.studio.domain.history.ChangeProposalStatus;
import com.svalyn.studio.domain.history.Review;
import com.svalyn.studio.domain.history.repositories.IChangeProposalRepository;
import com.svalyn.studio.domain.history.services.api.IChangeProposalCreationService;
Expand Down Expand Up @@ -116,12 +117,13 @@ public Optional<ChangeProposalDTO> findById(UUID id) {

@Override
@Transactional(readOnly = true)
public Page<ChangeProposalDTO> findAllByProjectId(UUID projectId, int page, int rowsPerPage) {
var changesProposals = this.changeProposalRepository.findAllByProjectId(projectId, page * rowsPerPage, rowsPerPage)
public Page<ChangeProposalDTO> findAllByProjectIdAndStatus(UUID projectId, List<ChangeProposalStatus> status, int page, int rowsPerPage) {
var stringStatus = status.stream().map(Object::toString).toList();
var changesProposals = this.changeProposalRepository.findAllByProjectIdAndStatus(projectId, stringStatus, page * rowsPerPage, rowsPerPage)
.stream()
.flatMap(changeProposal -> this.toDTO(changeProposal).stream())
.toList();
var count = this.changeProposalRepository.countAllByProjectId(projectId);
var count = this.changeProposalRepository.countAllByProjectIdAndStatus(projectId, stringStatus);
return new PageImpl<>(changesProposals, PageRequest.of(page, rowsPerPage), count);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.svalyn.studio.application.controllers.history.dto.UpdateChangeProposalReadMeInput;
import com.svalyn.studio.application.controllers.history.dto.UpdateChangeProposalStatusInput;
import com.svalyn.studio.application.controllers.dto.IPayload;
import com.svalyn.studio.domain.history.ChangeProposalStatus;
import org.springframework.data.domain.Page;

import java.util.List;
Expand All @@ -41,7 +42,7 @@
* @author sbegaudeau
*/
public interface IChangeProposalService {
Page<ChangeProposalDTO> findAllByProjectId(UUID projectId, int page, int rowsPerPage);
Page<ChangeProposalDTO> findAllByProjectIdAndStatus(UUID projectId, List<ChangeProposalStatus> status, int page, int rowsPerPage);

IPayload createChangeProposal(CreateChangeProposalInput input);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ type Project {
activityEntries(page: Int!, rowsPerPage: Int!): ProjectActivityEntriesConnection!
branch(name: String!): Branch
branches(page: Int!, rowsPerPage: Int!): ProjectBranchesConnection!
changeProposals(page: Int!, rowsPerPage: Int!): ProjectChangeProposalsConnection!
changeProposals(status: [ChangeProposalStatus!]!, page: Int!, rowsPerPage: Int!): ProjectChangeProposalsConnection!
}

type ProjectTagsConnection {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,16 @@
public interface IChangeProposalRepository extends PagingAndSortingRepository<ChangeProposal, UUID>, ListCrudRepository<ChangeProposal, UUID> {
@Query("""
SELECT * FROM change_proposal changeProposal
WHERE changeProposal.project_id = :projectId
WHERE changeProposal.project_id = :projectId AND changeProposal.status IN (:status)
ORDER BY changeProposal.created_on DESC
LIMIT :limit
OFFSET :offset
""")
List<ChangeProposal> findAllByProjectId(UUID projectId, long offset, int limit);
List<ChangeProposal> findAllByProjectIdAndStatus(UUID projectId, List<String> status, long offset, int limit);

@Query("""
SELECT count(*) FROM change_proposal changeProposal
WHERE changeProposal.project_id = :projectId
WHERE changeProposal.project_id = :projectId AND changeProposal.status IN (:status)
""")
long countAllByProjectId(UUID projectId);
long countAllByProjectIdAndStatus(UUID projectId, List<String> status);
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,12 @@ export const ChangeProposalStatus = ({ changeProposal, onStatusUpdated }: Change
gap: (theme) => theme.spacing(2),
}}
>
<Button variant="outlined" startIcon={<CommentIcon />} onClick={openReviewDialog}>
<Button
variant="outlined"
startIcon={<CommentIcon />}
onClick={openReviewDialog}
disabled={changeProposal.status !== 'OPEN'}
>
Review Change Proposal
</Button>
<ButtonGroup
Expand Down Expand Up @@ -293,7 +298,7 @@ const ChangeProposalStatusHeader = ({ changeProposal }: ChangeProposalStatusHead
const ChangeProposalReviews = ({ changeProposal }: ChangeProposalReviewsProps) => {
const hasReviews = changeProposal.reviews.edges.length > 0;
if (!hasReviews) {
return <Typography>No reviews have been performed yet</Typography>;
return <Typography sx={{ paddingX: (theme) => theme.spacing(2) }}>No reviews have been performed yet</Typography>;
}

const approvedReviews = changeProposal.reviews.edges
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ export const HomeViewLeftPanel = ({}: HomeViewLeftPanelProps) => {
})}
</List>
) : (
<Typography variant="body2">No organization found</Typography>
<Typography variant="body2" sx={{ paddingX: (theme) => theme.spacing(2) }}>
No organization found
</Typography>
)}
</Paper>
<ErrorSnackbar open={!!state.message} message={state.message} onClose={handleCloseSnackbar} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,12 @@ export const NotificationsView = () => {
/>
</TableCell>
<TableCell>
<Link variant="subtitle1" component={RouterLink} to={notification.relatedUrl}>
<Link
variant="subtitle1"
component={RouterLink}
to={notification.relatedUrl}
underline="hover"
>
{notification.title}
</Link>
</TableCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,28 @@ import TableCell from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';

import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import Select, { SelectChangeEvent } from '@mui/material/Select';
import { ChangeProposalsTableHeadProps } from './ChangeProposalsTableHead.types';

export const ChangeProposalsTableHead = ({
filter,
onFilterChange,
changeProposalsCount,
selectedChangeProposalsCount,
onSelectAll,
}: ChangeProposalsTableHeadProps) => {
const handleChange = (event: SelectChangeEvent<'OPEN' | 'CLOSED'>) => {
const {
target: { value },
} = event;
if (value === 'OPEN' || value === 'CLOSED') {
onFilterChange(value);
}
};

return (
<TableHead>
<TableRow sx={{ borderLeft: '3px solid transparent' }}>
Expand All @@ -39,7 +54,15 @@ export const ChangeProposalsTableHead = ({
indeterminate={selectedChangeProposalsCount > 0 && selectedChangeProposalsCount < changeProposalsCount}
/>
</TableCell>
<TableCell>Name</TableCell>
<TableCell>
<FormControl sx={{ minWidth: (theme) => theme.spacing(15) }} size="small">
<InputLabel id="change-proposal-status-label">Status</InputLabel>
<Select labelId="change-proposal-status-label" value={filter} label="Status" onChange={handleChange}>
<MenuItem value="OPEN">Open</MenuItem>
<MenuItem value="CLOSED">Close</MenuItem>
</Select>
</FormControl>
</TableCell>
</TableRow>
</TableHead>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
*/

export interface ChangeProposalsTableHeadProps {
filter: ChangeProposalStatusFilter;
onFilterChange: (filter: ChangeProposalStatusFilter) => void;
changeProposalsCount: number;
selectedChangeProposalsCount: number;
onSelectAll: (event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => void;
}

export type ChangeProposalStatusFilter = 'OPEN' | 'CLOSED';
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { ChangeProposalsTableHead } from './ChangeProposalsTableHead';
import { ChangeProposalsTableToolbar } from './ChangeProposalsTableToolbar';
import {
ChangeProposal,
ChangeProposalStatusFilter,
DeleteChangeProposalsData,
DeleteChangeProposalsVariables,
ErrorPayload,
Expand All @@ -50,10 +51,10 @@ import {
} from './ProjectChangeProposal.types';

const getChangeProposalsQuery = gql`
query getChangeProposals($identifier: ID!, $page: Int!, $rowsPerPage: Int!) {
query getChangeProposals($identifier: ID!, $status: [ChangeProposalStatus!]!, $page: Int!, $rowsPerPage: Int!) {
viewer {
project(identifier: $identifier) {
changeProposals(page: $page, rowsPerPage: $rowsPerPage) {
changeProposals(status: $status, page: $page, rowsPerPage: $rowsPerPage) {
edges {
node {
id
Expand Down Expand Up @@ -83,13 +84,15 @@ export const ProjectChangeProposal = ({ projectIdentifier, role }: ProjectChange
const [state, setState] = useState<ProjectChangeProposalState>({
project: null,
selectedChangeProposalIds: [],
filter: 'OPEN',
page: 0,
rowsPerPage: 10,
message: null,
});

const variables: GetChangeProposalsVariables = {
identifier: projectIdentifier,
status: state.filter === 'OPEN' ? ['OPEN'] : ['INTEGRATED', 'CLOSED'],
page: state.page,
rowsPerPage: state.rowsPerPage,
};
Expand Down Expand Up @@ -187,6 +190,9 @@ export const ProjectChangeProposal = ({ projectIdentifier, role }: ProjectChange
setState((prevState) => ({ ...prevState, page }));
};

const handleFilterChange = (filter: ChangeProposalStatusFilter) =>
setState((prevState) => ({ ...prevState, filter }));

const handleCloseSnackbar = () => setState((prevState) => ({ ...prevState, message: null }));

const changeProposals = state.project?.changeProposals.edges.map((edge) => edge.node) ?? [];
Expand Down Expand Up @@ -214,42 +220,47 @@ export const ProjectChangeProposal = ({ projectIdentifier, role }: ProjectChange
New Change Proposal
</Button>
</Toolbar>
{state.project && changeProposals.length > 0 ? (
<Container maxWidth="lg" sx={{ py: (theme) => theme.spacing(4) }}>
<ChangeProposalsTableToolbar
onDelete={handleDelete}
selectedChangeProposalsCount={state.selectedChangeProposalIds.length}
role={role}
/>
<TableContainer component={Paper} variant="outlined">
<Table>
<ChangeProposalsTableHead
changeProposalsCount={changeProposals.length}
selectedChangeProposalsCount={state.selectedChangeProposalIds.length}
onSelectAll={selectAllChangeProposals}
/>
<TableBody>
{changeProposals.map((changeProposal) => {
const isChangeProposalSelected = state.selectedChangeProposalIds.includes(changeProposal.id);
return (
<TableRow
key={changeProposal.id}
onClick={(event) => selectChangeProposal(event, changeProposal)}
>
<TableCell padding="checkbox">
<Checkbox checked={isChangeProposalSelected} />
</TableCell>
<TableCell>
<Link variant="subtitle1" component={RouterLink} to={`/changeproposals/${changeProposal.id}`}>
{changeProposal.name}
</Link>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>

<Container maxWidth="lg" sx={{ py: (theme) => theme.spacing(4) }}>
<ChangeProposalsTableToolbar
onDelete={handleDelete}
selectedChangeProposalsCount={state.selectedChangeProposalIds.length}
role={role}
/>
<TableContainer component={Paper} variant="outlined">
<Table>
<ChangeProposalsTableHead
filter={state.filter}
onFilterChange={handleFilterChange}
changeProposalsCount={changeProposals.length}
selectedChangeProposalsCount={state.selectedChangeProposalIds.length}
onSelectAll={selectAllChangeProposals}
/>
<TableBody>
{changeProposals.map((changeProposal) => {
const isChangeProposalSelected = state.selectedChangeProposalIds.includes(changeProposal.id);
return (
<TableRow key={changeProposal.id} onClick={(event) => selectChangeProposal(event, changeProposal)}>
<TableCell padding="checkbox">
<Checkbox checked={isChangeProposalSelected} />
</TableCell>
<TableCell>
<Link
variant="subtitle1"
component={RouterLink}
to={`/changeproposals/${changeProposal.id}`}
underline="hover"
>
{changeProposal.name}
</Link>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
{state.project && changeProposals.length > 0 ? (
<TablePagination
component="div"
count={state.project.changeProposals.pageInfo.count}
Expand All @@ -258,14 +269,14 @@ export const ProjectChangeProposal = ({ projectIdentifier, role }: ProjectChange
rowsPerPage={state.rowsPerPage}
rowsPerPageOptions={[state.rowsPerPage]}
/>
</Container>
) : (
<Box sx={{ paddingY: (theme) => theme.spacing(12) }}>
<Typography variant="h6" align="center">
No change proposals found
</Typography>
</Box>
)}
) : (
<Box sx={{ paddingY: (theme) => theme.spacing(12) }}>
<Typography variant="h6" align="center">
No change proposals found
</Typography>
</Box>
)}
</Container>
<ErrorSnackbar open={state.message !== null} message={state.message} onClose={handleCloseSnackbar} />
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,15 @@ export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE';

export interface ProjectChangeProposalState {
project: Project | null;
filter: ChangeProposalStatusFilter;
selectedChangeProposalIds: string[];
page: number;
rowsPerPage: number;
message: string | null;
}

export type ChangeProposalStatusFilter = 'OPEN' | 'CLOSED';

export interface GetChangeProposalsData {
viewer: Viewer;
}
Expand Down Expand Up @@ -64,10 +67,13 @@ export interface ChangeProposal {

export interface GetChangeProposalsVariables {
identifier: string;
status: ChangeProposalStatus[];
page: number;
rowsPerPage: number;
}

export type ChangeProposalStatus = 'OPEN' | 'CLOSED' | 'INTEGRATED';

export interface DeleteChangeProposalsData {
deleteChangeProposals: DeleteChangeProposalsPayload;
}
Expand Down

0 comments on commit 7517902

Please sign in to comment.