diff --git a/frontend/src/AdminPage.js b/frontend/src/AdminPage.js
index 2985793478..ce7f197a1b 100644
--- a/frontend/src/AdminPage.js
+++ b/frontend/src/AdminPage.js
@@ -1,5 +1,5 @@
import React, { useState } from 'react'
-import { Stack, Text, Select, useToast } from '@chakra-ui/core'
+import { Stack, Text, Select, useToast, Icon, Divider } from '@chakra-ui/core'
import { Trans, t } from '@lingui/macro'
import { Layout } from './Layout'
import AdminPanel from './AdminPanel'
@@ -8,6 +8,8 @@ import { useQuery } from '@apollo/client'
import { useUserState } from './UserState'
import { ErrorFallbackMessage } from './ErrorFallbackMessage'
import { LoadingMessage } from './LoadingMessage'
+import { TrackerButton } from './TrackerButton'
+import { Link as RouteLink } from 'react-router-dom'
export default function AdminPage() {
const { currentUser } = useUserState()
@@ -90,7 +92,7 @@ export default function AdminPage() {
Welcome, Admin
-
+
Organization:
@@ -105,6 +107,16 @@ export default function AdminPage() {
>
{options}
+
+
+ Create Organization
+
{options.length > 1 && orgDetails ? (
@@ -125,9 +137,23 @@ export default function AdminPage() {
)
} else {
return (
-
- You do not have admin permissions in any organization
-
+
+
+
+ You do not have admin permissions in any organization
+
+
+
+
+ Create Organization
+
+
+
)
}
}
diff --git a/frontend/src/App.js b/frontend/src/App.js
index 02fa6e4d3f..26f03577d3 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -36,6 +36,7 @@ const TwoFactorAuthenticatePage = lazy(() =>
import('./TwoFactorAuthenticatePage'),
)
const EmailValidationPage = lazy(() => import('./EmailValidationPage'))
+const CreateOrganizationPage = lazy(() => import('./CreateOrganizationPage'))
export default function App() {
// Hooks to be used with this functional component
@@ -203,6 +204,13 @@ export default function App() {
+
+
+
+
diff --git a/frontend/src/CreateOrganizationField.js b/frontend/src/CreateOrganizationField.js
new file mode 100644
index 0000000000..f3111d43d9
--- /dev/null
+++ b/frontend/src/CreateOrganizationField.js
@@ -0,0 +1,49 @@
+import React from 'react'
+import { elementType, func, oneOfType, shape, string } from 'prop-types'
+import {
+ FormControl,
+ FormErrorMessage,
+ FormLabel,
+ Input,
+} from '@chakra-ui/core'
+import { useField } from 'formik'
+import WithPseudoBox from './withPseudoBox'
+
+const OrganizationCreateField = WithPseudoBox(function OrganizationCreateField({
+ name,
+ label,
+ language,
+ forwardedRef,
+ ...props
+}) {
+ const [field, meta] = useField(name)
+
+ return (
+
+
+ {label} ({language})
+
+
+ {meta.error}
+
+ )
+})
+
+OrganizationCreateField.propTypes = {
+ name: string.isRequired,
+ forwardedRef: oneOfType([func, shape({ current: elementType })]),
+}
+
+const withForwardedRef = React.forwardRef((props, ref) => {
+ return
+})
+withForwardedRef.displayName = 'OrganizationCreateField'
+
+export default withForwardedRef
diff --git a/frontend/src/CreateOrganizationPage.js b/frontend/src/CreateOrganizationPage.js
new file mode 100644
index 0000000000..b828a33d9b
--- /dev/null
+++ b/frontend/src/CreateOrganizationPage.js
@@ -0,0 +1,287 @@
+import React from 'react'
+import { Stack, useToast, Box, Button, Heading } from '@chakra-ui/core'
+import { Trans, t } from '@lingui/macro'
+import { CREATE_ORGANIZATION } from './graphql/mutations'
+import { useMutation } from '@apollo/client'
+import { useUserState } from './UserState'
+import { LoadingMessage } from './LoadingMessage'
+import { Formik } from 'formik'
+import { useHistory, Link as RouteLink } from 'react-router-dom'
+import { TrackerButton } from './TrackerButton'
+import { object, string } from 'yup'
+import { fieldRequirements } from './fieldRequirements'
+import CreateOrganizationField from './CreateOrganizationField'
+import { i18n } from '@lingui/core'
+
+export default function CreateOrganizationPage() {
+ const { currentUser } = useUserState()
+ const toast = useToast()
+ const history = useHistory()
+
+ const validationSchema = object().shape({
+ nameEN: string().required(i18n._(fieldRequirements.field.required.message)),
+ nameFR: string().required(i18n._(fieldRequirements.field.required.message)),
+ acronymEN: string()
+ .matches(
+ fieldRequirements.acronym.matches.regex,
+ i18n._(fieldRequirements.acronym.matches.message),
+ )
+ .max(
+ fieldRequirements.acronym.max.maxLength,
+ i18n._(fieldRequirements.acronym.max.message),
+ )
+ .required(i18n._(fieldRequirements.field.required.message)),
+ acronymFR: string()
+ .matches(
+ fieldRequirements.acronym.matches.regex,
+ i18n._(fieldRequirements.acronym.matches.message),
+ )
+ .max(
+ fieldRequirements.acronym.max.maxLength,
+ i18n._(fieldRequirements.acronym.max.message),
+ )
+ .required(i18n._(fieldRequirements.field.required.message)),
+ zoneEN: string().required(i18n._(fieldRequirements.field.required.message)),
+ zoneFR: string().required(i18n._(fieldRequirements.field.required.message)),
+ sectorEN: string().required(
+ i18n._(fieldRequirements.field.required.message),
+ ),
+ sectorFR: string().required(
+ i18n._(fieldRequirements.field.required.message),
+ ),
+ cityEN: string().required(i18n._(fieldRequirements.field.required.message)),
+ cityFR: string().required(i18n._(fieldRequirements.field.required.message)),
+ provinceEN: string().required(
+ i18n._(fieldRequirements.field.required.message),
+ ),
+ provinceFR: string().required(
+ i18n._(fieldRequirements.field.required.message),
+ ),
+ countryEN: string().required(
+ i18n._(fieldRequirements.field.required.message),
+ ),
+ countryFR: string().required(
+ i18n._(fieldRequirements.field.required.message),
+ ),
+ })
+
+ const [createOrganization, { loading }] = useMutation(CREATE_ORGANIZATION, {
+ context: {
+ headers: {
+ authorization: currentUser.jwt,
+ },
+ },
+ onError(error) {
+ toast({
+ title: t`An error occurred.`,
+ description: error.message,
+ status: 'error',
+ duration: 9000,
+ isClosable: true,
+ position: 'top-left',
+ })
+ },
+ onCompleted({ createOrganization }) {
+ if (createOrganization.result.__typename === 'Organization') {
+ toast({
+ title: t`Organization created`,
+ description: t`${createOrganization.result.name} was created`,
+ status: 'success',
+ duration: 9000,
+ isClosable: true,
+ position: 'top-left',
+ })
+ history.push('/admin')
+ } else if (createOrganization.result.__typename === 'OrganizationError') {
+ toast({
+ title: t`Unable to create new organization.`,
+ description: createOrganization.result.description,
+ status: 'error',
+ duration: 9000,
+ isClosable: true,
+ position: 'top-left',
+ })
+ } else {
+ toast({
+ title: t`Incorrect send method received.`,
+ description: t`Incorrect createOrganization.result typename.`,
+ status: 'error',
+ duration: 9000,
+ isClosable: true,
+ position: 'top-left',
+ })
+ console.log('Incorrect createOrganization.result typename.')
+ }
+ },
+ })
+
+ if (loading) return
+
+ return (
+
+ {
+ createOrganization({
+ variables: {
+ nameEN: values.nameEN,
+ nameFR: values.nameFR,
+ acronymEN: values.acronymEN,
+ acronymFR: values.acronymFR,
+ zoneEN: values.zoneEN,
+ zoneFR: values.zoneFR,
+ sectorEN: values.sectorEN,
+ sectorFR: values.sectorFR,
+ countryEN: values.countryEN,
+ countryFR: values.countryFR,
+ provinceEN: values.provinceEN,
+ provinceFR: values.provinceFR,
+ cityEN: values.cityEN,
+ cityFR: values.cityFR,
+ },
+ })
+ }}
+ >
+ {({ handleSubmit, isSubmitting }) => (
+
+ )}
+
+
+ )
+}
diff --git a/frontend/src/__tests__/CreateOrganizationField.test.js b/frontend/src/__tests__/CreateOrganizationField.test.js
new file mode 100644
index 0000000000..4463864023
--- /dev/null
+++ b/frontend/src/__tests__/CreateOrganizationField.test.js
@@ -0,0 +1,56 @@
+import React from 'react'
+import { object, string } from 'yup'
+import { waitFor, render, fireEvent } from '@testing-library/react'
+import { ThemeProvider, theme } from '@chakra-ui/core'
+import CreateOrganizationField from '../CreateOrganizationField'
+import { Formik } from 'formik'
+import { I18nProvider } from '@lingui/react'
+import { setupI18n } from '@lingui/core'
+
+const i18n = setupI18n({
+ locale: 'en',
+ messages: {
+ en: {},
+ },
+ localeData: {
+ en: {},
+ },
+})
+
+describe('', () => {
+ describe('when validation fails', () => {
+ it('displays an error message', async () => {
+ const validationSchema = object().shape({
+ acronym: string().required('sadness'),
+ })
+
+ const { getByTestId, getByText } = render(
+
+
+
+ {() => (
+
+ )}
+
+
+ ,
+ )
+
+ const input = getByTestId('CreateOrganizationField')
+ fireEvent.blur(input)
+
+ await waitFor(() => {
+ expect(getByText(/sadness/)).toBeInTheDocument()
+ })
+ })
+ })
+})
diff --git a/frontend/src/__tests__/CreateOrganizationPage.test.js b/frontend/src/__tests__/CreateOrganizationPage.test.js
new file mode 100644
index 0000000000..98185584e4
--- /dev/null
+++ b/frontend/src/__tests__/CreateOrganizationPage.test.js
@@ -0,0 +1,128 @@
+import React from 'react'
+import { ThemeProvider, theme } from '@chakra-ui/core'
+import { MemoryRouter } from 'react-router-dom'
+import { fireEvent, render, waitFor } from '@testing-library/react'
+import { MockedProvider } from '@apollo/client/testing'
+import CreateOrganizationPage from '../CreateOrganizationPage'
+import { CREATE_ORGANIZATION } from '../graphql/mutations'
+import { I18nProvider } from '@lingui/react'
+import { setupI18n } from '@lingui/core'
+import { UserStateProvider } from '../UserState'
+
+const i18n = setupI18n({
+ locale: 'en',
+ messages: {
+ en: {},
+ },
+ localeData: {
+ en: {},
+ },
+})
+
+const mocks = [
+ {
+ request: {
+ query: CREATE_ORGANIZATION,
+ },
+ result: {
+ data: {
+ organization: {
+ name: 'New Test Org',
+ },
+ },
+ },
+ },
+]
+
+describe('', () => {
+ it('renders', async () => {
+ const { getByText } = render(
+
+
+
+
+
+
+
+
+
+
+ ,
+ )
+
+ const welcomeMessage = /Create an organization by filling out the following info in both English and French/i
+
+ await waitFor(() => expect(getByText(welcomeMessage)).toBeInTheDocument())
+ })
+
+ describe('acronym fields', () => {
+ it('displays an error message when incorrect format is used', async () => {
+ const { container, getByText } = render(
+
+
+
+
+
+
+
+
+
+
+ ,
+ )
+
+ const acronymEN = container.querySelector('#acronymEN')
+ const errorMessage = /Acronyms can only use upper case letters and underscores/i
+
+ await waitFor(() => {
+ fireEvent.change(acronymEN, { target: { value: 'test' } })
+ })
+
+ await waitFor(() => {
+ fireEvent.blur(acronymEN)
+ })
+
+ await waitFor(() => expect(getByText(errorMessage)).toBeInTheDocument())
+ })
+
+ it('displays an error message when input is too large', async () => {
+ const { container, getByText } = render(
+
+
+
+
+
+
+
+
+
+
+ ,
+ )
+
+ const acronymEN = container.querySelector('#acronymEN')
+ const errorMessage = /Acronyms must be at most 50 characters/i
+
+ await waitFor(() => {
+ fireEvent.change(acronymEN, {
+ target: {
+ value:
+ 'THIS_ACRONYM_IS_OVER_FIFTY_CHARACTERS_WHICH_MAKES_IT_INVALID',
+ },
+ })
+ })
+
+ await waitFor(() => {
+ fireEvent.blur(acronymEN)
+ })
+
+ await waitFor(() => expect(getByText(errorMessage)).toBeInTheDocument())
+ })
+ })
+})
diff --git a/frontend/src/fieldRequirements.js b/frontend/src/fieldRequirements.js
index 4ca8195f07..3e82b541d1 100644
--- a/frontend/src/fieldRequirements.js
+++ b/frontend/src/fieldRequirements.js
@@ -35,4 +35,17 @@ export const fieldRequirements = {
},
required: { message: t`Phone number field must not be empty` },
},
+ acronym: {
+ matches: {
+ regex: /^[A-Z]+(?:_[A-Z]+)*$/g,
+ message: t`Acronyms can only use upper case letters and underscores`,
+ },
+ max: {
+ maxLength: 50,
+ message: t`Acronyms must be at most 50 characters`,
+ },
+ },
+ field: {
+ required: { message: t`This field cannot be empty` },
+ },
}
diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js
index 3ad5381d1b..f1a651ac6c 100644
--- a/frontend/src/graphql/mutations.js
+++ b/frontend/src/graphql/mutations.js
@@ -380,4 +380,52 @@ export const VERIFY_ACCOUNT = gql`
}
`
+export const CREATE_ORGANIZATION = gql`
+ mutation CreateOrganization(
+ $acronymEN: Acronym!
+ $acronymFR: Acronym!
+ $nameEN: String!
+ $nameFR: String!
+ $zoneEN: String!
+ $zoneFR: String!
+ $sectorEN: String!
+ $sectorFR: String!
+ $countryEN: String!
+ $countryFR: String!
+ $provinceEN: String!
+ $provinceFR: String!
+ $cityEN: String!
+ $cityFR: String!
+ ) {
+ createOrganization(
+ input: {
+ acronymEN: $acronymEN
+ acronymFR: $acronymFR
+ nameEN: $nameEN
+ nameFR: $nameFR
+ zoneEN: $zoneEN
+ zoneFR: $zoneFR
+ sectorEN: $sectorEN
+ sectorFR: $sectorFR
+ countryEN: $countryEN
+ countryFR: $countryFR
+ provinceEN: $provinceEN
+ provinceFR: $provinceFR
+ cityEN: $cityEN
+ cityFR: $cityFR
+ }
+ ) {
+ result {
+ ... on Organization {
+ name
+ }
+ ... on OrganizationError {
+ code
+ description
+ }
+ }
+ }
+ }
+`
+
export default ''
diff --git a/frontend/src/locales/en.po b/frontend/src/locales/en.po
index 83a6159f8d..8025c92334 100644
--- a/frontend/src/locales/en.po
+++ b/frontend/src/locales/en.po
@@ -168,10 +168,11 @@ msgstr "An error occurred while verifying your phone number."
#: src/AdminDomains.js:147
#: src/AdminDomains.js:199
#: src/AdminDomains.js:275
-#: src/TwoFactorAuthenticatePage.js:36
-#: src/UserList.js:153
-#: src/UserList.js:203
-#: src/UserList.js:246
+#: src/TwoFactorAuthenticatePage.js:34
+#: src/UserList.js:136
+#: src/UserList.js:184
+#: src/UserList.js:237
+#: src/CreateOrganizationPage.js:61
msgid "An error occurred."
msgstr "An error occurred."
@@ -980,10 +981,11 @@ msgstr "Incorrect resetPassword.result typename."
#: src/EditableUserPhoneNumber.js:129
#: src/EditableUserTFAMethod.js:69
#: src/ResetPasswordPage.js:60
-#: src/SignInPage.js:99
-#: src/TwoFactorAuthenticatePage.js:83
-#: src/UserList.js:129
-#: src/UserList.js:182
+#: src/SignInPage.js:98
+#: src/TwoFactorAuthenticatePage.js:80
+#: src/UserList.js:114
+#: src/UserList.js:165
+#: src/CreateOrganizationPage.js:
msgid "Incorrect send method received."
msgstr "Incorrect send method received."
@@ -2455,6 +2457,80 @@ msgstr "New user email"
msgid "Edit Role"
msgstr "Edit Role"
+#: src/AdminPage.js:114
+#: src/AdminPage.js:149
+#: src/CreateOrganizationPage.js:367
+msgid "Create Organization"
+msgstr "Create Organization"
+
+#: src/fieldRequirements.js:41
+msgid "Acronyms can only use upper case letters and underscores"
+msgstr "Acronyms can only use upper case letters and underscores"
+
+#: src/fieldRequirements.js:45
+msgid "Acronyms must be at most 50 characters"
+msgstr "Acronyms must be at most 50 characters"
+
+#: src/fieldRequirements.js:49
+msgid "This field cannot be empty"
+msgstr "This field cannot be empty"
+
+#: src/CreateOrganizationPage.js:72
+msgid "Organization created"
+msgstr "Organization created"
+
+#: src/CreateOrganizationPage.js:73
+msgid "{0} was created"
+msgstr "{0} was created"
+
+#: src/CreateOrganizationPage.js:82
+msgid "Unable to create new organization."
+msgstr "Unable to create new organization."
+
+#: src/CreateOrganizationPage.js:92
+msgid "Incorrect createOrganization.result typename."
+msgstr "Incorrect createOrganization.result typename."
+
+#: src/CreateOrganizationPage.js:157
+msgid "Create an organization by filling out the following info in both English and French"
+msgstr "Create an organization by filling out the following info in both English and French"
+
+#: src/CreateOrganizationPage.js:164
+msgid "Name"
+msgstr "Name"
+
+#: src/CreateOrganizationPage.js:192
+msgid "Acronym"
+msgstr "Acronym"
+
+#: src/CreateOrganizationPage.js:222
+msgid "Zone"
+msgstr "Zone"
+
+#: src/CreateOrganizationPage.js:250
+msgid "Sector"
+msgstr "Sector"
+
+#: src/CreateOrganizationPage.js:278
+msgid "City"
+msgstr "City"
+
+#: src/CreateOrganizationPage.js:306
+msgid "Province"
+msgstr "Province"
+
+#: src/CreateOrganizationPage.js:334
+msgid "Country"
+msgstr "Country"
+
+#: src/CreateOrganizationPage.js:163
+msgid "English"
+msgstr "English"
+
+#: src/CreateOrganizationPage.js:172
+msgid "French"
+msgstr "French"
+
#: src/ScanCategoryDetails.js:44
msgid "Implementation:"
msgstr "Implementation:"
diff --git a/frontend/src/locales/fr.po b/frontend/src/locales/fr.po
index d2f7037ab1..aa9173a221 100644
--- a/frontend/src/locales/fr.po
+++ b/frontend/src/locales/fr.po
@@ -2447,6 +2447,80 @@ msgstr "Courriel du nouvel utilisateur"
msgid "Edit Role"
msgstr "Rôle d'édition"
+#: src/AdminPage.js:114
+#: src/AdminPage.js:149
+#: src/CreateOrganizationPage.js:367
+msgid "Create Organization"
+msgstr "Créer une organisation"
+
+#: src/fieldRequirements.js:41
+msgid "Acronyms can only use upper case letters and underscores"
+msgstr "Les acronymes ne peuvent utiliser que des lettres majuscules et des caractères de soulignement."
+
+#: src/fieldRequirements.js:45
+msgid "Acronyms must be at most 50 characters"
+msgstr "Les acronymes doivent comporter au maximum 50 caractères."
+
+#: src/fieldRequirements.js:49
+msgid "This field cannot be empty"
+msgstr "Ce champ ne peut pas être vide"
+
+#: src/CreateOrganizationPage.js:72
+msgid "Organization created"
+msgstr "Organisation créée"
+
+#: src/CreateOrganizationPage.js:73
+msgid "{0} was created"
+msgstr "{0} a été créée"
+
+#: src/CreateOrganizationPage.js:82
+msgid "Unable to create new organization."
+msgstr "Impossible de créer une nouvelle organisation."
+
+#: src/CreateOrganizationPage.js:92
+msgid "Incorrect createOrganization.result typename."
+msgstr "createOrganization.result incorrecte typename."
+
+#: src/CreateOrganizationPage.js:157
+msgid "Create an organization by filling out the following info in both English and French"
+msgstr "Créez une organisation en remplissant les informations suivantes en anglais et en français"
+
+#: src/CreateOrganizationPage.js:164
+msgid "Name"
+msgstr "Nom"
+
+#: src/CreateOrganizationPage.js:192
+msgid "Acronym"
+msgstr "Acronyme"
+
+#: src/CreateOrganizationPage.js:222
+msgid "Zone"
+msgstr "Zone"
+
+#: src/CreateOrganizationPage.js:250
+msgid "Sector"
+msgstr "Secteur"
+
+#: src/CreateOrganizationPage.js:278
+msgid "City"
+msgstr "Ville"
+
+#: src/CreateOrganizationPage.js:306
+msgid "Province"
+msgstr "Province"
+
+#: src/CreateOrganizationPage.js:334
+msgid "Country"
+msgstr "Pays"
+
+#: src/CreateOrganizationPage.js:163
+msgid "English"
+msgstr "Anglais"
+
+#: src/CreateOrganizationPage.js:172
+msgid "French"
+msgstr "Français"
+
#: src/ScanCategoryDetails.js:44
msgid "Implementation:"
msgstr "Mise en œuvre:"