Skip to content

Commit 27aabf6

Browse files
authored
Merge pull request #3035 from sumitdaga/issue-2177
fixes issue 2177
2 parents 678b940 + b33a327 commit 27aabf6

File tree

15 files changed

+253
-129
lines changed

15 files changed

+253
-129
lines changed

src/components/ScrollToAnchors.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function scrollToAnchors(Component) {
4343
componentDidMount() {
4444
const { hash } = window.location
4545

46-
if (hash !== '') {
46+
if (!this.props.disableAutoScrolling && hash !== '') {
4747
// Push onto callback queue so it runs after the DOM is updated,
4848
// this is required when navigating from a different page so that
4949
// the element is rendered on the page before trying to getElementById.

src/projects/actions/projectDashboard.js

Lines changed: 4 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
import _ from 'lodash'
22
import { loadMembers } from '../../actions/members'
3-
import { loadProject, loadProjectInvite, loadDirectProjectData, loadProjectPhasesWithProducts } from './project'
3+
import { loadProject, loadProjectInvite, loadDirectProjectData } from './project'
4+
import { loadProjectPlan } from './projectPlan'
45
import { loadProjectsMetadata } from '../../actions/templates'
5-
import { loadProductTimelineWithMilestones } from './productsTimelines'
6-
import { loadFeedsForPhases } from './phasesTopics'
7-
import { LOAD_PROJECT_DASHBOARD,
8-
LOAD_ADDITIONAL_PROJECT_DATA,
9-
DISCOURSE_BOT_USERID,
10-
CODER_BOT_USERID,
11-
TC_SYSTEM_USERID
12-
} from '../../config/constants'
6+
import { LOAD_PROJECT_DASHBOARD, LOAD_ADDITIONAL_PROJECT_DATA } from '../../config/constants'
137

148
/**
159
* Load all project data to paint the dashboard
@@ -44,28 +38,7 @@ const getDashboardData = (dispatch, getState, projectId, isOnlyLoadProjectInfo)
4438
// for new projects load phases, products, project template and product templates
4539
if (project.version === 'v3') {
4640
promises.push(
47-
dispatch(loadProjectPhasesWithProducts(projectId))
48-
.then(({ value: phases }) => {
49-
loadFeedsForPhases(projectId, phases, dispatch)
50-
.then((phaseFeeds) => {
51-
let phaseUserIds = []
52-
_.forEach(phaseFeeds, phaseFeed => {
53-
phaseUserIds = _.union(phaseUserIds, _.map(phaseFeed.topics, 'userId'))
54-
_.forEach(phaseFeed.topics, topic => {
55-
phaseUserIds = _.union(phaseUserIds, _.map(topic.posts, 'userId'))
56-
})
57-
// this is to remove any nulls from the list (dev had some bad data)
58-
_.remove(phaseUserIds, i => !i || [DISCOURSE_BOT_USERID, CODER_BOT_USERID, TC_SYSTEM_USERID].indexOf(i) > -1)
59-
})
60-
// take difference of userIds identified from project members
61-
phaseUserIds = _.difference(phaseUserIds, userIds)
62-
63-
dispatch(loadMembers(phaseUserIds))
64-
})
65-
// load timelines for phase products here together with all dashboard data
66-
// as we need to know timeline data not only inside timeline container
67-
loadTimelinesForPhasesProducts(phases, dispatch)
68-
})
41+
dispatch(loadProjectPlan(projectId, userIds))
6942
)
7043
}
7144

@@ -96,26 +69,6 @@ const getData = (dispatch, getState, projectId, isOnlyLoadProjectInfo) => {
9669
.catch(() => getDashboardData(dispatch, getState, projectId, isOnlyLoadProjectInfo))
9770
}
9871

99-
/**
100-
* Load timelines for phase's products
101-
*
102-
* @param {Array} phases list of phases
103-
* @param {Function} dispatch dispatch function
104-
*/
105-
function loadTimelinesForPhasesProducts(phases, dispatch) {
106-
const products = []
107-
108-
phases.forEach((phase) => {
109-
phase.products.forEach((product) => {
110-
products.push(product)
111-
})
112-
})
113-
114-
return Promise.all(
115-
products.map((product) => dispatch(loadProductTimelineWithMilestones(product.id)))
116-
)
117-
}
118-
11972
export function loadProjectDashboard(projectId, isOnlyLoadProjectInfo = false) {
12073
return (dispatch, getState) => {
12174
return dispatch({
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import _ from 'lodash'
2+
import { loadMembers } from '../../actions/members'
3+
import { loadProjectPhasesWithProducts } from './project'
4+
import { loadFeedsForPhases } from './phasesTopics'
5+
import { loadProductTimelineWithMilestones } from './productsTimelines'
6+
import {
7+
DISCOURSE_BOT_USERID,
8+
CODER_BOT_USERID,
9+
TC_SYSTEM_USERID
10+
} from '../../config/constants'
11+
12+
13+
/**
14+
* Load timelines for phase's products
15+
*
16+
* @param {Array} phases list of phases
17+
* @param {Function} dispatch dispatch function
18+
*/
19+
function loadTimelinesForPhasesProducts(phases, dispatch) {
20+
const products = []
21+
22+
phases.forEach((phase) => {
23+
phase.products.forEach((product) => {
24+
products.push(product)
25+
})
26+
})
27+
28+
return Promise.all(
29+
products.map((product) => dispatch(loadProductTimelineWithMilestones(product.id)))
30+
)
31+
}
32+
33+
export function loadProjectPlan(projectId, existingUserIds) {
34+
return (dispatch) => {
35+
return dispatch(loadProjectPhasesWithProducts(projectId))
36+
.then(({ value: phases }) => {
37+
loadFeedsForPhases(projectId, phases, dispatch)
38+
.then((phaseFeeds) => {
39+
let phaseUserIds = []
40+
_.forEach(phaseFeeds, phaseFeed => {
41+
phaseUserIds = _.union(phaseUserIds, _.map(phaseFeed.topics, 'userId'))
42+
_.forEach(phaseFeed.topics, topic => {
43+
phaseUserIds = _.union(phaseUserIds, _.map(topic.posts, 'userId'))
44+
})
45+
// this is to remove any nulls from the list (dev had some bad data)
46+
_.remove(phaseUserIds, i => !i || [DISCOURSE_BOT_USERID, CODER_BOT_USERID, TC_SYSTEM_USERID].indexOf(i) > -1)
47+
})
48+
// take difference of existingUserIds identified from project members
49+
phaseUserIds = _.difference(phaseUserIds, existingUserIds)
50+
51+
dispatch(loadMembers(phaseUserIds))
52+
})
53+
// load timelines for phase products here together with all dashboard data
54+
// as we need to know timeline data not only inside timeline container
55+
loadTimelinesForPhasesProducts(phases, dispatch)
56+
})
57+
}
58+
}

src/projects/detail/ProjectDetail.jsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,6 @@ const ProjectDetailView = (props) => {
6464
if (!currentMemberRole && props.currentUserRoles && props.currentUserRoles.length > 0) {
6565
currentMemberRole = props.currentUserRoles[0]
6666
}
67-
const regex = /#(feed-([0-9]+)|comment-([0-9]+))/
68-
const match = props.location.hash.match(regex)
69-
const ids = match ? { feedId: match[2], commentId: match[3] } : {}
7067

7168
const { component: Component } = props
7269
const componentProps = {
@@ -79,7 +76,7 @@ const ProjectDetailView = (props) => {
7976
isProcessing: props.isProcessing,
8077
allProductTemplates: props.allProductTemplates,
8178
productsTimelines: props.productsTimelines,
82-
...ids
79+
location: props.location,
8380
}
8481
return <Component {...componentProps} />
8582
}

src/projects/detail/components/EditProjectForm/EditProjectForm.jsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ class EditProjectForm extends Component {
268268

269269

270270
render() {
271-
const { isEdittable, showHidden, productTemplates, productCategories } = this.props
271+
const { isEdittable, showHidden, productTemplates, productCategories, disableAutoScrolling } = this.props
272272
const { template } = this.state
273273
const { project, dirtyProject } = this.state
274274
const onLeaveMessage = this.onLeave() || ''
@@ -287,6 +287,7 @@ class EditProjectForm extends Component {
287287
sectionNumber={idx + 1}
288288
resetFeatures={this.onFeaturesSaveAttachedClick}
289289
showFeaturesDialog={this.showFeaturesDialog}
290+
disableAutoScrolling={disableAutoScrolling}
290291
// TODO we shoudl not update the props (section is coming from props)
291292
validate={(isInvalid) => section.isInvalid = isInvalid}
292293
showHidden={showHidden}
@@ -353,7 +354,8 @@ class EditProjectForm extends Component {
353354
}
354355

355356
EditProjectForm.defaultProps = {
356-
shouldUpdateTemplate: false
357+
shouldUpdateTemplate: false,
358+
disableAutoScrolling: false,
357359
}
358360

359361
EditProjectForm.propTypes = {
@@ -371,6 +373,7 @@ EditProjectForm.propTypes = {
371373
updateAttachment: PropTypes.func.isRequired,
372374
removeAttachment: PropTypes.func.isRequired,
373375
shouldUpdateTemplate: PropTypes.bool,
376+
disableAutoScrolling: PropTypes.bool,
374377
}
375378

376379
export default EditProjectForm

src/projects/detail/components/PhaseFeed/PhaseFeed.jsx

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,49 @@
55
*/
66
import React from 'react'
77
import _ from 'lodash'
8+
import { withRouter } from 'react-router-dom'
89

910
import ScrollableFeed from '../../../../components/Feed/ScrollableFeed'
1011
import spinnerWhileLoading from '../../../../components/LoadingSpinner'
12+
import { scrollToHash } from '../../../../components/ScrollToAnchors'
1113

1214
import './PhaseFeed.scss'
1315

14-
const PhaseFeedView = (props) => (
15-
<div styleName="container">
16-
<ScrollableFeed
17-
{...{
18-
..._.omit(props, 'feed'),
19-
...props.feed,
20-
id: (props.feed ? props.feed.id.toString() : '0'),
21-
commentId: props.commentId
22-
}}
23-
/>
24-
</div>
25-
)
16+
class PhaseFeedView extends React.Component {
17+
constructor(props) {
18+
super(props)
19+
}
20+
21+
componentDidMount() {
22+
!_.isEmpty(this.props.location.hash) && this.handleUrlHash(this.props)
23+
}
24+
25+
// when the phase feed is actually loaded/rendered scroll to the appropriate post depending on url hash
26+
handleUrlHash(props) {
27+
const hashParts = _.split(location.hash.substring(1), '-')
28+
const phaseId = hashParts[0] === 'phase' ? parseInt(hashParts[1], 10) : null
29+
if (phaseId === props.phaseId) {
30+
setTimeout(() => scrollToHash(props.location.hash), 100)
31+
}
32+
}
33+
34+
render() {
35+
return (
36+
<div styleName="container">
37+
<ScrollableFeed
38+
{...{
39+
..._.omit(this.props, 'feed'),
40+
...this.props.feed,
41+
id: (this.props.feed ? this.props.feed.id.toString() : '0'),
42+
commentId: this.props.commentId
43+
}}
44+
/>
45+
</div>
46+
)
47+
}
48+
}
2649

2750
const enhance = spinnerWhileLoading(props => !props.isLoading)
28-
const EnhancedPhaseFeedView = enhance(PhaseFeedView)
51+
const EnhancedPhaseFeedView = enhance(withRouter(PhaseFeedView))
2952

3053
export default EnhancedPhaseFeedView

src/projects/detail/components/ProjectStage.jsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React from 'react'
55
import PT from 'prop-types'
66
import _ from 'lodash'
77
import uncontrollable from 'uncontrollable'
8+
import { withRouter } from 'react-router-dom'
89

910
import { formatNumberWithCommas } from '../../../helpers/format'
1011
import { getPhaseActualData } from '../../../helpers/projectHelper'
@@ -22,7 +23,6 @@ import ProductTimelineContainer from '../containers/ProductTimelineContainer'
2223
import NotificationsReader from '../../../components/NotificationsReader'
2324
import { phaseFeedHOC } from '../containers/PhaseFeedHOC'
2425
import spinnerWhileLoading from '../../../components/LoadingSpinner'
25-
import { scrollToHash } from '../../../components/ScrollToAnchors'
2626

2727
const enhance = spinnerWhileLoading(props => !props.processing)
2828
const EnhancedEditProjectForm = enhance(EditProjectForm)
@@ -130,23 +130,28 @@ class ProjectStage extends React.Component{
130130
expandProjectPhase(phase.id, tab)
131131
}
132132

133-
componentDidUpdate() {
134-
const { phaseState } = this.props
135-
if (_.get(phaseState, 'isExpanded')) {
136-
const scrollTo = window.location.hash ? window.location.hash.substring(1) : null
137-
if (scrollTo) {
138-
scrollToHash(scrollTo)
139-
}
140-
}
133+
componentDidMount() {
134+
!_.isEmpty(this.props.location.hash) && this.handleUrlHash(this.props)
141135
}
142136

143-
componentWillReceiveProps(nextProps) {
144-
const { feedId, commentId, phase, phaseState, expandProjectPhase } = this.props
145-
const { feed } = nextProps
146-
if (!_.get(phaseState, 'isExpanded') && feed && (feed.id === parseInt(feedId) || feed.postIds.includes(parseInt(commentId)))){
147-
expandProjectPhase(phase.id, 'posts')
137+
componentDidUpdate(prevProps) {
138+
const { location } = this.props
139+
if (!_.isEmpty(location.hash) && location.hash !== prevProps.location.hash) {
140+
this.handleUrlHash(this.props)
148141
}
142+
}
143+
144+
// expand a phase if necessary depending on the url hash
145+
handleUrlHash(props) {
146+
const { expandProjectPhase, phase, location } = props
149147

148+
const hashParts = _.split(location.hash.substring(1), '-')
149+
const phaseId = hashParts[0] === 'phase' ? parseInt(hashParts[1], 10) : null
150+
151+
if (phaseId && phase.id === phaseId) {
152+
const tab = hashParts[2]
153+
expandProjectPhase(phaseId, tab)
154+
}
150155
}
151156

152157
render() {
@@ -169,6 +174,7 @@ class ProjectStage extends React.Component{
169174
collapseProjectPhase,
170175
expandProjectPhase,
171176
commentAnchorPrefix,
177+
isLoading,
172178

173179
// comes from phaseFeedHOC
174180
currentUser,
@@ -253,6 +259,8 @@ class ProjectStage extends React.Component{
253259
projectMembers={projectMembers}
254260
onSaveMessage={onSaveMessage}
255261
commentAnchorPrefix={commentAnchorPrefix}
262+
phaseId={phase.id}
263+
isLoading={isLoading}
256264
/>
257265
)}
258266

@@ -278,6 +286,7 @@ class ProjectStage extends React.Component{
278286
removeAttachment={this.removeProductAttachment}
279287
attachmentsStorePath={attachmentsStorePath}
280288
canManageAttachments={!!currentMemberRole}
289+
disableAutoScrolling
281290
/>
282291
</div>
283292
}
@@ -311,7 +320,7 @@ ProjectStage.propTypes = {
311320
commentAnchorPrefix: PT.string,
312321
}
313322

314-
const ProjectStageUncontrollable = uncontrollable(ProjectStage, {
323+
const ProjectStageUncontrollable = uncontrollable(withRouter(ProjectStage), {
315324
activeTab: 'onTabClick',
316325
})
317326

src/projects/detail/components/ProjectStages.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { withRouter } from 'react-router-dom'
1010
import { formatNumberWithCommas } from '../../../helpers/format'
1111
import { getPhaseActualData } from '../../../helpers/projectHelper'
1212

13+
import spinnerWhileLoading from '../../../components/LoadingSpinner'
1314
import Section from '../components/Section'
1415
import ProjectStage from '../components/ProjectStage'
1516
import PhaseCardListHeader from '../components/PhaseCardListHeader'
@@ -93,13 +94,15 @@ const ProjectStages = ({
9394
collapseProjectPhase,
9495
feedId,
9596
commentId,
97+
location
9698
}) => (
9799
<Section>
98100

99101
<PhaseCardListHeader {...formatPhaseCardListHeaderProps(phases)}/>
100102
{
101103
phases.map((phase, index) => (
102104
<ProjectStage
105+
location={location}
103106
key={phase.id}
104107
phaseState={phasesStates[phase.id]}
105108
productTemplates={productTemplates}
@@ -151,6 +154,10 @@ ProjectStages.propTypes = {
151154
updateProductAttachment: PT.func.isRequired,
152155
removeProductAttachment: PT.func.isRequired,
153156
deleteProjectPhase: PT.func.isRequired,
157+
isLoadingPhases: PT.bool.isRequired,
154158
}
155159

156-
export default withRouter(ProjectStages)
160+
const enhance = spinnerWhileLoading(props => !props.isLoadingPhases)
161+
const EnhancedProjectStages = enhance(ProjectStages)
162+
163+
export default withRouter(EnhancedProjectStages)

src/projects/detail/components/SpecSection.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ SpecSection.propTypes = {
425425
updateAttachment: PropTypes.func,
426426
removeAttachment: PropTypes.func,
427427
productCategories: PropTypes.array.isRequired,
428+
disableAutoScrolling: PropTypes.bool,
428429
}
429430

430431
export default scrollToAnchors(SpecSection)

0 commit comments

Comments
 (0)