Skip to content

Commit f290962

Browse files
authored
Merge pull request #384 from developmentseed/add/badges-to-org-members-table
Add badges to org members table
2 parents 5aa7ebd + 7326937 commit f290962

File tree

6 files changed

+186
-9
lines changed

6 files changed

+186
-9
lines changed

cypress.config.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const db = require('./src/lib/db')
33
const Team = require('./src/models/team')
44
const Organization = require('./src/models/organization')
55
const TeamInvitation = require('./src/models/team-invitation')
6+
const Badge = require('./src/models/badge')
67
const { pick } = require('ramda')
78

89
module.exports = defineConfig({
@@ -16,6 +17,10 @@ module.exports = defineConfig({
1617
await db.raw('TRUNCATE TABLE organization RESTART IDENTITY CASCADE')
1718
await db.raw('TRUNCATE TABLE users RESTART IDENTITY CASCADE')
1819
await db.raw('TRUNCATE TABLE osm_users RESTART IDENTITY CASCADE')
20+
await db.raw(
21+
'TRUNCATE TABLE organization_badge RESTART IDENTITY CASCADE'
22+
)
23+
await db.raw('TRUNCATE TABLE user_badges RESTART IDENTITY CASCADE')
1924
return null
2025
},
2126
'db:seed:create-teams': async ({ teams, moderatorId }) => {
@@ -75,6 +80,23 @@ module.exports = defineConfig({
7580
}
7681
return null
7782
},
83+
'db:seed:create-organization-badges': async ({ orgId, badges }) => {
84+
for (let i = 0; i < badges.length; i++) {
85+
const badge = badges[i]
86+
await db('organization_badge').insert({
87+
organization_id: orgId,
88+
...pick(['id', 'name', 'color'], badge),
89+
})
90+
}
91+
return null
92+
},
93+
'db:seed:assign-badge-to-users': async ({ badgeId, users }) => {
94+
for (let i = 0; i < users.length; i++) {
95+
const user = users[i]
96+
await Badge.assignUserBadge(badgeId, user.id, new Date())
97+
}
98+
return null
99+
},
78100
})
79101
},
80102
},
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
const {
2+
generateSequenceArray,
3+
addZeroPadding,
4+
} = require('../../../src/lib/utils')
5+
6+
// Generate org member
7+
const org1Members = generateSequenceArray(30, 1).map((i) => ({
8+
id: i,
9+
name: `User ${addZeroPadding(i, 3)}`,
10+
}))
11+
12+
const [user1, ...org1Team1Members] = org1Members
13+
14+
// Organization meta
15+
const org1 = {
16+
id: 1,
17+
name: 'Org 1',
18+
ownerId: user1.id,
19+
}
20+
21+
const org1Team1 = {
22+
id: 1,
23+
name: 'Org 1 Team 1',
24+
}
25+
26+
const BADGES_COUNT = 30
27+
28+
const org1Badges = generateSequenceArray(BADGES_COUNT, 1).map((i) => ({
29+
id: i,
30+
name: `Badge ${addZeroPadding(i, 3)}`,
31+
color: `rgba(255,0,0,${i / BADGES_COUNT})`,
32+
}))
33+
34+
const [org1Badge1, org1Badge2, org1Badge3] = org1Badges
35+
36+
describe('Organization page', () => {
37+
before(() => {
38+
cy.task('db:reset')
39+
40+
// Create organization
41+
cy.task('db:seed:create-organizations', [org1])
42+
43+
// Add org teams
44+
cy.task('db:seed:create-organization-teams', {
45+
orgId: org1.id,
46+
teams: [org1Team1],
47+
managerId: user1.id,
48+
})
49+
50+
// Add members to org team 1
51+
cy.task('db:seed:add-members-to-team', {
52+
teamId: org1Team1.id,
53+
members: org1Team1Members,
54+
})
55+
56+
// Create org badges
57+
cy.task('db:seed:create-organization-badges', {
58+
orgId: org1.id,
59+
badges: org1Badges,
60+
})
61+
62+
// Assign badge 1 to the first five users
63+
cy.task('db:seed:assign-badge-to-users', {
64+
badgeId: org1Badge1.id,
65+
users: org1Team1Members.slice(0, 4),
66+
})
67+
68+
// Assign badge 2 to five users, starting at user 3
69+
cy.task('db:seed:assign-badge-to-users', {
70+
badgeId: org1Badge2.id,
71+
users: org1Team1Members.slice(2, 7),
72+
})
73+
74+
// Assign badge 3 to five users, starting at user 5
75+
cy.task('db:seed:assign-badge-to-users', {
76+
badgeId: org1Badge3.id,
77+
users: org1Team1Members.slice(4, 9),
78+
})
79+
})
80+
81+
it('Organization members table display badges', () => {
82+
cy.login(user1)
83+
84+
cy.visit('/organizations/1')
85+
86+
cy.get('[data-cy=org-members-table]')
87+
.find('tbody tr:nth-child(6) td:nth-child(3)')
88+
.contains('Badge 002')
89+
cy.get('[data-cy=org-members-table]')
90+
.find('tbody tr:nth-child(6) td:nth-child(3)')
91+
.contains('Badge 003')
92+
cy.get('[data-cy=org-members-table]')
93+
.find('tbody tr:nth-child(10) td:nth-child(3)')
94+
.contains('Badge 003')
95+
})
96+
})

src/components/tables/users.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ function UsersTable({ type, orgId, onRowClick, isSearchable }) {
3333
columns = [
3434
{ key: 'name', sortable: true },
3535
{ key: 'id', label: 'OSM ID', sortable: true },
36+
{
37+
key: 'badges',
38+
render: ({ badges }) => (
39+
<>
40+
{badges?.length > 0 &&
41+
badges.map((b) => <div key={b.id}>{b.name}</div>)}
42+
</>
43+
),
44+
},
3645
{
3746
key: 'External Profiles',
3847
render: ({ name }) => (

src/models/badge.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const db = require('../lib/db')
2+
3+
/**
4+
*
5+
* Assign existing badge to an user.
6+
*
7+
* @param {int} userId - User id
8+
* @param {int} badgeId - Badge id
9+
* @param {Date} assignedAt - Badge assignment date
10+
* @param {Date} validUntil - Badge expiration date
11+
* @returns
12+
*/
13+
async function assignUserBadge(badgeId, userId, assignedAt, validUntil) {
14+
const [badge] = await db('user_badges')
15+
.insert({
16+
user_id: userId,
17+
badge_id: badgeId,
18+
assigned_at: assignedAt,
19+
valid_until: validUntil ? validUntil : null,
20+
})
21+
.returning('*')
22+
return badge
23+
}
24+
25+
module.exports = {
26+
assignUserBadge,
27+
}

src/models/organization.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,32 @@ async function getMembersPaginated(organizationId, options) {
310310
perPage,
311311
})
312312

313-
return query
313+
// Execute query
314+
const membersPage = await query
315+
316+
// Query badges assigned to the users in the list
317+
const userBadges = await db('user_badges')
318+
.select(
319+
'user_badges.user_id',
320+
'user_badges.badge_id',
321+
'organization_badge.name'
322+
)
323+
.join('organization_badge', 'user_badges.badge_id', 'organization_badge.id')
324+
.whereIn(
325+
'user_badges.user_id',
326+
membersPage.data.map((u) => u.id)
327+
)
328+
.whereRaw(
329+
`user_badges.valid_until IS NULL OR user_badges.valid_until > NOW()`
330+
)
331+
332+
return {
333+
...membersPage,
334+
data: membersPage.data.map((m) => ({
335+
...m,
336+
badges: userBadges.filter((b) => b.user_id === m.id),
337+
})),
338+
}
314339
}
315340

316341
/**

src/pages/organizations/[id]/index.js

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,10 @@ class Organization extends Component {
180180

181181
renderBadges() {
182182
const { id: orgId } = this.props
183-
const columns = [{ key: 'name' }, { key: 'color' }]
183+
const columns = [
184+
{ key: 'name' },
185+
{ key: 'color', render: ({ color }) => <SvgSquare color={color} /> },
186+
]
184187

185188
// Do not render section if badges list cannot be fetched. This might happen
186189
// on network error but also when the user doesn't have privileges.
@@ -204,12 +207,7 @@ class Organization extends Component {
204207
{this.state.badges && (
205208
<Table
206209
data-cy='badges-table'
207-
rows={(this.state.badges || []).map((row) => {
208-
return {
209-
...row,
210-
color: () => <SvgSquare color={row.color} />,
211-
}
212-
})}
210+
rows={this.state.badges || []}
213211
columns={columns}
214212
onRowClick={({ id: badgeId }) =>
215213
Router.push(
@@ -358,7 +356,7 @@ class Organization extends Component {
358356
{isStaff ? (
359357
<div className='team__table'>
360358
<div className='section-actions'>
361-
<SectionHeader>Staff Members </SectionHeader>
359+
<SectionHeader>Staff Members</SectionHeader>
362360
{isOwner && (
363361
<AddMemberForm
364362
onSubmit={async ({ osmId }) => {

0 commit comments

Comments
 (0)