Skip to content

Commit ef9f62f

Browse files
authored
Merge pull request #4630 from 3drepo/ISSUE_4585
Issue 4585 - Select the next ticket/issue/risk if the user removes the current ticket/issue/risk from the filtered list
2 parents 043b69d + bafee68 commit ef9f62f

File tree

9 files changed

+148
-59
lines changed

9 files changed

+148
-59
lines changed

frontend/src/v4/routes/viewerGui/components/issues/components/issueDetails/issueDetails.component.tsx

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { IssueDetailsForm } from './issueDetailsForm.component';
3434
interface IProps {
3535
viewer: any;
3636
jobs: any[];
37+
filteredIssues: any[];
3738
issues: any[];
3839
topicTypes: any[];
3940
issue: any;
@@ -88,6 +89,7 @@ interface IProps {
8889
interface IState {
8990
logsLoaded: boolean;
9091
scrolled: boolean;
92+
listIndex: number;
9193
}
9294

9395
const UNASSIGNED_JOB = {
@@ -99,6 +101,7 @@ export class IssueDetails extends PureComponent<IProps, IState> {
99101
public state = {
100102
logsLoaded: false,
101103
scrolled: false,
104+
listIndex: (this.props.filteredIssues || []).findIndex(({ _id }) => _id === this.props.issue._id),
102105
};
103106

104107
public formRef = createRef<any>();
@@ -229,12 +232,23 @@ export class IssueDetails extends PureComponent<IProps, IState> {
229232
}
230233

231234
public componentWillUnmount() {
232-
const { teamspace, model, issue, unsubscribeOnIssueCommentsChanges } = this.props;
235+
const { teamspace, model, issue, filteredIssues, revision, unsubscribeOnIssueCommentsChanges, setActiveIssue } = this.props;
233236
unsubscribeOnIssueCommentsChanges(teamspace, model, issue._id);
234237

235-
if (this.props.issue.defaultHidden) {
236-
this.props.setActiveIssue({}, null, true);
238+
if (filteredIssues.find(({ _id }) => _id === issue._id)) {
239+
// item is part of the filtered items list, no action required
240+
return;
237241
}
242+
const { listIndex } = this.state;
243+
244+
if (filteredIssues.length && listIndex < filteredIssues.length) {
245+
// set next item in the list as active
246+
setActiveIssue(filteredIssues[listIndex % filteredIssues.length], revision, false);
247+
return;
248+
}
249+
250+
// the item was not in the filtered list
251+
setActiveIssue({}, null, true);
238252
}
239253

240254
public componentDidUpdate(prevProps) {
@@ -247,6 +261,11 @@ export class IssueDetails extends PureComponent<IProps, IState> {
247261
fetchIssue(teamspace, model, issue._id);
248262
subscribeOnIssueCommentsChanges(teamspace, model, issue._id);
249263
}
264+
265+
const newListIndex = (this.props.filteredIssues || []).findIndex(({ _id }) => _id === this.props.issue._id);
266+
if (newListIndex !== -1) {
267+
this.setState({ ...this.state, listIndex: newListIndex });
268+
}
250269
}
251270

252271
public handleHeaderClick = () => {
@@ -332,10 +351,10 @@ export class IssueDetails extends PureComponent<IProps, IState> {
332351

333352
public handlePanelScroll = (e) => {
334353
if (e.target.scrollTop > 0 && !this.state.scrolled) {
335-
this.setState({ scrolled: true });
354+
this.setState({ scrolled: true, listIndex: this.state.listIndex });
336355
}
337356
if (e.target.scrollTop === 0 && this.state.scrolled) {
338-
this.setState({ scrolled: false });
357+
this.setState({ scrolled: false, listIndex: this.state.listIndex });
339358
}
340359
}
341360

frontend/src/v4/routes/viewerGui/components/issues/components/issueDetails/issueDetails.container.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
selectNewComment,
3232
selectPostCommentIsPending,
3333
IssuesActions,
34+
selectFilteredIssues,
3435
} from '../../../../../../modules/issues';
3536
import { selectJobsList, selectMyJob } from '../../../../../../modules/jobs';
3637
import { selectPermissions, selectUnit } from '../../../../../../modules/model';
@@ -60,6 +61,7 @@ const mapStateToProps = createStructuredSelector({
6061
failedToLoad: selectFailedToLoad,
6162
postCommentIsPending: selectPostCommentIsPending,
6263
issues: selectIssues,
64+
filteredIssues: selectFilteredIssues,
6365
minSequenceDate: selectStartDate,
6466
maxSequenceDate: selectEndDate,
6567
selectedDate: selectSelectedStartingDate,

frontend/src/v4/routes/viewerGui/components/listNavigation/listNavigation.component.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,18 @@ interface IProps {
3030

3131
interface IState {
3232
currentIndex: number;
33+
initialItemsCount: number;
3334
}
3435

3536
export class ListNavigation extends PureComponent<IProps, IState> {
3637
public state = {
37-
currentIndex: 0
38+
currentIndex: 0,
39+
initialItemsCount: 0,
3840
};
3941

4042
public componentDidMount() {
4143
if (this.props.initialIndex) {
42-
this.setState({ currentIndex: this.props.initialIndex });
44+
this.setState({ currentIndex: this.props.initialIndex, initialItemsCount: this.props.itemsCount });
4345
}
4446
}
4547

@@ -49,15 +51,19 @@ export class ListNavigation extends PureComponent<IProps, IState> {
4951

5052
public handleNavigation = ( skip ) => {
5153
const index = (this.props.itemsCount + this.state.currentIndex + skip) % this.props.itemsCount ;
52-
this.setState({ currentIndex: index }, this.handleChange);
54+
this.setState({ currentIndex: index, initialItemsCount: this.props.itemsCount }, this.handleChange);
5355
}
5456

5557
public handlePrevItem = () => {
5658
this.handleNavigation(-1);
5759
}
5860

5961
public handleNextItem = () => {
60-
this.handleNavigation(1);
62+
if (this.state.initialItemsCount === this.props.itemsCount) {
63+
this.handleNavigation(1);
64+
} else {
65+
this.handleNavigation(0);
66+
}
6167
}
6268

6369
public render() {

frontend/src/v4/routes/viewerGui/components/reportedItems/reportedItems.component.tsx

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
* You should have received a copy of the GNU Affero General Public License
1515
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
*/
17-
import { PureComponent, ReactChildren, createRef } from 'react';
17+
import { PureComponent, ReactChildren, createRef, useEffect, useRef } from 'react';
1818
import IconButton from '@mui/material/IconButton';
1919
import AddIcon from '@mui/icons-material/Add';
2020
import ArrowBack from '@mui/icons-material/ArrowBack';
2121

2222
import { formatMessage } from '@/v5/services/intl';
23+
import { InvisibleContainer } from '@controls/invisibleContainer/invisibleContainer.styles';
2324
import { CREATE_ISSUE, VIEW_ISSUE } from '../../../../constants/issue-permissions';
2425
import { hasPermissions } from '../../../../helpers/permissions';
2526
import { renderWhenTrue } from '../../../../helpers/rendering';
@@ -67,11 +68,25 @@ interface IProps {
6768
id?: string;
6869
}
6970

70-
interface IState {
71-
prevScroll: number;
72-
}
71+
const PreviewListSingleItem = ({ active, index, ...props }) => {
72+
const ref = useRef<HTMLDivElement>();
73+
74+
useEffect(() => {
75+
if (active && ref.current) {
76+
// @ts-ignore
77+
ref.current.firstElementChild.scrollIntoView({ behavior: 'instant', block: 'nearest', inline: 'start' });
78+
}
79+
}, [active]);
80+
81+
return (
82+
<InvisibleContainer ref={ref}>
83+
{/* @ts-ignore */}
84+
<PreviewListItem {...props} active={active} key={index} />
85+
</InvisibleContainer>
86+
);
87+
};
7388

74-
export class ReportedItems extends PureComponent<IProps, IState> {
89+
export class ReportedItems extends PureComponent<IProps> {
7590

7691
get activeItemIndex() {
7792
return this.props.items.findIndex((item) => item._id === this.props.activeItemId);
@@ -91,12 +106,6 @@ export class ReportedItems extends PureComponent<IProps, IState> {
91106
}
92107
return 'Model is loading';
93108
}
94-
public state = {
95-
prevScroll: 0
96-
};
97-
98-
public listViewRef = createRef<HTMLDivElement>();
99-
public listContainerRef = createRef<any>();
100109

101110
public handleClickOutside = () => {
102111
const { onDeactivateItem } = this.props;
@@ -106,26 +115,32 @@ export class ReportedItems extends PureComponent<IProps, IState> {
106115
}
107116
}
108117

109-
public renderItemsList = renderWhenTrue(() => (
110-
<ListContainer ref={this.listContainerRef}>
111-
{this.props.items.map((item, index) => (
112-
<PreviewListItem
113-
{...item}
114-
key={index}
115-
onItemClick={this.handleItemFocus(item)}
116-
onArrowClick={this.handleShowItemDetails(item)}
117-
active={this.props.activeItemId === item._id}
118-
hasViewPermission={this.hasPermission(VIEW_ISSUE)}
119-
modelLoaded={this.props.isModelLoaded}
120-
panelName={this.props.type}
121-
/>
122-
))}
123-
</ListContainer>
124-
));
118+
public renderItemsList = renderWhenTrue(() => {
119+
const activeItemRef = createRef<any>();
120+
121+
return (
122+
<ListContainer>
123+
{this.props.items.map((item, index) => (
124+
<PreviewListSingleItem
125+
{...item}
126+
key={index}
127+
index={index}
128+
onItemClick={this.handleItemFocus(item)}
129+
onArrowClick={this.handleShowItemDetails(item)}
130+
active={this.props.activeItemId === item._id}
131+
ref={this.props.activeItemId === item._id ? activeItemRef : undefined}
132+
hasViewPermission={this.hasPermission(VIEW_ISSUE)}
133+
modelLoaded={this.props.isModelLoaded}
134+
panelName={this.props.type}
135+
/>
136+
))}
137+
</ListContainer>
138+
);
139+
});
125140

126141
public renderListView = renderWhenTrue(() => (
127142
<>
128-
<ViewerPanelContent onClick={this.handleClickOutside} ref={this.listViewRef}>
143+
<ViewerPanelContent onClick={this.handleClickOutside}>
129144
<div onClick={(event: React.MouseEvent<HTMLDivElement>) => event.stopPropagation()}>
130145
{this.renderEmptyState(!this.props.searchEnabled && !this.props.items.length)}
131146
{this.renderNotFound(this.props.searchEnabled && !this.props.items.length)}
@@ -177,21 +192,6 @@ export class ReportedItems extends PureComponent<IProps, IState> {
177192
<EmptyStateInfo>No entry matched</EmptyStateInfo>
178193
));
179194

180-
public componentDidUpdate(prevProps) {
181-
const { showDetails } = this.props;
182-
const detailsWasClosed = prevProps.showDetails !== showDetails && !showDetails;
183-
184-
const changes = {} as IState;
185-
186-
if (detailsWasClosed) {
187-
this.listViewRef.current.scrollTo(0, this.state.prevScroll);
188-
}
189-
190-
if (this.listViewRef.current) {
191-
this.setState({prevScroll: this.listViewRef.current.scrollTop});
192-
}
193-
}
194-
195195
public hasPermission = (permission) => {
196196
const { permissions: modelPermissions } = this.props;
197197
if (modelPermissions) {

frontend/src/v4/routes/viewerGui/components/risks/components/riskDetails/riskDetails.component.tsx

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { RiskDetailsForm } from './riskDetailsForm.component';
3535
interface IProps {
3636
viewer: any;
3737
jobs: any[];
38+
filteredRisks: any[];
3839
risks: any[];
3940
risk: any;
4041
comments: any[];
@@ -80,6 +81,7 @@ interface IProps {
8081
removeMeasurement: (uuid) => void;
8182
setMeasurementColor: (uuid, color) => void;
8283
setMeasurementName: (uuid, type, name) => void;
84+
setActiveRisk: (risk, revision, ignoreViewer) => void;
8385
minSequenceDate: Date;
8486
maxSequenceDate: Date;
8587
selectedDate: Date;
@@ -91,6 +93,7 @@ interface IProps {
9193
interface IState {
9294
logsLoaded: boolean;
9395
scrolled: boolean;
96+
listIndex: number;
9497
}
9598

9699
const UNASSIGNED_JOB = {
@@ -101,7 +104,8 @@ const UNASSIGNED_JOB = {
101104
export class RiskDetails extends PureComponent<IProps, IState> {
102105
public state = {
103106
logsLoaded: false,
104-
scrolled: false
107+
scrolled: false,
108+
listIndex: (this.props.filteredRisks || []).findIndex(({ _id }) => _id === this.props.risk._id),
105109
};
106110

107111
public formRef = createRef<any>();
@@ -232,8 +236,23 @@ export class RiskDetails extends PureComponent<IProps, IState> {
232236
}
233237

234238
public componentWillUnmount() {
235-
const { teamspace, model, risk, unsubscribeOnRiskCommentsChanges } = this.props;
239+
const { teamspace, model, risk, filteredRisks, revision, setActiveRisk, unsubscribeOnRiskCommentsChanges } = this.props;
236240
unsubscribeOnRiskCommentsChanges(teamspace, model, risk._id);
241+
242+
if (filteredRisks.find(({ _id }) => _id === risk._id)) {
243+
// item is part of the filtered items list, no action required
244+
return;
245+
}
246+
247+
const { listIndex } = this.state;
248+
if (filteredRisks.length && listIndex < filteredRisks.length) {
249+
// set next item in the list as active
250+
setActiveRisk(filteredRisks[listIndex % filteredRisks.length], revision, false);
251+
return;
252+
}
253+
254+
// the item was not in the filtered list
255+
setActiveRisk({}, null, true);
237256
}
238257

239258
public componentDidUpdate(prevProps) {
@@ -246,6 +265,11 @@ export class RiskDetails extends PureComponent<IProps, IState> {
246265
fetchRisk(teamspace, model, risk._id);
247266
subscribeOnRiskCommentsChanges(teamspace, model, risk._id);
248267
}
268+
269+
const newListIndex = (this.props.filteredRisks || []).findIndex(({ _id }) => _id === this.props.risk._id);
270+
if (newListIndex !== -1) {
271+
this.setState({ ...this.state, listIndex: newListIndex });
272+
}
249273
}
250274

251275
public handleHeaderClick = () => {
@@ -330,10 +354,10 @@ export class RiskDetails extends PureComponent<IProps, IState> {
330354

331355
public handlePanelScroll = (e) => {
332356
if (e.target.scrollTop > 0 && !this.state.scrolled) {
333-
this.setState({ scrolled: true });
357+
this.setState({ scrolled: true, listIndex: this.state.listIndex });
334358
}
335359
if (e.target.scrollTop === 0 && this.state.scrolled) {
336-
this.setState({ scrolled: false });
360+
this.setState({ scrolled: false, listIndex: this.state.listIndex });
337361
}
338362
}
339363

frontend/src/v4/routes/viewerGui/components/risks/components/riskDetails/riskDetails.container.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
selectPostCommentIsPending,
3636
selectRisks,
3737
RisksActions,
38+
selectFilteredRisks,
3839
} from '../../../../../../modules/risks';
3940
import {
4041
selectEndDate,
@@ -53,6 +54,7 @@ const mapStateToProps = createStructuredSelector({
5354
comments: selectActiveRiskComments,
5455
jobs: selectJobsList,
5556
risks: selectRisks,
57+
filteredRisks: selectFilteredRisks,
5658
expandDetails: selectExpandDetails,
5759
fetchingDetailsIsPending: selectFetchingDetailsIsPending,
5860
newComment: selectNewComment,
@@ -75,6 +77,7 @@ export const mapDispatchToProps = (dispatch) => bindActionCreators({
7577
fetchRisk: RisksActions.fetchRisk,
7678
saveRisk: RisksActions.saveRisk,
7779
updateRisk: RisksActions.updateRisk,
80+
setActiveRisk: RisksActions.setActiveRisk,
7881
updateViewpoint: RisksActions.updateActiveRiskViewpoint,
7982
cloneRisk: RisksActions.cloneRisk,
8083
postComment: RisksActions.postComment,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Copyright (C) 2023 3D Repo Ltd
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License, or (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
import styled from 'styled-components';
19+
20+
export const InvisibleContainer = styled.div`
21+
display: contents;
22+
`;

0 commit comments

Comments
 (0)