Skip to content

Commit

Permalink
Merge pull request #438 from developmentseed/feature/search-by-username
Browse files Browse the repository at this point in the history
Add OSM users by username
  • Loading branch information
willemarcel authored May 5, 2023
2 parents 8805e38 + 8313f36 commit 8a5d760
Show file tree
Hide file tree
Showing 8 changed files with 359 additions and 16 deletions.
2 changes: 1 addition & 1 deletion DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ HYDRA_AUTHZ_PATH=/hyauth/oauth2/auth

## Deployment

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

```docker
docker-compose -f compose.yml -f compose.prod.yml up
Expand Down
159 changes: 154 additions & 5 deletions src/components/add-member-form.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import React from 'react'
import React, { useState } from 'react'
import { Formik, Field, Form, FormErrorMessage } from 'formik'
import { Button, Flex, Input } from '@chakra-ui/react'
import {
Box,
Button,
Flex,
Input,
List,
ListItem,
ListIcon,
Link,
Code,
Text,
} from '@chakra-ui/react'
import { AtSignIcon, AddIcon } from '@chakra-ui/icons'
import join from 'url-join'

import logger from '../lib/logger'

export default function AddMemberForm({ onSubmit }) {
const APP_URL = process.env.APP_URL
const OSM_DOMAIN = process.env.OSM_DOMAIN

export function AddMemberByIdForm({ onSubmit }) {
return (
<Formik
initialValues={{ osmId: '' }}
Expand All @@ -24,15 +41,21 @@ export default function AddMemberForm({ onSubmit }) {
const addMemberText = `Add Member ${isSubmitting ? ' 🕙' : ''}`

return (
<Flex as={Form} alignItems='center'>
<Flex
as={Form}
alignItems='center'
justifyContent='space-between'
width={'100%'}
gap={2}
>
<Field
as={Input}
type='text'
name='osmId'
id='osmId'
placeholder='OSM ID'
value={values.osmId}
style={{ width: '6rem' }}
flex={1}
/>
{status && status.msg && (
<FormErrorMessage>{status.msg}</FormErrorMessage>
Expand All @@ -53,3 +76,129 @@ export default function AddMemberForm({ onSubmit }) {
/>
)
}

export function AddMemberByUsernameForm({ onSubmit }) {
const [searchResult, setSearchResult] = useState()
const searchUsername = async (data, setStatus) => {
setStatus('searching')
let res = await fetch(join(APP_URL, `/api/users?search=${data.username}`))
if (res.status === 200) {
const data = await res.json()
if (data?.users.length) {
setSearchResult(data.users)
setStatus('successSearch')
} else {
setSearchResult([])
setStatus('noResults')
}
} else {
setSearchResult([])
setStatus('noResults')
}
}
const submit = async (uid, username, actions) => {
actions.setSubmitting(true)

try {
await onSubmit({ osmId: uid, username })
actions.setSubmitting(false)
actions.resetForm({ username: '' })
setSearchResult([])
} catch (e) {
logger.error(e)
actions.setSubmitting(false)
actions.setStatus(e.message)
}
}
return (
<Formik
initialValues={{ username: '' }}
render={({
status,
setStatus,
isSubmitting,
values,
setSubmitting,
resetForm,
}) => {
return (
<>
<Flex
as={Form}
alignItems='center'
justifyContent='space-between'
width={'100%'}
gap={2}
>
<Field
as={Input}
type='text'
name='username'
id='username'
placeholder='Search OSM Username'
value={values.username}
flex={1}
/>
{status && status.msg && (
<FormErrorMessage>{status.msg}</FormErrorMessage>
)}
<Button
textTransform={'lowercase'}
onClick={() => searchUsername(values, setStatus)}
variant='outline'
isLoading={status === 'searching'}
loadingText='Searching'
isDisabled={status === 'searching' || !values.username}
>
Search
</Button>
</Flex>
<Box display='flex' justifyContent='stretch' py={3} px={1}>
<List spacing={5} fontSize='sm' width={'100%'}>
{searchResult?.length &&
searchResult.map((u) => (
<ListItem
key={u.id}
display='flex'
alignItems='center'
justifyContent='space-between'
marginTop='1rem'
>
<ListIcon as={AtSignIcon} color='brand.600' />
<Link href={join(OSM_DOMAIN, '/user', u.name)} isExternal>
{u.name}
</Link>
<Code ml={2}>{u.id}</Code>
<Button
ml='auto'
textTransform='lowercase'
onClick={async () =>
submit(u.id, u.name, {
setStatus,
setSubmitting,
resetForm,
})
}
size='sm'
isLoading={isSubmitting}
loadingText='Adding'
isDisabled={isSubmitting}
leftIcon={<AddIcon />}
>
Add
</Button>
</ListItem>
))}
{status === 'noResults' && (
<Text as='b'>
No results found. Try typing the exact OSM username.
</Text>
)}
</List>
</Box>
</>
)
}}
/>
)
}
48 changes: 48 additions & 0 deletions src/components/add-member-modal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
Box,
Heading,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
} from '@chakra-ui/react'
import { AddMemberByIdForm, AddMemberByUsernameForm } from './add-member-form'

export function AddMemberModal({ isOpen, onClose, onSubmit }) {
return (
<Modal
isCentered
isOpen={isOpen}
onClose={onClose}
scrollBehavior={'inside'}
>
<ModalOverlay />
<ModalContent flexDirection={'column'} as='article' gap={2}>
<ModalHeader gap={4} display='flex' flexDir={'column'}>
<Heading size='sm' as='h3'>
Add Member
</Heading>
<ModalCloseButton onClick={() => onClose()} />
</ModalHeader>
<ModalBody display='flex' flexDirection={'column'} gap={2}>
<Box>
<Heading size='sm' as='h4'>
OSM ID
</Heading>
<AddMemberByIdForm onSubmit={onSubmit} />
</Box>
<Box>
<Heading size='sm' as='h4'>
OSM Username
</Heading>
<AddMemberByUsernameForm onSubmit={onSubmit} />
</Box>
</ModalBody>
<ModalFooter></ModalFooter>
</ModalContent>
</Modal>
)
}
21 changes: 21 additions & 0 deletions src/models/users.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const db = require('../lib/db')

/**
* Get paginated list of teams
*
* @param options
* @param {username} options.username - filter by OSM username
* @return {Promise[Array]}
**/
async function list(options = {}) {
// Apply search
let query = await db('osm_users')
.select('id', 'name')
.whereILike('name', `%${options.username}%`)

return query
}

module.exports = {
list,
}
68 changes: 68 additions & 0 deletions src/pages/api/users/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
var fetch = require('node-fetch')
const join = require('url-join')
import * as Yup from 'yup'

import Users from '../../../models/users'
import { createBaseHandler } from '../../../middlewares/base-handler'
import { validate } from '../../../middlewares/validation'
import isAuthenticated from '../../../middlewares/can/authenticated'

const handler = createBaseHandler()

/**
* @swagger
* /users:
* get:
* summary: Get OSM users by username
* tags:
* - users
* responses:
* 200:
* description: A list of OSM users
* content:
* application/json:
* schema:
* type: object
* properties:
* users:
* $ref: '#/components/schemas/TeamMemberList'
*/
handler.get(
isAuthenticated,
validate({
query: Yup.object({
search: Yup.string().required(),
}).required(),
}),
async function getUsers(req, res) {
const { search } = req.query
let users = await Users.list({ username: search })

if (!users.length) {
const resp = await fetch(
join(
process.env.OSM_API,
`/api/0.6/changesets.json?display_name=${search}`
)
)
if ([200, 304].includes(resp.status)) {
const data = await resp.json()
if (data.changesets) {
const changeset = data.changesets[0]
users = [
{
id: changeset.uid,
name: changeset.user,
},
]
}
}
}

let responseObject = Object.assign({}, { users })

return res.send(responseObject)
}
)

export default handler
4 changes: 2 additions & 2 deletions src/pages/organizations/[id]/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import { getUserOrgProfile } from '../../../lib/profiles-api'
import { Box, Container, Heading, Button, Flex } from '@chakra-ui/react'
import Table from '../../../components/tables/table'
import AddMemberForm from '../../../components/add-member-form'
import { AddMemberByIdForm } from '../../../components/add-member-form'
import ProfileModal from '../../../components/profile-modal'
import { map, pick } from 'ramda'
import join from 'url-join'
Expand Down Expand Up @@ -375,7 +375,7 @@ class Organization extends Component {
<Flex justifyContent={'space-between'}>
<Heading variant='sectionHead'>Staff Members</Heading>
{isOwner && (
<AddMemberForm
<AddMemberByIdForm
onSubmit={async ({ osmId }) => {
await addManager(org.data.id, osmId)
return this.getOrg()
Expand Down
Loading

0 comments on commit 8a5d760

Please sign in to comment.