Skip to content

Commit

Permalink
Merge pull request #671 from digitalfabrik/665-create-admin-from-admi…
Browse files Browse the repository at this point in the history
…nistration

665: Create admin from administration
  • Loading branch information
michael-markl authored Dec 26, 2022
2 parents 78ad1bb + 6dc19e0 commit 2b5c6f1
Show file tree
Hide file tree
Showing 16 changed files with 474 additions and 101 deletions.
2 changes: 1 addition & 1 deletion administration/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const Navigation = (props: Props) => {
const region = useContext(RegionContext)
const role = useContext(AuthContext).data?.administrator.role
return (
<Navbar>
<Navbar style={{ height: 'auto' }}>
<Navbar.Group>
<Navbar.Heading>{config.name} Verwaltung</Navbar.Heading>
<Navbar.Divider />
Expand Down
62 changes: 43 additions & 19 deletions administration/src/components/RegionSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import React, { useContext } from 'react'
import { Button, Menu, MenuItem, Spinner } from '@blueprintjs/core'
import { ItemListRenderer, ItemRenderer, Select } from '@blueprintjs/select'
import React, { useContext, useMemo } from 'react'
import { Button, Menu, Spinner } from '@blueprintjs/core'
import { Classes, ItemListRenderer, ItemRenderer, Select } from '@blueprintjs/select'
import { Region, useGetRegionsQuery } from '../generated/graphql'
import { ProjectConfigContext } from '../project-configs/ProjectConfigContext'

const RegionSelect = Select.ofType<Region>()

const getTitle = (region: Region) => `${region.prefix} ${region.name}`

const renderMenu: ItemListRenderer<Region> = ({ items, itemsParentRef, renderItem }) => {
const renderedItems = items.map(renderItem).filter(item => item != null)
const renderMenu: ItemListRenderer<Region> = ({ itemsParentRef, renderItem, filteredItems }) => {
const renderedItems = filteredItems.map(renderItem).filter(item => item != null)
return (
<Menu ulRef={itemsParentRef} style={{ maxHeight: 500, overflow: 'auto' }}>
{renderedItems}
Expand All @@ -19,36 +19,60 @@ const renderMenu: ItemListRenderer<Region> = ({ items, itemsParentRef, renderIte

const itemRenderer: ItemRenderer<Region> = (region, { handleClick, modifiers }) => {
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
<Button
style={{ display: 'block' }}
fill
key={region.id}
minimal
onClick={handleClick}
text={getTitle(region)}
/>
active={modifiers.active}
disabled={modifiers.disabled}>
{getTitle(region)}
</Button>
)
}

const RegionSelector = (props: { onRegionSelect: (region: Region) => void; activeRegionId: number }) => {
const RegionSelector = (props: { onSelect: (region: Region) => void; selectedId: number | null }) => {
const projectId = useContext(ProjectConfigContext).projectId
const { loading, error, data, refetch } = useGetRegionsQuery({
variables: { project: projectId },
})
const regions = useMemo(() => (data ? [...data.regions].sort((a, b) => a.name.localeCompare(b.name)) : null), [data])
if (loading) return <Spinner />
if (error || !data) return <Button icon='repeat' onClick={() => refetch()} />
const regions = data.regions
const activeItem = regions.find((other: Region) => props.activeRegionId === other.id)
if (error || regions === null) return <Button icon='repeat' onClick={() => refetch()} />
const activeItem = regions.find((other: Region) => props.selectedId === other.id)
return (
<RegionSelect
activeItem={activeItem}
items={regions}
itemRenderer={itemRenderer}
filterable={false}
filterable={true}
itemListPredicate={(filter, items) =>
items.filter(region => getTitle(region).toLowerCase().includes(filter.toLowerCase()))
}
fill
itemListRenderer={renderMenu}
onItemSelect={props.onRegionSelect}>
<span>
Region: <Button text={activeItem ? getTitle(activeItem) : 'Auswählen...'} rightIcon='double-caret-vertical' />
</span>
onItemSelect={props.onSelect}>
<div style={{ position: 'relative' }}>
{/* Make the browser think there is an actual select element to make it validate the form. */}
<select
style={{ height: '30px', opacity: 0, pointerEvents: 'none', position: 'absolute' }}
value={activeItem?.id ?? ''}
onChange={() => {}}
required
tabIndex={-1}>
<option value={activeItem?.id ?? ''} disabled>
{activeItem ? getTitle(activeItem) : 'Auswählen...'}
</option>
</select>
<Button
className={Classes.SELECT}
style={{ justifyContent: 'space-between', padding: '0 10px' }}
fill
rightIcon='double-caret-vertical'>
{activeItem ? getTitle(activeItem) : 'Auswählen...'}
</Button>
</div>
</RegionSelect>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const ForgotPasswordController = () => {
e.preventDefault()
submit()
}}>
<FormGroup label='E-Mail Adresse'>
<FormGroup label='Email-Adresse'>
<InputGroup
value={email}
onChange={e => setEmail(e.target.value)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const ResetPasswordController = () => {
e.preventDefault()
submit()
}}>
<FormGroup label='E-Mail Adresse'>
<FormGroup label='Email-Adresse'>
<InputGroup
value={email}
onChange={e => setEmail(e.target.value)}
Expand Down
157 changes: 157 additions & 0 deletions administration/src/components/users/CreateUserDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { Button, Checkbox, Classes, Dialog, FormGroup, HTMLSelect, InputGroup } from '@blueprintjs/core'
import { useContext, useState } from 'react'
import styled from 'styled-components'
import { Role, useCreateAdministratorMutation } from '../../generated/graphql'
import { useAppToaster } from '../AppToaster'
import RoleHelpButton from './RoleHelpButton'
import { roleToText } from './UsersTable'
import { ProjectConfigContext } from '../../project-configs/ProjectConfigContext'
import RegionSelector from '../RegionSelector'
import { ApolloError } from '@apollo/client'

const RoleFormGroupLabel = styled.span`
& span {
display: inline-block !important;
}
`

const RoleSelector = ({
role,
onChange,
hideProjectAdmin,
}: {
role: Role | null
onChange: (role: Role | null) => void
hideProjectAdmin: boolean
}) => {
return (
<HTMLSelect fill onChange={e => onChange((e.target.value as Role | null) ?? null)} value={role ?? ''} required>
<option value='' disabled>
Auswählen...
</option>
{hideProjectAdmin ? null : <option value={Role.ProjectAdmin}>{roleToText(Role.ProjectAdmin)}</option>}
<option value={Role.RegionAdmin}>{roleToText(Role.RegionAdmin)}</option>
<option value={Role.RegionManager}>{roleToText(Role.RegionManager)}</option>
</HTMLSelect>
)
}

const getMessageFromApolloError = (error: ApolloError): string => {
const defaultMessage = 'Etwas ist schief gelaufen.'
if (error.graphQLErrors.length !== 1) {
return defaultMessage
}
const graphQLError = error.graphQLErrors[0]
if ('code' in graphQLError.extensions) {
switch (graphQLError.extensions['code']) {
case 'EMAIL_ALREADY_EXISTS':
return 'Die Email-Adresse wird bereits verwendet.'
}
}
return defaultMessage
}

const CreateUserDialog = ({
isOpen,
onClose,
onSuccess,
regionIdOverride,
}: {
isOpen: boolean
onClose: () => void
onSuccess: () => void
// If regionIdOverride is set, the region selector will be hidden, and only RegionAdministrator and RegionManager
// roles are selectable.
regionIdOverride: number | null
}) => {
const appToaster = useAppToaster()
const [email, setEmail] = useState('')
const [role, setRole] = useState<Role | null>(null)
const [regionId, setRegionId] = useState<number | null>(null)
const [sendWelcomeMail, setSendWelcomeMail] = useState(true)
const project = useContext(ProjectConfigContext).projectId
const rolesWithRegion = [Role.RegionManager, Role.RegionAdmin]

const [createAdministrator, { loading }] = useCreateAdministratorMutation({
onError: error => {
console.error(error)
appToaster?.show({ intent: 'danger', message: 'Fehler: ' + getMessageFromApolloError(error) })
},
onCompleted: () => {
appToaster?.show({ intent: 'success', message: 'Benutzer erfolgreich erstellt.' })
onClose()
onSuccess()
// Reset State
setEmail('')
setRole(null)
setRegionId(null)
setSendWelcomeMail(true)
},
})

const getRegionId = () => {
if (regionIdOverride !== null) {
return regionIdOverride
} else {
return role !== null && rolesWithRegion.includes(role) ? regionId : null
}
}

return (
<Dialog title='Benutzer hinzufügen' isOpen={isOpen} onClose={onClose}>
<form
onSubmit={e => {
e.preventDefault()
createAdministrator({
variables: {
project,
email,
role: role as Role,
regionId: getRegionId(),
sendWelcomeMail,
},
})
}}>
<div className={Classes.DIALOG_BODY}>
<FormGroup label='Email-Adresse'>
<InputGroup
value={email}
required
onChange={e => setEmail(e.target.value)}
type='email'
placeholder='[email protected]'
/>
</FormGroup>
<FormGroup
label={
<RoleFormGroupLabel>
Rolle <RoleHelpButton />
</RoleFormGroupLabel>
}>
<RoleSelector role={role} onChange={setRole} hideProjectAdmin={regionIdOverride !== null} />
</FormGroup>
{regionIdOverride !== null || role === null || !rolesWithRegion.includes(role) ? null : (
<FormGroup label='Region'>
<RegionSelector onSelect={region => setRegionId(region.id)} selectedId={regionId} />
</FormGroup>
)}
<FormGroup>
<Checkbox checked={sendWelcomeMail} onChange={e => setSendWelcomeMail(e.currentTarget.checked)}>
<b>Sende eine Willkommens-Email.</b>
<br />
Diese Email enthält einen Link, mit dem das Passwort des Accounts gesetzt werden kann. Der Link ist 24
Stunden gültig.
</Checkbox>
</FormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button type='submit' intent='success' text='Benutzer hinzufügen' icon='add' loading={loading} />
</div>
</div>
</form>
</Dialog>
)
}

export default CreateUserDialog
10 changes: 5 additions & 5 deletions administration/src/components/users/ManageUsersController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ const RefetchCard = (props: { refetch: () => void }) => {
const UsersTableContainer = ({ children, title }: { children: ReactElement; title: string }) => {
return (
<StandaloneCenter>
<Card style={{ maxWidth: '800px', margin: '16px' }}>
<H3>{title}</H3>
<Card style={{ maxWidth: '1200px', margin: '16px' }}>
<H3 style={{ textAlign: 'center' }}>{title}</H3>
{children}
</Card>
</StandaloneCenter>
Expand All @@ -53,7 +53,7 @@ const ManageProjectUsers = () => {

return (
<UsersTableContainer title={`Alle Benutzer von '${projectName} - Verwaltung'`}>
<UsersTable users={users} regions={regions} showRegion={true} />
<UsersTable users={users} regions={regions} refetch={usersQuery.refetch} />
</UsersTableContainer>
)
}
Expand All @@ -75,8 +75,8 @@ const ManageRegionUsers = ({ region }: { region: Region }) => {
const users = usersQuery.data!!.users

return (
<UsersTableContainer title={`Alle Verwalter der Region '${region.name}'`}>
<UsersTable users={users} regions={regions} showRegion={false} />
<UsersTableContainer title={`Alle Verwalter der Region '${region.prefix} ${region.name}'`}>
<UsersTable users={users} regions={regions} selectedRegionId={region.id} refetch={usersQuery.refetch} />
</UsersTableContainer>
)
}
Expand Down
42 changes: 42 additions & 0 deletions administration/src/components/users/RoleHelpButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Button, H4 } from '@blueprintjs/core'
import { Popover2 } from '@blueprintjs/popover2'
import { Role } from '../../generated/graphql'
import { roleToText } from './UsersTable'

const RoleHelpButton = () => {
return (
<Popover2
content={
<div style={{ padding: '10px' }}>
<H4 style={{ textAlign: 'center' }}>Welche Rollen haben welche Berechtigungen?</H4>
<ul>
<li>
<b>{roleToText(Role.ProjectAdmin)}:</b>
<ul>
<li>Kann verwaltende Benutzer in allen Regionen verwalten.</li>
</ul>
</li>
<li>
<b>{roleToText(Role.RegionAdmin)}:</b>
<ul>
<li>Kann verwaltende Benutzer in seiner Region verwalten.</li>
<li>Kann digitale Karten in seiner Region erstellen.</li>
<li>Kann Anträge in seiner Region verwalten.</li>
</ul>
</li>
<li>
<b>{roleToText(Role.RegionManager)}:</b>
<ul>
<li>Kann digitale Karten in seiner Region erstellen.</li>
<li>Kann Anträge in seiner Region verwalten.</li>
</ul>
</li>
</ul>
</div>
}>
<Button icon='help' minimal />
</Popover2>
)
}

export default RoleHelpButton
Loading

0 comments on commit 2b5c6f1

Please sign in to comment.