Skip to content

Commit

Permalink
Merge pull request #1613 from topcoder-platform/PM-875_project-filters
Browse files Browse the repository at this point in the history
PM-875 - move projects to new container, add filters
  • Loading branch information
vas3a authored Mar 4, 2025
2 parents bf96af0 + f5feace commit ead28d2
Show file tree
Hide file tree
Showing 12 changed files with 383 additions and 105 deletions.
96 changes: 96 additions & 0 deletions src/actions/projects.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import _ from 'lodash'

import {
PROJECT_TYPE_TAAS,
PROJECTS_PAGE_SIZE,
LOAD_PROJECTS_PENDING,
LOAD_PROJECTS_SUCCESS,
UNLOAD_PROJECTS_SUCCESS,
LOAD_PROJECTS_FAILURE,
LOAD_PROJECT_BILLING_ACCOUNT,
LOAD_CHALLENGE_MEMBERS_SUCCESS,
LOAD_PROJECT_DETAILS,
Expand All @@ -21,8 +29,96 @@ import {
getProjectTypes,
createProjectApi,
fetchBillingAccounts,
fetchMemberProjects,
updateProjectApi
} from '../services/projects'
import { checkAdmin } from '../util/tc'

function _loadProjects (projectNameOrIdFilter = '', paramFilters = {}) {
return (dispatch, getState) => {
dispatch({
type: LOAD_PROJECTS_PENDING
})

const filters = {
sort: 'lastActivityAt desc',
perPage: PROJECTS_PAGE_SIZE,
...paramFilters
}

if (!_.isEmpty(projectNameOrIdFilter)) {
if (!isNaN(projectNameOrIdFilter)) { // if it is number
filters['id'] = parseInt(projectNameOrIdFilter, 10)
} else { // text search
filters['keyword'] = decodeURIComponent(projectNameOrIdFilter)
}
}

if (!checkAdmin(getState().auth.token)) {
filters['memberOnly'] = true
}

// eslint-disable-next-line no-debugger
const state = getState().projects
fetchMemberProjects(filters).then(({ projects, pagination }) => dispatch({
filters,
type: LOAD_PROJECTS_SUCCESS,
projects: _.uniqBy((filters.page ? state.projects || [] : []).concat(projects), 'id'),
total: pagination.xTotal,
page: pagination.xPage
})).catch(() => dispatch({
type: LOAD_PROJECTS_FAILURE
}))
}
}

export function loadProjects (projectNameOrIdFilter = '', paramFilters = {}) {
return async (dispatch, getState) => {
const _filters = _.assign({}, paramFilters)
if (_.isEmpty(_filters) || !_filters.type) {
let projectTypes = getState().projects.projectTypes

if (!projectTypes.length) {
dispatch({
type: LOAD_PROJECTS_PENDING
})
await loadProjectTypes()(dispatch)
projectTypes = getState().projects.projectTypes
}

_.assign(_filters, {
type: projectTypes.filter(d => d.key !== PROJECT_TYPE_TAAS).map(d => d.key)
})
}

return _loadProjects(projectNameOrIdFilter, _filters)(dispatch, getState)
}
}

/**
* Load more projects for the authenticated user
*/
export function loadMoreProjects () {
return (dispatch, getState) => {
const { projectFilters, projectsPage } = getState().projects

loadProjects('', _.assign({}, projectFilters, {
perPage: PROJECTS_PAGE_SIZE,
page: projectsPage + 1
}))(dispatch, getState)
}
}

/**
* Unloads projects of the authenticated user
*/
export function unloadProjects () {
return (dispatch) => {
dispatch({
type: UNLOAD_PROJECTS_SUCCESS
})
}
}

/**
* Loads project details
Expand Down
21 changes: 1 addition & 20 deletions src/actions/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function loadProjects (filterProjectName = '', paramFilters = {}) {
})

const filters = {
status: 'active',
sort: 'lastActivityAt desc',
perPage: PROJECTS_PAGE_SIZE,
...paramFilters
Expand Down Expand Up @@ -66,26 +67,6 @@ export function loadProjects (filterProjectName = '', paramFilters = {}) {
}
}

/**
* Load more projects for the authenticated user
*/
export function loadMoreProjects (filterProjectName = '', paramFilters = {}) {
return (dispatch, getState) => {
const state = getState().sidebar

loadProjects(filterProjectName, _.assignIn({}, paramFilters, {
perPage: PROJECTS_PAGE_SIZE,
page: state.page + 1
}))(dispatch, getState)
}
}

export function loadTaasProjects (filterProjectName = '', paramFilters = {}) {
return loadProjects(filterProjectName, Object.assign({
type: 'talent-as-a-service'
}, paramFilters))
}

/**
* Unloads projects of the authenticated user
*/
Expand Down
6 changes: 2 additions & 4 deletions src/components/ProjectCard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ import { PROJECT_STATUSES } from '../../config/constants'

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

const ProjectCard = ({ projectName, projectStatus, projectId, selected, setActiveProject }) => {
const ProjectCard = ({ projectName, projectStatus, projectId, selected }) => {
return (
<div className={styles.container}>
<Link
to={`/projects/${projectId}/challenges`}
className={cn(styles.projectName, { [styles.selected]: selected })}
onClick={() => setActiveProject(parseInt(projectId))}
>
<div className={styles.name}>
<span>{projectName}</span>
Expand All @@ -29,8 +28,7 @@ ProjectCard.propTypes = {
projectStatus: PT.string.isRequired,
projectId: PT.number.isRequired,
projectName: PT.string.isRequired,
selected: PT.bool.isRequired,
setActiveProject: PT.func
selected: PT.bool
}

export default ProjectCard
2 changes: 2 additions & 0 deletions src/config/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -451,3 +451,5 @@ export const ATTACHMENT_TYPE_LINK = 'link'
*/
export const PROJECT_ASSETS_SHARED_WITH_ALL_MEMBERS = 'All Project Members'
export const PROJECT_ASSETS_SHARED_WITH_ADMIN = 'Only Admins'

export const PROJECT_TYPE_TAAS = 'talent-as-a-service'
45 changes: 1 addition & 44 deletions src/containers/Challenges/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import React, { Component, Fragment } from 'react'
// import { Redirect } from 'react-router-dom'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import ChallengesComponent from '../../components/ChallengesComponent'
import ProjectCard from '../../components/ProjectCard'
// import Loader from '../../components/Loader'
import {
loadChallengesByPage,
Expand All @@ -18,15 +16,11 @@ import {
} from '../../actions/challenges'
import { loadProject, updateProject } from '../../actions/projects'
import {
loadMoreProjects,
loadProjects,
setActiveProject,
resetSidebarActiveParams
} from '../../actions/sidebar'
import styles from './Challenges.module.scss'
import { checkAdmin, checkAdminOrCopilot } from '../../util/tc'
import { PrimaryButton } from '../../components/Buttons'
import InfiniteLoadTrigger from '../../components/InfiniteLoadTrigger'
import { checkAdmin } from '../../util/tc'

class Challenges extends Component {
constructor (props) {
Expand Down Expand Up @@ -145,46 +139,11 @@ class Challenges extends Component {
metadata
} = this.props
const { challengeTypes = [] } = metadata
const projectInfo = _.find(projects, { id: activeProjectId }) || {}
const projectComponents =
!dashboard &&
projects.map((p) => (
<li key={p.id}>
<ProjectCard
projectStatus={p.status}
projectName={p.name}
projectId={p.id}
selected={activeProjectId === `${p.id}`}
setActiveProject={setActiveProject}
/>
</li>
))
return (
<Fragment>
{!dashboard &&
(!!projectComponents.length ||
(activeProjectId === -1 && !selfService)) ? (
<div className={!dashboard && styles.projectSearch}>
{activeProjectId === -1 && !selfService && (
<div className={styles.buttonNewProjectWrapper}>
<div>No project selected. Select one below</div>
{checkAdminOrCopilot(auth.token) && (
<Link className={styles.buttonNewProject} to={`/projects/new`}>
<PrimaryButton text={'Create Project'} type={'info'} />
</Link>
)}
</div>
)}
<ul>{projectComponents}</ul>
{projects && !!projects.length && (
<InfiniteLoadTrigger onLoadMore={this.props.loadMoreProjects} />
)}
</div>
) : null}
{(dashboard || activeProjectId !== -1 || selfService) && (
<ChallengesComponent
activeProject={{
...projectInfo,
...(reduxProjectInfo && reduxProjectInfo.id === activeProjectId
? reduxProjectInfo
: {})
Expand Down Expand Up @@ -270,7 +229,6 @@ Challenges.propTypes = {
dashboard: PropTypes.bool,
auth: PropTypes.object.isRequired,
loadChallengeTypes: PropTypes.func,
loadMoreProjects: PropTypes.func,
metadata: PropTypes.shape({
challengeTypes: PropTypes.array
})
Expand Down Expand Up @@ -298,7 +256,6 @@ const mapStateToProps = ({ challenges, sidebar, projects, auth }) => ({
const mapDispatchToProps = {
loadChallengesByPage,
resetSidebarActiveParams,
loadMoreProjects,
loadProject,
loadProjects,
updateProject,
Expand Down
132 changes: 132 additions & 0 deletions src/containers/Projects/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React, { useEffect, useMemo, useState } from 'react'
import cn from 'classnames'
import { DebounceInput } from 'react-debounce-input'
import { withRouter, Link } from 'react-router-dom'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import Loader from '../../components/Loader'
import { checkAdminOrCopilot } from '../../util/tc'
import { PrimaryButton } from '../../components/Buttons'
import Select from '../../components/Select'
import ProjectCard from '../../components/ProjectCard'
import InfiniteLoadTrigger from '../../components/InfiniteLoadTrigger'
import { loadProjects, loadMoreProjects, unloadProjects } from '../../actions/projects'
import { PROJECT_STATUSES } from '../../config/constants'

import styles from './styles.module.scss'

const Projects = ({ projects, auth, isLoading, projectsCount, loadProjects, loadMoreProjects, unloadProjects }) => {
const [search, setSearch] = useState()
const [projectStatus, setProjectStatus] = useState('')
const selectedStatus = useMemo(() => PROJECT_STATUSES.find(s => s.value === projectStatus))

useEffect(() => {
loadProjects(search, projectStatus ? { status: projectStatus } : {})
}, [search, projectStatus])

// unload projects on dismount
useEffect(() => () => unloadProjects, [])

if (isLoading && projects.length === 0) {
return (
<div className={styles.container}>
<Loader />
</div>
)
}

return (
<div className={styles.container}>
<div className={styles.headerLine}>
<h2>Projects</h2>
{checkAdminOrCopilot(auth.token) && (
<Link className={styles.buttonNewProject} to={`/projects/new`}>
<PrimaryButton text={'New Project'} type={'info'} />
</Link>
)}
</div>
<div className={styles.searchWrapper}>
<div className={styles['col-6']}>
<div className={cn(styles.field, styles.input1)}>
<label>Search :</label>
</div>
<div className={styles.searchInputWrapper}>
<DebounceInput
className={styles.searchInput}
minLength={2}
debounceTimeout={300}
placeholder='Keyword'
onChange={e => setSearch(e.target.value)}
value={search}
/>
</div>
</div>
<div className={styles['col-6']}>
<div className={cn(styles.field, styles.input1)}>
<label>Project Status:</label>
</div>
<div className={styles.searchInputWrapper}>
<Select
name='projectStatus'
options={PROJECT_STATUSES}
placeholder='All'
value={selectedStatus}
onChange={e => setProjectStatus(e ? e.value : '')}
isClearable
/>
</div>
</div>
</div>
{projects.length > 0 ? (
<>
<ul>
{projects.map(p => (
<li key={p.id}>
<ProjectCard
projectStatus={p.status}
projectName={p.name}
projectId={p.id}
/>
</li>
))}
</ul>
{projects && projects.length < projectsCount - 1 && (
// fix
<InfiniteLoadTrigger onLoadMore={loadMoreProjects} />
)}
</>
) : (
<span>No projects available yet</span>
)}
</div>
)
}

Projects.propTypes = {
projectsCount: PropTypes.number.isRequired,
projects: PropTypes.array,
auth: PropTypes.object.isRequired,
isLoading: PropTypes.bool.isRequired,
unloadProjects: PropTypes.func.isRequired,
loadProjects: PropTypes.func.isRequired,
loadMoreProjects: PropTypes.func.isRequired
}

const mapStateToProps = ({ projects, auth }) => {
return {
projectsCount: projects.projectsCount,
projects: projects.projects,
isLoading: projects.isLoading,
auth
}
}

const mapDispatchToProps = {
unloadProjects: unloadProjects,
loadProjects: loadProjects,
loadMoreProjects: loadMoreProjects
}

export default withRouter(
connect(mapStateToProps, mapDispatchToProps)(Projects)
)
Loading

0 comments on commit ead28d2

Please sign in to comment.