From befd0a55c3b814ff53f26ce7a380c184a8e1f0ac Mon Sep 17 00:00:00 2001 From: Vitor George Date: Fri, 11 Mar 2022 12:33:07 +0000 Subject: [PATCH 01/20] Add badges table to organisation page --- lib/api-client.js | 105 ++++++++++++++++++++++++++++++++++++++++++ pages/organization.js | 78 +++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 lib/api-client.js diff --git a/lib/api-client.js b/lib/api-client.js new file mode 100644 index 00000000..991a0dea --- /dev/null +++ b/lib/api-client.js @@ -0,0 +1,105 @@ +import getConfig from 'next/config' + +const { publicRuntimeConfig } = getConfig() + +class ApiClient { + constructor (props = {}) { + this.defaultOptions = { + headers: { + 'Content-Type': 'application/json' + } + } + } + + baseUrl (subpath) { + return `${publicRuntimeConfig.APP_URL}/api${subpath}` + } + + async fetch (method, path, data, config = {}) { + const url = this.baseUrl(path) + + const defaultConfig = { + format: 'json' + } + const requestConfig = { + ...defaultConfig, + ...config + } + + const { format, headers } = requestConfig + var requestHeaders = this.defaultOptions.headers + for (const key in headers) { + requestHeaders[key] = headers[key] + } + + const options = { + ...this.defaultOptions, + method, + format, + headers: requestHeaders + } + + if (data) { + options.body = JSON.stringify(data) + } + + // Fetch data and let errors to be handle by the caller + // Fetch data and let errors to be handle by the caller + const res = await fetchJSON(url, options) + return res.body + } + + get (path, config) { + return this.fetch('GET', path, null, config) + } + + post (path, data, config) { + return this.fetch('POST', path, data, config) + } + + patch (path, data, config) { + return this.fetch('PATCH', path, data, config) + } + + delete (path, config) { + return this.fetch('DELETE', path, config) + } +} + +export default ApiClient + +/** + * Performs a request to the given url returning the response in json format + * or throwing an error. + * + * @param {string} url Url to query + * @param {object} options Options for fetch + */ +export async function fetchJSON (url, options) { + let response + options = options || {} + const format = options.format || 'json' + let data + try { + response = await fetch(url, options) + if (format === 'json') { + data = await response.json() + } else if (format === 'binary') { + data = await response.arrayBuffer() + } else { + data = await response.text() + } + + if (response.status >= 400) { + const err = new Error(data.message) + err.statusCode = response.status + err.data = data + throw err + } + + return { body: data, headers: response.headers } + } catch (error) { + error.statusCode = response ? response.status || null : null + throw error + } +} diff --git a/pages/organization.js b/pages/organization.js index 40b1f931..ec91a00f 100644 --- a/pages/organization.js +++ b/pages/organization.js @@ -11,7 +11,24 @@ import Button from '../components/button' import Modal from 'react-modal' import ProfileModal from '../components/profile-modal' import { assoc, propEq, find, contains, prop, map } from 'ramda' +import APIClient from '../lib/api-client' +const apiClient = new APIClient() + +export function SectionWrapper (props) { + return ( +
+ {props.children} + +
+ ) +} export default class Organization extends Component { static async getInitialProps ({ query }) { if (query) { @@ -36,11 +53,14 @@ export default class Organization extends Component { } this.closeProfileModal = this.closeProfileModal.bind(this) + this.renderBadges = this.renderBadges.bind(this) + this.getBadges = this.getBadges.bind(this) } async componentDidMount () { await this.getOrg() await this.getOrgStaff() + await this.getBadges() return this.getMembers(0) } @@ -159,6 +179,63 @@ export default class Organization extends Component { /> } + async getBadges () { + try { + const { id: orgId } = this.props + + const badges = await apiClient.get(`/organizations/${orgId}/badges`) + this.setState({ + badges + }) + } catch (e) { + console.error(e) + this.setState({ + error: e, + team: null, + loading: false + }) + } + } + + renderBadges () { + const self = this + const { id: orgId } = this.props + const columns = [ + { key: 'name' }, + { key: 'color' } + ] + + async function addBadge () { + try { + await apiClient.post(`/organizations/${orgId}/badges`, { + name: 'badge 1', + color: 'red' + }) + self.getBadges() + } catch (error) { + console.log(error) + } + } + + return +
+
+ Badges +
+ +
+
+
+ { + this.state.badges && + } + + + } + renderMembers (memberRows) { const columns = [ { key: 'id' }, @@ -296,6 +373,7 @@ export default class Organization extends Component { {!this.state.loading ? this.renderMembers(members) : 'Loading...'} ) :
} + {this.renderBadges()} Date: Wed, 16 Mar 2022 17:49:50 +0000 Subject: [PATCH 02/20] Add badges page --- app/manage/index.js | 7 ++ components/layout.js | 6 ++ pages/badges/add.js | 162 ++++++++++++++++++++++++++++++++++++++++++ pages/organization.js | 45 +++++++----- 4 files changed, 204 insertions(+), 16 deletions(-) create mode 100644 pages/badges/add.js diff --git a/app/manage/index.js b/app/manage/index.js index e8500d5a..a6813823 100644 --- a/app/manage/index.js +++ b/app/manage/index.js @@ -275,6 +275,13 @@ function manageRouter (nextApp) { return nextApp.render(req, res, '/org-edit-team-profile', { id: req.params.id }) }) + /** + * Badged pages + * */ + router.get('/organizations/:id/badges/add', can('organization:edit'), (req, res) => { + return nextApp.render(req, res, '/badges/add', { id: req.params.id }) + }) + return router } diff --git a/components/layout.js b/components/layout.js index ecba4b85..fea506c7 100644 --- a/components/layout.js +++ b/components/layout.js @@ -203,6 +203,12 @@ function Layout (props) { padding: 0.5rem 1rem 0.5rem 0.25rem; margin-right: 1rem; border: 2px solid ${theme.colors.primaryColor}; + + } + + .form-control :global(input[type="color"]) { + padding: 3px; + height: 2.5rem; } .status--alert { diff --git a/pages/badges/add.js b/pages/badges/add.js new file mode 100644 index 00000000..91de66db --- /dev/null +++ b/pages/badges/add.js @@ -0,0 +1,162 @@ +import React, { Component } from 'react' +import join from 'url-join' +import { Formik, Field, Form } from 'formik' +import APIClient from '../../lib/api-client' +import { getOrg } from '../../lib/org-api' +import Button from '../../components/button' +import Router from 'next/router' + +const apiClient = new APIClient() + +function validateName (value) { + if (!value) return 'Name field is required' +} + +function renderError (text) { + return
{text}
+} + +function renderErrors (errors) { + const keys = Object.keys(errors) + return keys.map((key) => { + return renderError(errors[key]) + }) +} + +export default class OrgCreate extends Component { + static async getInitialProps ({ query }) { + if (query) { + return { + id: query.id + } + } + } + + constructor (props) { + super(props) + this.state = {} + + this.getOrg = this.getOrg.bind(this) + } + + async componentDidMount () { + this.getOrg() + } + + async getOrg () { + console.log('getOrg') + try { + let org = await getOrg(this.props.id) + this.setState({ + org + }) + } catch (e) { + console.error(e) + this.setState({ + error: e, + org: null, + loading: false + }) + } + } + + render () { + console.log('render') + console.log(this.org) + + if (!this.state.org) { + return
Loading...
+ } + + return ( +
+
+
+

New badge

+
+ +
+

Organization: {this.state.org.name}

+
+ + { + try { + // await apiClient.post(`/organizations/${orgId}/badges`, { + await apiClient.post(`/organizations/1/badges`, { + name, + color + }) + Router.push( + join(URL, `/organizations/${this.props.id}`) + ) + } catch (error) { + console.log(error) + } + }} + render={({ + status, + isSubmitting, + submitForm, + values, + errors, + setFieldValue, + setErrors, + setStatus + }) => { + return ( +
+
+ + + {errors.name && renderError(errors.name)} +
+
+ + + {errors.color && renderError(errors.color)} +
+
+ { (status && status.errors) && (renderErrors(status.errors)) } +
+ + ) + }} + /> +
+
+ ) + } +} diff --git a/pages/organization.js b/pages/organization.js index ec91a00f..78ae2ebc 100644 --- a/pages/organization.js +++ b/pages/organization.js @@ -1,4 +1,5 @@ import React, { Component } from 'react' +import Router from 'next/router' import { getOrg, getOrgStaff, getMembers, addManager, removeManager, addOwner, removeOwner } from '../lib/org-api' import { getUserOrgProfile } from '../lib/profiles-api' import Card from '../components/card' @@ -12,6 +13,11 @@ import Modal from 'react-modal' import ProfileModal from '../components/profile-modal' import { assoc, propEq, find, contains, prop, map } from 'ramda' import APIClient from '../lib/api-client' +import getConfig from 'next/config' +import join from 'url-join' + +const { publicRuntimeConfig } = getConfig() +const URL = publicRuntimeConfig.APP_URL const apiClient = new APIClient() @@ -217,23 +223,30 @@ export default class Organization extends Component { } } - return -
-
- Badges -
- + return ( + +
+
+ Badges +
+ +
-
-
- { - this.state.badges &&
- } - - + + {this.state.badges && ( +
+ )} + + ) } renderMembers (memberRows) { From d88bd72215629479bc0c8f61cb69c6730902e6a4 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Thu, 17 Mar 2022 10:19:15 +0000 Subject: [PATCH 03/20] Add color input, cancel button to "add badge page --- lib/utils.js | 12 ++++++++++ pages/badges/add.js | 51 ++++++++++++++++++++++++++++++------------- pages/organization.js | 13 ----------- 3 files changed, 48 insertions(+), 28 deletions(-) create mode 100644 lib/utils.js diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..6a3656ca --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,12 @@ +function getRandomColor () { + var letters = '0123456789ABCDEF' + var color = '#' + for (var i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)] + } + return color +} + +module.exports = { + getRandomColor +} diff --git a/pages/badges/add.js b/pages/badges/add.js index 91de66db..f3524359 100644 --- a/pages/badges/add.js +++ b/pages/badges/add.js @@ -5,6 +5,11 @@ import APIClient from '../../lib/api-client' import { getOrg } from '../../lib/org-api' import Button from '../../components/button' import Router from 'next/router' +import { getRandomColor } from '../../lib/utils' +import getConfig from 'next/config' + +const { publicRuntimeConfig } = getConfig() +const URL = publicRuntimeConfig.APP_URL const apiClient = new APIClient() @@ -23,6 +28,17 @@ function renderErrors (errors) { }) } +function ButtonWrapper ({ children }) { + return
+ {children} + +
+} + export default class OrgCreate extends Component { static async getInitialProps ({ query }) { if (query) { @@ -61,35 +77,32 @@ export default class OrgCreate extends Component { } render () { - console.log('render') - console.log(this.org) + const self = this if (!this.state.org) { - return
Loading...
+ return
Loading...
} return (
-

New badge

+

{this.state.org.name}

-
-

Organization: {this.state.org.name}

+

New badge

{ try { - // await apiClient.post(`/organizations/${orgId}/badges`, { await apiClient.post(`/organizations/1/badges`, { name, color }) Router.push( - join(URL, `/organizations/${this.props.id}`) + join(URL, `/organizations/${self.props.id}`) ) } catch (error) { console.log(error) @@ -122,9 +135,7 @@ export default class OrgCreate extends Component { {errors.name && renderError(errors.name)}
- + {errors.color && renderError(errors.color)}
-
- { (status && status.errors) && (renderErrors(status.errors)) } + + {status && status.errors && renderErrors(status.errors)}
+
+
+ ) + } +} diff --git a/pages/organization.js b/pages/organization.js index 03ad8546..0a4aa7c1 100644 --- a/pages/organization.js +++ b/pages/organization.js @@ -230,7 +230,12 @@ export default class Organization extends Component { {this.state.badges && ( -
+
Router.push( + join(URL, `/organizations/${orgId}/badges/${badgeId}`) + ) + + } /> )} ) From 598978d9d28b41df6fcb0390c8584b7bd85b8d15 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Wed, 23 Mar 2022 10:33:55 +0000 Subject: [PATCH 05/20] Add react toast --- package.json | 1 + pages/_app.js | 3 +++ pages/badges/add.js | 3 ++- pages/badges/edit.js | 2 ++ yarn.lock | 12 ++++++++++++ 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 167ba0ca..ed5ecb03 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "react-dom": "^16.14.0", "react-leaflet": "^2.8.0", "react-modal": "^3.14.4", + "react-toastify": "^8.2.0", "reactjs-popup": "^1.5.0", "request": "^2.88.2", "request-promise-native": "^1.0.9", diff --git a/pages/_app.js b/pages/_app.js index 1e58752a..352dd580 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -5,6 +5,7 @@ import Sidebar from '../components/sidebar' import Layout from '../components/layout.js' import PageBanner from '../components/banner' import Button from '../components/button' +import { ToastContainer } from 'react-toastify' class OSMHydra extends App { static async getInitialProps ({ Component, ctx }) { @@ -55,6 +56,7 @@ class OSMHydra extends App { integrity='sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==' crossOrigin='' /> + @@ -66,6 +68,7 @@ class OSMHydra extends App { + ) } diff --git a/pages/badges/add.js b/pages/badges/add.js index f3c83877..c9421c82 100644 --- a/pages/badges/add.js +++ b/pages/badges/add.js @@ -7,6 +7,7 @@ import Button from '../../components/button' import Router from 'next/router' import { getRandomColor } from '../../lib/utils' import getConfig from 'next/config' +import { toast } from 'react-toastify' const { publicRuntimeConfig } = getConfig() const URL = publicRuntimeConfig.APP_URL @@ -95,7 +96,6 @@ export default class AddBadge extends Component { initialValues={{ name: '', color: getRandomColor() }} onSubmit={async ({ name, color }, actions) => { actions.setSubmitting(true) - console.log('onSubmit') try { await apiClient.post(`/organizations/${orgId}/badges`, { name, @@ -104,6 +104,7 @@ export default class AddBadge extends Component { Router.push(join(URL, `/organizations/${orgId}`)) } catch (error) { console.log(error) + toast.error(`There was an error creating badge '${name}'. Please try again later.`) } finally { actions.setSubmitting(false) } diff --git a/pages/badges/edit.js b/pages/badges/edit.js index 78c8d51b..fdf439b2 100644 --- a/pages/badges/edit.js +++ b/pages/badges/edit.js @@ -6,6 +6,7 @@ import { getOrg } from '../../lib/org-api' import Button from '../../components/button' import Router from 'next/router' import getConfig from 'next/config' +import { toast } from 'react-toastify' const { publicRuntimeConfig } = getConfig() const URL = publicRuntimeConfig.APP_URL @@ -130,6 +131,7 @@ export default class EditBadge extends Component { ) Router.push(join(URL, `/organizations/${orgId}`)) } catch (error) { + toast.error(`There was an error editing badge '${name}'. Please try again later.`) console.log(error) } }} diff --git a/yarn.lock b/yarn.lock index 8712cf59..47c869eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3568,6 +3568,11 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= +clsx@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== + code-excerpt@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/code-excerpt/-/code-excerpt-3.0.0.tgz#fcfb6748c03dba8431c19f5474747fad3f250f10" @@ -8714,6 +8719,13 @@ react-ssr-prepass@1.0.2: dependencies: object-is "^1.0.1" +react-toastify@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-8.2.0.tgz#ef7d56bdfdc6272ca6b228368ab564721c3a3244" + integrity sha512-Pg2Ju7NngAamarFvLwqrFomJ57u/Ay6i6zfLurt/qPynWkAkOthu6vxfqYpJCyNhHRhR4hu7+bySSeWWJu6PAg== + dependencies: + clsx "^1.1.1" + react@^16.14.0: version "16.14.0" resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" From bc953bf4b5f43de32d640c52ff9ba81ab0bb8b2c Mon Sep 17 00:00:00 2001 From: Vitor George Date: Wed, 23 Mar 2022 11:34:00 +0000 Subject: [PATCH 06/20] Add delete badge button --- app/manage/badges.js | 5 ++++- pages/badges/edit.js | 31 +++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/app/manage/badges.js b/app/manage/badges.js index 8403cdb7..e2dbf035 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -141,7 +141,10 @@ const deleteBadge = routeWrapper({ await conn('organization_badge') .delete() .where('id', req.params.badgeId) - return reply.sendStatus(200) + return reply.send({ + status: 200, + message: `Badge ${req.params.badgeId} deleted successfully.` + }) } catch (err) { console.log(err) return reply.boom.badRequest(err.message) diff --git a/pages/badges/edit.js b/pages/badges/edit.js index fdf439b2..802e33f2 100644 --- a/pages/badges/edit.js +++ b/pages/badges/edit.js @@ -131,15 +131,13 @@ export default class EditBadge extends Component { ) Router.push(join(URL, `/organizations/${orgId}`)) } catch (error) { - toast.error(`There was an error editing badge '${name}'. Please try again later.`) + toast.error( + `There was an error editing badge '${name}'. Please try again later.` + ) console.log(error) } }} - render={({ - isSubmitting, - values, - errors - }) => { + render={({ isSubmitting, values, errors }) => { return (
@@ -188,6 +186,27 @@ export default class EditBadge extends Component { ) }} /> +
+
) From 5707fa3dcc7e5febfb19c85f150dfd9faa1a7a2b Mon Sep 17 00:00:00 2001 From: Vitor George Date: Wed, 23 Mar 2022 11:55:28 +0000 Subject: [PATCH 07/20] Render colors at badges table --- pages/organization.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pages/organization.js b/pages/organization.js index 0a4aa7c1..5afd4afe 100644 --- a/pages/organization.js +++ b/pages/organization.js @@ -205,10 +205,7 @@ export default class Organization extends Component { renderBadges () { const { id: orgId } = this.props - const columns = [ - { key: 'name' }, - { key: 'color' } - ] + const columns = [{ key: 'name' }, { key: 'color' }] return ( @@ -230,7 +227,16 @@ export default class Organization extends Component {
{this.state.badges && ( -
{ + return { + ...row, + color: () => ( + + + + ) + } + })} columns={columns} onRowClick={ ({ id: badgeId }) => Router.push( join(URL, `/organizations/${orgId}/badges/${badgeId}`) ) From f8051763c6e721579f87d7878d9cde985a19997f Mon Sep 17 00:00:00 2001 From: Vitor George Date: Wed, 23 Mar 2022 12:35:19 +0000 Subject: [PATCH 08/20] Add delete confirmation --- pages/badges/edit.js | 85 +++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/pages/badges/edit.js b/pages/badges/edit.js index 802e33f2..2943982b 100644 --- a/pages/badges/edit.js +++ b/pages/badges/edit.js @@ -7,6 +7,7 @@ import Button from '../../components/button' import Router from 'next/router' import getConfig from 'next/config' import { toast } from 'react-toastify' +import theme from '../../styles/theme' const { publicRuntimeConfig } = getConfig() const URL = publicRuntimeConfig.APP_URL @@ -186,28 +187,70 @@ export default class EditBadge extends Component { ) }} /> -
-
+
+

Danger zone

+

+ Delete this badge and remove it from all assigned members. +

+ {this.state.isDeleting ? ( + <> + + + + ) : ( +
+ ) } From 9e86ef54c6f374066bbed3c04cee2d31c1d74abb Mon Sep 17 00:00:00 2001 From: Vitor George Date: Mon, 28 Mar 2022 14:13:04 +0100 Subject: [PATCH 09/20] Fix migration --- app/db/migrations/20220302104250_add_user_badges.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/db/migrations/20220302104250_add_user_badges.js b/app/db/migrations/20220302104250_add_user_badges.js index df91e74a..679339d7 100644 --- a/app/db/migrations/20220302104250_add_user_badges.js +++ b/app/db/migrations/20220302104250_add_user_badges.js @@ -8,7 +8,7 @@ exports.up = async function (knex) { table .integer('user_id') .references('id') - .inTable('organization_badge') + .inTable('users') .onDelete('CASCADE') table.datetime('assigned_at').defaultTo(knex.fn.now()) table.datetime('valid_until') From 7f13b01218879dcd3e0462bb081b6dd01eb98dec Mon Sep 17 00:00:00 2001 From: Vitor George Date: Mon, 28 Mar 2022 14:18:14 +0100 Subject: [PATCH 10/20] Add assignment section to badge page --- app/manage/badges.js | 60 ++++++++++++++----- pages/badges/edit.js | 139 ++++++++++++++++++++++++++++--------------- 2 files changed, 134 insertions(+), 65 deletions(-) diff --git a/app/manage/badges.js b/app/manage/badges.js index e2dbf035..0be7e5a5 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -83,7 +83,18 @@ const getBadge = routeWrapper({ .select('*') .where('id', req.params.badgeId) .returning('*') - reply.send(badge) + + const users = await conn('user_badge') + .select('*') + .leftJoin( + 'organization_badge', + 'user_badge.badge_id', + 'organization_badge.id' + ) + .where('badge_id', req.params.badgeId) + .returning('*') + + reply.send({ ...badge, users }) } catch (err) { console.log(err) return reply.boom.badRequest(err.message) @@ -138,9 +149,7 @@ const deleteBadge = routeWrapper({ handler: async function (req, reply) { try { const conn = await db() - await conn('organization_badge') - .delete() - .where('id', req.params.badgeId) + await conn('organization_badge').delete().where('id', req.params.badgeId) return reply.send({ status: 200, message: `Badge ${req.params.badgeId} deleted successfully.` @@ -164,18 +173,38 @@ const assignUserBadge = routeWrapper({ userId: yup.number().required().positive().integer() }) .required(), - body: yup - .object({ - assigned_at: yup.date().optional(), - valid_until: yup.date().optional() - }) + body: yup.object({ + assigned_at: yup.date().optional(), + valid_until: yup.date().optional() + }) }, handler: async function (req, reply) { try { const conn = await db() - // user is member - await organization.isMember(req.params.id, req.params.userId) + // user is related to org? + const isMember = await organization.isMember( + req.params.id, + req.params.userId + ) + if (!isMember) { + const isManager = await organization.isManager( + req.params.id, + req.params.userId + ) + if (!isManager) { + const isOwner = await organization.isOwner( + req.params.id, + req.params.userId + ) + if (!isOwner) { + } else { + return reply.boom.badRequest( + 'User is not part of the organization.' + ) + } + } + } // assign badge const [badge] = await conn('user_badge') @@ -228,11 +257,10 @@ const updateUserBadge = routeWrapper({ userId: yup.number().required().positive().integer() }) .required(), - body: yup - .object({ - assigned_at: yup.date().optional(), - valid_until: yup.date().optional() - }) + body: yup.object({ + assigned_at: yup.date().optional(), + valid_until: yup.date().optional() + }) }, handler: async function (req, reply) { try { diff --git a/pages/badges/edit.js b/pages/badges/edit.js index 2943982b..e9ce5c5a 100644 --- a/pages/badges/edit.js +++ b/pages/badges/edit.js @@ -8,6 +8,8 @@ import Router from 'next/router' 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' const { publicRuntimeConfig } = getConfig() const URL = publicRuntimeConfig.APP_URL @@ -94,6 +96,45 @@ export default class EditBadge extends Component { } } + renderAssignedMembers ({ orgId, badgeId }) { + const columns = [{ key: 'id' }, { key: 'name' }, { key: 'role' }] + + const sampleData = [ + { id: 1, username: 'user1' }, + { id: 2, username: 'user2' } + ] + + return ( +
+
+

Assigned Members

+
+ + { + try { + await apiClient.post( + `/organizations/${orgId}/badges/${badgeId}/assign/${osmId}` + ) + // Router.push(join(URL, `/organizations/${orgId}`)) + } catch (error) { + console.log(error) + toast.error( + `There was an error assigning the badge. Please try again later.` + ) + } + }} + /> + +
this.openProfileModal(row)} + /> + + ) + } + render () { const self = this @@ -111,14 +152,13 @@ export default class EditBadge extends Component { return (
+
+

{this.state.org.name}

+
-
-

{this.state.org.name}

-

Edit Badge

- { @@ -187,55 +227,56 @@ export default class EditBadge extends Component { ) }} /> -
-

Danger zone

-

- Delete this badge and remove it from all assigned members. -

- {this.state.isDeleting ? ( - <> - - - - ) : ( +
+ + {this.renderAssignedMembers({ orgId, badgeId })} + +
+

Danger zone

+

Delete this badge and remove it from all assigned members.

+ {this.state.isDeleting ? ( + <>
+ > + Cancel + + + + ) : ( +
From c03cd04cb168009a4c57f32a9dae431002d335af Mon Sep 17 00:00:00 2001 From: Vitor George Date: Fri, 1 Apr 2022 15:26:36 +0100 Subject: [PATCH 14/20] Check if user is part of organization before badge assignment --- app/manage/badges.js | 1 - pages/badges/edit.js | 20 ++++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/manage/badges.js b/app/manage/badges.js index 5a3cd83f..5e6b9a01 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -212,7 +212,6 @@ const assignUserBadge = routeWrapper({ req.params.userId ) if (!isOwner) { - } else { return reply.boom.badRequest( 'User is not part of the organization.' ) diff --git a/pages/badges/edit.js b/pages/badges/edit.js index d17696ae..9e33ffbf 100644 --- a/pages/badges/edit.js +++ b/pages/badges/edit.js @@ -51,6 +51,8 @@ export default class EditBadge extends Component { constructor (props) { super(props) this.state = {} + + this.loadData = this.loadData.bind(this) } async componentDidMount () { @@ -81,8 +83,7 @@ export default class EditBadge extends Component { const columns = [ { key: 'id', label: 'OSM ID' }, { key: 'displayName', label: 'Display Name' }, - { key: 'assignedAt', label: 'Assigned At' }, - { key: 'validUntil', label: 'Valid Until' } + { key: 'assignedAt', label: 'Assigned At' } ] const { badge } = this.state @@ -102,10 +103,17 @@ export default class EditBadge extends Component { ) // Router.push(join(URL, `/organizations/${orgId}`)) } catch (error) { - console.log(error) - toast.error( - `There was an error assigning the badge. Please try again later.` - ) + if ( + error.message === 'User is not part of the organization.' + ) { + toast.error( + `User is not part of the organization, badge was not assigned.` + ) + } else { + toast.error( + `An unexpected error occurred, please try again later.` + ) + } } }} /> From 01a477e681efcb67e0ba1f4a08dc0b9a3b5d5d8b Mon Sep 17 00:00:00 2001 From: Vitor George Date: Fri, 1 Apr 2022 15:28:01 +0100 Subject: [PATCH 15/20] Use plural for consistency --- .../20220302104250_add_user_badges.js | 4 ++-- app/lib/profile.js | 6 +++--- app/manage/badges.js | 18 +++++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/db/migrations/20220302104250_add_user_badges.js b/app/db/migrations/20220302104250_add_user_badges.js index 679339d7..fc1f22e7 100644 --- a/app/db/migrations/20220302104250_add_user_badges.js +++ b/app/db/migrations/20220302104250_add_user_badges.js @@ -1,5 +1,5 @@ exports.up = async function (knex) { - await knex.schema.createTable('user_badge', (table) => { + await knex.schema.createTable('user_badges', (table) => { table .integer('badge_id') .references('id') @@ -16,5 +16,5 @@ exports.up = async function (knex) { } exports.down = async function (knex) { - await knex.schema.dropTable('user_badge') + await knex.schema.dropTable('user_badges') } diff --git a/app/lib/profile.js b/app/lib/profile.js index e0a16d7b..1b7d38d9 100644 --- a/app/lib/profile.js +++ b/app/lib/profile.js @@ -256,7 +256,7 @@ async function getUserManageToken (id) { async function getUserBadges (id) { const conn = await db() - return conn('user_badge') + return conn('user_badges') .select([ 'id', 'assigned_at', @@ -267,10 +267,10 @@ async function getUserBadges (id) { ]) .leftJoin( 'organization_badge', - 'user_badge.badge_id', + 'user_badges.badge_id', 'organization_badge.id' ) - .where('user_badge.user_id', id) + .where('user_badges.user_id', id) } module.exports = { diff --git a/app/manage/badges.js b/app/manage/badges.js index 5e6b9a01..ed4ad7c2 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -84,19 +84,19 @@ const getBadge = routeWrapper({ .where('id', req.params.badgeId) .returning('*') - const users = await conn('user_badge') + const users = await conn('user_badges') .select({ - id: 'user_badge.user_id', + id: 'user_badges.user_id', profile: 'profile', - assignedAt: 'user_badge.assigned_at', - validUntil: 'user_badge.valid_until' + assignedAt: 'user_badges.assigned_at', + validUntil: 'user_badges.valid_until' }) .leftJoin( 'organization_badge', - 'user_badge.badge_id', + 'user_badges.badge_id', 'organization_badge.id' ) - .leftJoin('users', 'user_badge.user_id', 'users.id') + .leftJoin('users', 'user_badges.user_id', 'users.id') .where('badge_id', req.params.badgeId) .returning('*') @@ -220,7 +220,7 @@ const assignUserBadge = routeWrapper({ } // assign badge - const [badge] = await conn('user_badge') + const [badge] = await conn('user_badges') .insert({ user_id: req.params.userId, badge_id: req.params.badgeId, @@ -280,7 +280,7 @@ const updateUserBadge = routeWrapper({ const conn = await db() // assign badge - const [badge] = await conn('user_badge') + const [badge] = await conn('user_badges') .update({ assigned_at: req.body.assigned_at, valid_until: req.body.valid_until @@ -316,7 +316,7 @@ const removeUserBadge = routeWrapper({ const conn = await db() // delete user badge - await conn('user_badge').delete().where({ + await conn('user_badges').delete().where({ user_id: req.params.userId, badge_id: req.params.badgeId }) From 8e9f2ab61c61eb06b68d5c3bae786255fbcd616c Mon Sep 17 00:00:00 2001 From: Vitor George Date: Fri, 1 Apr 2022 16:32:35 +0100 Subject: [PATCH 16/20] Fix test route --- app/manage/badges.js | 21 +++++---------------- app/tests/api/badges-api.test.js | 2 +- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/app/manage/badges.js b/app/manage/badges.js index ed4ad7c2..3458adfe 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -197,26 +197,15 @@ const assignUserBadge = routeWrapper({ const conn = await db() // user is related to org? - const isMember = await organization.isMember( + const isMemberOrStaff = await organization.isMemberOrStaff( req.params.id, req.params.userId ) - if (!isMember) { - const isManager = await organization.isManager( - req.params.id, - req.params.userId + + if (!isMemberOrStaff) { + return reply.boom.badRequest( + 'User is not part of the organization.' ) - if (!isManager) { - const isOwner = await organization.isOwner( - req.params.id, - req.params.userId - ) - if (!isOwner) { - return reply.boom.badRequest( - 'User is not part of the organization.' - ) - } - } } // assign badge diff --git a/app/tests/api/badges-api.test.js b/app/tests/api/badges-api.test.js index 0329224c..d6e112ec 100644 --- a/app/tests/api/badges-api.test.js +++ b/app/tests/api/badges-api.test.js @@ -76,7 +76,7 @@ test.before(async () => { // Add team member await orgOwner.agent - .put(`/api/team/${orgTeam1.id}/${orgTeamMember.id}`) + .put(`/api/teams/add/${orgTeam1.id}/${orgTeamMember.id}`) .expect(200) // Add manager From 7b5758391bdd559e1b417ad4246f484937c48634 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Fri, 1 Apr 2022 16:55:56 +0100 Subject: [PATCH 17/20] Make badges assignment unique --- .../20220302104250_add_user_badges.js | 1 + app/manage/badges.js | 11 ++++--- pages/badges/edit.js | 32 +++++++------------ 3 files changed, 19 insertions(+), 25 deletions(-) diff --git a/app/db/migrations/20220302104250_add_user_badges.js b/app/db/migrations/20220302104250_add_user_badges.js index fc1f22e7..026d2b00 100644 --- a/app/db/migrations/20220302104250_add_user_badges.js +++ b/app/db/migrations/20220302104250_add_user_badges.js @@ -12,6 +12,7 @@ exports.up = async function (knex) { .onDelete('CASCADE') table.datetime('assigned_at').defaultTo(knex.fn.now()) table.datetime('valid_until') + table.unique(['badge_id', 'user_id']) }) } diff --git a/app/manage/badges.js b/app/manage/badges.js index 3458adfe..e258e77e 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -203,9 +203,7 @@ const assignUserBadge = routeWrapper({ ) if (!isMemberOrStaff) { - return reply.boom.badRequest( - 'User is not part of the organization.' - ) + return reply.boom.badRequest('User is not part of the organization.') } // assign badge @@ -220,8 +218,11 @@ const assignUserBadge = routeWrapper({ reply.send(badge) } catch (err) { - console.log(err) - return reply.boom.badRequest(err.message) + if (err.code === '23505') { + return reply.boom.badRequest('User is already assigned to badge.') + } else { + return reply.boom.badRequest('Unexpected error, please try again later.') + } } } }) diff --git a/pages/badges/edit.js b/pages/badges/edit.js index 9e33ffbf..bbef3b0d 100644 --- a/pages/badges/edit.js +++ b/pages/badges/edit.js @@ -101,31 +101,23 @@ export default class EditBadge extends Component { await apiClient.post( `/organizations/${orgId}/badges/${badgeId}/assign/${osmId}` ) - // Router.push(join(URL, `/organizations/${orgId}`)) + this.loadData() } catch (error) { - if ( - error.message === 'User is not part of the organization.' - ) { - toast.error( - `User is not part of the organization, badge was not assigned.` - ) - } else { - toast.error( - `An unexpected error occurred, please try again later.` - ) - } + toast.error(error.message) } }} /> - ({ - ...u, - assignedAt: u.assignedAt && toDateString(u.assignedAt), - validUntil: u.validUntil && toDateString(u.validUntil) - }))} - columns={columns} - /> + {users.length > 0 && ( +
({ + ...u, + assignedAt: u.assignedAt && toDateString(u.assignedAt), + validUntil: u.validUntil && toDateString(u.validUntil) + }))} + columns={columns} + /> + )} ) } From ed4dbc77c3ffefffe9c1fa1bd9db8fc068de47fd Mon Sep 17 00:00:00 2001 From: Vitor George Date: Fri, 1 Apr 2022 16:58:14 +0100 Subject: [PATCH 18/20] fix spacing --- pages/badges/edit.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pages/badges/edit.js b/pages/badges/edit.js index bbef3b0d..0c55b89c 100644 --- a/pages/badges/edit.js +++ b/pages/badges/edit.js @@ -283,6 +283,10 @@ export default class EditBadge extends Component { .danger-zone .button { margin-right: 2rem; } + + section { + margin-bottom: 20px; + } `} From 2c60733a1a17311a5dea7720e3f6f57a4d532d24 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Thu, 7 Apr 2022 19:40:58 +0100 Subject: [PATCH 19/20] Resolve display names from OSM API instead of users table --- .../20220302104250_add_user_badges.js | 6 +--- app/manage/badges.js | 30 ++++++++++++++----- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/db/migrations/20220302104250_add_user_badges.js b/app/db/migrations/20220302104250_add_user_badges.js index 026d2b00..bff7b27e 100644 --- a/app/db/migrations/20220302104250_add_user_badges.js +++ b/app/db/migrations/20220302104250_add_user_badges.js @@ -5,11 +5,7 @@ exports.up = async function (knex) { .references('id') .inTable('organization_badge') .onDelete('CASCADE') - table - .integer('user_id') - .references('id') - .inTable('users') - .onDelete('CASCADE') + table.integer('user_id') table.datetime('assigned_at').defaultTo(knex.fn.now()) table.datetime('valid_until') table.unique(['badge_id', 'user_id']) diff --git a/app/manage/badges.js b/app/manage/badges.js index e258e77e..191b4a6b 100644 --- a/app/manage/badges.js +++ b/app/manage/badges.js @@ -3,6 +3,7 @@ const yup = require('yup') const organization = require('../lib/organization') const profile = require('../lib/profile') const { routeWrapper } = require('./utils') +const team = require('../lib/team') /** * Get the list of badges of an organization @@ -84,10 +85,9 @@ const getBadge = routeWrapper({ .where('id', req.params.badgeId) .returning('*') - const users = await conn('user_badges') + let users = await conn('user_badges') .select({ id: 'user_badges.user_id', - profile: 'profile', assignedAt: 'user_badges.assigned_at', validUntil: 'user_badges.valid_until' }) @@ -96,18 +96,29 @@ const getBadge = routeWrapper({ 'user_badges.badge_id', 'organization_badge.id' ) - .leftJoin('users', 'user_badges.user_id', 'users.id') .where('badge_id', req.params.badgeId) .returning('*') - reply.send({ - ...badge, - users: users.map((u) => ({ + if (users.length > 0) { + // Get user profiles + const userProfiles = ( + await team.resolveMemberNames(users.map((u) => u.id)) + ).reduce((acc, u) => { + acc[u.id] = u + return acc + }, {}) + + users = users.map((u) => ({ id: u.id, assignedAt: u.assignedAt, validUntil: u.validUntil, - displayName: u.profile.displayName + displayName: userProfiles[u.id] ? userProfiles[u.id].name : '' })) + } + + reply.send({ + ...badge, + users }) } catch (err) { console.log(err) @@ -218,10 +229,13 @@ const assignUserBadge = routeWrapper({ reply.send(badge) } catch (err) { + console.log(err) if (err.code === '23505') { return reply.boom.badRequest('User is already assigned to badge.') } else { - return reply.boom.badRequest('Unexpected error, please try again later.') + return reply.boom.badRequest( + 'Unexpected error, please try again later.' + ) } } } From 5b3e0f00ea194c4bcb71fb729deb7c8347417742 Mon Sep 17 00:00:00 2001 From: Vitor George Date: Mon, 11 Apr 2022 09:11:05 +0100 Subject: [PATCH 20/20] Do not render badges section if user doesn't have privileges --- pages/organization.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pages/organization.js b/pages/organization.js index 5afd4afe..0283195a 100644 --- a/pages/organization.js +++ b/pages/organization.js @@ -188,18 +188,16 @@ export default class Organization extends Component { async getBadges () { try { const { id: orgId } = this.props - const badges = await apiClient.get(`/organizations/${orgId}/badges`) this.setState({ badges }) } catch (e) { - console.error(e) - this.setState({ - error: e, - team: null, - loading: false - }) + if (e.statusCode === 401) { + console.log("User doesn't have access to organization badges.") + } else { + console.error(e) + } } } @@ -207,7 +205,9 @@ export default class Organization extends Component { const { id: orgId } = this.props const columns = [{ key: 'name' }, { key: 'color' }] - return ( + // Do not render section if badges list cannot be fetched. This might happen + // on network error but also when the user doesn't have privileges. + return this.state.badges ? (
@@ -244,7 +244,7 @@ export default class Organization extends Component { } /> )} - ) + ) : null } renderMembers (memberRows) {