Skip to content

Commit ead28d2

Browse files
authored
Merge pull request #1613 from topcoder-platform/PM-875_project-filters
PM-875 - move projects to new container, add filters
2 parents bf96af0 + f5feace commit ead28d2

File tree

12 files changed

+383
-105
lines changed

12 files changed

+383
-105
lines changed

src/actions/projects.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1+
import _ from 'lodash'
2+
13
import {
4+
PROJECT_TYPE_TAAS,
5+
PROJECTS_PAGE_SIZE,
6+
LOAD_PROJECTS_PENDING,
7+
LOAD_PROJECTS_SUCCESS,
8+
UNLOAD_PROJECTS_SUCCESS,
9+
LOAD_PROJECTS_FAILURE,
210
LOAD_PROJECT_BILLING_ACCOUNT,
311
LOAD_CHALLENGE_MEMBERS_SUCCESS,
412
LOAD_PROJECT_DETAILS,
@@ -21,8 +29,96 @@ import {
2129
getProjectTypes,
2230
createProjectApi,
2331
fetchBillingAccounts,
32+
fetchMemberProjects,
2433
updateProjectApi
2534
} from '../services/projects'
35+
import { checkAdmin } from '../util/tc'
36+
37+
function _loadProjects (projectNameOrIdFilter = '', paramFilters = {}) {
38+
return (dispatch, getState) => {
39+
dispatch({
40+
type: LOAD_PROJECTS_PENDING
41+
})
42+
43+
const filters = {
44+
sort: 'lastActivityAt desc',
45+
perPage: PROJECTS_PAGE_SIZE,
46+
...paramFilters
47+
}
48+
49+
if (!_.isEmpty(projectNameOrIdFilter)) {
50+
if (!isNaN(projectNameOrIdFilter)) { // if it is number
51+
filters['id'] = parseInt(projectNameOrIdFilter, 10)
52+
} else { // text search
53+
filters['keyword'] = decodeURIComponent(projectNameOrIdFilter)
54+
}
55+
}
56+
57+
if (!checkAdmin(getState().auth.token)) {
58+
filters['memberOnly'] = true
59+
}
60+
61+
// eslint-disable-next-line no-debugger
62+
const state = getState().projects
63+
fetchMemberProjects(filters).then(({ projects, pagination }) => dispatch({
64+
filters,
65+
type: LOAD_PROJECTS_SUCCESS,
66+
projects: _.uniqBy((filters.page ? state.projects || [] : []).concat(projects), 'id'),
67+
total: pagination.xTotal,
68+
page: pagination.xPage
69+
})).catch(() => dispatch({
70+
type: LOAD_PROJECTS_FAILURE
71+
}))
72+
}
73+
}
74+
75+
export function loadProjects (projectNameOrIdFilter = '', paramFilters = {}) {
76+
return async (dispatch, getState) => {
77+
const _filters = _.assign({}, paramFilters)
78+
if (_.isEmpty(_filters) || !_filters.type) {
79+
let projectTypes = getState().projects.projectTypes
80+
81+
if (!projectTypes.length) {
82+
dispatch({
83+
type: LOAD_PROJECTS_PENDING
84+
})
85+
await loadProjectTypes()(dispatch)
86+
projectTypes = getState().projects.projectTypes
87+
}
88+
89+
_.assign(_filters, {
90+
type: projectTypes.filter(d => d.key !== PROJECT_TYPE_TAAS).map(d => d.key)
91+
})
92+
}
93+
94+
return _loadProjects(projectNameOrIdFilter, _filters)(dispatch, getState)
95+
}
96+
}
97+
98+
/**
99+
* Load more projects for the authenticated user
100+
*/
101+
export function loadMoreProjects () {
102+
return (dispatch, getState) => {
103+
const { projectFilters, projectsPage } = getState().projects
104+
105+
loadProjects('', _.assign({}, projectFilters, {
106+
perPage: PROJECTS_PAGE_SIZE,
107+
page: projectsPage + 1
108+
}))(dispatch, getState)
109+
}
110+
}
111+
112+
/**
113+
* Unloads projects of the authenticated user
114+
*/
115+
export function unloadProjects () {
116+
return (dispatch) => {
117+
dispatch({
118+
type: UNLOAD_PROJECTS_SUCCESS
119+
})
120+
}
121+
}
26122

27123
/**
28124
* Loads project details

src/actions/sidebar.js

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export function loadProjects (filterProjectName = '', paramFilters = {}) {
3737
})
3838

3939
const filters = {
40+
status: 'active',
4041
sort: 'lastActivityAt desc',
4142
perPage: PROJECTS_PAGE_SIZE,
4243
...paramFilters
@@ -66,26 +67,6 @@ export function loadProjects (filterProjectName = '', paramFilters = {}) {
6667
}
6768
}
6869

69-
/**
70-
* Load more projects for the authenticated user
71-
*/
72-
export function loadMoreProjects (filterProjectName = '', paramFilters = {}) {
73-
return (dispatch, getState) => {
74-
const state = getState().sidebar
75-
76-
loadProjects(filterProjectName, _.assignIn({}, paramFilters, {
77-
perPage: PROJECTS_PAGE_SIZE,
78-
page: state.page + 1
79-
}))(dispatch, getState)
80-
}
81-
}
82-
83-
export function loadTaasProjects (filterProjectName = '', paramFilters = {}) {
84-
return loadProjects(filterProjectName, Object.assign({
85-
type: 'talent-as-a-service'
86-
}, paramFilters))
87-
}
88-
8970
/**
9071
* Unloads projects of the authenticated user
9172
*/

src/components/ProjectCard/index.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ import { PROJECT_STATUSES } from '../../config/constants'
88

99
import styles from './ProjectCard.module.scss'
1010

11-
const ProjectCard = ({ projectName, projectStatus, projectId, selected, setActiveProject }) => {
11+
const ProjectCard = ({ projectName, projectStatus, projectId, selected }) => {
1212
return (
1313
<div className={styles.container}>
1414
<Link
1515
to={`/projects/${projectId}/challenges`}
1616
className={cn(styles.projectName, { [styles.selected]: selected })}
17-
onClick={() => setActiveProject(parseInt(projectId))}
1817
>
1918
<div className={styles.name}>
2019
<span>{projectName}</span>
@@ -29,8 +28,7 @@ ProjectCard.propTypes = {
2928
projectStatus: PT.string.isRequired,
3029
projectId: PT.number.isRequired,
3130
projectName: PT.string.isRequired,
32-
selected: PT.bool.isRequired,
33-
setActiveProject: PT.func
31+
selected: PT.bool
3432
}
3533

3634
export default ProjectCard

src/config/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,3 +451,5 @@ export const ATTACHMENT_TYPE_LINK = 'link'
451451
*/
452452
export const PROJECT_ASSETS_SHARED_WITH_ALL_MEMBERS = 'All Project Members'
453453
export const PROJECT_ASSETS_SHARED_WITH_ADMIN = 'Only Admins'
454+
455+
export const PROJECT_TYPE_TAAS = 'talent-as-a-service'

src/containers/Challenges/index.js

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ import React, { Component, Fragment } from 'react'
66
// import { Redirect } from 'react-router-dom'
77
import PropTypes from 'prop-types'
88
import { connect } from 'react-redux'
9-
import { Link } from 'react-router-dom'
109
import ChallengesComponent from '../../components/ChallengesComponent'
11-
import ProjectCard from '../../components/ProjectCard'
1210
// import Loader from '../../components/Loader'
1311
import {
1412
loadChallengesByPage,
@@ -18,15 +16,11 @@ import {
1816
} from '../../actions/challenges'
1917
import { loadProject, updateProject } from '../../actions/projects'
2018
import {
21-
loadMoreProjects,
2219
loadProjects,
2320
setActiveProject,
2421
resetSidebarActiveParams
2522
} from '../../actions/sidebar'
26-
import styles from './Challenges.module.scss'
27-
import { checkAdmin, checkAdminOrCopilot } from '../../util/tc'
28-
import { PrimaryButton } from '../../components/Buttons'
29-
import InfiniteLoadTrigger from '../../components/InfiniteLoadTrigger'
23+
import { checkAdmin } from '../../util/tc'
3024

3125
class Challenges extends Component {
3226
constructor (props) {
@@ -145,46 +139,11 @@ class Challenges extends Component {
145139
metadata
146140
} = this.props
147141
const { challengeTypes = [] } = metadata
148-
const projectInfo = _.find(projects, { id: activeProjectId }) || {}
149-
const projectComponents =
150-
!dashboard &&
151-
projects.map((p) => (
152-
<li key={p.id}>
153-
<ProjectCard
154-
projectStatus={p.status}
155-
projectName={p.name}
156-
projectId={p.id}
157-
selected={activeProjectId === `${p.id}`}
158-
setActiveProject={setActiveProject}
159-
/>
160-
</li>
161-
))
162142
return (
163143
<Fragment>
164-
{!dashboard &&
165-
(!!projectComponents.length ||
166-
(activeProjectId === -1 && !selfService)) ? (
167-
<div className={!dashboard && styles.projectSearch}>
168-
{activeProjectId === -1 && !selfService && (
169-
<div className={styles.buttonNewProjectWrapper}>
170-
<div>No project selected. Select one below</div>
171-
{checkAdminOrCopilot(auth.token) && (
172-
<Link className={styles.buttonNewProject} to={`/projects/new`}>
173-
<PrimaryButton text={'Create Project'} type={'info'} />
174-
</Link>
175-
)}
176-
</div>
177-
)}
178-
<ul>{projectComponents}</ul>
179-
{projects && !!projects.length && (
180-
<InfiniteLoadTrigger onLoadMore={this.props.loadMoreProjects} />
181-
)}
182-
</div>
183-
) : null}
184144
{(dashboard || activeProjectId !== -1 || selfService) && (
185145
<ChallengesComponent
186146
activeProject={{
187-
...projectInfo,
188147
...(reduxProjectInfo && reduxProjectInfo.id === activeProjectId
189148
? reduxProjectInfo
190149
: {})
@@ -270,7 +229,6 @@ Challenges.propTypes = {
270229
dashboard: PropTypes.bool,
271230
auth: PropTypes.object.isRequired,
272231
loadChallengeTypes: PropTypes.func,
273-
loadMoreProjects: PropTypes.func,
274232
metadata: PropTypes.shape({
275233
challengeTypes: PropTypes.array
276234
})
@@ -298,7 +256,6 @@ const mapStateToProps = ({ challenges, sidebar, projects, auth }) => ({
298256
const mapDispatchToProps = {
299257
loadChallengesByPage,
300258
resetSidebarActiveParams,
301-
loadMoreProjects,
302259
loadProject,
303260
loadProjects,
304261
updateProject,

src/containers/Projects/index.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import React, { useEffect, useMemo, useState } from 'react'
2+
import cn from 'classnames'
3+
import { DebounceInput } from 'react-debounce-input'
4+
import { withRouter, Link } from 'react-router-dom'
5+
import { connect } from 'react-redux'
6+
import PropTypes from 'prop-types'
7+
import Loader from '../../components/Loader'
8+
import { checkAdminOrCopilot } from '../../util/tc'
9+
import { PrimaryButton } from '../../components/Buttons'
10+
import Select from '../../components/Select'
11+
import ProjectCard from '../../components/ProjectCard'
12+
import InfiniteLoadTrigger from '../../components/InfiniteLoadTrigger'
13+
import { loadProjects, loadMoreProjects, unloadProjects } from '../../actions/projects'
14+
import { PROJECT_STATUSES } from '../../config/constants'
15+
16+
import styles from './styles.module.scss'
17+
18+
const Projects = ({ projects, auth, isLoading, projectsCount, loadProjects, loadMoreProjects, unloadProjects }) => {
19+
const [search, setSearch] = useState()
20+
const [projectStatus, setProjectStatus] = useState('')
21+
const selectedStatus = useMemo(() => PROJECT_STATUSES.find(s => s.value === projectStatus))
22+
23+
useEffect(() => {
24+
loadProjects(search, projectStatus ? { status: projectStatus } : {})
25+
}, [search, projectStatus])
26+
27+
// unload projects on dismount
28+
useEffect(() => () => unloadProjects, [])
29+
30+
if (isLoading && projects.length === 0) {
31+
return (
32+
<div className={styles.container}>
33+
<Loader />
34+
</div>
35+
)
36+
}
37+
38+
return (
39+
<div className={styles.container}>
40+
<div className={styles.headerLine}>
41+
<h2>Projects</h2>
42+
{checkAdminOrCopilot(auth.token) && (
43+
<Link className={styles.buttonNewProject} to={`/projects/new`}>
44+
<PrimaryButton text={'New Project'} type={'info'} />
45+
</Link>
46+
)}
47+
</div>
48+
<div className={styles.searchWrapper}>
49+
<div className={styles['col-6']}>
50+
<div className={cn(styles.field, styles.input1)}>
51+
<label>Search :</label>
52+
</div>
53+
<div className={styles.searchInputWrapper}>
54+
<DebounceInput
55+
className={styles.searchInput}
56+
minLength={2}
57+
debounceTimeout={300}
58+
placeholder='Keyword'
59+
onChange={e => setSearch(e.target.value)}
60+
value={search}
61+
/>
62+
</div>
63+
</div>
64+
<div className={styles['col-6']}>
65+
<div className={cn(styles.field, styles.input1)}>
66+
<label>Project Status:</label>
67+
</div>
68+
<div className={styles.searchInputWrapper}>
69+
<Select
70+
name='projectStatus'
71+
options={PROJECT_STATUSES}
72+
placeholder='All'
73+
value={selectedStatus}
74+
onChange={e => setProjectStatus(e ? e.value : '')}
75+
isClearable
76+
/>
77+
</div>
78+
</div>
79+
</div>
80+
{projects.length > 0 ? (
81+
<>
82+
<ul>
83+
{projects.map(p => (
84+
<li key={p.id}>
85+
<ProjectCard
86+
projectStatus={p.status}
87+
projectName={p.name}
88+
projectId={p.id}
89+
/>
90+
</li>
91+
))}
92+
</ul>
93+
{projects && projects.length < projectsCount - 1 && (
94+
// fix
95+
<InfiniteLoadTrigger onLoadMore={loadMoreProjects} />
96+
)}
97+
</>
98+
) : (
99+
<span>No projects available yet</span>
100+
)}
101+
</div>
102+
)
103+
}
104+
105+
Projects.propTypes = {
106+
projectsCount: PropTypes.number.isRequired,
107+
projects: PropTypes.array,
108+
auth: PropTypes.object.isRequired,
109+
isLoading: PropTypes.bool.isRequired,
110+
unloadProjects: PropTypes.func.isRequired,
111+
loadProjects: PropTypes.func.isRequired,
112+
loadMoreProjects: PropTypes.func.isRequired
113+
}
114+
115+
const mapStateToProps = ({ projects, auth }) => {
116+
return {
117+
projectsCount: projects.projectsCount,
118+
projects: projects.projects,
119+
isLoading: projects.isLoading,
120+
auth
121+
}
122+
}
123+
124+
const mapDispatchToProps = {
125+
unloadProjects: unloadProjects,
126+
loadProjects: loadProjects,
127+
loadMoreProjects: loadMoreProjects
128+
}
129+
130+
export default withRouter(
131+
connect(mapStateToProps, mapDispatchToProps)(Projects)
132+
)

0 commit comments

Comments
 (0)