diff --git a/.circleci/config.yml b/.circleci/config.yml index 7f1a247caf..4b55743ad8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -353,14 +353,17 @@ workflows: - PROD-4183 - changelog - remove_submission_review + - standardised_skills + - TSJR-275 - skills_updates + - IC-15 # This is alternate dev env for parallel testing - "build-test": context : org-global filters: branches: only: - - PROD-4251 + - IC-13 # This is alternate dev env for parallel testing - "build-qa": context : org-global diff --git a/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap b/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap index 5fd5c91cba..5882241535 100644 --- a/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/challenge-listing/__snapshots__/index.jsx.snap @@ -7,10 +7,12 @@ exports[`Matches shallow shapshot 1 shapshot 1 1`] = ` > @@ -70,10 +72,12 @@ exports[`Matches shallow shapshot 2 shapshot 2 1`] = ` > diff --git a/config/default.js b/config/default.js index a9d32159af..571a5d4645 100644 --- a/config/default.js +++ b/config/default.js @@ -476,4 +476,5 @@ module.exports = { MEMBER_PROFILE_REDIRECT_URL: 'https://profiles.topcoder-dev.com', MEMBER_SEARCH_REDIRECT_URL: 'https://talent-search.topcoder-dev.com', ACCOUNT_SETTINGS_REDIRECT_URL: 'https://account-settings.topcoder-dev.com', + INNOVATION_CHALLENGES_TAG: 'Innovation Challenge', }; diff --git a/src/shared/actions/challenge-listing/index.js b/src/shared/actions/challenge-listing/index.js index 15e5cba2e7..f7d109ca89 100644 --- a/src/shared/actions/challenge-listing/index.js +++ b/src/shared/actions/challenge-listing/index.js @@ -156,6 +156,27 @@ function getMyPastChallengesInit(uuid, page, frontFilter) { // return getAllActiveChallengesWithUsersDone(uuid, tokenV3, filter); // } +/** + * Extract search from front filter + * + * @param {Object} frontFilter + * @returns + */ +function extractSearchFilter(frontFilter = {}) { + const searchs = []; + if (frontFilter.search) { + searchs.push(frontFilter.search); + } + if (frontFilter.isInnovationChallenge === 'true') { + searchs.push('Innovation Challenge'); + } + + return { + search: _.uniq(searchs).join(' '), + isInnovationChallenge: '', // remove isInnovationChallenge from challenges query + }; +} + /** * Gets 1 page of active challenges (including marathon matches) from the backend. * Once this action is completed any active challenges saved to the state before @@ -178,6 +199,7 @@ function getActiveChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter backendFilter, frontFilter: { ...frontFilter, + ...extractSearchFilter(frontFilter), status: 'Active', currentPhaseName: 'Submission', registrationEndDateEnd: new Date().toISOString(), @@ -240,6 +262,7 @@ function getOpenForRegistrationChallengesDone(uuid, page, backendFilter, backendFilter, frontFilter: { ...frontFilter, + ...extractSearchFilter(frontFilter), status: 'Active', currentPhaseName: 'Registration', perPage: PAGE_SIZE, @@ -275,6 +298,7 @@ function getMyChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter = { backendFilter, frontFilter: { ...frontFilter, + ...extractSearchFilter(frontFilter), status: 'Active', memberId: userId, perPage: PAGE_SIZE, @@ -300,6 +324,7 @@ function getAllChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter = backendFilter, frontFilter: { ...frontFilter, + ...extractSearchFilter(frontFilter), status: 'Active', perPage: PAGE_SIZE, page: page + 1, @@ -325,6 +350,7 @@ function getMyPastChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter backendFilter, frontFilter: { ...frontFilter, + ...extractSearchFilter(frontFilter), status: 'Completed', memberId: userId, perPage: PAGE_SIZE, @@ -352,6 +378,7 @@ function getTotalChallengesCountDone(uuid, tokenV3, frontFilter = {}) { backendFilter: {}, frontFilter: { ...frontFilter, + ...extractSearchFilter(frontFilter), status: 'Active', isLightweight: true, perPage: 1, @@ -434,6 +461,7 @@ function getPastChallengesDone(uuid, page, backendFilter, tokenV3, frontFilter = backendFilter, frontFilter: { ...frontFilter, + ...extractSearchFilter(frontFilter), status: 'Completed', perPage: PAGE_SIZE, page: page + 1, diff --git a/src/shared/actions/dashboard.js b/src/shared/actions/dashboard.js index ce468c8e19..2810b4b042 100644 --- a/src/shared/actions/dashboard.js +++ b/src/shared/actions/dashboard.js @@ -1,16 +1,24 @@ -import _ from 'lodash'; import { createActions } from 'redux-actions'; import { getService } from '../services/dashboard'; const service = getService(); -function fetchChallenges(query) { - return service.getChallenges(query); +function fetchChallengesInit(title) { + return title; +} + +async function fetchChallenges(title, query) { + const challenges = await service.getChallenges(query); + + return { + challenges, + title, + }; } export default createActions({ DASHBOARD: { - FETCH_CHALLENGES_INIT: _.noop, + FETCH_CHALLENGES_INIT: fetchChallengesInit, FETCH_CHALLENGES_DONE: fetchChallenges, }, }); diff --git a/src/shared/components/Dashboard/Challenges/index.jsx b/src/shared/components/Dashboard/Challenges/index.jsx index bc8096a786..4f2829228b 100644 --- a/src/shared/components/Dashboard/Challenges/index.jsx +++ b/src/shared/components/Dashboard/Challenges/index.jsx @@ -2,6 +2,7 @@ import _ from 'lodash'; import LoadingIndicator from 'components/LoadingIndicator'; import PT from 'prop-types'; import React from 'react'; +import qs from 'qs'; import { config } from 'topcoder-react-utils'; @@ -11,14 +12,16 @@ export default function ChallengesFeed({ challenges, loading, theme, + title, + challengeListingQuery, }) { - return ( + return challenges && challenges.length ? (
- CHALLENGES + {title} View all challenges @@ -26,7 +29,7 @@ export default function ChallengesFeed({
{loading ?
- : challenges.map(challenge => ( + : (challenges || []).map(challenge => (
- ); + ) : null; } ChallengesFeed.defaultProps = { challenges: [], theme: 'light', + title: 'CHALLENGES', + challengeListingQuery: undefined, }; ChallengesFeed.propTypes = { challenges: PT.arrayOf(PT.shape()), loading: PT.bool.isRequired, theme: PT.oneOf(['dark', 'light']), + title: PT.string, + challengeListingQuery: PT.shape(), }; diff --git a/src/shared/components/GUIKit/DropdownSkills/index.jsx b/src/shared/components/GUIKit/DropdownSkills/index.jsx index a8f894aebb..d90405b006 100644 --- a/src/shared/components/GUIKit/DropdownSkills/index.jsx +++ b/src/shared/components/GUIKit/DropdownSkills/index.jsx @@ -90,7 +90,7 @@ function DropdownSkills({ className="dropdownContainer" styleName={`container ${ selectedOption && !!selectedOption.length ? 'haveValue' : '' - } ${errorMsg ? 'haveError' : ''} ${_.every(internalTerms, { selected: true }) ? 'isEmptySelectList' : ''}`} + } ${errorMsg ? 'haveError' : ''}`} >
{ const past = isPastBucket(activeBucket); const [currentSelected, setCurrentSelected] = useState(past); const [isTabClosed, setIsTabClosed] = useState(true); const currentTabName = useMemo(() => { if (location.pathname && location.pathname.indexOf(config.GIGS_PAGES_PATH) >= 0) { - return 'GIGS'; + return TAB_NAME.GIGS; } - return currentSelected ? 'PAST CHALLENGES' : 'ACTIVE CHALLENGES'; - }, [location, currentSelected]); + if (filterState.isInnovationChallenge === 'true') { + return TAB_NAME.INNOVATION_CHALLENGE; + } + return currentSelected ? TAB_NAME.PAST_CHALLENGES : TAB_NAME.ACTIVE_CHALLENGES; + }, [location, currentSelected, filterState]); const pageTitle = useMemo(() => { if (location.pathname && location.pathname.indexOf(config.GIGS_PAGES_PATH) >= 0) { return 'GIG WORK OPPORTUNITIES'; @@ -39,18 +52,24 @@ const ChallengeTab = ({ setCurrentSelected(isPastBucket(activeBucket)); }, [activeBucket]); - const moveToChallengesPage = (selectedBucket) => { - if (currentTabName === 'GIGS') { - const queryParams = getUpdateQuery({ bucket: selectedBucket }); + const moveToChallengesPage = (selectedBucket, targetTabName) => { + if (currentTabName === TAB_NAME.GIGS) { + const params = { bucket: selectedBucket }; + if (targetTabName === TAB_NAME.INNOVATION_CHALLENGE) { + params.isInnovationChallenge = 'true'; + } + const queryParams = getUpdateQuery(params); history.push(`/challenges${queryParams || ''}`); } }; const onActiveClick = () => { - if (!past && currentTabName !== 'GIGS') { + if (currentTabName === TAB_NAME.ACTIVE_CHALLENGES) { return; } - setPreviousBucketOfPastChallengesTab(activeBucket); + if (past) { + setPreviousBucketOfPastChallengesTab(activeBucket); + } setCurrentSelected(0); setIsTabClosed(true); let selectedBucket = ''; @@ -59,15 +78,40 @@ const ChallengeTab = ({ } else { selectedBucket = BUCKETS.OPEN_FOR_REGISTRATION; } - moveToChallengesPage(selectedBucket); + moveToChallengesPage(selectedBucket, TAB_NAME.ACTIVE_CHALLENGES); selectBucket(selectedBucket); + if (filterState.isInnovationChallenge === 'true') { + setFilterState({ + ..._.cloneDeep(filterState), + isInnovationChallenge: undefined, + }); + } + }; + + const onInnovationClick = () => { + if (currentTabName === TAB_NAME.INNOVATION_CHALLENGE) { + return; + } + if (!past) { + setPreviousBucketOfActiveTab(activeBucket); + } else { + setPreviousBucketOfPastChallengesTab(activeBucket); + } + setFilterState({ + ..._.cloneDeep(filterState), + isInnovationChallenge: 'true', + }); + moveToChallengesPage(BUCKETS.OPEN_FOR_REGISTRATION, TAB_NAME.INNOVATION_CHALLENGE); + selectBucket(BUCKETS.OPEN_FOR_REGISTRATION); }; const onPastChallengesClick = () => { - if (past && currentTabName !== 'GIGS') { + if (currentTabName === TAB_NAME.PAST_CHALLENGES) { return; } - setPreviousBucketOfActiveTab(activeBucket); + if (!past) { + setPreviousBucketOfActiveTab(activeBucket); + } setCurrentSelected(1); setIsTabClosed(true); let selectedBucket = ''; @@ -76,8 +120,14 @@ const ChallengeTab = ({ } else { selectedBucket = BUCKETS.ALL_PAST; } - moveToChallengesPage(selectedBucket); + moveToChallengesPage(selectedBucket, TAB_NAME.PAST_CHALLENGES); selectBucket(selectedBucket); + if (filterState.isInnovationChallenge === 'true') { + setFilterState({ + ..._.cloneDeep(filterState), + isInnovationChallenge: undefined, + }); + } }; const onGigsClick = () => { @@ -93,7 +143,7 @@ const ChallengeTab = ({
  • { if (e.key !== 'Enter') { @@ -103,11 +153,25 @@ const ChallengeTab = ({ }} role="presentation" > - ACTIVE CHALLENGES + {TAB_NAME.ACTIVE_CHALLENGES} +
  • +
  • { + if (e.key !== 'Enter') { + return; + } + onInnovationClick(); + }} + role="presentation" + > + {TAB_NAME.INNOVATION_CHALLENGE}
  • { if (e.key !== 'Enter') { @@ -117,11 +181,11 @@ const ChallengeTab = ({ }} role="presentation" > - PAST CHALLENGES + {TAB_NAME.PAST_CHALLENGES}
  • { if (e.key !== 'Enter') { @@ -131,7 +195,7 @@ const ChallengeTab = ({ }} role="presentation" > - GIGS + {TAB_NAME.GIGS}
); @@ -158,23 +222,30 @@ const ChallengeTab = ({
+

{TAB_NAME.ACTIVE_CHALLENGES}

+
+
-

ACTIVE CHALLENGES

+

{TAB_NAME.INNOVATION_CHALLENGE}

-

PAST CHALLENGES

+

{TAB_NAME.PAST_CHALLENGES}

-

GIGS

+

{TAB_NAME.GIGS}

) @@ -194,6 +265,8 @@ const ChallengeTab = ({ ChallengeTab.defaultProps = { activeBucket: null, selectBucket: () => {}, + setFilterState: () => {}, + filterState: {}, setPreviousBucketOfActiveTab: () => {}, setPreviousBucketOfPastChallengesTab: () => {}, previousBucketOfActiveTab: null, @@ -212,6 +285,8 @@ ChallengeTab.propTypes = { previousBucketOfActiveTab: PT.string, selectBucket: PT.func, previousBucketOfPastChallengesTab: PT.string, + setFilterState: PT.func, + filterState: PT.shape(), }; export default ChallengeTab; diff --git a/src/shared/components/challenge-listing/index.jsx b/src/shared/components/challenge-listing/index.jsx index 85d65b32aa..7a4a6c3655 100644 --- a/src/shared/components/challenge-listing/index.jsx +++ b/src/shared/components/challenge-listing/index.jsx @@ -168,6 +168,8 @@ export default function ChallengeListing(props) { previousBucketOfActiveTab={previousBucketOfActiveTab} selectBucket={selectBucket} location={location} + filterState={props.filterState} + setFilterState={props.setFilterState} />
diff --git a/src/shared/containers/Dashboard/ChallengesFeed.jsx b/src/shared/containers/Dashboard/ChallengesFeed.jsx index 5dbbfd7e56..5e9ed2aef5 100644 --- a/src/shared/containers/Dashboard/ChallengesFeed.jsx +++ b/src/shared/containers/Dashboard/ChallengesFeed.jsx @@ -2,6 +2,7 @@ * ChallengesFeed component */ import React from 'react'; +import _ from 'lodash'; import PT from 'prop-types'; import ChallengesFeed from 'components/Dashboard/Challenges'; import { connect } from 'react-redux'; @@ -9,28 +10,57 @@ import actions from '../../actions/dashboard'; class ChallengesFeedContainer extends React.Component { componentDidMount() { - const { getChallenges, challenges, itemCount } = this.props; + const { + getChallenges, challenges, itemCount, tags, + includeAllTags, projectId, excludeTags, title, tracks, + } = this.props; if (!challenges || challenges.length === 0) { - getChallenges({ - page: 1, - perPage: itemCount, - types: ['CH', 'F2F', 'MM'], - tracks: ['DES', 'DEV', 'DEV', 'DS', 'QA'], - status: 'Active', - sortBy: 'updated', - sortOrder: 'desc', - isLightweight: true, - currentPhaseName: 'Registration', - }); + getChallenges( + title, + _.omitBy({ + page: 1, + perPage: excludeTags && excludeTags.length ? undefined : itemCount, + types: ['CH', 'F2F', 'MM'], + tracks, + status: 'Active', + sortBy: 'updated', + sortOrder: 'desc', + isLightweight: true, + currentPhaseName: 'Registration', + tags: tags && tags.length ? tags : undefined, + includeAllTags: !!includeAllTags || undefined, + projectId: projectId || undefined, + }, _.isUndefined), + ); } } render() { - const { challenges, theme, loading } = this.props; + const { + theme, loading, excludeTags, itemCount, title, challengeListingQuery, + } = this.props; + let { challenges } = this.props; + + // this is a workaround for excluding challenges by tags + // there is no API support for this, so we have to do it manually + // in taht case we load more challenges, not limited to itemCount and filter out by tags + // default value for perPage is 20 when not specified + if (excludeTags && excludeTags.length) { + // filter out by excluded tags + challenges = challenges.filter(c => !c.tags.some(t => excludeTags.includes(t))); + // limit to itemCount + challenges = challenges.slice(0, itemCount); + } return ( - + ); } } @@ -40,6 +70,13 @@ ChallengesFeedContainer.defaultProps = { challenges: [], loading: true, theme: 'light', + tags: [], + includeAllTags: false, + projectId: null, + excludeTags: [], + title: 'CHALLENGES', + challengeListingQuery: undefined, + tracks: ['DES', 'DEV', 'DS', 'QA'], }; ChallengesFeedContainer.propTypes = { @@ -48,19 +85,35 @@ ChallengesFeedContainer.propTypes = { getChallenges: PT.func.isRequired, loading: PT.bool, theme: PT.oneOf(['dark', 'light']), + tags: PT.arrayOf(PT.string), + includeAllTags: PT.bool, + projectId: PT.number, + excludeTags: PT.arrayOf(PT.string), + title: PT.string, + challengeListingQuery: PT.shape(), + tracks: PT.arrayOf(PT.string), }; -const mapStateToProps = state => ({ - challenges: state.dashboard.challenges, - loading: state.dashboard.loading, -}); +function mapStateToProps(state, ownProps) { + const { dashboard } = state; + const id = ownProps.title || 'CHALLENGES'; + + if (dashboard[id]) { + return { + challenges: dashboard[id].challenges, + loading: dashboard[id].loading, + }; + } + + return state; +} const mapDispatchToProps = dispatch => ({ - getChallenges: (query) => { + getChallenges: (title, query) => { const a = actions.dashboard; - dispatch(a.fetchChallengesInit()); - dispatch(a.fetchChallengesDone(query)); + dispatch(a.fetchChallengesInit(title)); + dispatch(a.fetchChallengesDone(title, query)); }, }); diff --git a/src/shared/containers/Dashboard/index.jsx b/src/shared/containers/Dashboard/index.jsx index 1997e25331..085d98a129 100644 --- a/src/shared/containers/Dashboard/index.jsx +++ b/src/shared/containers/Dashboard/index.jsx @@ -24,11 +24,16 @@ import darkTheme from './themes/dark.scss'; const THEMES = { dark: darkTheme, }; +const { INNOVATION_CHALLENGES_TAG } = config; function SlashTCContainer(props) { const theme = THEMES.dark; // for v1 only dark theme const isTabletOrMobile = useMediaQuery({ maxWidth: 768 }); const title = 'Home | Topcoder'; + const challengeListingQuery = { + search: INNOVATION_CHALLENGES_TAG, + isInnovationChallenge: true, + }; useEffect(() => { if (props.tokenV3 && !isTokenExpired(props.tokenV3)) return; @@ -49,7 +54,15 @@ function SlashTCContainer(props) {
- + + @@ -70,7 +83,15 @@ function SlashTCContainer(props) { {/* Center column */}
- + +
diff --git a/src/shared/reducers/challenge-listing/index.js b/src/shared/reducers/challenge-listing/index.js index 98c3d390e8..add5ae8941 100644 --- a/src/shared/reducers/challenge-listing/index.js +++ b/src/shared/reducers/challenge-listing/index.js @@ -407,7 +407,7 @@ function onSetFilter(state, { payload }) { * do it very carefuly (many params are not validated). */ const filter = _.pickBy(_.pick( payload, - ['tags', 'types', 'search', 'startDateEnd', 'endDateStart', 'groups', 'events', 'tracks', 'tco'], + ['tags', 'types', 'search', 'startDateEnd', 'endDateStart', 'groups', 'events', 'tracks', 'tco', 'isInnovationChallenge'], ), value => (!_.isArray(value) && value && value !== '') || (_.isArray(value) && value.length > 0)); const emptyArrayAllowedFields = ['types']; diff --git a/src/shared/reducers/dashboard.js b/src/shared/reducers/dashboard.js index c0fe816909..d04254c0f3 100644 --- a/src/shared/reducers/dashboard.js +++ b/src/shared/reducers/dashboard.js @@ -5,6 +5,17 @@ import actions from 'actions/dashboard'; import { redux } from 'topcoder-react-utils'; +function onInit(state, { payload }) { + return { + ...state, + [payload]: { + details: null, + failed: false, + loading: true, + }, + }; +} + /** * Handles done actions. * @param {Object} state Previous state. @@ -13,9 +24,11 @@ import { redux } from 'topcoder-react-utils'; function onDone(state, action) { return { ...state, - challenges: action.error ? null : action.payload, - failed: action.error, - loading: false, + [action.payload.title]: { + challenges: action.error ? null : action.payload.challenges, + failed: action.error, + loading: false, + }, }; } @@ -26,14 +39,7 @@ function onDone(state, action) { */ function create(initialState) { return redux.handleActions({ - [actions.dashboard.fetchChallengesInit](state) { - return { - ...state, - details: null, - failed: false, - loading: true, - }; - }, + [actions.dashboard.fetchChallengesInit]: onInit, [actions.dashboard.fetchChallengesDone]: onDone, }, initialState || {}); } diff --git a/src/shared/utils/challenge-listing/buckets.js b/src/shared/utils/challenge-listing/buckets.js index ca85e0572a..18e2c7faaa 100644 --- a/src/shared/utils/challenge-listing/buckets.js +++ b/src/shared/utils/challenge-listing/buckets.js @@ -195,6 +195,7 @@ export function filterChanged(filter, prevFilter) { } return (!_.isEqual(filter.tracks, prevFilter.tracks)) || (filter.search !== prevFilter.search) + || (filter.isInnovationChallenge !== prevFilter.isInnovationChallenge) || (filter.tco !== prevFilter.tco) || (filter.startDateEnd !== prevFilter.startDateEnd) || (filter.endDateStart !== prevFilter.endDateStart)