diff --git a/app/manage/badges.js b/app/manage/badges.js index 191b4a6b..66c9055d 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -199,8 +199,8 @@ const assignUserBadge = routeWrapper({ }) .required(), body: yup.object({ - assigned_at: yup.date().optional(), - valid_until: yup.date().optional() + assigned_at: yup.date().required(), + valid_until: yup.date().nullable() }) }, handler: async function (req, reply) { @@ -218,12 +218,13 @@ const assignUserBadge = routeWrapper({ } // assign badge + const { assigned_at, valid_until } = req.body const [badge] = await conn('user_badges') .insert({ user_id: req.params.userId, badge_id: req.params.badgeId, - assigned_at: req.body.assigned_at, - valid_until: req.body.valid_until + assigned_at: assigned_at.toISOString(), + valid_until: valid_until ? valid_until.toISOString() : null }) .returning('*') @@ -275,19 +276,22 @@ const updateUserBadge = routeWrapper({ }) .required(), body: yup.object({ - assigned_at: yup.date().optional(), - valid_until: yup.date().optional() + assigned_at: yup.date().required(), + valid_until: yup.date().nullable() }) }, handler: async function (req, reply) { try { const conn = await db() - // assign badge + const { assigned_at, valid_until } = req.body + + // Yup validation returns time-zoned dates, update query use UTC strings + // to avoid that. const [badge] = await conn('user_badges') .update({ - assigned_at: req.body.assigned_at, - valid_until: req.body.valid_until + assigned_at: assigned_at.toISOString(), + valid_until: valid_until ? valid_until.toISOString() : null }) .where({ user_id: req.params.userId, diff --git a/app/manage/index.js b/app/manage/index.js index 553d9d90..9b74da64 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -301,6 +301,13 @@ function manageRouter (nextApp) { }) } ) + router.get( + '/organizations/:id/badges/:badgeId/assign/:userId', + can('organization:edit'), + (req, res) => { + return nextApp.render(req, res, '/badges/assign', req.params) + } + ) return router } diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js index d6e112ec..ea994f4f 100644 --- a/app/tests/api/badges-api.test.js +++ b/app/tests/api/badges-api.test.js @@ -366,6 +366,7 @@ test('Update badge', async (t) => { const badgeAssignment = (await orgOwner.agent .patch(updateBadgeRoute) .send({ + assigned_at: '2020-01-01Z', valid_until: '2021-01-01Z' }) .expect(200)).body @@ -373,6 +374,7 @@ test('Update badge', async (t) => { t.like(badgeAssignment, { badge_id: badge2.id, user_id: orgTeamMember.id, + assigned_at: '2020-01-01T00:00:00.000Z', valid_until: '2021-01-01T00:00:00.000Z' }) }) diff --git a/components/button.js b/components/button.js index 77472078..15db8c78 100644 --- a/components/button.js +++ b/components/button.js @@ -78,7 +78,7 @@ export default function Button ({ name, id, value, variant, type, disabled, href if (href) { let fullUrl (href.startsWith('http')) ? (fullUrl = href) : (fullUrl = join(publicRuntimeConfig.APP_URL, href)) - return {children} + return {children || value} } return
{children}
} diff --git a/package.json b/package.json index ed5ecb03..daf9c176 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "connect-session-knex": "^2.1.1", "cors": "^2.8.5", "csurf": "^1.11.0", + "date-fns": "^2.28.0", "dotenv": "^6.2.0", "dotenv-webpack": "^1.8.0", "express": "^4.17.2", diff --git a/pages/badges/assign.js b/pages/badges/assign.js new file mode 100644 index 00000000..3ee4b7aa --- /dev/null +++ b/pages/badges/assign.js @@ -0,0 +1,171 @@ +import React, { Component } from 'react' +import { Formik, Field, Form } from 'formik' +import APIClient from '../../lib/api-client' +import Button from '../../components/button' +import { format } from 'date-fns' +import { toast } from 'react-toastify' + +const apiClient = new APIClient() + +function ButtonWrapper ({ children }) { + return ( +
+ {children} + +
+ ) +} + +export default class AssignBadge extends Component { + static async getInitialProps ({ query }) { + if (query) { + return { + orgId: query.id, + badgeId: query.badgeId, + userId: query.userId + } + } + } + + constructor (props) { + super(props) + this.state = {} + + this.loadData = this.loadData.bind(this) + } + + async componentDidMount () { + this.loadData() + } + + async loadData () { + const { orgId, badgeId, userId } = this.props + try { + const [org, badge] = await Promise.all([ + apiClient.get(`/organizations/${orgId}`), + apiClient.get(`/organizations/${orgId}/badges/${badgeId}`) + ]) + + // Check if user already has the badge + const user = + badge.users && badge.users.find((u) => u.id === parseInt(userId)) + + this.setState({ + org, + badge, + user + }) + } catch (error) { + console.error(error) + this.setState({ + error, + loading: false + }) + } + } + + renderPageInner () { + if (this.state.error) { + return
An unexpected error occurred, please try again later.
+ } + + if (!this.state.org && !this.state.badge) { + return
Loading...
+ } + + const { orgId, badgeId, userId } = this.props + const { badge, user } = this.state + + return ( + <> +
+

{badge.name} Badge

+
+
+
+

User: {userId} (OSM id)

+
+ { + try { + const payload = { + assigned_at: assignedAt, + valid_until: validUntil !== '' ? validUntil : null + } + + if (!user) { + await apiClient.post( + `/organizations/${orgId}/badges/${badgeId}/assign/${userId}`, + payload + ) + toast.info('Badge assigned successfully.') + } else { + await apiClient.patch( + `/organizations/${orgId}/member/${userId}/badge/${badgeId}`, + payload + ) + toast.info('Badge updated successfully.') + } + this.loadData() + } catch (error) { + console.log(error) + toast.error(`Unexpected error, please try again later.`) + } + }} + render={({ isSubmitting, values, errors }) => { + return ( +
+
+ + +
+
+ + +
+ +
+ + ) + } + + render () { + return
{this.renderPageInner()}
+ } +} diff --git a/pages/badges/edit.js b/pages/badges/edit.js index 0c55b89c..917b2ceb 100644 --- a/pages/badges/edit.js +++ b/pages/badges/edit.js @@ -9,7 +9,6 @@ import getConfig from 'next/config' import { toast } from 'react-toastify' import theme from '../../styles/theme' import Table from '../../components/table' -import AddMemberForm from '../../components/add-member-form' import { toDateString } from '../../app/lib/utils' const { publicRuntimeConfig } = getConfig() @@ -62,13 +61,21 @@ export default class EditBadge extends Component { async loadData () { const { orgId, badgeId } = this.props try { - const [org, badge] = await Promise.all([ - getOrg(orgId), - apiClient.get(`/organizations/${orgId}/badges/${badgeId}`) - ]) + const [org, badge, { members }, { managers, owners }] = await Promise.all( + [ + getOrg(orgId), + apiClient.get(`/organizations/${orgId}/badges/${badgeId}`), + apiClient.get(`/organizations/${orgId}/members`), + apiClient.get(`/organizations/${orgId}/staff`) + ] + ) + + const assignablePeople = members.concat(managers).concat(owners) + this.setState({ org, - badge + badge, + assignablePeople }) } catch (error) { console.error(error) @@ -83,31 +90,57 @@ export default class EditBadge extends Component { const columns = [ { key: 'id', label: 'OSM ID' }, { key: 'displayName', label: 'Display Name' }, - { key: 'assignedAt', label: 'Assigned At' } + { key: 'assignedAt', label: 'Assigned At' }, + { key: 'validUntil', label: 'Valid Until' } ] - const { badge } = this.state + const { badge, assignablePeople } = this.state const users = (badge && badge.users) || [] return (
-
-

Assigned Members

+
+
+

Assigned Members

+ { + const user = assignablePeople.find( + (p) => + p.id === osmIdentifier || + p.name.toLowerCase() === osmIdentifier.toLowerCase() + ) + if (!user) { + toast.error('User is not part of this organization.') + } else { + Router.push( + join( + URL, + `/organizations/${orgId}/badges/${badgeId}/assign/${user.id}` + ) + ) + } + }} + render={({ values }) => { + return ( +
+ + + + ) + }} + /> +
- { - try { - await apiClient.post( - `/organizations/${orgId}/badges/${badgeId}/assign/${osmId}` - ) - this.loadData() - } catch (error) { - toast.error(error.message) - } - }} - /> - {users.length > 0 && ( ({ @@ -116,6 +149,14 @@ export default class EditBadge extends Component { validUntil: u.validUntil && toDateString(u.validUntil) }))} columns={columns} + onRowClick={({ id }) => + Router.push( + join( + URL, + `/organizations/${orgId}/badges/${badgeId}/assign/${id}` + ) + ) + } /> )} @@ -163,7 +204,7 @@ export default class EditBadge extends Component { color } ) - Router.push(join(URL, `/organizations/${orgId}`)) + toast.success('Badge updated successfully.') } catch (error) { toast.error( `There was an error editing badge '${name}'. Please try again later.` @@ -207,13 +248,8 @@ export default class EditBadge extends Component { />