From 7beae3aacf15ae85fa04e4c09005a4f471846e3f Mon Sep 17 00:00:00 2001 From: Marc Farra Date: Tue, 16 May 2023 18:49:29 +0300 Subject: [PATCH 1/5] Add locations API endpoint --- src/lib/org-api.js | 4 +- .../api/organizations/[orgId]/locations.js | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 src/pages/api/organizations/[orgId]/locations.js diff --git a/src/lib/org-api.js b/src/lib/org-api.js index e32ca061..f84408b4 100644 --- a/src/lib/org-api.js +++ b/src/lib/org-api.js @@ -63,8 +63,8 @@ export async function getOrg(id) { * @param {integer} id * @returns {response} */ -export async function getOrgTeams(id) { - let res = await fetch(join(ORG_URL, `${id}`, 'teams')) +export async function getOrgLocations(id) { + let res = await fetch(join(ORG_URL, `${id}`, 'locations')) if (res.status === 200) { return res.json() diff --git a/src/pages/api/organizations/[orgId]/locations.js b/src/pages/api/organizations/[orgId]/locations.js new file mode 100644 index 00000000..51fb6712 --- /dev/null +++ b/src/pages/api/organizations/[orgId]/locations.js @@ -0,0 +1,55 @@ +import { createBaseHandler } from '../../../../middlewares/base-handler' +import { validate } from '../../../../middlewares/validation' +import Team from '../../../../models/team' +import * as Yup from 'yup' +import canViewOrgTeams from '../../../../middlewares/can/view-org-teams' + +const handler = createBaseHandler() + +/** + * @swagger + * /organizations/{id}/teams: + * get: + * summary: Get locations of teams of an organization + * tags: + * - organizations + * parameters: + * - in: path + * name: id + * required: true + * description: Numeric ID of the organization the teams are part of. + * schema: + * type: integer + * responses: + * 200: + * description: A list of teams. + * content: + * application/json: + * schema: + * type: object + * properties: + * data: + * $ref: '#/components/schemas/ArrayOfTeams' + */ +handler.get( + canViewOrgTeams, + validate({ + query: Yup.object({ + orgId: Yup.number().required().positive().integer(), + }).required(), + }), + async function (req, res) { + const { orgId } = req.query + const { + org: { isMember, isOwner, isManager }, + } = req + const teamList = await Team.list({ + organizationId: orgId, + includePrivate: isMember || isManager || isOwner, + }) + + return res.send({ data: teamList }) + } +) + +export default handler From 51a10a56850a7ac85dac3da0c12c2dc9a4727161 Mon Sep 17 00:00:00 2001 From: Marc Farra Date: Tue, 16 May 2023 18:50:44 +0300 Subject: [PATCH 2/5] Allow api to accept bbox for paginatedList --- src/components/tables/teams.js | 18 +++++++++++------- src/models/team.js | 19 +++++++++++++++---- src/pages/api/organizations/[orgId]/teams.js | 13 ++++++++++++- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/components/tables/teams.js b/src/components/tables/teams.js index c841ba47..4cb9b0a2 100644 --- a/src/components/tables/teams.js +++ b/src/components/tables/teams.js @@ -10,7 +10,7 @@ import qs from 'qs' const APP_URL = process.env.APP_URL -function TeamsTable({ type, orgId }) { +function TeamsTable({ type, orgId, bbox }) { const [page, setPage] = useState(1) const [search, setSearch] = useState(null) const [sort, setSort] = useState({ @@ -18,12 +18,16 @@ function TeamsTable({ type, orgId }) { direction: 'asc', }) - const querystring = qs.stringify({ - search, - page, - sort: sort.key, - order: sort.direction, - }) + const querystring = qs.stringify( + { + search, + page, + sort: sort.key, + order: sort.direction, + bbox: bbox, + }, + { arrayFormat: 'comma' } + ) let apiBasePath let emptyMessage diff --git a/src/models/team.js b/src/models/team.js index d6f9f74c..a1af6f2a 100644 --- a/src/models/team.js +++ b/src/models/team.js @@ -349,17 +349,16 @@ async function paginatedList(options = {}) { * * @param options * @param {Array[float]} options.bbox - filter for teams whose location is in bbox (xmin, ymin, xmax, ymax) + * @param {int} options.organizationId - filter by whether team belongs to organization * @return {[Array]} Array of teams **/ -async function list({ bbox }) { +async function list({ bbox, organizationId, includePrivate }) { // TODO: this method should be merged to the paginatedList() method when possible // for consistency, as they both return a list of teams. const st = knexPostgis(db) - let query = db('team') - .select(...teamAttributes, st.asGeoJSON('location')) - .where('privacy', 'public') + let query = db('team').select(...teamAttributes, st.asGeoJSON('location')) if (bbox) { query = query.where( @@ -367,6 +366,18 @@ async function list({ bbox }) { ) } + if (!includePrivate) { + query.where('privacy', 'public') + } + + if (organizationId) { + query = query.whereIn('id', function () { + this.select('team_id') + .from('organization_team') + .where('organization_id', organizationId) + }) + } + return query } diff --git a/src/pages/api/organizations/[orgId]/teams.js b/src/pages/api/organizations/[orgId]/teams.js index f9ebf05b..e380f48c 100644 --- a/src/pages/api/organizations/[orgId]/teams.js +++ b/src/pages/api/organizations/[orgId]/teams.js @@ -2,6 +2,7 @@ import { createBaseHandler } from '../../../../middlewares/base-handler' import { validate } from '../../../../middlewares/validation' import Organization from '../../../../models/organization' import Team from '../../../../models/team' +import Boom from '@hapi/boom' import * as Yup from 'yup' import canCreateOrgTeam from '../../../../middlewares/can/create-org-team' import canViewOrgTeams from '../../../../middlewares/can/view-org-teams' @@ -103,15 +104,25 @@ handler.get( }).required(), }), async function (req, res) { - const { orgId, page, perPage, search, sort, order } = req.query + const { orgId, page, perPage, search, sort, order, bbox } = req.query const { org: { isMember, isOwner, isManager }, } = req + + let bounds = bbox || null + if (bbox) { + bounds = bbox.split(',').map((num) => parseFloat(num)) + if (bounds.length !== 4) { + throw Boom.badRequest('error in bbox param') + } + } + return res.send( await Team.paginatedList({ organizationId: orgId, page, perPage, + bbox: bounds, search, sort, order, From 3685817d724f7a59abb7d573e17d44a33f949dec Mon Sep 17 00:00:00 2001 From: Marc Farra Date: Tue, 16 May 2023 18:51:00 +0300 Subject: [PATCH 3/5] frontend map handling --- src/pages/organizations/[id]/index.js | 70 ++++++++++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/src/pages/organizations/[id]/index.js b/src/pages/organizations/[id]/index.js index 0ddd8e84..1539e9db 100644 --- a/src/pages/organizations/[id]/index.js +++ b/src/pages/organizations/[id]/index.js @@ -7,11 +7,18 @@ import { removeManager, addOwner, removeOwner, - getOrgTeams, + getOrgLocations, getOrgStaff, } from '../../../lib/org-api' import { getUserOrgProfile } from '../../../lib/profiles-api' -import { Box, Container, Heading, Button, Flex } from '@chakra-ui/react' +import { + Box, + Checkbox, + Container, + Heading, + Button, + Flex, +} from '@chakra-ui/react' import Table from '../../../components/tables/table' import { AddMemberByIdForm } from '../../../components/add-member-form' import ProfileModal from '../../../components/profile-modal' @@ -66,6 +73,8 @@ class Organization extends Component { profileInfo: [], profileUserId: '', teams: [], + searchOnMapMove: false, + mapBounds: undefined, managers: [], owners: [], page: 0, @@ -82,7 +91,7 @@ class Organization extends Component { await this.getOrg() await this.getOrgStaff() await this.getBadges() - await this.getOrgTeams() + await this.getOrgLocations() } async openProfileModal(user) { @@ -165,10 +174,10 @@ class Organization extends Component { } } - async getOrgTeams() { + async getOrgLocations() { const { id } = this.props try { - let teams = await getOrgTeams(id) + let teams = await getOrgLocations(id) this.setState({ teams, }) @@ -258,6 +267,28 @@ class Organization extends Component { ) : null } + /** + * Bounds is a WESN box, refresh teams + */ + onMapBoundsChange(bounds) { + if (this.state.searchOnMapMove) { + this.setState({ + mapBounds: bounds, + }) + } else { + this.setState({ mapBounds: null }) + } + } + + setSearchOnMapMove(e) { + this.setState( + { + searchOnMapMove: e.target.checked, + }, + () => this.getOrgLocations() + ) + } + renderMap(teams) { const { data } = teams @@ -282,13 +313,14 @@ class Organization extends Component { zIndex: '10', marginBottom: '1rem', }} - onBoundsChange={() => {}} + onBoundsChange={this.onMapBoundsChange.bind(this)} /> ) } render() { - const { org, managers, owners, error, teams } = this.state + const { org, managers, owners, error, teams, searchOnMapMove, mapBounds } = + this.state const userId = parseInt(this.state?.session?.user_id) // Handle org loading errors @@ -367,7 +399,29 @@ class Organization extends Component { Teams {this.renderMap(teams)} - + this.setSearchOnMapMove(e)} + > + Filter teams by map + + {isStaff ? ( From faf0b3af4fd2d56f731bb415cc43ebb4a612dce3 Mon Sep 17 00:00:00 2001 From: Marc Farra Date: Wed, 17 May 2023 14:42:29 +0300 Subject: [PATCH 4/5] Update src/pages/organizations/[id]/index.js Co-authored-by: Lane Goodman --- src/pages/organizations/[id]/index.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/pages/organizations/[id]/index.js b/src/pages/organizations/[id]/index.js index 1539e9db..6ebdb3cc 100644 --- a/src/pages/organizations/[id]/index.js +++ b/src/pages/organizations/[id]/index.js @@ -315,6 +315,25 @@ class Organization extends Component { }} onBoundsChange={this.onMapBoundsChange.bind(this)} /> + this.setSearchOnMapMove(e)} + > + Filter teams by map + + ) } From 44d13f7095a6792832ec43f4804365d7a3e3df10 Mon Sep 17 00:00:00 2001 From: Marc Farra Date: Wed, 17 May 2023 15:48:06 +0300 Subject: [PATCH 5/5] Fix state and render bugs --- src/pages/organizations/[id]/index.js | 42 +++++++++------------------ 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/src/pages/organizations/[id]/index.js b/src/pages/organizations/[id]/index.js index 6ebdb3cc..0fe2608b 100644 --- a/src/pages/organizations/[id]/index.js +++ b/src/pages/organizations/[id]/index.js @@ -84,6 +84,7 @@ class Organization extends Component { this.closeProfileModal = this.closeProfileModal.bind(this) this.renderBadges = this.renderBadges.bind(this) + this.renderMap = this.renderMap.bind(this) } async componentDidMount() { @@ -306,16 +307,17 @@ class Organization extends Component { ) return ( - - + + this.setSearchOnMapMove(e)} > Filter teams by map @@ -418,24 +420,6 @@ class Organization extends Component { Teams {this.renderMap(teams)} - this.setSearchOnMapMove(e)} - > - Filter teams by map -