Skip to content

Commit 8666bec

Browse files
authored
Merge pull request #378 from developmentseed/add/third-party-buttons
2 parents b6dcdf2 + a1ae8cc commit 8666bec

File tree

14 files changed

+357
-14
lines changed

14 files changed

+357
-14
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+
})

public/static/icon-osmcha-logo.svg

Lines changed: 1 addition & 0 deletions
Loading

public/static/neis-one-logo.png

1.34 KB
Loading

public/static/osm_logo.png

191 KB
Loading

src/components/badge.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React from 'react'
2+
import theme from '../styles/theme'
3+
4+
const hexToRgb = (hex) =>
5+
hex
6+
.replace(
7+
/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
8+
(m, r, g, b) => '#' + r + r + g + g + b + b
9+
)
10+
.substring(1)
11+
.match(/.{2}/g)
12+
.map((x) => parseInt(x, 16))
13+
.join()
14+
15+
export default function Badge({ name, color, children }) {
16+
return (
17+
<span className='badge'>
18+
{children}
19+
{name}
20+
<style jsx>
21+
{`
22+
.badge {
23+
display: inline-flex;
24+
align-items: center;
25+
font-size: 0.75rem;
26+
background: rgba(${hexToRgb(color)}, 0.125);
27+
padding: 0.125rem 0.5rem;
28+
border-radius: 999px;
29+
box-shadow: 0 0 0 1px ${theme.colors.primaryLite};
30+
font-size: 12px;
31+
white-space: nowrap;
32+
margin-right: 4px;
33+
position: relative;
34+
overflow: hidden;
35+
}
36+
.badge::before {
37+
content: '';
38+
background-color: ${color};
39+
box-shadow: 0 0 0 1px ${theme.colors.primaryLite};
40+
height: 0.5rem;
41+
width: 0.5rem;
42+
border-radius: 999px;
43+
margin-right: 0.25rem;
44+
}
45+
`}
46+
</style>
47+
</span>
48+
)
49+
}

src/components/button.js

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ const URL = process.env.APP_URL
88

99
const ButtonStyles = css.global`
1010
.button {
11-
display: inline-block;
11+
display: inline-flex;
1212
text-align: center;
13+
justify-content: center;
14+
align-items: center;
15+
gap: 0.25rem;
1316
white-space: nowrap;
1417
vertical-align: middle;
1518
line-height: 1.5rem;
@@ -38,13 +41,29 @@ const ButtonStyles = css.global`
3841
box-shadow: 0 0;
3942
}
4043
44+
/* Button variations
45+
========================================================================== */
46+
4147
.button.primary {
4248
color: #ffffff;
4349
background: ${theme.colors.primaryColor};
4450
border: none;
4551
box-shadow: 2px 2px #ffffff, 4px 4px ${theme.colors.primaryColor};
4652
}
4753
54+
.borderless {
55+
border: none;
56+
box-shadow: none;
57+
}
58+
.transparent {
59+
background: transparent;
60+
}
61+
.unstyled {
62+
background: transparent;
63+
border: none;
64+
box-shadow: none;
65+
}
66+
4867
/* Button size modifiers
4968
========================================================================== */
5069
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import join from 'url-join'
2+
const URL = process.env.APP_URL
3+
4+
const ExternalProfileButton = ({ type, userId }) => {
5+
let targetLink
6+
let title
7+
let label
8+
let altText
9+
let logoImg
10+
11+
switch (type) {
12+
case 'osm-profile':
13+
targetLink = `https://www.openstreetmap.org/user/${userId}`
14+
title = 'View profile on OSM'
15+
label = 'OSM'
16+
altText = 'OSM Logo'
17+
logoImg = 'osm_logo.png'
18+
break
19+
case 'hdyc':
20+
targetLink = `https://hdyc.neis-one.org/?${userId}`
21+
title = 'View profile on HDYC'
22+
label = 'HDYC'
23+
altText = 'How Do You Contribute Logo'
24+
logoImg = 'neis-one-logo.png'
25+
break
26+
case 'osmcha':
27+
targetLink = `https://osmcha.org/?filters={"users":[{"label":"${userId}","value":"${userId}"}]}`
28+
title = 'View profile on OSMCha'
29+
label = 'OSMCha'
30+
altText = 'OSMCha Logo'
31+
logoImg = 'icon-osmcha-logo.svg'
32+
break
33+
default:
34+
return null
35+
}
36+
37+
return (
38+
<a
39+
onClick={(e) => {
40+
e.stopPropagation()
41+
}}
42+
href={targetLink}
43+
rel='noopener noreferrer'
44+
target='_blank'
45+
flat
46+
className='button unstyled small'
47+
title={title}
48+
>
49+
<img
50+
src={`${join(URL, `/static/${logoImg}`)}`}
51+
alt={altText}
52+
width='16'
53+
height='16'
54+
/>
55+
{label}
56+
</a>
57+
)
58+
}
59+
60+
export default ExternalProfileButton

src/components/tables/table.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,9 @@ function Row({ columns, row, index, onRowClick, showRowNumber }) {
5050
onRowClick && onRowClick(row, index)
5151
}}
5252
>
53-
{columns.map(({ key }) => {
53+
{columns.map(({ key, render }) => {
5454
let item =
55-
typeof row[key] === 'function'
56-
? row[key](row, index, columns)
57-
: row[key]
55+
typeof render === 'function' ? render(row, index, columns) : row[key]
5856
if (showRowNumber && key === ' ') {
5957
item = index + 1
6058
}

src/components/tables/users.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import T from 'prop-types'
22
import Table from './table'
3+
import Badge from '../badge'
34
import { useFetchList } from '../../hooks/use-fetch-list'
45
import { useState } from 'react'
56
import Pagination from '../pagination'
67
import qs from 'qs'
78
import SearchInput from './search-input'
9+
import ExternalProfileButton from '../external-profile-button'
810

911
function UsersTable({ type, orgId, onRowClick, isSearchable }) {
1012
const [page, setPage] = useState(1)
@@ -32,6 +34,29 @@ function UsersTable({ type, orgId, onRowClick, isSearchable }) {
3234
columns = [
3335
{ key: 'name', sortable: true },
3436
{ key: 'id', label: 'OSM ID', sortable: true },
37+
{
38+
key: 'badges',
39+
render: ({ badges }) => (
40+
<>
41+
{badges?.length > 0 &&
42+
badges.map((b) => (
43+
<Badge color={b.color} key={b.id}>
44+
{b.name}
45+
</Badge>
46+
))}
47+
</>
48+
),
49+
},
50+
{
51+
key: 'External Profiles',
52+
render: ({ name }) => (
53+
<>
54+
<ExternalProfileButton type='osm-profile' userId={name} />
55+
<ExternalProfileButton type='hdyc' userId={name} />
56+
<ExternalProfileButton type='osmcha' userId={name} />
57+
</>
58+
),
59+
},
3560
]
3661
break
3762
case 'org-staff':
@@ -41,6 +66,16 @@ function UsersTable({ type, orgId, onRowClick, isSearchable }) {
4166
{ key: 'name', sortable: true },
4267
{ key: 'id', label: 'OSM ID', sortable: true },
4368
{ key: 'type', sortable: true },
69+
{
70+
key: 'External Profiles',
71+
render: ({ name }) => (
72+
<>
73+
<ExternalProfileButton type='osm-profile' userId={name} />
74+
<ExternalProfileButton type='hdyc' userId={name} />
75+
<ExternalProfileButton type='osmcha' userId={name} />
76+
</>
77+
),
78+
},
4479
]
4580
break
4681
default:

0 commit comments

Comments
 (0)