Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 0 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ workflows:
branches:
only:
- develop
- PM-3532
- pm-2539
- PS-511
- PS-513-Hotfix

# Production builds are exectuted only on tagged commits to the
# master branch.
Expand Down
2 changes: 1 addition & 1 deletion src/common/prismaHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 maintainability]
The use of optional chaining (?.) in first.skill.skillEvents?.length is a good improvement for safety, but the subsequent _.orderBy(first.skill.skillEvents || [], 'createdAt', 'desc') still uses a fallback to an empty array. Consider removing the fallback since the optional chaining already ensures skillEvents is either defined or undefined. This will make the code cleaner and avoid unnecessary operations.

const events = _.orderBy(first.skill.skillEvents || [], 'createdAt', 'desc')
const grouped = _.groupBy(events, 'sourceType.name')
ret.lastUsedDate = events[0].createdAt
Expand Down
49 changes: 34 additions & 15 deletions src/common/profileTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ const styles = StyleSheet.create({
statusBar: {
backgroundColor: '#000000',
padding: 8,
marginBottom: 20
marginBottom: 10
},
statusBarSeparator: {
height: 1,
backgroundColor: '#AAAAAA',
marginTop: 10,
marginBottom: 10
},
statusBarText: {
color: '#FFFFFF',
Expand Down Expand Up @@ -174,7 +180,9 @@ const styles = StyleSheet.create({
},
itemSkills: {
fontSize: 10,
marginTop: 2,
marginTop: 4,
marginBottom: 6,
lineHeight: 1.4,
color: '#000000'
},
bulletPoint: {
Expand All @@ -184,16 +192,25 @@ 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 }
},
// 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: {
Expand Down Expand Up @@ -337,15 +354,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 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ design]
The addition of a statusBarSeparator when member.statusBarText is not present changes the layout logic. Ensure this behavior is intended, as it introduces a visual element that was not previously displayed.

)
)

Expand Down Expand Up @@ -567,7 +586,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
)
Expand Down
84 changes: 74 additions & 10 deletions src/services/MemberService.js
Original file line number Diff line number Diff line change
Expand Up @@ -1756,22 +1756,86 @@ 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({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Using Promise.all here is appropriate for parallel execution, but be aware that if any promise rejects, the entire operation will fail. Consider adding error handling to manage individual promise failures gracefully.

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,
winners: {
where: { userId: helper.bigIntToNumber(member.userId) },
select: { userId: true }
}
}
})
]).then(([resources, dbChallenges]) => {
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]))
skill.activity.challenge = {
count: challengeIds.length,
lastSources: challengeIds
.map(id => challengeMap.get(id))
.filter(Boolean)
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)
if (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)
})
}
}

// 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) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Sorting by endDate assumes that all challenges have a valid endDate. If any challenge lacks an endDate, this could lead to unexpected behavior. Consider handling cases where endDate might be null or undefined.

new Date(b.endDate || 0) - new Date(a.endDate || 0)
)
return [role, {
count: sorted.length,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[💡 design]
The use of slice(0, 3) limits the results to the last 3 challenges. Ensure that this behavior is intentional and documented, as it might not be obvious to all developers why only 3 are kept.

lastSources: sorted.slice(0, 3).map(c => ({
id: c.id,
name: c.name,
role
}))
}]
})
)
})
)
}
Expand Down
23 changes: 23 additions & 0 deletions src/services/MemberTraitService.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,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
Expand Down Expand Up @@ -344,12 +359,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') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The logic here assumes that t.industry is always defined. If t.industry is undefined, this condition will not delete t.otherIndustry even if it should be. Consider explicitly checking for undefined or using a more robust condition.

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 => {
Expand Down
22 changes: 22 additions & 0 deletions test/unit/MemberTraitService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The assertion should.not.equal(traits[0].traits.data[0].industry, '') checks that the industry field is not an empty string, but it doesn't verify if the field is properly handled when it contains only whitespace. Consider adding a check to ensure that the industry field is either trimmed or validated to prevent storing invalid data.

})

it('update member traits - trait not found', async () => {
try {
await service.updateTraits({ isMachine: true, sub: 'sub1' }, member1.handle, [{
Expand Down
Loading