Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • andretol/greg
1 result
Show changes
Commits on Source (28)
Showing
with 358 additions and 223 deletions
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
"roleStartDate": "From", "roleStartDate": "From",
"roleEndDate": "To", "roleEndDate": "To",
"comment": "Comment", "comment": "Comment",
"contact": "Contact person",
"searchable": "Available in search?", "searchable": "Available in search?",
"email": "E-mail", "email": "E-mail",
"fullName": "Full name", "fullName": "Full name",
...@@ -44,6 +43,7 @@ ...@@ -44,6 +43,7 @@
"bodyText": "Here you can add a new role to the same guest" "bodyText": "Here you can add a new role to the same guest"
}, },
"register": { "register": {
"noResults": "No guest matching your search found.",
"registerHeading": "Register new guest", "registerHeading": "Register new guest",
"registerText": "Please search for e-mail or phone number before registering a new guest to prevent duplicates.", "registerText": "Please search for e-mail or phone number before registering a new guest to prevent duplicates.",
"registerButtonText": "Register new guest" "registerButtonText": "Register new guest"
...@@ -62,20 +62,25 @@ ...@@ -62,20 +62,25 @@
"role": "Guest role", "role": "Guest role",
"period": "Period", "period": "Period",
"ou": "Organisation", "ou": "Organisation",
"department": "Department",
"choice": "Choices", "choice": "Choices",
"registerText": "Register new guest", "registerText": "Register new guest",
"waitingGuests": "Guests waiting for your confirmation", "waitingGuests": "Guests waiting for confirmation",
"waitingGuestsDescription": "See the table below for guests ", "waitingGuestsDescription": "Confirm your guests that <1>registered manually</1> here. Guests with FEIDE-user do not need confirmation.",
"noWaitingGuests": "No waiting guests", "noWaitingGuests": "No waiting guests",
"activeGuests": "Your active guests", "activeGuests": "Confirmed guests",
"activeGuestsDescription": "Make changes to your existing guests", "activeGuestsDescription": "Make changes to guest roles.",
"noActiveGuests": "No active guests", "noActiveGuests": "No active guests",
"sentInvitations": "Sent invitations", "sentInvitations": "Sent invitations",
"sentInvitationsDescription": "Invitations awaiting response from guest.", "sentInvitationsDescription": "Invitations awaiting response from guest.",
"noInvitations": "No invitations", "noInvitations": "No invitations",
"status": "Status", "status": "Status",
"active": "Active", "statusText": {
"expired": "Expired", "active": "Active",
"expired": "Expired",
"waitingForGuest": "Waiting for guest",
"waitingForSponsor": "Needs confirmation"
},
"details": "Details", "details": "Details",
"nationalIdNumber": "National ID number", "nationalIdNumber": "National ID number",
"validation": { "validation": {
...@@ -146,9 +151,20 @@ ...@@ -146,9 +151,20 @@
"cancelInvitationDescription": "Do you want to cancel the invitation?" "cancelInvitationDescription": "Do you want to cancel the invitation?"
}, },
"error": { "error": {
"error": "Error",
"invitationCreationFailedHeader": "Failed to create invite", "invitationCreationFailedHeader": "Failed to create invite",
"errorStatusCode": "Status code: {{statusCode}} (<3>{{statusText}}</3>)", "errorStatusCode": "Status code: {{statusCode}} (<3>{{statusText}}</3>)",
"genericServerErrorBody": "The server reported:<1>{{errorBodyText}}</1>", "genericServerErrorBody": "The server reported:<1>{{errorBodyText}}</1>",
"contactHelp": "Contact help through the link in the footer if the problem persists." "contactHelp": "Contact help through the link in the footer if the problem persists.",
"unknown": "An unknown error has occurred. If the problem persists, contact support.",
"contactHelp": "Contact help through the link in the footer if the problem persists.",
"invitationDataFetchFailed": "Failed to fetch invitation data",
"guestRegistrationFailed": "Failed to register your data",
"codes": {
"invalid_invite": "Invalid invite",
"invite_expired": "Invite has expired",
"cannot_update_fields": "Failed to update data",
"update_national_id_not_allowed": "Not allowed to update verified national ID"
}
} }
} }
{ {
"description": "Please choose how you want to log in to complete your registration. The recommended way is to log in with either Feide or ID-porten. If that is not possible you can manually fill out the registration form with your passport number.", "description": "Please choose how you want to log in to complete your registration. The recommended way is to log in with either Feide or ID-porten. If that is not possible you can manually fill out the registration form with your passport number or Norwegian national ID number.",
"header": "Guest Registration", "header": "Guest registration",
"login": "Log in with FEIDE", "login": "Log in with FEIDE",
"manual": "Registrate manually" "manual": "Register manually",
"errors": {
"invalidToken": "The invitation link you followed is invalid.",
"expiredToken": "The invitation link you followed has expired. Contact your host to get a new link."
}
} }
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
"roleStartDate": "Fra", "roleStartDate": "Fra",
"roleEndDate": "Til", "roleEndDate": "Til",
"comment": "Kommentar", "comment": "Kommentar",
"contact": "Kontaktperson",
"searchable": "Synlig i søk?", "searchable": "Synlig i søk?",
"email": "E-post", "email": "E-post",
"fullName": "Fullt navn", "fullName": "Fullt navn",
...@@ -44,6 +43,7 @@ ...@@ -44,6 +43,7 @@
"bodyText": "Her kan du legge til en ny rolle på samme gjest" "bodyText": "Her kan du legge til en ny rolle på samme gjest"
}, },
"register": { "register": {
"noResults": "Finner ingen gjestekontoer som matcher søket ditt.",
"registerHeading": "Registrer ny gjest", "registerHeading": "Registrer ny gjest",
"registerText": "Søk etter e-post eller mobilnummer før du registrerer ny gjest for å unngå dobbeltoppføringer.", "registerText": "Søk etter e-post eller mobilnummer før du registrerer ny gjest for å unngå dobbeltoppføringer.",
"registerButtonText": "Registrer ny gjest" "registerButtonText": "Registrer ny gjest"
...@@ -62,20 +62,25 @@ ...@@ -62,20 +62,25 @@
"role": "Gjesterolle", "role": "Gjesterolle",
"period": "Periode", "period": "Periode",
"ou": "Organisasjon", "ou": "Organisasjon",
"department": "Avdeling",
"choice": "Valg", "choice": "Valg",
"registerText": "Registrer ny gjest", "registerText": "Registrer ny gjest",
"waitingGuests": "Dine gjester som venter på godkjenning", "waitingGuests": "Gjester som venter på godkjenning",
"waitingGuestsDescription": "Under er en oversikt over kontoer som venter på godkjenning", "waitingGuestsDescription": "Her godkjenner du gjester som har <1>registrert seg manuelt</1>. Gjester som har FEIDE-bruker trenger ikke godkjenning.",
"noWaitingGuests": "Ingen gjester til godkjenning", "noWaitingGuests": "Ingen gjester til godkjenning",
"activeGuests": "Dine aktive gjester", "activeGuests": "Godkjente gjester",
"activeGuestsDescription": "Her kan du endre på eksisterende gjester", "activeGuestsDescription": "Her kan du endre på gjesteroller",
"noActiveGuests": "Ingen aktive gjester", "noActiveGuests": "Ingen aktive gjester",
"sentInvitations": "Sendte invitasjoner", "sentInvitations": "Sendte invitasjoner",
"sentInvitationsDescription": "Invitasjoner som venter på at gjesten skal ferdigstille registreringen.", "sentInvitationsDescription": "Invitasjoner som venter på at gjesten skal ferdigstille registreringen.",
"noInvitations": "Ingen invitasjoner", "noInvitations": "Ingen invitasjoner",
"status": "Status", "status": "Status",
"active": "Aktiv", "statusText": {
"expired": "Utgått", "active": "Aktiv",
"expired": "Utgått",
"waitingForGuest": "Venter på gjest",
"waitingForSponsor": "Trenger godkjenning"
},
"details": "Detaljer", "details": "Detaljer",
"nationalIdNumber": "Fødselsnummer/D-nummer", "nationalIdNumber": "Fødselsnummer/D-nummer",
"validation": { "validation": {
...@@ -146,9 +151,20 @@ ...@@ -146,9 +151,20 @@
"cancelInvitationDescription": "Vil du kansellere invitasjonen?" "cancelInvitationDescription": "Vil du kansellere invitasjonen?"
}, },
"error": { "error": {
"error": "Feil",
"invitationCreationFailedHeader": "Kunne ikke opprette invitasjon", "invitationCreationFailedHeader": "Kunne ikke opprette invitasjon",
"errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)", "errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)",
"genericServerErrorBody": "Respons fra server:<1>{{errorBodyText}}</1>", "genericServerErrorBody": "Respons fra server:<1>{{errorBodyText}}</1>",
"contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer." "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.",
"unknown": "En ukjent feil har oppstått. Om problemet vedvarer, kontakt brukerstøtte.",
"contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.",
"invitationDataFetchFailed": "Klarte ikke å hente invitasjonsdata",
"guestRegistrationFailed": "Klarte ikke å registrere dataene dine",
"codes": {
"invalid_invite": "Ugyldig invitasjon",
"invite_expired": "Invitasjonen har utløpt",
"cannot_update_fields": "Klarte ikke å oppdatere dataene",
"update_national_id_not_allowed": "Ikke tillatt å endre verifisert fødselsnummer/D-nummer"
}
} }
} }
...@@ -2,5 +2,9 @@ ...@@ -2,5 +2,9 @@
"description": "Vennligst velg hvordan du vil logge inn for å fullføre registreringen. Den anbefalte metoden er å logge inn gjennom Feide eller ID-porten. Dersom det ikke er mulig kan du fylle ut registreringskjemaet manuelt med passnummer", "description": "Vennligst velg hvordan du vil logge inn for å fullføre registreringen. Den anbefalte metoden er å logge inn gjennom Feide eller ID-porten. Dersom det ikke er mulig kan du fylle ut registreringskjemaet manuelt med passnummer",
"header": "Gjestetjenesten", "header": "Gjestetjenesten",
"login": "Logg inn med FEIDE", "login": "Logg inn med FEIDE",
"manual": "Registrer manuelt" "manual": "Registrer manuelt",
"errors": {
"invalidToken": "Denne invitasjonslenka er ugyldig.",
"expiredToken": "Denne invitasjonslenka er utløpt. Kontakt verten din for å få tilsendt en ny."
}
} }
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
"roleStartDate": "Frå", "roleStartDate": "Frå",
"roleEndDate": "Til", "roleEndDate": "Til",
"comment": "Kommentar", "comment": "Kommentar",
"contact": "Kontaktperson",
"searchable": "Synleg i søk?", "searchable": "Synleg i søk?",
"email": "E-post", "email": "E-post",
"fullName": "Fullt namn", "fullName": "Fullt namn",
...@@ -45,6 +44,7 @@ ...@@ -45,6 +44,7 @@
"bodyText": "Her kan du legge til en ny rolle på samme gjest" "bodyText": "Her kan du legge til en ny rolle på samme gjest"
}, },
"register": { "register": {
"noResults": "Finner ingen gjestekontoer som matcher søket ditt.",
"registerHeading": "Registrer ny gjest", "registerHeading": "Registrer ny gjest",
"registerText": "Søk etter e-post eller mobilnummer før du registrerer ny gjest for å unngå dobbeltoppføringer.", "registerText": "Søk etter e-post eller mobilnummer før du registrerer ny gjest for å unngå dobbeltoppføringer.",
"registerButtonText": "Registrer ny gjest" "registerButtonText": "Registrer ny gjest"
...@@ -63,20 +63,25 @@ ...@@ -63,20 +63,25 @@
"role": "Gjesterolle", "role": "Gjesterolle",
"period": "Periode", "period": "Periode",
"ou": "Organisasjon", "ou": "Organisasjon",
"department": "Avdeling",
"choice": "Valg", "choice": "Valg",
"registerText": "Registrer ny gjest", "registerText": "Registrer ny gjest",
"waitingGuests": "Dine gjester som venter på godkjenning", "waitingGuests": "Gjester som venter på godkjenning",
"waitingGuestsDescription": "Under er en oversikt over kontoer som venter på godkjenning", "waitingGuestsDescription": "Her godkjenner du gjester som har <1>registrert seg manuelt</1>. Gjester som har FEIDE-bruker trenger ikke godkjenning.",
"noWaitingGuests": "Ingen gjester til godkjenning", "noWaitingGuests": "Ingen gjester til godkjenning",
"activeGuests": "Dine aktive gjester", "activeGuests": "Godkjente gjester",
"activeGuestsDescription": "Her kan du endre på eksisterende gjester", "activeGuestsDescription": "Her kan du endre på gjesteroller.",
"noActiveGuests": "Ingen aktive gjester", "noActiveGuests": "Ingen aktive gjester",
"sentInvitations": "Sendte invitasjonar", "sentInvitations": "Sendte invitasjonar",
"sentInvitationsDescription": "Invitasjonar som venter på at gjesten skal ferdigstille registreringa.", "sentInvitationsDescription": "Invitasjonar som venter på at gjesten skal ferdigstille registreringa.",
"noInvitations": "Ingen invitasjonar", "noInvitations": "Ingen invitasjonar",
"status": "Status", "status": "Status",
"active": "Aktiv", "statusText": {
"expired": "Utgått", "active": "Aktiv",
"expired": "Utgått",
"waitingForGuest": "Venter på gjest",
"waitingForSponsor": "Trenger godkjenning"
},
"details": "Detaljer", "details": "Detaljer",
"nationalIdNumber": "Fødselsnummer/D-nummer", "nationalIdNumber": "Fødselsnummer/D-nummer",
"validation": { "validation": {
...@@ -147,9 +152,20 @@ ...@@ -147,9 +152,20 @@
"cancelInvitationDescription": "Vil du kansellere invitasjonen?" "cancelInvitationDescription": "Vil du kansellere invitasjonen?"
}, },
"error": { "error": {
"error": "Feil",
"invitationCreationFailedHeader": "Kunne ikkje opprette invitasjon", "invitationCreationFailedHeader": "Kunne ikkje opprette invitasjon",
"errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)", "errorStatusCode": "Statuskode: {{statusCode}} (<3>{{statusText}}</3>)",
"genericServerErrorBody": "Respons frå server:<1>{{errorBodyText}}</1>", "genericServerErrorBody": "Respons frå server:<1>{{errorBodyText}}</1>",
"contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer." "contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.",
"unknown": "Ein uventa feil oppstod. Om problemet varer ved, kontakt brukarstøtte.",,
"contactHelp": "Kontakt hjelp via link i footer om problemet vedvarer.",
"invitationDataFetchFailed": "Klarte ikkje å hente invitasjonsdata",
"guestRegistrationFailed": "Klarte ikkje å registrere dataene dine",
"codes": {
"invalid_invite": "Ugyldig invitasjon",
"invite_expired": "Invitasjonen har utløpe",
"cannot_update_fields": "Klarte ikkje å oppdatere dataene",
"update_national_id_not_allowed": "Ikkje tillete å endre verifisert fødselsnummer/D-nummer"
}
} }
} }
...@@ -2,5 +2,9 @@ ...@@ -2,5 +2,9 @@
"description": "Ver venleg og vel korleis du vil logge inn for å fullføre registreringa. Den anbefalte metoden er å logge inn gjennom Feide eller ID-porten. Dersom det ikkje er mogeleg kan du fylle ut registreringskjemaet manuelt med passnummer", "description": "Ver venleg og vel korleis du vil logge inn for å fullføre registreringa. Den anbefalte metoden er å logge inn gjennom Feide eller ID-porten. Dersom det ikkje er mogeleg kan du fylle ut registreringskjemaet manuelt med passnummer",
"header": "Gjestetjenesten", "header": "Gjestetjenesten",
"login": "Logg inn med FEIDE", "login": "Logg inn med FEIDE",
"manual": "Registrer manuelt" "manual": "Registrer manuelt",
"errors": {
"invalidToken": "Denne invitasjonslenka er ugyldig.",
"expiredToken": "Denne invitasjonslenka har utløpe. Kontakt verten din for å få tilsendt ei ny."
}
} }
...@@ -4,16 +4,7 @@ import { useTranslation } from 'react-i18next' ...@@ -4,16 +4,7 @@ import { useTranslation } from 'react-i18next'
import CheckIcon from '@mui/icons-material/Check' import CheckIcon from '@mui/icons-material/Check'
import ClearIcon from '@mui/icons-material/Clear' import ClearIcon from '@mui/icons-material/Clear'
import { import { Box, Table, TableBody, TableRow, TableCell } from '@mui/material'
Box,
Button,
Table,
TableBody,
TableRow,
TableCell,
Stack,
Divider,
} from '@mui/material'
import { appInst, appTimezone, appVersion } from 'appConfig' import { appInst, appTimezone, appVersion } from 'appConfig'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
...@@ -26,8 +17,6 @@ export const Debug = () => { ...@@ -26,8 +17,6 @@ export const Debug = () => {
const [apiHealth, setApiHealth] = useState('not yet') const [apiHealth, setApiHealth] = useState('not yet')
const [didContactApi, setDidContactApi] = useState(false) const [didContactApi, setDidContactApi] = useState(false)
const { i18n } = useTranslation(['common']) const { i18n } = useTranslation(['common'])
const [csrf, setCsrf] = useState<String | null>(null)
const [username, setUsername] = useState(undefined)
const { user } = useUserContext() const { user } = useUserContext()
if (!didContactApi) { if (!didContactApi) {
...@@ -47,87 +36,6 @@ export const Debug = () => { ...@@ -47,87 +36,6 @@ export const Debug = () => {
}) })
} }
const getCSRF = () => {
fetch('/api/ui/v1/csrf/', {
credentials: 'same-origin',
})
.then((res) => {
const csrfToken = res.headers.get('X-CSRFToken')
setCsrf(csrfToken)
console.log(csrfToken)
})
.catch((err) => {
console.log(err)
})
}
const getSession = () => {
fetch('/api/ui/v1/session/?format=json', {
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
})
.then((res) => res.json())
.then((data) => {
console.log(data)
getCSRF()
})
.catch((err) => {
console.log(err)
})
}
const testMail = () => {
fetch('/api/ui/v1/testmail/', {
credentials: 'same-origin',
})
.then((data) => {
console.log(data)
})
.catch((err) => {
console.log(err)
})
}
const whoami = () => {
fetch('/api/ui/v1/whoami/', {
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
})
.then((res) => res.json())
.then((data) => {
setUsername(data.username)
console.log(`You are logged in as: ${data.username}`)
})
.catch((err) => {
console.log(err)
})
}
function isResponseOk(response: any) {
if (response.status >= 200 && response.status <= 299) {
return response.json()
}
throw Error(response.statusText)
}
const logout = () => {
fetch('/api/ui/v1/logout/', {
credentials: 'same-origin',
})
.then(isResponseOk)
.then((data) => {
console.log(data)
getCSRF()
})
.catch((err) => {
console.log(err)
})
}
const d = [ const d = [
['NODE_ENV', process.env.NODE_ENV], ['NODE_ENV', process.env.NODE_ENV],
['Version', appVersion], ['Version', appVersion],
...@@ -137,8 +45,6 @@ export const Debug = () => { ...@@ -137,8 +45,6 @@ export const Debug = () => {
['Institution', appInst], ['Institution', appInst],
['API reachable?', apiHealth === 'yes' ? <Yes /> : apiHealth], ['API reachable?', apiHealth === 'yes' ? <Yes /> : apiHealth],
['Authenticated?', user.auth ? <Yes /> : <No />], ['Authenticated?', user.auth ? <Yes /> : <No />],
['Username', username],
['CSRF', csrf],
] ]
return ( return (
<Box> <Box>
...@@ -163,25 +69,6 @@ export const Debug = () => { ...@@ -163,25 +69,6 @@ export const Debug = () => {
</ul> </ul>
</p> </p>
<h3>Debug</h3> <h3>Debug</h3>
<Stack
direction="row"
spacing={1}
divider={<Divider orientation="vertical" />}
>
<Button type="button" onClick={() => getSession()}>
AM I AUTHENTICATED?
</Button>
<Button type="button" onClick={() => whoami()}>
WHO AM I?
</Button>
<Button type="button" onClick={() => logout()}>
LOGOUT
</Button>
<Button type="button" onClick={() => testMail()}>
SEND TEST MAIL
</Button>
</Stack>
<Box sx={{ maxWidth: '30rem' }}> <Box sx={{ maxWidth: '30rem' }}>
<Table> <Table>
<TableBody> <TableBody>
......
...@@ -2,7 +2,7 @@ import { Alert, AlertTitle } from '@mui/material' ...@@ -2,7 +2,7 @@ import { Alert, AlertTitle } from '@mui/material'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import React from 'react' import React from 'react'
interface ErrorReportProps { export interface ErrorReportProps {
errorHeading: string errorHeading: string
statusCode?: number statusCode?: number
statusText?: string statusText?: string
......
...@@ -5,11 +5,14 @@ export interface IFeatureContext { ...@@ -5,11 +5,14 @@ export interface IFeatureContext {
displayContactAtUnit: boolean displayContactAtUnit: boolean
// Controls whether the optional field is shown in the register new guest wizard // Controls whether the optional field is shown in the register new guest wizard
displayComment: boolean displayComment: boolean
// Should the contact at unit field be shown for the guest when he registers his information?
displayContactAtUnitGuestInput: boolean
} }
export const FeatureContext = createContext<IFeatureContext>({ export const FeatureContext = createContext<IFeatureContext>({
displayContactAtUnit: true, displayContactAtUnit: true,
displayComment: true, displayComment: true,
displayContactAtUnitGuestInput: true,
}) })
export const useFeatureContext = () => useContext(FeatureContext) export const useFeatureContext = () => useContext(FeatureContext)
...@@ -12,7 +12,7 @@ function useRoleTypes(): RoleTypeData[] { ...@@ -12,7 +12,7 @@ function useRoleTypes(): RoleTypeData[] {
const [roleTypes, setRoleTypes] = useState<RoleTypeData[]>([]) const [roleTypes, setRoleTypes] = useState<RoleTypeData[]>([])
async function fetchRoleTypes() { async function fetchRoleTypes() {
fetch(`/api/ui/v1/roletypes`) fetch(`/api/ui/v1/roletypes/`)
.then((data) => data.text()) .then((data) => data.text())
.then((result) => { .then((result) => {
// The response is a JSON-array // The response is a JSON-array
......
...@@ -13,12 +13,20 @@ function FeatureProvider(props: FeatureProviderProps) { ...@@ -13,12 +13,20 @@ function FeatureProvider(props: FeatureProviderProps) {
let features: IFeatureContext let features: IFeatureContext
switch (appInst) { switch (appInst) {
case 'uib': case 'uib':
features = { displayContactAtUnit: false, displayComment: false } features = {
displayContactAtUnit: false,
displayComment: false,
displayContactAtUnitGuestInput: false,
}
break break
case 'uio': case 'uio':
default: default:
features = { displayContactAtUnit: true, displayComment: true } features = {
displayContactAtUnit: true,
displayComment: true,
displayContactAtUnitGuestInput: true,
}
break break
} }
......
import PersonIcon from '@mui/icons-material/Person' import PersonIcon from '@mui/icons-material/Person'
import { Box, IconButton, Theme } from '@mui/material' import { Box, IconButton, styled, Theme } from '@mui/material'
import PersonAddIcon from '@mui/icons-material/PersonAdd' import PersonAddIcon from '@mui/icons-material/PersonAdd'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
...@@ -10,6 +10,13 @@ interface SponsorGuestButtonsProps { ...@@ -10,6 +10,13 @@ interface SponsorGuestButtonsProps {
registerNewGuestActive?: boolean registerNewGuestActive?: boolean
} }
const StyledIconButton = styled(IconButton)({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
fontSize: '22px',
})
export default function SponsorGuestButtons(props: SponsorGuestButtonsProps) { export default function SponsorGuestButtons(props: SponsorGuestButtonsProps) {
const { yourGuestsActive, registerNewGuestActive } = props const { yourGuestsActive, registerNewGuestActive } = props
const { t } = useTranslation(['common']) const { t } = useTranslation(['common'])
...@@ -30,22 +37,20 @@ export default function SponsorGuestButtons(props: SponsorGuestButtonsProps) { ...@@ -30,22 +37,20 @@ export default function SponsorGuestButtons(props: SponsorGuestButtonsProps) {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-evenly', justifyContent: 'space-evenly',
marginBottom: '2rem', marginBottom: '2rem',
fontSize: '22px',
}} }}
> >
<IconButton <StyledIconButton
onClick={goToOverview} onClick={goToOverview}
sx={{ sx={{
display: 'flex', color: () => (yourGuestsActive ? 'primary.main' : ''),
flexDirection: 'column',
alignItems: 'center',
typography: 'caption',
textDecorationLine: () => (yourGuestsActive ? 'underline' : ''), textDecorationLine: () => (yourGuestsActive ? 'underline' : ''),
}} }}
> >
<PersonIcon <PersonIcon
fontSize="large"
sx={{ sx={{
borderRadius: '2rem', fontSize: '80px',
borderRadius: '4rem',
borderStyle: 'solid', borderStyle: 'solid',
borderColor: (theme: Theme) => borderColor: (theme: Theme) =>
yourGuestsActive yourGuestsActive
...@@ -59,22 +64,19 @@ export default function SponsorGuestButtons(props: SponsorGuestButtonsProps) { ...@@ -59,22 +64,19 @@ export default function SponsorGuestButtons(props: SponsorGuestButtonsProps) {
}} }}
/> />
{t('yourGuests')} {t('yourGuests')}
</IconButton> </StyledIconButton>
<IconButton <StyledIconButton
onClick={goToRegister} onClick={goToRegister}
sx={{ sx={{
display: 'flex', color: () => (registerNewGuestActive ? 'primary.main' : ''),
flexDirection: 'column',
alignItems: 'center',
typography: 'caption',
textDecorationLine: () => (registerNewGuestActive ? 'underline' : ''), textDecorationLine: () => (registerNewGuestActive ? 'underline' : ''),
}} }}
> >
<PersonAddIcon <PersonAddIcon
fontSize="large"
sx={{ sx={{
borderRadius: '2rem', fontSize: '80px',
borderRadius: '4rem',
borderStyle: 'solid', borderStyle: 'solid',
borderColor: (theme: Theme) => borderColor: (theme: Theme) =>
registerNewGuestActive registerNewGuestActive
...@@ -88,7 +90,7 @@ export default function SponsorGuestButtons(props: SponsorGuestButtonsProps) { ...@@ -88,7 +90,7 @@ export default function SponsorGuestButtons(props: SponsorGuestButtonsProps) {
}} }}
/> />
{t('registerNewGuest')} {t('registerNewGuest')}
</IconButton> </StyledIconButton>
</Box> </Box>
) )
} }
......
...@@ -14,7 +14,7 @@ export type GuestInviteInformation = { ...@@ -14,7 +14,7 @@ export type GuestInviteInformation = {
role_name_nb: string role_name_nb: string
role_start: string role_start: string
role_end: string role_end: string
comment?: string contact_person_unit?: string
feide_id?: string feide_id?: string
email?: string email?: string
......
...@@ -25,7 +25,6 @@ const testData = { ...@@ -25,7 +25,6 @@ const testData = {
start: '2021-08-10', start: '2021-08-10',
end: '2021-08-16', end: '2021-08-16',
contact_person_unit: 'Test contact person', contact_person_unit: 'Test contact person',
comments: 'Test comment',
}, },
meta: { meta: {
session_type: 'invite', session_type: 'invite',
...@@ -66,5 +65,7 @@ test('Field showing values correctly', async () => { ...@@ -66,5 +65,7 @@ test('Field showing values correctly', async () => {
await screen.findByDisplayValue( await screen.findByDisplayValue(
`${testData.role.start} - ${testData.role.end}` `${testData.role.start} - ${testData.role.end}`
) )
await screen.findByDisplayValue(testData.role.comments)
// For the default setup the contact person at unit field should be showing
await screen.findByDisplayValue(testData.role.contact_person_unit)
}) })
...@@ -21,12 +21,9 @@ import AuthenticationMethod from './authenticationMethod' ...@@ -21,12 +21,9 @@ import AuthenticationMethod from './authenticationMethod'
import GuestRegisterStep from './steps/register' import GuestRegisterStep from './steps/register'
import GuestConsentStep from './steps/consent' import GuestConsentStep from './steps/consent'
import GuestSuccessStep from './steps/success' import GuestSuccessStep from './steps/success'
import ServerErrorReport, {
enum SubmitState { ErrorReportProps,
NotSubmitted, } from '../../../components/errorReport'
Submitted,
SubmittedError,
}
enum Step { enum Step {
RegisterStep, RegisterStep,
...@@ -56,7 +53,7 @@ type InvitationData = { ...@@ -56,7 +53,7 @@ type InvitationData = {
role_name_en: string role_name_en: string
start: string start: string
end: string end: string
comments: string contact_person_unit: string
} }
meta: { meta: {
session_type: string session_type: string
...@@ -72,12 +69,6 @@ export default function GuestRegister() { ...@@ -72,12 +69,6 @@ export default function GuestRegister() {
const guestRegisterRef = useRef<GuestRegisterCallableMethods>(null) const guestRegisterRef = useRef<GuestRegisterCallableMethods>(null)
const guestConsentRef = useRef<GuestRegisterCallableMethods>(null) const guestConsentRef = useRef<GuestRegisterCallableMethods>(null)
// TODO On submit successful the user should be directed to some page telling
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [submitState, setSubmitState] = useState<SubmitState>(
SubmitState.NotSubmitted
)
const [activeStep, setActiveStep] = useState(0) const [activeStep, setActiveStep] = useState(0)
const [initialGuestData, setInitialGuestData] = const [initialGuestData, setInitialGuestData] =
...@@ -87,12 +78,39 @@ export default function GuestRegister() { ...@@ -87,12 +78,39 @@ export default function GuestRegister() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [guestConsentData, setGuestConsentData] = const [guestConsentData, setGuestConsentData] =
useState<GuestConsentData | null>(null) useState<GuestConsentData | null>(null)
const [fetchInvitationDataError, setFetchInvitationDataError] =
useState<ErrorReportProps | null>(null)
const [submitGuestDataError, setSubmitGuestDataError] =
useState<ErrorReportProps | null>(null)
const fetchInvitationData = async () => { const fetchInvitationData = async () => {
const response = await fetch('/api/ui/v1/invited/', fetchJsonOpts()) const response = await fetch('/api/ui/v1/invited/', fetchJsonOpts())
if (!response.ok) { if (!response.ok) {
try {
// Expect that the error will contain some JSON-data in the body
const errorData = await response.json()
setFetchInvitationDataError({
errorHeading: t('error.invitationDataFetchFailed'),
statusCode: response.status,
statusText: response.statusText,
errorBodyText: t(`error.codes.${errorData?.code}`),
})
} catch (e: unknown) {
console.error(e)
// Probably some unknown data in the body, create an error message
// using the rest of the information in the response
setFetchInvitationDataError({
errorHeading: t('error.invitationDataFetchFailed'),
statusCode: response.status,
statusText: response.statusText,
errorBodyText: undefined,
})
}
return return
} }
setFetchInvitationDataError(null)
const data: InvitationData = await response.json() const data: InvitationData = await response.json()
const authenticationMethod = const authenticationMethod =
...@@ -149,7 +167,7 @@ export default function GuestRegister() { ...@@ -149,7 +167,7 @@ export default function GuestRegister() {
role_name_nb: data.role.role_name_nb ?? '', role_name_nb: data.role.role_name_nb ?? '',
role_start: data.role.start ?? '', role_start: data.role.start ?? '',
role_end: data.role.end ?? '', role_end: data.role.end ?? '',
comment: data.role.comments ?? '', contact_person_unit: data.role.contact_person_unit ?? '',
authentication_method: authenticationMethod, authentication_method: authenticationMethod,
}) })
...@@ -192,6 +210,8 @@ export default function GuestRegister() { ...@@ -192,6 +210,8 @@ export default function GuestRegister() {
const handleBack = () => { const handleBack = () => {
if (activeStep === Step.ConsentStep) { if (activeStep === Step.ConsentStep) {
setActiveStep(Step.RegisterStep) setActiveStep(Step.RegisterStep)
// Clear error if any
setSubmitGuestDataError(null)
} }
} }
...@@ -257,15 +277,43 @@ export default function GuestRegister() { ...@@ -257,15 +277,43 @@ export default function GuestRegister() {
fetch('/api/ui/v1/invited/', submitJsonOpts('POST', payload)) fetch('/api/ui/v1/invited/', submitJsonOpts('POST', payload))
.then((response) => { .then((response) => {
if (response.ok) { if (response.ok) {
setSubmitState(SubmitState.Submitted)
setActiveStep(Step.SuccessStep) setActiveStep(Step.SuccessStep)
setSubmitGuestDataError(null)
} else { } else {
setSubmitState(SubmitState.SubmittedError) // Expect that the error will contain JSON-data in the body
console.error(`Server responded with status: ${response.status}`) // and that there will be a code field there
response
.json()
.then((errorData) => {
setSubmitGuestDataError({
errorHeading: t('error.guestRegistrationFailed'),
statusCode: response.status,
statusText: response.statusText,
errorBodyText: t(`error.codes.${errorData?.code}`),
})
})
.catch((error) => {
console.error(error)
// Probably some unknown data in the body, create an error message
// using the rest of the information in the response
setSubmitGuestDataError({
errorHeading: t('error.guestRegistrationFailed'),
statusCode: response.status,
statusText: response.statusText,
errorBodyText: undefined,
})
})
} }
}) })
.catch((error) => { .catch((error) => {
setSubmitState(SubmitState.SubmittedError) // Something went wrong before/during the backend was called
setSubmitGuestDataError({
errorHeading: t('error.guestRegistrationFailed'),
statusCode: undefined,
statusText: undefined,
errorBodyText: undefined,
})
console.error(error) console.error(error)
}) })
} }
...@@ -372,6 +420,24 @@ export default function GuestRegister() { ...@@ -372,6 +420,24 @@ export default function GuestRegister() {
</Button> </Button>
)} )}
</Box> </Box>
{fetchInvitationDataError !== null && (
<ServerErrorReport
errorHeading={fetchInvitationDataError?.errorHeading}
statusCode={fetchInvitationDataError?.statusCode}
statusText={fetchInvitationDataError?.statusText}
errorBodyText={fetchInvitationDataError?.errorBodyText}
/>
)}
{submitGuestDataError !== null && (
<ServerErrorReport
errorHeading={submitGuestDataError?.errorHeading}
statusCode={submitGuestDataError?.statusCode}
statusText={submitGuestDataError?.statusText}
errorBodyText={submitGuestDataError?.errorBodyText}
/>
)}
</Page> </Page>
) )
} }
...@@ -11,6 +11,7 @@ import { SubmitHandler, Controller, useForm } from 'react-hook-form' ...@@ -11,6 +11,7 @@ import { SubmitHandler, Controller, useForm } from 'react-hook-form'
import React, { import React, {
forwardRef, forwardRef,
Ref, Ref,
useContext,
useEffect, useEffect,
useImperativeHandle, useImperativeHandle,
useState, useState,
...@@ -30,9 +31,11 @@ import { GuestInviteInformation } from '../guestDataForm' ...@@ -30,9 +31,11 @@ import { GuestInviteInformation } from '../guestDataForm'
import { GuestRegisterData } from '../enteredGuestData' import { GuestRegisterData } from '../enteredGuestData'
import { GuestRegisterCallableMethods } from '../guestRegisterCallableMethods' import { GuestRegisterCallableMethods } from '../guestRegisterCallableMethods'
import AuthenticationMethod from '../authenticationMethod' import AuthenticationMethod from '../authenticationMethod'
import { FeatureContext } from '../../../../contexts'
interface GuestRegisterProperties { interface GuestRegisterProperties {
nextHandler(registerData: GuestRegisterData): void nextHandler(registerData: GuestRegisterData): void
initialGuestData: GuestInviteInformation initialGuestData: GuestInviteInformation
registerData: GuestRegisterData | null registerData: GuestRegisterData | null
} }
...@@ -57,6 +60,7 @@ const GuestRegisterStep = forwardRef( ...@@ -57,6 +60,7 @@ const GuestRegisterStep = forwardRef(
string | undefined string | undefined
>(undefined) >(undefined)
const [idErrorState, setIdErrorState] = useState<string>('') const [idErrorState, setIdErrorState] = useState<string>('')
const { displayContactAtUnitGuestInput } = useContext(FeatureContext)
console.log('register step registerData', registerData) console.log('register step registerData', registerData)
...@@ -528,14 +532,14 @@ const GuestRegisterStep = forwardRef( ...@@ -528,14 +532,14 @@ const GuestRegisterStep = forwardRef(
disabled disabled
/> />
<TextField {displayContactAtUnitGuestInput && (
id="comment" <TextField
label={t('input.comment')} id="contactPersonUnit"
multiline label={t('input.contactPersonUnit')}
rows={5} value={initialGuestData.contact_person_unit}
value={initialGuestData.comment} disabled
disabled />
/> )}
</Stack> </Stack>
</form> </form>
</Box> </Box>
......
...@@ -5,24 +5,24 @@ import { styled } from '@mui/system' ...@@ -5,24 +5,24 @@ import { styled } from '@mui/system'
import { CssBaseline } from '@mui/material' import { CssBaseline } from '@mui/material'
import fetchIntercept from 'fetch-intercept' import fetchIntercept from 'fetch-intercept'
import { registerLocale } from 'i18n-iso-countries'
import i18n_iso_countries_en from 'i18n-iso-countries/langs/en.json'
import i18n_iso_countries_nb from 'i18n-iso-countries/langs/nb.json'
import i18n_iso_countries_nn from 'i18n-iso-countries/langs/nn.json'
import { useUserContext } from 'contexts' import { useUserContext } from 'contexts'
import { getCookie, deleteCookie } from 'utils' import { getCookie, deleteCookie } from 'utils'
import GuestRegister from 'routes/guest/register'
import Sponsor from 'routes/sponsor' import Sponsor from 'routes/sponsor'
import Register from 'routes/sponsor/register' import Register from 'routes/sponsor/register'
import FrontPage from 'routes/frontpage' import FrontPage from 'routes/frontpage'
import Invite from 'routes/invite' import Invite from 'routes/invite'
import InviteLink from 'routes/invitelink' import LogoutInviteSession from 'routes/invite/logout'
import LogoutInviteSession from 'routes/invitelink/logout'
import Footer from 'routes/components/footer' import Footer from 'routes/components/footer'
import Header from 'routes/components/header' import Header from 'routes/components/header'
import NotFound from 'routes/components/notFound' import NotFound from 'routes/components/notFound'
import ProtectedRoute from 'components/protectedRoute' import ProtectedRoute from 'components/protectedRoute'
import { registerLocale } from 'i18n-iso-countries'
import i18n_iso_countries_en from 'i18n-iso-countries/langs/en.json'
import i18n_iso_countries_nb from 'i18n-iso-countries/langs/nb.json'
import i18n_iso_countries_nn from 'i18n-iso-countries/langs/nn.json'
import GuestRegister from './guest/register'
const AppWrapper = styled('div')({ const AppWrapper = styled('div')({
display: 'flex', display: 'flex',
...@@ -77,7 +77,6 @@ export default function App() { ...@@ -77,7 +77,6 @@ export default function App() {
<ProtectedRoute path="/register"> <ProtectedRoute path="/register">
<Register /> <Register />
</ProtectedRoute> </ProtectedRoute>
<Route path="/invitelink/" component={InviteLink} />
<Route path="/invite/logout" component={LogoutInviteSession} /> <Route path="/invite/logout" component={LogoutInviteSession} />
<Route path="/invite/" component={Invite} /> <Route path="/invite/" component={Invite} />
<Route path="/guestregister" component={GuestRegister} /> <Route path="/guestregister" component={GuestRegister} />
......
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Page from 'components/page' import Page from 'components/page'
import Loading from 'components/loading'
import { styled } from '@mui/material/styles' import { styled } from '@mui/material/styles'
import { useUserContext } from 'contexts'
import { HrefButton } from 'components/button' import { HrefButton } from 'components/button'
import { HrefLineButton } from 'components/button/linebutton' import { HrefLineButton } from 'components/button/linebutton'
import { submitJsonOpts } from 'utils'
const FlexDiv = styled('div')(() => ({ const FlexDiv = styled('div')(() => ({
display: 'flex', display: 'flex',
gap: '0.5rem', gap: '0.5rem',
})) }))
function Invite() { function ChooseRegistrationMethod() {
const { t } = useTranslation(['invite']) const { t } = useTranslation(['invite'])
return ( return (
<Page> <Page>
<h1>{t('header')}</h1> <h1>{t('header')}</h1>
<p> <p>{t('description')}</p>
{t('description')}
</p>
<FlexDiv> <FlexDiv>
<HrefButton to="/oidc/authenticate/">{t('login')}</HrefButton> <HrefButton to="/oidc/authenticate/">{t('login')}</HrefButton>
<HrefLineButton to="/guestregister/">{t('manual')}</HrefLineButton> <HrefLineButton to="/guestregister/">{t('manual')}</HrefLineButton>
...@@ -28,4 +30,125 @@ function Invite() { ...@@ -28,4 +30,125 @@ function Invite() {
) )
} }
interface ShowFeedbackProps {
title: string
description: string
}
function ShowFeedback(props: ShowFeedbackProps) {
const { title, description } = props
return (
<Page>
<h1>{title}</h1>
<p>{description}</p>
</Page>
)
}
function Invite() {
const { t } = useTranslation(['invite'])
const { user, fetchUserInfo } = useUserContext()
const [inviteToken, setInviteToken] = useState('')
const [tokenChecked, setTokenChecked] = useState(false)
const [isCheckingToken, setIsCheckingToken] = useState(false)
const [tokenOk, setTokenOk] = useState(false)
const [checkError, setCheckError] = useState('')
async function checkToken(token: string) {
try {
const response = await fetch(
'/api/ui/v1/invitecheck/',
submitJsonOpts('POST', { invite_token: token })
)
if (response.status === 200) {
setTokenOk(true)
return
}
const data = await response.json()
if ('code' in data) {
setCheckError(data.code)
} else {
setCheckError('unknown')
}
} catch (error) {
console.error(error)
setCheckError('unknown')
} finally {
setTokenChecked(true)
setIsCheckingToken(false)
}
}
console.log({ inviteToken, isCheckingToken, tokenChecked, tokenOk, user })
useEffect(() => {
setIsCheckingToken(true)
// This may seem unecessary, but race conditions have been
// observed where the userinfo endpoint is called too fast
// and no invite_id is found in the server-side session
setTimeout(fetchUserInfo, 100)
}, [setTokenOk])
if (user.auth) {
return <ChooseRegistrationMethod />
}
if (isCheckingToken || (tokenOk && !user.auth)) {
return <Loading />
}
if (inviteToken !== '' && !tokenChecked) {
checkToken(inviteToken)
return <Loading />
}
if (checkError !== '') {
if (
checkError === 'missing_invite_token' ||
checkError === 'invalid_invite_token'
) {
// Missing or invalid token
return (
<ShowFeedback
title={t('common:error.error')}
description={t('invite:errors.invalidToken')}
/>
)
}
if (checkError === 'expired_invite_token') {
// Expired token
return (
<ShowFeedback
title={t('common:error.error')}
description={t('invite:errors.expiredToken')}
/>
)
}
// Unknown error
return (
<ShowFeedback
title={t('common:error.error')}
description={t('common:error.unknown')}
/>
)
}
const providedToken = window.location.hash.slice(1).trim()
if (!inviteToken && providedToken) {
setInviteToken(providedToken)
return <Loading />
}
// We'll end up here if no token was provided in the URL
return (
<ShowFeedback
title={t('common:error.error')}
description={t('invite:errors.invalidToken')}
/>
)
}
export default Invite export default Invite
import { useEffect } from 'react'
import { Redirect } from 'react-router-dom'
import { submitJsonOpts, setCookie } from 'utils'
function InviteLink() {
// Fetch backend endpoint to preserve invite_id in backend session then redirect
// to generic invite page with info about feide login or manual with passport.
const id = window.location.hash.slice(1)
useEffect(() => {
fetch('/api/ui/v1/invitecheck/', submitJsonOpts('POST', { uuid: id }))
}, [])
setCookie('redirect', '/guestregister')
return <Redirect to="/invite" />
}
export default InviteLink