Skip to content

Commit de74483

Browse files
authored
Merge pull request #375 from developmentseed/add/org-view-table-sort
Adds search and sort to org teams, members, staff tables
2 parents 99bd854 + 24456a3 commit de74483

File tree

13 files changed

+162
-34
lines changed

13 files changed

+162
-34
lines changed

cypress/e2e/organizations/pagination.cy.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ describe('Organization page', () => {
8080
cy.contains('Showing 1-3 of 3')
8181
})
8282

83+
// Sort by user id
84+
cy.get('[data-cy=org-staff-table-head-column-type]').click()
85+
cy.get('[data-cy=org-staff-table]')
86+
.find('tbody tr:nth-child(3) td:nth-child(1)')
87+
.contains('User 003')
88+
8389
// Perform search by username
8490
cy.get('[data-cy=org-staff-table-search-input]').type('2')
8591
cy.get('[data-cy=org-staff-table-search-submit]').click()
@@ -122,6 +128,26 @@ describe('Organization page', () => {
122128
*/
123129
cy.get('[data-cy=org-teams-table]').contains('Org 1 Team 010')
124130

131+
// Sort by name
132+
cy.get('[data-cy=org-teams-table-head-column-name]').click()
133+
cy.get('[data-cy=org-teams-table]')
134+
.find('tbody tr:nth-child(4) td:nth-child(1)')
135+
.contains('Team 032')
136+
137+
// Reset sort
138+
cy.get('[data-cy=org-teams-table-head-column-name]').click()
139+
140+
// Perform search by username
141+
cy.get('[data-cy=org-teams-table-search-input]').type('02')
142+
cy.get('[data-cy=org-teams-table-search-submit]').click()
143+
cy.get('[data-cy=org-teams-table-pagination]').contains(
144+
'Showing 1-10 of 11'
145+
)
146+
cy.get('[data-cy=org-teams-table]')
147+
.find('tbody tr:nth-child(9) td:nth-child(1)')
148+
.contains('Team 027')
149+
cy.get('[data-cy=org-teams-table-search-input]').clear()
150+
125151
// Verify index, then click on last page button
126152
cy.get('[data-cy=org-teams-table-pagination]').within(() => {
127153
cy.contains('Showing 1-10 of 35')
@@ -190,6 +216,18 @@ describe('Organization page', () => {
190216
// Item from page 2 is present
191217
cy.get('[data-cy=org-members-table]').contains('User 207')
192218

219+
// Sort by user name
220+
cy.get('[data-cy=org-members-table-head-column-name]').click()
221+
cy.get('[data-cy=org-members-table]')
222+
.find('tbody tr:nth-child(4) td:nth-child(1)')
223+
.contains('User 301')
224+
cy.get('[data-cy=org-members-table]')
225+
.find('tbody tr:nth-child(10) td:nth-child(1)')
226+
.contains('User 215')
227+
228+
// Reset sort
229+
cy.get('[data-cy=org-members-table-head-column-name]').click()
230+
193231
// Perform search by username
194232
cy.get('[data-cy=org-members-table-search-input]').type('User 2')
195233
cy.get('[data-cy=org-members-table-search-submit]').click()

cypress/e2e/teams/index.cy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('Teams page', () => {
3232
cy.get('[data-cy=teams-table-pagination]').contains('Showing 1-10 of 35')
3333

3434
// Sort by team name
35-
cy.get('[data-cy=table-head-column-name]').click()
35+
cy.get('[data-cy=teams-table-head-column-name]').click()
3636
cy.get('[data-cy=teams-table]')
3737
.find('tbody tr:nth-child(1) td:nth-child(2)')
3838
.contains('Team 035')

cypress/e2e/teams/view.cy.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('Teams page', () => {
9797
)
9898

9999
// Perform sort by username
100-
cy.get('[data-cy=table-head-column-name]').click()
100+
cy.get('[data-cy=team-members-table-head-column-name]').click()
101101
cy.get('[data-cy=team-members-table]').contains('User 025')
102102
cy.get('[data-cy=team-members-table]').contains('User 016')
103103

src/components/layout.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export const globalStyles = css.global`
185185
margin-bottom: 0.5rem;
186186
display: flex;
187187
justify-content: space-between;
188-
align-items: center;
188+
align-items: stretch;
189189
}
190190
191191
.form-control__vertical {
@@ -217,6 +217,7 @@ export const globalStyles = css.global`
217217
}
218218
219219
.form-control input#search {
220+
margin-block: revert;
220221
margin: 0;
221222
padding: 0.375rem 1rem 0.375rem 0.5rem;
222223
}

src/components/tables/table.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import React from 'react'
22
import theme from '../../styles/theme'
33

4-
function TableHead({ columns, sort, setSort, onClick }) {
4+
function TableHead({ dataCy, columns, sort, setSort, onClick }) {
55
return (
66
<thead>
77
<tr>
88
{columns.map(({ sortable, label, key }) => {
9-
const header = label || key
10-
11-
const isSorted = sortable && sort.key === header
9+
const isSorted = sortable && sort.key === key
1210
const currentSortDirection = (isSorted && sort.direction) || 'none'
1311
const nextSortDirection =
1412
currentSortDirection === 'asc' ? 'desc' : 'asc'
@@ -20,7 +18,7 @@ function TableHead({ columns, sort, setSort, onClick }) {
2018
return (
2119
<th
2220
key={`table-head-column-${key}`}
23-
data-cy={`table-head-column-${key}`}
21+
data-cy={`${dataCy}-head-column-${key}`}
2422
className={sortable && 'sortable'}
2523
title={sortable && `Click to sort by ${key}`}
2624
onClick={() => {
@@ -34,7 +32,7 @@ function TableHead({ columns, sort, setSort, onClick }) {
3432
}
3533
}}
3634
>
37-
{header}
35+
{label || key}
3836
{sortable && ` ${sortIcon}`}
3937
</th>
4038
)
@@ -123,7 +121,12 @@ export default function Table({
123121
showRowNumbers && columns.unshift({ key: ' ' })
124122
return (
125123
<table data-cy={dataCy}>
126-
<TableHead columns={columns} sort={sort} setSort={setSort} />
124+
<TableHead
125+
dataCy={dataCy}
126+
columns={columns}
127+
sort={sort}
128+
setSort={setSort}
129+
/>
127130
<TableBody
128131
columns={columns}
129132
rows={rows}

src/components/tables/teams.js

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,25 @@ import join from 'url-join'
55
import { useFetchList } from '../../hooks/use-fetch-list'
66
import { useState } from 'react'
77
import Pagination from '../pagination'
8+
import SearchInput from './search-input'
9+
import qs from 'qs'
810

911
const APP_URL = process.env.APP_URL
1012

1113
function TeamsTable({ type, orgId }) {
1214
const [page, setPage] = useState(1)
15+
const [search, setSearch] = useState(null)
16+
const [sort, setSort] = useState({
17+
key: 'name',
18+
direction: 'asc',
19+
})
20+
21+
const querystring = qs.stringify({
22+
search,
23+
page,
24+
sort: sort.key,
25+
order: sort.direction,
26+
})
1327

1428
let apiBasePath
1529
let emptyMessage
@@ -33,22 +47,33 @@ function TeamsTable({ type, orgId }) {
3347
const {
3448
result: { data, pagination },
3549
isLoading,
36-
} = useFetchList(`${apiBasePath}?page=${page}`)
50+
} = useFetchList(`${apiBasePath}?${querystring}`)
3751

38-
const columns = [{ key: 'name' }, { key: 'members' }]
52+
const columns = [
53+
{ key: 'name', sortable: true },
54+
{ key: 'members', sortable: true },
55+
]
3956

4057
return (
4158
<>
59+
<SearchInput
60+
data-cy={`${type}-table`}
61+
onSearch={(search) => {
62+
// Reset to page 1 and search
63+
setPage(1)
64+
setSearch(search)
65+
}}
66+
placeholder='Search by team name'
67+
/>
4268
<Table
4369
data-cy={`${type}-table`}
4470
rows={data}
4571
columns={columns}
4672
emptyPlaceHolder={isLoading ? 'Loading...' : emptyMessage}
73+
sort={sort}
74+
setSort={setSort}
4775
onRowClick={(row) => {
48-
Router.push(
49-
join(APP_URL, `/team?id=${row.id}`),
50-
join(APP_URL, `/teams/${row.id}`)
51-
)
76+
Router.push(join(APP_URL, `/teams/${row.id}`))
5277
}}
5378
/>
5479
{pagination?.total > 0 && (

src/components/tables/users.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import SearchInput from './search-input'
99
function UsersTable({ type, orgId, onRowClick, isSearchable }) {
1010
const [page, setPage] = useState(1)
1111
const [search, setSearch] = useState(null)
12+
const [sort, setSort] = useState({
13+
key: 'name',
14+
direction: 'asc',
15+
})
1216

1317
let apiBasePath
1418
let emptyMessage
@@ -17,21 +21,26 @@ function UsersTable({ type, orgId, onRowClick, isSearchable }) {
1721
const querystring = qs.stringify({
1822
search,
1923
page,
24+
sort: sort.key,
25+
order: sort.direction,
2026
})
2127

2228
switch (type) {
2329
case 'org-members':
2430
apiBasePath = `/organizations/${orgId}/members`
2531
emptyMessage = 'No members yet.'
26-
columns = [{ key: 'name' }, { key: 'id', label: 'OSM ID' }]
32+
columns = [
33+
{ key: 'name', sortable: true },
34+
{ key: 'id', label: 'OSM ID', sortable: true },
35+
]
2736
break
2837
case 'org-staff':
2938
apiBasePath = `/organizations/${orgId}/staff`
3039
emptyMessage = 'No staff found.'
3140
columns = [
32-
{ key: 'name' },
33-
{ key: 'id', label: 'OSM ID' },
34-
{ key: 'type' },
41+
{ key: 'name', sortable: true },
42+
{ key: 'id', label: 'OSM ID', sortable: true },
43+
{ key: 'type', sortable: true },
3544
]
3645
break
3746
default:
@@ -61,6 +70,8 @@ function UsersTable({ type, orgId, onRowClick, isSearchable }) {
6170
/>
6271
)}
6372
<Table
73+
sort={sort}
74+
setSort={setSort}
6475
data-cy={`${type}-table`}
6576
rows={data}
6677
columns={columns}

src/middlewares/base-handler.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,22 @@ export function createBaseHandler() {
3434
attachParams: true,
3535
onError: (err, req, res) => {
3636
logger.error(err)
37+
3738
// Handle Boom errors
3839
if (err.isBoom) {
3940
const { statusCode, payload } = err.output
4041
return res.status(statusCode).json(payload)
4142
}
4243

44+
// Handle Yup validation errors
45+
if (err.name === 'ValidationError') {
46+
return res.status(400).json({
47+
statusCode: 400,
48+
error: 'Validation Error',
49+
message: err.message,
50+
})
51+
}
52+
4353
// Generic error
4454
return res.status(500).json({
4555
statusCode: 500,

src/models/organization.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,9 @@ async function getMembers(organizationId, page) {
279279
*/
280280
async function getMembersPaginated(organizationId, options) {
281281
const currentPage = options?.page || 1
282+
const sort = options?.sort || 'name'
283+
const order = options?.order || 'asc'
284+
const perPage = options?.perPage || DEFAULT_PAGE_SIZE
282285

283286
// Sub-query for all org teams
284287
const allOrgTeamsQuery = db('organization_team')
@@ -291,18 +294,20 @@ async function getMembersPaginated(organizationId, options) {
291294
.select('member.osm_id as id', 'osm_users.name')
292295
.where('member.team_id', 'in', allOrgTeamsQuery)
293296
.groupBy('member.osm_id', 'osm_users.name')
294-
.orderBy('member.osm_id')
295297

296298
// Apply search
297299
if (options.search) {
298300
query = query.whereILike('osm_users.name', `%${options.search}%`)
299301
}
300302

303+
// Apply sort
304+
query = query.orderBy(sort, order)
305+
301306
// Add pagination
302307
query = query.paginate({
303308
isLengthAware: true,
304309
currentPage,
305-
perPage: DEFAULT_PAGE_SIZE,
310+
perPage,
306311
})
307312

308313
return query
@@ -442,12 +447,17 @@ async function getOrgStaff(options) {
442447
* @param {Object} options.osmId - filter by osm id
443448
*/
444449
async function getOrgStaffPaginated(organizationId, options = {}) {
450+
const currentPage = options?.page || 1
451+
const sort = options?.sort || 'name'
452+
const order = options?.order || 'asc'
453+
const perPage = options?.perPage || DEFAULT_PAGE_SIZE
454+
445455
// Get owners
446456
let ownerQuery = db('organization_owner')
447457
.join('osm_users', 'organization_owner.osm_id', 'osm_users.id')
448458
.select(
449459
'organization_owner.organization_id',
450-
'organization_owner.osm_id',
460+
'organization_owner.osm_id as id',
451461
db.raw("'owner' as type"),
452462
'osm_users.name'
453463
)
@@ -463,7 +473,7 @@ async function getOrgStaffPaginated(organizationId, options = {}) {
463473
.join('osm_users', 'organization_manager.osm_id', 'osm_users.id')
464474
.select(
465475
'organization_manager.organization_id',
466-
'organization_manager.osm_id',
476+
'organization_manager.osm_id as id',
467477
db.raw("'manager' as type"),
468478
'osm_users.name'
469479
)
@@ -486,11 +496,14 @@ async function getOrgStaffPaginated(organizationId, options = {}) {
486496
// Unite owner and manager queries
487497
let staffQuery = ownerQuery.unionAll(managerQuery)
488498

499+
// Apply sort
500+
staffQuery = staffQuery.orderBy(sort, order)
501+
489502
// Execute staff query with pagination
490503
return await staffQuery.paginate({
491504
isLengthAware: true,
492-
currentPage: options.page || 1,
493-
perPage: options.perPage || DEFAULT_PAGE_SIZE,
505+
currentPage,
506+
perPage,
494507
})
495508
}
496509

0 commit comments

Comments
 (0)