From 3b57c17edd310b97432363c5dc21586c6d4d2e05 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 9 Feb 2026 11:06:01 +0200 Subject: [PATCH 01/10] PM-3717 - enhance the data returned for user skill --- src/services/MemberService.js | 54 ++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/src/services/MemberService.js b/src/services/MemberService.js index 9bfdd91..9b84da8 100644 --- a/src/services/MemberService.js +++ b/src/services/MemberService.js @@ -1756,22 +1756,56 @@ async function getMemberSkill (currentUser, handle, skillId) { if (skill.activity) { const fetchPromises = [] - // Prepare challenge fetch + // Prepare challenge fetch – group by resource role, last 3 per role by endDate const challengeSources = _.get(skill, 'activity.challenge.sources', []) if (challengeSources.length > 0) { const challengeIds = challengeSources + fetchPromises.push( - challengesPrisma.Challenge.findMany({ - where: { id: { in: challengeIds.slice(0, 3) } }, - select: { id: true, name: true } - }).then(dbChallenges => { + Promise.all([ + resourcesPrisma.resource.findMany({ + where: { + memberId: String(member.userId), + challengeId: { in: challengeIds } + }, + select: { challengeId: true, resourceRole: { select: { name: true } } } + }), + // Get challenge details (including endDate for ordering) from challenges DB + challengesPrisma.Challenge.findMany({ + where: { id: { in: challengeIds } }, + select: { id: true, name: true, endDate: true, taskIsTask: true } + }) + ]).then(([resources, dbChallenges]) => { + const roleMap = new Map(resources.map(r => [r.challengeId, r.resourceRole.name])) const challengeMap = new Map(dbChallenges.map(c => [c.id, c])) - skill.activity.challenge = { - count: challengeIds.length, - lastSources: challengeIds - .map(id => challengeMap.get(id)) - .filter(Boolean) + + // Group challenges by role + const groups = {} + for (const challengeId of challengeIds) { + const challenge = challengeMap.get(challengeId) + const roleName = roleMap.get(challengeId) || (challenge.taskIsTask ? 'Task' : 'Unknown') + if (challenge) { + if (!groups[roleName]) groups[roleName] = [] + groups[roleName].push(challenge) + } } + + // For each role: sort by endDate desc, keep last 3, include total count + skill.activity.challenge = Object.fromEntries( + Object.entries(groups).map(([role, challenges]) => { + const sorted = challenges.sort((a, b) => + new Date(b.endDate || 0) - new Date(a.endDate || 0) + ) + return [role, { + count: sorted.length, + lastSources: sorted.slice(0, 3).map(c => ({ + id: c.id, + name: c.name, + role + })) + }] + }) + ) }) ) } From 82c0277cfb972d28ed2d73c0a065e23ae121dc08 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 9 Feb 2026 12:32:52 +0200 Subject: [PATCH 02/10] Fix check for skill events --- src/common/prismaHelper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/prismaHelper.js b/src/common/prismaHelper.js index 50ea920..1eed63e 100644 --- a/src/common/prismaHelper.js +++ b/src/common/prismaHelper.js @@ -107,7 +107,7 @@ function buildMemberSkills (skillList) { ret.displayMode = _.pick(first.userSkillDisplayMode, ['id', 'name']) } - if (first.skill && first.skill.skillEvents) { + if (first.skill && first.skill.skillEvents?.length) { const events = _.orderBy(first.skill.skillEvents || [], 'createdAt', 'desc') const grouped = _.groupBy(events, 'sourceType.name') ret.lastUsedDate = events[0].createdAt From 36ddd0a942f2f9bbabd68144ba13953d420cccd2 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 9 Feb 2026 16:31:44 +0200 Subject: [PATCH 03/10] safe check for challenge task --- src/services/MemberService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/MemberService.js b/src/services/MemberService.js index 9b84da8..e5e4370 100644 --- a/src/services/MemberService.js +++ b/src/services/MemberService.js @@ -1783,7 +1783,7 @@ async function getMemberSkill (currentUser, handle, skillId) { const groups = {} for (const challengeId of challengeIds) { const challenge = challengeMap.get(challengeId) - const roleName = roleMap.get(challengeId) || (challenge.taskIsTask ? 'Task' : 'Unknown') + const roleName = roleMap.get(challengeId) || (challenge?.taskIsTask ? 'Task' : 'Unknown') if (challenge) { if (!groups[roleName]) groups[roleName] = [] groups[roleName].push(challenge) From e892488c21f9271049aa4a18c1853a3c5f14efb1 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 10 Feb 2026 00:06:22 +0530 Subject: [PATCH 04/10] fix: description number list line spacing --- .circleci/config.yml | 2 +- src/common/profileTemplate.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 98b66ba..d254240 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -68,7 +68,7 @@ workflows: only: - develop - PM-3532 - - pm-2539 + - pm-3732 - PS-511 - PS-513-Hotfix diff --git a/src/common/profileTemplate.js b/src/common/profileTemplate.js index a0f21ea..3efe5e8 100644 --- a/src/common/profileTemplate.js +++ b/src/common/profileTemplate.js @@ -184,8 +184,9 @@ const styles = StyleSheet.create({ lineHeight: 1.4, color: '#000000' }, - // Work description HTML list alignment (ul/ol/li) + // Work description HTML: tighten paragraph spacing so typed (p) and pasted (br) look consistent descriptionListStylesheet: { + p: { margin: 0, marginBottom: 2 }, ul: { paddingLeft: 15, marginTop: 3, marginBottom: 3 }, ol: { paddingLeft: 15, marginTop: 3, marginBottom: 3 }, li: { marginBottom: 2 } From 9e675d4e044fc4141784ea6942cb24164d52ae93 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 10 Feb 2026 00:35:37 +0530 Subject: [PATCH 05/10] fix: pdf spacing issues --- .circleci/config.yml | 2 +- src/common/profileTemplate.js | 44 ++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 98b66ba..8991152 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -68,7 +68,7 @@ workflows: only: - develop - PM-3532 - - pm-2539 + - pm-3733 - PS-511 - PS-513-Hotfix diff --git a/src/common/profileTemplate.js b/src/common/profileTemplate.js index a0f21ea..e71f1b2 100644 --- a/src/common/profileTemplate.js +++ b/src/common/profileTemplate.js @@ -79,6 +79,12 @@ const styles = StyleSheet.create({ padding: 8, marginBottom: 20 }, + statusBarSeparator: { + height: 1, + backgroundColor: '#AAAAAA', + marginTop: 10, + marginBottom: 20 + }, statusBarText: { color: '#FFFFFF', fontSize: 10, @@ -174,7 +180,9 @@ const styles = StyleSheet.create({ }, itemSkills: { fontSize: 10, - marginTop: 2, + marginTop: 4, + marginBottom: 6, + lineHeight: 1.4, color: '#000000' }, bulletPoint: { @@ -190,10 +198,18 @@ const styles = StyleSheet.create({ ol: { paddingLeft: 15, marginTop: 3, marginBottom: 3 }, li: { marginBottom: 2 } }, - // Certifications + // Certifications & Courses certificationItem: { fontSize: 10, - marginBottom: 3, + marginBottom: 8, + lineHeight: 1.5, + color: '#000000' + }, + courseItem: { + fontSize: 10, + marginTop: 10, + marginBottom: 8, + lineHeight: 1.5, color: '#000000' }, certificationLabel: { @@ -337,15 +353,17 @@ function buildProfileTemplate (pdfData) { { style: styles.handleInfo }, `Topcoder Handle: ${member.handle}${member.createdAt ? ` | Member Since ${new Date(member.createdAt).getFullYear()}` : ''}` ), - member.statusBarText ? React.createElement( - View, - { style: styles.statusBar }, - React.createElement( - Text, - { style: styles.statusBarText }, - member.statusBarText - ) - ) : null + member.statusBarText + ? React.createElement( + View, + { style: styles.statusBar }, + React.createElement( + Text, + { style: styles.statusBarText }, + member.statusBarText + ) + ) + : React.createElement(View, { style: styles.statusBarSeparator }) ) ) @@ -567,7 +585,7 @@ function buildProfileTemplate (pdfData) { certContent.push( React.createElement( Text, - { key: 'courses', style: [styles.certificationItem, { marginTop: 5 }] }, + { key: 'courses', style: styles.courseItem }, React.createElement(Text, { style: styles.certificationLabel }, 'Courses: '), coursesText ) From 04fd9f45d001a4e5e25d59553f029dcbd956bc0c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 10 Feb 2026 00:44:41 +0530 Subject: [PATCH 06/10] Trigger Build From d05be8984a06acbf083850d99ae4e95430c16de5 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 10 Feb 2026 01:00:34 +0530 Subject: [PATCH 07/10] fix: pdf spacing issues --- src/common/profileTemplate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/profileTemplate.js b/src/common/profileTemplate.js index e71f1b2..0e574be 100644 --- a/src/common/profileTemplate.js +++ b/src/common/profileTemplate.js @@ -77,13 +77,13 @@ const styles = StyleSheet.create({ statusBar: { backgroundColor: '#000000', padding: 8, - marginBottom: 20 + marginBottom: 10 }, statusBarSeparator: { height: 1, backgroundColor: '#AAAAAA', marginTop: 10, - marginBottom: 20 + marginBottom: 10 }, statusBarText: { color: '#FFFFFF', From 4eb0d7edc69cb37693abc4d7d473d61f9eb28eaf Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 10 Feb 2026 07:15:40 +1100 Subject: [PATCH 08/10] Fixes for 500 error when saving trait with no industry value --- src/services/MemberTraitService.js | 27 +++++++++++++++++++++++++-- test/unit/MemberTraitService.test.js | 22 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/services/MemberTraitService.js b/src/services/MemberTraitService.js index dd3e223..0000ca3 100644 --- a/src/services/MemberTraitService.js +++ b/src/services/MemberTraitService.js @@ -140,8 +140,8 @@ function convertPrismaToRes (traitData, userId, traitIds = TRAIT_IDS) { traits: { traitId: 'personalization', data: _.map(traitData.personalization, t => ({ - [t.key]: t.value - })) + [t.key]: t.value + })) } }) } @@ -285,6 +285,21 @@ async function validateWorkAssociatedSkills (skillIds) { } } +/** + * Remove a field if it's provided as an empty/blank string. + * @param {Object} item object containing data fields + * @param {String} key field name + */ +function omitBlankStringField (item, key) { + if (!Object.prototype.hasOwnProperty.call(item, key)) { + return + } + + if (_.isString(item[key]) && _.trim(item[key]) === '') { + delete item[key] + } +} + /** * Build prisma data for creating/updating traits * @param {Object} data query data @@ -342,12 +357,20 @@ function buildTraitPrismaData (data, operatorId, result) { if (t.timePeriodTo && !t.endDate) { t.endDate = new Date(t.timePeriodTo) } + // industry is optional; treat blank values as omitted + omitBlankStringField(t, 'industry') + omitBlankStringField(t, 'otherIndustry') + if (t.industry !== 'Other') { + delete t.otherIndustry + } // Remove unknown keys that Prisma model does not accept delete t.company delete t.timePeriodFrom delete t.timePeriodTo return t }) + // Keep downstream response/event payloads aligned with normalized DB payload + item.traits.data = payload } _.forEach(payload, t => { diff --git a/test/unit/MemberTraitService.test.js b/test/unit/MemberTraitService.test.js index c37c73d..8634fef 100644 --- a/test/unit/MemberTraitService.test.js +++ b/test/unit/MemberTraitService.test.js @@ -214,6 +214,28 @@ describe('member trait service unit tests', () => { // should.equal(result[0].updatedBy, 'sub2') }) + it('update member traits successfully when industry is blank', async () => { + await service.updateTraits({ isMachine: true, sub: 'sub2' }, member1.handle, [{ + traitId: 'work', + categoryName: 'Work', + traits: { + traitId: 'work', + data: [{ + industry: ' ', + companyName: 'JP Morgan 3', + position: 'Manager 3' + }] + } + }]) + + const traits = await service.getTraits({}, member1.handle, { traitIds: 'work' }) + should.equal(traits.length, 1) + should.equal(traits[0].traitId, 'work') + should.equal(traits[0].traits.data.length, 1) + should.equal(traits[0].traits.data[0].companyName, 'JP Morgan 3') + should.not.equal(traits[0].traits.data[0].industry, '') + }) + it('update member traits - trait not found', async () => { try { await service.updateTraits({ isMachine: true, sub: 'sub1' }, member1.handle, [{ From 3b592b17fe4e245e52bb7e3063b5f211937a3874 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 10 Feb 2026 09:05:29 +0200 Subject: [PATCH 09/10] Member skill: return "Winner" position for earned skill --- src/services/MemberService.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/services/MemberService.js b/src/services/MemberService.js index e5e4370..9d1d76a 100644 --- a/src/services/MemberService.js +++ b/src/services/MemberService.js @@ -1773,17 +1773,34 @@ async function getMemberSkill (currentUser, handle, skillId) { // Get challenge details (including endDate for ordering) from challenges DB challengesPrisma.Challenge.findMany({ where: { id: { in: challengeIds } }, - select: { id: true, name: true, endDate: true, taskIsTask: true } + select: { + id: true, + name: true, + endDate: true, + taskIsTask: true, + winners: { + where: { userId: helper.bigIntToNumber(member.userId) }, + select: { userId: true } + } + } }) ]).then(([resources, dbChallenges]) => { const roleMap = new Map(resources.map(r => [r.challengeId, r.resourceRole.name])) const challengeMap = new Map(dbChallenges.map(c => [c.id, c])) + const winnerSet = new Set( + dbChallenges + .filter(c => c.winners && c.winners.length > 0) + .map(c => c.id) + ) // Group challenges by role const groups = {} for (const challengeId of challengeIds) { const challenge = challengeMap.get(challengeId) - const roleName = roleMap.get(challengeId) || (challenge?.taskIsTask ? 'Task' : 'Unknown') + const baseRoleName = roleMap.get(challengeId) || (challenge?.taskIsTask ? 'Task' : 'Unknown') + const roleName = baseRoleName === 'Submitter' && winnerSet.has(challengeId) + ? 'Winner' + : baseRoleName if (challenge) { if (!groups[roleName]) groups[roleName] = [] groups[roleName].push(challenge) From f8d0eb86ef02b90a83ffb3b8f321bd0c112c3ed9 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 10 Feb 2026 10:20:03 +0200 Subject: [PATCH 10/10] Account for multiple roles in member skill --- src/services/MemberService.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/services/MemberService.js b/src/services/MemberService.js index 9d1d76a..d6a1651 100644 --- a/src/services/MemberService.js +++ b/src/services/MemberService.js @@ -1785,7 +1785,13 @@ async function getMemberSkill (currentUser, handle, skillId) { } }) ]).then(([resources, dbChallenges]) => { - const roleMap = new Map(resources.map(r => [r.challengeId, r.resourceRole.name])) + const roleMap = new Map() + resources.forEach(resource => { + if (!roleMap.has(resource.challengeId)) { + roleMap.set(resource.challengeId, new Set()) + } + roleMap.get(resource.challengeId).add(resource.resourceRole.name) + }) const challengeMap = new Map(dbChallenges.map(c => [c.id, c])) const winnerSet = new Set( dbChallenges @@ -1797,13 +1803,20 @@ async function getMemberSkill (currentUser, handle, skillId) { const groups = {} for (const challengeId of challengeIds) { const challenge = challengeMap.get(challengeId) - const baseRoleName = roleMap.get(challengeId) || (challenge?.taskIsTask ? 'Task' : 'Unknown') - const roleName = baseRoleName === 'Submitter' && winnerSet.has(challengeId) - ? 'Winner' - : baseRoleName if (challenge) { - if (!groups[roleName]) groups[roleName] = [] - groups[roleName].push(challenge) + const roles = roleMap.get(challengeId) + const roleNames = roles && roles.size + ? Array.from(roles).map(role => ( + role === 'Submitter' && winnerSet.has(challengeId) + ? 'Winner' + : role + )) + : [challenge?.taskIsTask ? 'Task' : 'Unknown'] + + roleNames.forEach(roleName => { + if (!groups[roleName]) groups[roleName] = [] + groups[roleName].push(challenge) + }) } }