Skip to content

Commit 8a5d760

Browse files
authored
Merge pull request #438 from developmentseed/feature/search-by-username
Add OSM users by username
2 parents 8805e38 + 8313f36 commit 8a5d760

File tree

8 files changed

+359
-16
lines changed

8 files changed

+359
-16
lines changed

DEPLOYMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ HYDRA_AUTHZ_PATH=/hyauth/oauth2/auth
8181

8282
## Deployment
8383

84-
Once the environment variables, `hydra.yml` and the reverse proxy are created, we can then run:
84+
Once the environment variables, `hydra-config/hydra.yml` and the reverse proxy are created, we can then run:
8585

8686
```docker
8787
docker-compose -f compose.yml -f compose.prod.yml up

src/components/add-member-form.js

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1-
import React from 'react'
1+
import React, { useState } from 'react'
22
import { Formik, Field, Form, FormErrorMessage } from 'formik'
3-
import { Button, Flex, Input } from '@chakra-ui/react'
3+
import {
4+
Box,
5+
Button,
6+
Flex,
7+
Input,
8+
List,
9+
ListItem,
10+
ListIcon,
11+
Link,
12+
Code,
13+
Text,
14+
} from '@chakra-ui/react'
15+
import { AtSignIcon, AddIcon } from '@chakra-ui/icons'
16+
import join from 'url-join'
17+
418
import logger from '../lib/logger'
519

6-
export default function AddMemberForm({ onSubmit }) {
20+
const APP_URL = process.env.APP_URL
21+
const OSM_DOMAIN = process.env.OSM_DOMAIN
22+
23+
export function AddMemberByIdForm({ onSubmit }) {
724
return (
825
<Formik
926
initialValues={{ osmId: '' }}
@@ -24,15 +41,21 @@ export default function AddMemberForm({ onSubmit }) {
2441
const addMemberText = `Add Member ${isSubmitting ? ' 🕙' : ''}`
2542

2643
return (
27-
<Flex as={Form} alignItems='center'>
44+
<Flex
45+
as={Form}
46+
alignItems='center'
47+
justifyContent='space-between'
48+
width={'100%'}
49+
gap={2}
50+
>
2851
<Field
2952
as={Input}
3053
type='text'
3154
name='osmId'
3255
id='osmId'
3356
placeholder='OSM ID'
3457
value={values.osmId}
35-
style={{ width: '6rem' }}
58+
flex={1}
3659
/>
3760
{status && status.msg && (
3861
<FormErrorMessage>{status.msg}</FormErrorMessage>
@@ -53,3 +76,129 @@ export default function AddMemberForm({ onSubmit }) {
5376
/>
5477
)
5578
}
79+
80+
export function AddMemberByUsernameForm({ onSubmit }) {
81+
const [searchResult, setSearchResult] = useState()
82+
const searchUsername = async (data, setStatus) => {
83+
setStatus('searching')
84+
let res = await fetch(join(APP_URL, `/api/users?search=${data.username}`))
85+
if (res.status === 200) {
86+
const data = await res.json()
87+
if (data?.users.length) {
88+
setSearchResult(data.users)
89+
setStatus('successSearch')
90+
} else {
91+
setSearchResult([])
92+
setStatus('noResults')
93+
}
94+
} else {
95+
setSearchResult([])
96+
setStatus('noResults')
97+
}
98+
}
99+
const submit = async (uid, username, actions) => {
100+
actions.setSubmitting(true)
101+
102+
try {
103+
await onSubmit({ osmId: uid, username })
104+
actions.setSubmitting(false)
105+
actions.resetForm({ username: '' })
106+
setSearchResult([])
107+
} catch (e) {
108+
logger.error(e)
109+
actions.setSubmitting(false)
110+
actions.setStatus(e.message)
111+
}
112+
}
113+
return (
114+
<Formik
115+
initialValues={{ username: '' }}
116+
render={({
117+
status,
118+
setStatus,
119+
isSubmitting,
120+
values,
121+
setSubmitting,
122+
resetForm,
123+
}) => {
124+
return (
125+
<>
126+
<Flex
127+
as={Form}
128+
alignItems='center'
129+
justifyContent='space-between'
130+
width={'100%'}
131+
gap={2}
132+
>
133+
<Field
134+
as={Input}
135+
type='text'
136+
name='username'
137+
id='username'
138+
placeholder='Search OSM Username'
139+
value={values.username}
140+
flex={1}
141+
/>
142+
{status && status.msg && (
143+
<FormErrorMessage>{status.msg}</FormErrorMessage>
144+
)}
145+
<Button
146+
textTransform={'lowercase'}
147+
onClick={() => searchUsername(values, setStatus)}
148+
variant='outline'
149+
isLoading={status === 'searching'}
150+
loadingText='Searching'
151+
isDisabled={status === 'searching' || !values.username}
152+
>
153+
Search
154+
</Button>
155+
</Flex>
156+
<Box display='flex' justifyContent='stretch' py={3} px={1}>
157+
<List spacing={5} fontSize='sm' width={'100%'}>
158+
{searchResult?.length &&
159+
searchResult.map((u) => (
160+
<ListItem
161+
key={u.id}
162+
display='flex'
163+
alignItems='center'
164+
justifyContent='space-between'
165+
marginTop='1rem'
166+
>
167+
<ListIcon as={AtSignIcon} color='brand.600' />
168+
<Link href={join(OSM_DOMAIN, '/user', u.name)} isExternal>
169+
{u.name}
170+
</Link>
171+
<Code ml={2}>{u.id}</Code>
172+
<Button
173+
ml='auto'
174+
textTransform='lowercase'
175+
onClick={async () =>
176+
submit(u.id, u.name, {
177+
setStatus,
178+
setSubmitting,
179+
resetForm,
180+
})
181+
}
182+
size='sm'
183+
isLoading={isSubmitting}
184+
loadingText='Adding'
185+
isDisabled={isSubmitting}
186+
leftIcon={<AddIcon />}
187+
>
188+
Add
189+
</Button>
190+
</ListItem>
191+
))}
192+
{status === 'noResults' && (
193+
<Text as='b'>
194+
No results found. Try typing the exact OSM username.
195+
</Text>
196+
)}
197+
</List>
198+
</Box>
199+
</>
200+
)
201+
}}
202+
/>
203+
)
204+
}

src/components/add-member-modal.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import {
2+
Box,
3+
Heading,
4+
Modal,
5+
ModalBody,
6+
ModalCloseButton,
7+
ModalContent,
8+
ModalFooter,
9+
ModalHeader,
10+
ModalOverlay,
11+
} from '@chakra-ui/react'
12+
import { AddMemberByIdForm, AddMemberByUsernameForm } from './add-member-form'
13+
14+
export function AddMemberModal({ isOpen, onClose, onSubmit }) {
15+
return (
16+
<Modal
17+
isCentered
18+
isOpen={isOpen}
19+
onClose={onClose}
20+
scrollBehavior={'inside'}
21+
>
22+
<ModalOverlay />
23+
<ModalContent flexDirection={'column'} as='article' gap={2}>
24+
<ModalHeader gap={4} display='flex' flexDir={'column'}>
25+
<Heading size='sm' as='h3'>
26+
Add Member
27+
</Heading>
28+
<ModalCloseButton onClick={() => onClose()} />
29+
</ModalHeader>
30+
<ModalBody display='flex' flexDirection={'column'} gap={2}>
31+
<Box>
32+
<Heading size='sm' as='h4'>
33+
OSM ID
34+
</Heading>
35+
<AddMemberByIdForm onSubmit={onSubmit} />
36+
</Box>
37+
<Box>
38+
<Heading size='sm' as='h4'>
39+
OSM Username
40+
</Heading>
41+
<AddMemberByUsernameForm onSubmit={onSubmit} />
42+
</Box>
43+
</ModalBody>
44+
<ModalFooter></ModalFooter>
45+
</ModalContent>
46+
</Modal>
47+
)
48+
}

src/models/users.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const db = require('../lib/db')
2+
3+
/**
4+
* Get paginated list of teams
5+
*
6+
* @param options
7+
* @param {username} options.username - filter by OSM username
8+
* @return {Promise[Array]}
9+
**/
10+
async function list(options = {}) {
11+
// Apply search
12+
let query = await db('osm_users')
13+
.select('id', 'name')
14+
.whereILike('name', `%${options.username}%`)
15+
16+
return query
17+
}
18+
19+
module.exports = {
20+
list,
21+
}

src/pages/api/users/index.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
var fetch = require('node-fetch')
2+
const join = require('url-join')
3+
import * as Yup from 'yup'
4+
5+
import Users from '../../../models/users'
6+
import { createBaseHandler } from '../../../middlewares/base-handler'
7+
import { validate } from '../../../middlewares/validation'
8+
import isAuthenticated from '../../../middlewares/can/authenticated'
9+
10+
const handler = createBaseHandler()
11+
12+
/**
13+
* @swagger
14+
* /users:
15+
* get:
16+
* summary: Get OSM users by username
17+
* tags:
18+
* - users
19+
* responses:
20+
* 200:
21+
* description: A list of OSM users
22+
* content:
23+
* application/json:
24+
* schema:
25+
* type: object
26+
* properties:
27+
* users:
28+
* $ref: '#/components/schemas/TeamMemberList'
29+
*/
30+
handler.get(
31+
isAuthenticated,
32+
validate({
33+
query: Yup.object({
34+
search: Yup.string().required(),
35+
}).required(),
36+
}),
37+
async function getUsers(req, res) {
38+
const { search } = req.query
39+
let users = await Users.list({ username: search })
40+
41+
if (!users.length) {
42+
const resp = await fetch(
43+
join(
44+
process.env.OSM_API,
45+
`/api/0.6/changesets.json?display_name=${search}`
46+
)
47+
)
48+
if ([200, 304].includes(resp.status)) {
49+
const data = await resp.json()
50+
if (data.changesets) {
51+
const changeset = data.changesets[0]
52+
users = [
53+
{
54+
id: changeset.uid,
55+
name: changeset.user,
56+
},
57+
]
58+
}
59+
}
60+
}
61+
62+
let responseObject = Object.assign({}, { users })
63+
64+
return res.send(responseObject)
65+
}
66+
)
67+
68+
export default handler

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
import { getUserOrgProfile } from '../../../lib/profiles-api'
1414
import { Box, Container, Heading, Button, Flex } from '@chakra-ui/react'
1515
import Table from '../../../components/tables/table'
16-
import AddMemberForm from '../../../components/add-member-form'
16+
import { AddMemberByIdForm } from '../../../components/add-member-form'
1717
import ProfileModal from '../../../components/profile-modal'
1818
import { map, pick } from 'ramda'
1919
import join from 'url-join'
@@ -375,7 +375,7 @@ class Organization extends Component {
375375
<Flex justifyContent={'space-between'}>
376376
<Heading variant='sectionHead'>Staff Members</Heading>
377377
{isOwner && (
378-
<AddMemberForm
378+
<AddMemberByIdForm
379379
onSubmit={async ({ osmId }) => {
380380
await addManager(org.data.id, osmId)
381381
return this.getOrg()

0 commit comments

Comments
 (0)