Skip to content

Feature member search list #138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Changes from 1 commit
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
52ddd0a
Switch to tc-core-library-js for fetching m2m token
ThomasKranitsas Mar 18, 2019
2b303e3
Bump npm version
ThomasKranitsas Mar 18, 2019
7281ec4
Merge pull request #108 from topcoder-platform/develop
sushilshinde Nov 6, 2019
5f528e0
Merge pull request #112 from topcoder-platform/develop
sushilshinde Nov 21, 2019
6575f2a
Code 30108491
suppermancool Dec 2, 2019
1b91701
Fix tests
ThomasKranitsas Dec 3, 2019
5e470c6
Merge pull request #116 from suppermancool/code-30108491
codeMinter Dec 6, 2019
2f96814
fix filtering of provisionalScoreComplete logic
codeMinter Dec 7, 2019
5f15971
make build based on env to pick up right json file
codeMinter Dec 7, 2019
43c61a5
Merge pull request #119 from topcoder-platform/develop
sushilshinde Dec 11, 2019
0ade41c
Fix build issues
ThomasKranitsas Dec 13, 2019
57ad31c
Adding test tag
sushilshinde Dec 14, 2019
e9ce6b6
Test release
sushilshinde Dec 14, 2019
540e0cb
Test release npm version v1000.7.0
sushilshinde Dec 14, 2019
baf0da0
added test tag
sushilshinde Dec 18, 2019
69c15ed
merged with develop
sushilshinde Dec 18, 2019
9906441
fix dups
sushilshinde Jan 6, 2020
f765197
Merge pull request #122 from topcoder-platform/develop-fix-m2m
sushilshinde Jan 6, 2020
f02f334
test release npm version
sushilshinde Jan 6, 2020
616c103
Merge remote-tracking branch 'origin/develop' into feature-mm-submiss…
veshu Jan 16, 2020
c60cc83
Merge remote-tracking branch 'origin/master' into feature-mm-submissions
sushilshinde Feb 17, 2020
16ee657
added comments
sushilshinde Feb 17, 2020
a0047b2
output from challenge 30115867
LieutenantRoger Feb 23, 2020
aea841e
fix test
LieutenantRoger Feb 23, 2020
6435cd4
fix m2m token
LieutenantRoger Feb 24, 2020
35915ff
Merge pull request #131 from LieutenantRoger/jan-develop-fix-m2m
sushilshinde Feb 24, 2020
a8db62a
test release npm ver bump up to 1000.8.1
sushilshinde Feb 24, 2020
6f2df59
Merge pull request #132 from topcoder-platform/feature-mm-submissions
sushilshinde Feb 25, 2020
754b17e
resolved conflicts
sushilshinde Feb 25, 2020
9243403
resolved conflicts
sushilshinde Feb 25, 2020
89c01b9
Merge pull request #133 from topcoder-platform/jan-develop-fix-m2m
sushilshinde Feb 25, 2020
82f2317
Merge pull request #135 from topcoder-platform/develop-mm-sub-merge-fix
sushilshinde Mar 2, 2020
6ad4342
ci: test release after merge m2m
sushilshinde Mar 2, 2020
f1c2380
removed test tag, npm version bump up for prod release
sushilshinde Mar 3, 2020
c05dafb
Merge remote-tracking branch 'upstream/develop' into feature-member-s…
LieutenantRoger Mar 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
output from challenge 30115867
LieutenantRoger committed Feb 23, 2020
commit a0047b24eff9038c78c343a53d0d1e3476bcc3ef
2 changes: 2 additions & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import reviewOpportunityActions from './reviewOpportunity';
import lookupActions from './lookup';
import settingsActions from './settings';
import lookerActions from './looker';
import memberSearchActions from './member-search';

export const actions = {
auth: authActions.auth,
@@ -30,6 +31,7 @@ export const actions = {
lookup: lookupActions.lookup,
settings: settingsActions.settings,
looker: lookerActions.looker,
memberSearch: memberSearchActions.memberSearch,
};

export default undefined;
80 changes: 80 additions & 0 deletions src/actions/member-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* @module "actions.member-search"
* @desc Actions for management of members search.
*/
import _ from 'lodash'
import { createActions } from 'redux-actions';
import { getService } from '../services/member-search';

/**
* @desc Creates an action that fetchs the members data for a search term, and
* adds result to the store cumulatively.
* @param {String} searchTerm the search term
* @param {Number} offset the number of records to skip
* @param {Number} limit the maximum number of the return results
* @return {Action}
*/
function loadMemberSearch(searchTerm, offset = 0, limit = 10) {
return getService().getUsernameMatches(searchTerm, offset, limit);
}

/**
* @static
* @desc Creates an action that fetchs the members data for a search tag, and
* adds result to the store.
* @param {Object} tag the tag
* @return {Action}
*/
function loadMemberSearchForTag(tag) {
return getService().getTopMembers(tag);
}

/**
* @static
* @desc Creates an action that check if the term is a tag name. If it is unable to check,
* or invalid data returned then resets the members data and search terms data in the store
* to intial values.
* @param {String} searchTerm the search term
* @return {Action}
*/
function checkIfSearchTermIsATag(searchTerm) {
return getService().checkIfSearchTermIsATag(searchTerm);
}

/**
* @static
* @desc Creates an action that saves the current search term.
* @param {String} searchTerm the search term
* @return {Action}
*/
function setSearchTerm(searchTerm) {
return {
previousSearchTerm: searchTerm
}
}

/**
* @static
* @desc Creates an action that saves the current search tag.
* @param {Object} searchTag the search tag
* @return {Action}
*/
function setSearchTag(searchTag) {
return {
searchTermTag: searchTag
}
}

export default createActions({
MEMBER_SEARCH: {
USERNAME_SEARCH_SUCCESS: loadMemberSearch,
CHECK_IF_SEARCH_TERM_IS_A_TAG: checkIfSearchTermIsATag,
TOP_MEMBER_SEARCH_SUCCESS: loadMemberSearchForTag,
CLEAR_MEMBER_SEARCH: _.noop,
LOAD_MORE_USERNAMES: _.noop,
MEMBER_SEARCH_SUCCESS: _.noop,
SET_SEARCH_TERM: setSearchTerm,
SET_SEARCH_TAG: setSearchTag,
RESET_SEARCH_TERM: _.noop
}
});
4 changes: 3 additions & 1 deletion src/reducers/index.js
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ import settings, { factory as settingsFactory }
from './settings';
import looker, { factory as lookerFactory }
from './looker';

import memberSearch, { factory as memberSearchFactory } from './member-search';

export function factory(options) {
return redux.resolveReducers({
@@ -41,6 +41,7 @@ export function factory(options) {
mySubmissionsManagement: mySubmissionsManagementFactory(options),
settings: settingsFactory(options),
looker: lookerFactory(options),
memberSearch: memberSearchFactory(options),
});
}

@@ -60,4 +61,5 @@ export default ({
mySubmissionsManagement,
settings,
looker,
memberSearch,
});
245 changes: 245 additions & 0 deletions src/reducers/member-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
/**
* @module "reducers.member-search"
* @desc Reducer for {@link module:actions.member-search} actions.
*
* State segment managed by this reducer has the following structure:
* @param {Boolean} pageLoaded `true` if loading members data for a search term is done
* `false`if starting loading members data for a search term,
* or loading failed
* @param {Boolean} loadingMore `true` if request for loading more data is in progress;
* otherwise `false`
* @param {Boolean} error `true` if failed to load member data; otherwise `false`
* @param {Number} totalCount the number of matched members for a search term
* @param {Boolean} moreMatchesAvailable `true` if there are more matched members, for
* a search term, to load; otherwise `false`
* @param {Array<{}>} usernameMatches contains loaded members data for a search term
* @param {Array<{}>} topMembers contains loaded members data for a search tag
* @param {String} previousSearchTerm the current search term
* @param {Object} searchTermTag the current search tag data if the search term is a tag name;
* otherwise `null`
*/
import _ from 'lodash';
import { redux } from 'topcoder-react-utils';
import actions from '../actions/member-search';
import { fireErrorMessage } from '../utils/errors';

/**
* @private
* Returns the new state with the intial members data.
*/
function memberSearchFailure(state) {
return Object.assign({}, state, {
loadingMore: false,
error: true,
totalCount: 0,
usernameMatches: [],
topMembers: [],
});
}

/**
* @private
* Returns the new state with the intial search terms data.
*/
function resetSearchTerm(state) {
return Object.assign({}, state, {
pageLoaded: false,
previousSearchTerm: null,
searchTermTag: null,
});
}

/**
* @private
* Returns the new state with the intial members and search terms data.
*/
function memberSearchFailureAndResetSearchTerm(state) {
let newState = state;
newState = memberSearchFailure(newState);
newState = resetSearchTerm(newState);
return newState;
}

/**
* Handles the actual results of loading members data for a search term cumulatively,
* and clear members data on request failure.
* @param {Object} state
* @param {Object} action
* @return {Object} New state.
*/
function onUsernameSearchSuccess(state, action) {
const { payload } = action;
if (action.error) {
fireErrorMessage('Could not fetch username matches', '');
return memberSearchFailure(state);
}

return Object.assign({}, state, {
loadingMore: false,
totalCount: payload.totalCount,
moreMatchesAvailable: state.usernameMatches.length + payload.usernameMatches.length
< payload.totalCount,
usernameMatches: state.usernameMatches.concat(payload.usernameMatches),
});
}

/**
* Clear members data and search terms data on request failure of checking if the search term is
* a tag name.
* @param {Object} state
* @param {Object} action
* @return {Object} New state if error; otherwise the same state.
*/
function onCheckIfSearchTermIsATag(state, action) {
if (action.error) {
fireErrorMessage('Could not determine if search term is a tag', '');
return memberSearchFailureAndResetSearchTerm(state);
}

return state;
}

/**
* Handles the actual results of loading members data for a search tag, and
* clear members data and search terms data on request failure.
* @param {Object} state
* @param {Object} action
* @return {Object} New state.
*/
function onTopMemberSearchSuccess(state, action) {
const { payload } = action;
if (action.error) {
fireErrorMessage('Could not fetch top members', '');
return memberSearchFailureAndResetSearchTerm(state);
}

return Object.assign({}, state, {
topMembers: payload.topMembers,
});
}

/**
* Clear members data to the intial state.
* @param {Object} state
* @param {Object} action
* @return {Object} New state.
*/
function onClearMemberSearch(state) {
return Object.assign({}, state, {
pageLoaded: false,
loadingMore: false,
error: false,
totalCount: 0,
usernameMatches: [],
topMembers: [],
});
}

/**
* Marks the request of loading more members data for a search term as in progress
* @param {Object} state
* @param {Object} action
* @return {Object} New state.
*/
function onLoadMoreUsernames(state) {
return Object.assign({}, state, {
loadingMore: true,
});
}

/**
* Marks the loaded members data for a search term or search tag (if any) as ready.
* @param {Object} state
* @param {Object} action
* @return {Object} New state.
*/
function onMemberSearchSuccess(state) {
return Object.assign({}, state, {
pageLoaded: true,
});
}

/**
* Handles setting the current search term.
* @param {Object} state
* @param {Object} action
* @return {Object} New state.
*/
function onSetSearchTerm(state, action) {
const { payload } = action;
return Object.assign({}, state, {
error: false,
previousSearchTerm: payload.previousSearchTerm,
});
}

/**
* Handles setting the current search tag.
* @param {Object} state
* @param {Object} action
* @return {Object} New state.
*/
function onSetSearchTag(state, action) {
const { payload } = action;
return Object.assign({}, state, {
searchTermTag: payload.searchTermTag,
});
}

/**
* Handles clearing the current search term and search tag.
* @param {Object} state
* @param {Object} action
* @return {Object} New state.
*/
function onResetSearchTerm(state) {
return resetSearchTerm(state);
}

/**
* Creates a new member search reducer with the specified initial state.
* @param {Object} initialState Optional. Initial state.
* @return {Function} member search reducer.
*/
function create(initialState = {}) {
const a = actions.memberSearch;
return redux.handleActions({
[a.usernameSearchSuccess]: onUsernameSearchSuccess,
[a.checkIfSearchTermIsATag]: onCheckIfSearchTermIsATag,
[a.topMemberSearchSuccess]: onTopMemberSearchSuccess,
[a.clearMemberSearch]: onClearMemberSearch,
[a.loadMoreUsernames]: onLoadMoreUsernames,
[a.memberSearchSuccess]: onMemberSearchSuccess,
[a.setSearchTerm]: onSetSearchTerm,
[a.setSearchTag]: onSetSearchTag,
[a.resetSearchTerm]: onResetSearchTerm,
}, _.defaults(initialState, {
pageLoaded: false,
loadingMore: false,
error: false,
totalCount: 0,
moreMatchesAvailable: false,
usernameMatches: [],
topMembers: [],
previousSearchTerm: null,
searchTermTag: null,
}));
}

/**
* Factory which creates a new reducer with its initial state tailored to the
* given options object, if specified (for server-side rendering). If options
* object is not specified, it creates just the default reducer. Accepted options are:
* @return {Promise}
* @resolves {Function(state, action): state} New reducer.
*/
export function factory() {
return Promise.resolve(create());
}

/**
* @static
* @member default
* @desc Reducer with default initial state.
*/
export default create();
2 changes: 2 additions & 0 deletions src/services/index.js
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ import * as user from './user';
import * as lookup from './lookup';
import * as userTraits from './user-traits';
import * as submissions from './submissions';
import * as memberSearch from './member-search';

export const services = {
api,
@@ -31,6 +32,7 @@ export const services = {
lookup,
userTraits,
submissions,
memberSearch,
};

export default undefined;
121 changes: 121 additions & 0 deletions src/services/member-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* @module "services.member-search"
* @desc This module provides a service for searching members.
*/
import _ from 'lodash';
import qs from 'qs';
import { getApi } from './api';
import { checkResponseSucess, mapTagToLeaderboardType } from '../utils/member-search';

/**
* Member search service class.
*/
class MemberSearchService {
/**
* @param {String} tokenV3 Optional. Auth token for Topcoder API v3.
*/
constructor(tokenV3) {
this.private = {
api: getApi('V3', tokenV3),
tokenV3,
};
}

/**
* Get matched members for a search term.
* @param {String} searchTerm the search term
* @param {Number} offset the number of members to skip from starting
* @param {Number} limit the maximum number of return members
* @return {Promise} Resolves to an object containing the array of matched members
* and the total count
*/
getUsernameMatches(searchTerm, offset, limit) {
const params = {
query: 'MEMBER_SEARCH',
handle: encodeURIComponent(searchTerm),
offset,
limit,
};

return this.private.api.get(`/members/_search/?${qs.stringify(params)}`)
.then(checkResponseSucess)
.then((data) => {
const usernameMatches = _.get(data, 'result.content');
const totalCount = _.get(data, 'result.metadata.totalCount');

if (!_.isArray(usernameMatches)) {
throw new Error('Expected array for username response results');
} else if (!_.isNumber(totalCount)) {
throw new Error('Expected number for metadata total count');
}

return {
usernameMatches,
totalCount,
};
})
.catch((err) => {
throw new Error(`Could not fetch username matches. Reason: ${err}`);
});
}

/**
* Check if the search term is a tag.
* @param {String} searchTerm the search term
* @return {Promise} Resolves to a tag object
*/
checkIfSearchTermIsATag(searchTerm) {
return this.private.api.get(`/tags/?filter=name%3D${encodeURIComponent(searchTerm)}`)
.then(checkResponseSucess)
.then((data) => {
const tagInfo = _.get(data, 'result.content');

if (!_.isArray(tagInfo)) {
throw new Error('Tag response must be an array');
}

return tagInfo[0];
})
.catch((err) => {
throw new Error(`Could not determine if search term is a tag. Reason: ${err}`);
});
}

/**
* Get matched members for a search tag.
* @param {Object} tag the tag
* @return {Promise} Resolves to an object containing the array of matched members
*/
getTopMembers(tag) {
const leaderboardType = mapTagToLeaderboardType(tag.domain);

return this.private.api.get(`/leaderboards/?filter=id%3D${tag.id}%26type%3D${leaderboardType}`)
.then(checkResponseSucess)
.then((data) => {
const topMembers = _.get(data, 'result.content', []);

return {
topMembers,
};
})
.catch((err) => {
throw new Error(`Could not fetch top members. Reason: ${err}`);
});
}
}

let lastInstance = null;

/**
* Returns a new or existing member-search service.
* @param {String} tokenV3 Optional. Auth token for Topcoder API v3.
* @return {MemberSearchService} Member search service object
*/
export function getService(tokenV3) {
if (!lastInstance || tokenV3 !== lastInstance.private.tokenV3) {
lastInstance = new MemberSearchService(tokenV3);
}
return lastInstance;
}

export default undefined;
18 changes: 18 additions & 0 deletions src/utils/member-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function mapTagToLeaderboardType(tagDomain) {
const tagToLeaderboardTypeMap = {
SKILLS: 'MEMBER_SKILL',
};

return tagDomain ? tagToLeaderboardTypeMap[tagDomain.toUpperCase()] : null;
}

export async function checkResponseSucess(res) {
if (!res.ok) {
throw new Error(res.statusText);
}
const x = await res.json();
if (!x.result.success) {
throw new Error(x.result.content);
}
return x;
}