From 7326937101dbb2f6ceadcb9d98e93c7721515887 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Mon, 16 Jan 2023 11:25:46 -0300 Subject: [PATCH] Add badges to the org members table --- cypress.config.js | 22 ++++++ cypress/e2e/organizations/badges.cy.js | 96 ++++++++++++++++++++++++++ src/components/tables/users.js | 9 +++ src/models/badge.js | 27 ++++++++ src/models/organization.js | 27 +++++++- src/pages/organizations/[id]/index.js | 14 ++-- 6 files changed, 186 insertions(+), 9 deletions(-) create mode 100644 cypress/e2e/organizations/badges.cy.js create mode 100644 src/models/badge.js diff --git a/cypress.config.js b/cypress.config.js index bdaa4b97..21d291c6 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -3,6 +3,7 @@ const db = require('./src/lib/db') const Team = require('./src/models/team') const Organization = require('./src/models/organization') const TeamInvitation = require('./src/models/team-invitation') +const Badge = require('./src/models/badge') const { pick } = require('ramda') module.exports = defineConfig({ @@ -16,6 +17,10 @@ module.exports = defineConfig({ await db.raw('TRUNCATE TABLE organization RESTART IDENTITY CASCADE') await db.raw('TRUNCATE TABLE users RESTART IDENTITY CASCADE') await db.raw('TRUNCATE TABLE osm_users RESTART IDENTITY CASCADE') + await db.raw( + 'TRUNCATE TABLE organization_badge RESTART IDENTITY CASCADE' + ) + await db.raw('TRUNCATE TABLE user_badges RESTART IDENTITY CASCADE') return null }, 'db:seed:create-teams': async ({ teams, moderatorId }) => { @@ -75,6 +80,23 @@ module.exports = defineConfig({ } return null }, + 'db:seed:create-organization-badges': async ({ orgId, badges }) => { + for (let i = 0; i < badges.length; i++) { + const badge = badges[i] + await db('organization_badge').insert({ + organization_id: orgId, + ...pick(['id', 'name', 'color'], badge), + }) + } + return null + }, + 'db:seed:assign-badge-to-users': async ({ badgeId, users }) => { + for (let i = 0; i < users.length; i++) { + const user = users[i] + await Badge.assignUserBadge(badgeId, user.id, new Date()) + } + return null + }, }) }, }, diff --git a/cypress/e2e/organizations/badges.cy.js b/cypress/e2e/organizations/badges.cy.js new file mode 100644 index 00000000..ebb68086 --- /dev/null +++ b/cypress/e2e/organizations/badges.cy.js @@ -0,0 +1,96 @@ +const { + generateSequenceArray, + addZeroPadding, +} = require('../../../src/lib/utils') + +// Generate org member +const org1Members = generateSequenceArray(30, 1).map((i) => ({ + id: i, + name: `User ${addZeroPadding(i, 3)}`, +})) + +const [user1, ...org1Team1Members] = org1Members + +// Organization meta +const org1 = { + id: 1, + name: 'Org 1', + ownerId: user1.id, +} + +const org1Team1 = { + id: 1, + name: 'Org 1 Team 1', +} + +const BADGES_COUNT = 30 + +const org1Badges = generateSequenceArray(BADGES_COUNT, 1).map((i) => ({ + id: i, + name: `Badge ${addZeroPadding(i, 3)}`, + color: `rgba(255,0,0,${i / BADGES_COUNT})`, +})) + +const [org1Badge1, org1Badge2, org1Badge3] = org1Badges + +describe('Organization page', () => { + before(() => { + cy.task('db:reset') + + // Create organization + cy.task('db:seed:create-organizations', [org1]) + + // Add org teams + cy.task('db:seed:create-organization-teams', { + orgId: org1.id, + teams: [org1Team1], + managerId: user1.id, + }) + + // Add members to org team 1 + cy.task('db:seed:add-members-to-team', { + teamId: org1Team1.id, + members: org1Team1Members, + }) + + // Create org badges + cy.task('db:seed:create-organization-badges', { + orgId: org1.id, + badges: org1Badges, + }) + + // Assign badge 1 to the first five users + cy.task('db:seed:assign-badge-to-users', { + badgeId: org1Badge1.id, + users: org1Team1Members.slice(0, 4), + }) + + // Assign badge 2 to five users, starting at user 3 + cy.task('db:seed:assign-badge-to-users', { + badgeId: org1Badge2.id, + users: org1Team1Members.slice(2, 7), + }) + + // Assign badge 3 to five users, starting at user 5 + cy.task('db:seed:assign-badge-to-users', { + badgeId: org1Badge3.id, + users: org1Team1Members.slice(4, 9), + }) + }) + + it('Organization members table display badges', () => { + cy.login(user1) + + cy.visit('/organizations/1') + + cy.get('[data-cy=org-members-table]') + .find('tbody tr:nth-child(6) td:nth-child(3)') + .contains('Badge 002') + cy.get('[data-cy=org-members-table]') + .find('tbody tr:nth-child(6) td:nth-child(3)') + .contains('Badge 003') + cy.get('[data-cy=org-members-table]') + .find('tbody tr:nth-child(10) td:nth-child(3)') + .contains('Badge 003') + }) +}) diff --git a/src/components/tables/users.js b/src/components/tables/users.js index f48976b3..8ec1d138 100644 --- a/src/components/tables/users.js +++ b/src/components/tables/users.js @@ -33,6 +33,15 @@ function UsersTable({ type, orgId, onRowClick, isSearchable }) { columns = [ { key: 'name', sortable: true }, { key: 'id', label: 'OSM ID', sortable: true }, + { + key: 'badges', + render: ({ badges }) => ( + <> + {badges?.length > 0 && + badges.map((b) =>
{b.name}
)} + + ), + }, { key: 'External Profiles', render: ({ name }) => ( diff --git a/src/models/badge.js b/src/models/badge.js new file mode 100644 index 00000000..14099702 --- /dev/null +++ b/src/models/badge.js @@ -0,0 +1,27 @@ +const db = require('../lib/db') + +/** + * + * Assign existing badge to an user. + * + * @param {int} userId - User id + * @param {int} badgeId - Badge id + * @param {Date} assignedAt - Badge assignment date + * @param {Date} validUntil - Badge expiration date + * @returns + */ +async function assignUserBadge(badgeId, userId, assignedAt, validUntil) { + const [badge] = await db('user_badges') + .insert({ + user_id: userId, + badge_id: badgeId, + assigned_at: assignedAt, + valid_until: validUntil ? validUntil : null, + }) + .returning('*') + return badge +} + +module.exports = { + assignUserBadge, +} diff --git a/src/models/organization.js b/src/models/organization.js index 18061ae2..ff9dff41 100644 --- a/src/models/organization.js +++ b/src/models/organization.js @@ -310,7 +310,32 @@ async function getMembersPaginated(organizationId, options) { perPage, }) - return query + // Execute query + const membersPage = await query + + // Query badges assigned to the users in the list + const userBadges = await db('user_badges') + .select( + 'user_badges.user_id', + 'user_badges.badge_id', + 'organization_badge.name' + ) + .join('organization_badge', 'user_badges.badge_id', 'organization_badge.id') + .whereIn( + 'user_badges.user_id', + membersPage.data.map((u) => u.id) + ) + .whereRaw( + `user_badges.valid_until IS NULL OR user_badges.valid_until > NOW()` + ) + + return { + ...membersPage, + data: membersPage.data.map((m) => ({ + ...m, + badges: userBadges.filter((b) => b.user_id === m.id), + })), + } } /** diff --git a/src/pages/organizations/[id]/index.js b/src/pages/organizations/[id]/index.js index c79b028d..a97f916f 100644 --- a/src/pages/organizations/[id]/index.js +++ b/src/pages/organizations/[id]/index.js @@ -180,7 +180,10 @@ class Organization extends Component { renderBadges() { const { id: orgId } = this.props - const columns = [{ key: 'name' }, { key: 'color' }] + const columns = [ + { key: 'name' }, + { key: 'color', render: ({ color }) => }, + ] // Do not render section if badges list cannot be fetched. This might happen // on network error but also when the user doesn't have privileges. @@ -204,12 +207,7 @@ class Organization extends Component { {this.state.badges && ( { - return { - ...row, - color: () => , - } - })} + rows={this.state.badges || []} columns={columns} onRowClick={({ id: badgeId }) => Router.push( @@ -358,7 +356,7 @@ class Organization extends Component { {isStaff ? (
- Staff Members + Staff Members {isOwner && ( {