From 83ebc12dafcce36ab48a6e5154f840e4dad790f6 Mon Sep 17 00:00:00 2001 From: Nicholas Jaunsen <3789764+skykanin@users.noreply.github.com> Date: Mon, 17 Jun 2024 14:54:21 +0200 Subject: [PATCH 1/5] Fixup from feedback --- .env | 1 - src/pages/CreateTeamForm/CreateTeamForm.tsx | 573 ++++++++++-------- .../CreateTeamForm/FormSubmissionResult.tsx | 35 +- .../CreateTeamForm/createTeamForm.module.scss | 12 +- src/utils/utils.ts | 25 + src/vite-env.d.ts | 1 - 6 files changed, 372 insertions(+), 275 deletions(-) diff --git a/.env b/.env index e38635dd..e249c5fe 100644 --- a/.env +++ b/.env @@ -1,4 +1,3 @@ DAPLA_TEAM_API_URL= DAPLA_CTRL_ADMIN_GROUPS= DAPLA_CTRL_DOCUMENTATION_URL= -DAPLA_CTRL_DAPLA_START_URL= \ No newline at end of file diff --git a/src/pages/CreateTeamForm/CreateTeamForm.tsx b/src/pages/CreateTeamForm/CreateTeamForm.tsx index 3bbb0107..e6f33451 100644 --- a/src/pages/CreateTeamForm/CreateTeamForm.tsx +++ b/src/pages/CreateTeamForm/CreateTeamForm.tsx @@ -1,310 +1,365 @@ import styles from './createTeamForm.module.scss' import { - Button, - Card, - CheckboxGroup, - Dialog, - Dropdown, - Glossary, - Input, - Text, - TextArea, + Button, + Card, + CheckboxGroup, + Dialog, + Dropdown, + Glossary, + Input, + Text, + TextArea, } from '@statisticsnorway/ssb-component-library' import * as C from '@statisticsnorway/ssb-component-library' import { Skeleton } from '@mui/material' import { useEffect, useState, useMemo } from 'react' import { Array as A, Console, Effect, Option as O, pipe } from 'effect' -import FormSubmissionResult from './FormSubmissionResult.tsx' +import FormSubmissionResult, { FormSubmissionResultProps } from './FormSubmissionResult.tsx' import PageLayout from '../../components/PageLayout/PageLayout' import * as Klass from '../../services/klass' import { AutonomyLevel, CreateTeamRequest, createTeam } from '../../services/createTeam' import { User } from '../../@types/user' +import * as Utils from '../../utils/utils.ts' interface DisplayAutonomyLevel { - id: AutonomyLevel - title: string + id: AutonomyLevel + title: string } interface DisplaySSBSection { - id: number - title: string + id: number + title: string } interface FormError { - id: number - field: string - errorMessage: string -} - -interface FormSubmissionResult { - success: boolean - message: string + id: number + field: string + errorMessage: string } const CreateTeamForm = () => { - const uniformNameLengthLimit = 17 - // TODO: These should be fetched from the dapla-team-api instead of being hardcoded - const teamAutonomyLevels: DisplayAutonomyLevel[] = [ - { id: 'managed', title: 'Managed' }, - { id: 'semi-managed', title: 'Semi-Managed' }, - { id: 'autonomous', title: 'Autonomous' }, - ] - const teamNameGlossaryExplanation = ` - Teamets navn (for eksempel: "Pålegg Brunost"). Dette kan endres senere. + const uniformNameLengthLimit = 17 + // TODO: These should be fetched from the dapla-team-api instead of being hardcoded + const teamAutonomyLevels: DisplayAutonomyLevel[] = [ + { id: 'managed', title: 'Managed' }, + { id: 'semi-managed', title: 'Semi-Managed' }, + { id: 'autonomous', title: 'Self-Managed' }, + ] + const teamNameGlossaryExplanation = ` + Teamets navn i et lesevennlig format. Navnet bør bestå av et hoveddomenet og et subdomenet, f.eks. "Skatt Næring" og det er tillatt med mellomrom og norske tegn (Æ, Ø, Å). + ` + const uniformNameGlossaryExplanation = ` + Teamets navn i et maskinvennlig format som bl.a. benyttes i filstier til lagringsbøtter. Det er ikke tillatt med mellomrom og norske tegn (Æ, Ø, Å). ` - const uniformNameGlossaryExplanation = - 'Det tekniske teamnavnet som brukes internt i IT-systemene. Her er det flere restriksjoner på hvilke tegn som kan brukes.' - const sectionGlossaryExplanation = 'SSB seksjonen som teamet tilhører.' + const sectionGlossaryExplanation = 'Ansvarlig seksjon for teamet.' - const displayNameLabel = 'Visningsnavn' - const [displayName, setDisplayName] = useState('') + const autonomyLevelGlossaryExplanation = ` + Nivå av frihet et team har til å definere sin egen infrastruktur. Statistikkproduserende team er vanligvis i kategorien "Managed", mens IT-team er "Self Managed". Les mer her. + ` - const uniformNameLabel = 'Teknisk teamnavn' - const [uniformName, setUniformName] = useState('') - const [overrideUniformName, setOverrideUniformName] = useState(false) - const [uniformNameErrorMsg, setUniformNameErrorMsg] = useState('') + const additionalInformationGlossaryExplanation = ` + Informasjon som kan være nyttig for den som oppretter teamet. F.eks. kan man liste opp hvem som skal legges i tilgangsgruppene data-admins og developers her. +` - const sectionLabel = 'Eierseksjon' - const [sections, setSections] = useState([]) - const [selectedSection, setSelectedSection] = useState>(O.none()) + const displayNameLabel = 'Visningsnavn' + const [displayName, setDisplayName] = useState('') - const [userName, setUserName] = useState>(O.none) + const uniformNameLabel = 'Teknisk teamnavn' + const [uniformName, setUniformName] = useState('') + const [overrideUniformName, setOverrideUniformName] = useState(false) + const [uniformNameErrorMsg, setUniformNameErrorMsg] = useState('') - const [selectedAutonomyLevel, setSelectedAutonomyLevel] = useState(teamAutonomyLevels[0]) + const sectionLabel = 'Eierseksjon' + const [sections, setSections] = useState([]) + const [selectedSection, setSelectedSection] = useState>(O.none()) - const [additionalInformation, setAdditionalInformation] = useState('') + const [user, setUser] = useState>(O.none) - const [submitButtonClicked, setSubmitButtonClicked] = useState(false) + const [selectedAutonomyLevel, setSelectedAutonomyLevel] = useState(teamAutonomyLevels[0]) - const missingFieldErrorMessage = 'mangler' - const validationErrorMessage = 'har en valideringsfeil' + const [additionalInformation, setAdditionalInformation] = useState('') - const formErrors: FormError[] = useMemo( - () => - pipe( - [ - { guard: displayName === '', field: displayNameLabel, errorMessage: missingFieldErrorMessage }, - { guard: uniformName === '', field: uniformNameLabel, errorMessage: missingFieldErrorMessage }, - { guard: '' !== uniformNameErrorMsg, field: uniformNameLabel, errorMessage: validationErrorMessage }, - { guard: O.isNone(selectedSection), field: sectionLabel, errorMessage: missingFieldErrorMessage }, - ], - (errors) => A.zipWith(A.range(0, errors.length), errors, (idx, error) => ({ id: idx, ...error })), - A.flatMap((mapping) => - mapping.guard ? [{ id: mapping.id, field: mapping.field, errorMessage: mapping.errorMessage }] : [] - ) - ), - [displayName, uniformName, selectedSection, uniformNameErrorMsg] - ) + const [submitButtonClicked, setSubmitButtonClicked] = useState(false) + + const missingFieldErrorMessage = 'mangler' + const validationErrorMessage = 'har en valideringsfeil' - const [formSubmissionResult, setFormSubmissionResult] = useState>(O.none()) + const resetForm = () => { + setDisplayName('') + setUniformName('') + setOverrideUniformName(false) + setUniformNameErrorMsg('') + setSelectedSection(O.none()) - useEffect(() => { - if (A.isNonEmptyArray(formErrors)) { - setFormSubmissionResult(O.none()) + setSelectedAutonomyLevel(teamAutonomyLevels[0]) + setAdditionalInformation('') + setSubmitButtonClicked(false) + } + + const formErrors: FormError[] = useMemo( + () => + pipe( + [ + { guard: displayName === '', field: displayNameLabel, errorMessage: missingFieldErrorMessage }, + { guard: uniformName === '', field: uniformNameLabel, errorMessage: missingFieldErrorMessage }, + { + guard: '' !== uniformNameErrorMsg, + field: uniformNameLabel, + errorMessage: validationErrorMessage, + }, + { guard: O.isNone(selectedSection), field: sectionLabel, errorMessage: missingFieldErrorMessage }, + ], + (errors) => A.zipWith(A.range(0, errors.length), errors, (idx, error) => ({ id: idx, ...error })), + A.flatMap((mapping) => + mapping.guard ? [{ id: mapping.id, field: mapping.field, errorMessage: mapping.errorMessage }] : [] + ) + ), + [displayName, uniformName, selectedSection, uniformNameErrorMsg] + ) + + const [formSubmissionResult, setFormSubmissionResult] = useState({ + loading: false, + formSubmissionResult: O.none(), + }) + + useEffect(() => { + Effect.gen(function* (_) { + const sections: DisplaySSBSection[] = yield* _( + Klass.fetchSSBSectionInformation().pipe( + Effect.map((sections: Klass.SSBSections) => + sections.map((section) => ({ id: section.code, title: `${section.code} - ${section.name}` })) + ) + ) + ) + + const storedUserProfile = localStorage.getItem('userProfile') + const maybeUserProfile: O.Option = O.fromNullable(storedUserProfile).pipe(O.map(JSON.parse)) + + const userProfile: User = yield* _( + Effect.try({ + try: () => O.getOrThrow(maybeUserProfile), + catch: (error) => new Error(`Element not present: ${error}`), + }) + ) + // Setting the selectedSection won't be visible beause of a ssb-component bug: https://github.com/statisticsnorway/ssb-component-library/pull/1111 + //const sectionCode = yield* getUserSectionCode(userProfile.principal_name) + setUser(O.some(userProfile)) + setSections(sections) + //setSelectedSection(A.findFirst(sections, (s) => s.id === sectionCode)) + }).pipe(Effect.runPromise) + }, []) + + const handleSubmit = (event: Event) => { + event.preventDefault() + // Only submit the form if no form errors are present + if (A.isEmptyArray(formErrors)) { + const userPrincipalName = O.getOrThrow(user).principal_name + const req: CreateTeamRequest = { + teamDisplayName: displayName, + uniformTeamName: uniformName, + sectionCode: O.getOrThrow(selectedSection).id.toString(), + additionalInformation: `This PR was created through Dapla Ctrl. Additional information from user ${userPrincipalName}:\n ${additionalInformation}`, + autonomyLevel: selectedAutonomyLevel.id, + features: [], + } + + setFormSubmissionResult({ loading: true, formSubmissionResult: O.none() }) + + Effect.gen(function* () { + const clientResponse = yield* createTeam(req) + yield* Console.log('ClientResponse', clientResponse) + return O.some( + clientResponse.status !== 200 + ? { + success: false, + message: `Det oppstod en feil ved opprettelse av team. Statuskode: ${clientResponse.status}`, + } + : { success: true, message: 'Opprettelse av team ble registert.' } + ) + }) + .pipe( + Effect.catchTags({ + ResponseError: (error) => Effect.succeed(O.some({ success: false, message: error.message })), + RequestError: (error) => Effect.succeed(O.some({ success: false, message: error.message })), + BodyError: (error) => + Effect.succeed( + O.some({ success: false, message: `Failed to parse body: ${error.reason._tag}` }) + ), + }), + Effect.runPromise + ) + .then((res: O.Option<{ success: boolean; message: string }>) => { + if ( + Utils.option( + res, + () => false, + (r) => r.success + ) + ) { + resetForm() + } + setFormSubmissionResult({ loading: false, formSubmissionResult: res }) + }) + } + } + + const toggleUniformNameInput = (checkboxes: string[]): void => { + const isOverridden = checkboxes.includes('override') + setOverrideUniformName(isOverridden) + // If the user unselects the override option generate the uniform name + // based on the display name again and clear all errors. + if (!isOverridden) { + setUniformNameErrorMsg('') + setUniformName(generateUniformName(displayName)) + } } - }, [formErrors]) - - useEffect(() => { - Effect.gen(function* (_) { - const sections: DisplaySSBSection[] = yield* _( - Klass.fetchSSBSectionInformation().pipe( - Effect.map((sections: Klass.SSBSections) => - sections.map((section) => ({ id: section.code, title: `${section.code} - ${section.name}` })) - ) - ) - ) - const storedUserProfile = localStorage.getItem('userProfile') - const maybeUserProfile: O.Option = O.fromNullable(storedUserProfile).pipe(O.map(JSON.parse)) + const generateUniformName = (displayName: string): string => + displayName + .toLowerCase() + .replaceAll('team ', '') + .replaceAll('æ', 'ae') + .replaceAll('ø', 'oe') + .replaceAll('å', 'aa') + .slice(0, uniformNameLengthLimit) + .trim() + .replaceAll(' ', '-') + + const validateUniformName = (name: string): O.Option => { + const validUniformName = generateUniformName(name) + if (name.length > uniformNameLengthLimit) { + return O.some(`Teknisk navn kan ikke være lengre enn ${uniformNameLengthLimit} tegn`) + } else if (validUniformName !== name) { + return O.some('Teknisk navn er ugyldig') + } else { + return O.none() + } + } - const userProfile: User = yield* _( - Effect.try({ - try: () => O.getOrThrow(maybeUserProfile), - catch: (error) => new Error(`Element not present: ${error}`), + const handleUniformNameInput = (name: string): void => { + O.match(validateUniformName(name), { + onNone: () => { + setUniformNameErrorMsg('') + setUniformName(name) + }, + onSome: (errorMsg: string) => setUniformNameErrorMsg(errorMsg), }) - ) - // Setting the selectedSection won't be visible beause of a ssb-component bug: https://github.com/statisticsnorway/ssb-component-library/pull/1111 - //const sectionCode = yield* getUserSectionCode(userProfile.principal_name) - setUserName(O.some(userProfile.display_name)) - setSections(sections) - //setSelectedSection(A.findFirst(sections, (s) => s.id === sectionCode)) - }).pipe(Effect.runPromise) - }, []) - - const handleSubmit = (event: React.FormEvent) => { - event.preventDefault() - // Only submit the form if no form errors are present - if (A.isEmptyArray(formErrors)) { - const req: CreateTeamRequest = { - teamDisplayName: displayName, - uniformTeamName: uniformName, - sectionCode: O.getOrThrow(selectedSection).id.toString(), - additionalInformation: additionalInformation, - autonomyLevel: selectedAutonomyLevel.id, - features: [], - } - - Effect.gen(function* () { - const clientResponse = yield* createTeam(req) - yield* Console.log('ClientResponse', clientResponse) - return O.some( - clientResponse.status !== 200 - ? { - success: false, - message: `Det oppstod en feil ved opprettelse av team. Statuskode: ${clientResponse.status}`, - } - : { success: true, message: 'Opprettelse av team ble registert.' } + } + + const renderUniformNameField = () => { + const label = ( + + {uniformNameLabel} + ) - }) - .pipe( - Effect.catchTags({ - ResponseError: (error) => Effect.succeed(O.some({ success: false, message: error.message })), - RequestError: (error) => Effect.succeed(O.some({ success: false, message: error.message })), - BodyError: (error) => - Effect.succeed(O.some({ success: false, message: `Failed to parse body: ${error.reason._tag}` })), - }), - Effect.runPromise + return !overrideUniformName ? ( +
+ {label} + +
+ ) : ( + ) - .then(setFormSubmissionResult) - } - } - - const toggleUniformNameInput = (checkboxes: string[]): void => { - const isOverridden = checkboxes.includes('override') - setOverrideUniformName(isOverridden) - // If the user unselects the override option generate the uniform name - // based on the display name again and clear all errors. - if (!isOverridden) { - setUniformNameErrorMsg('') - setUniformName(generateUniformName(displayName)) } - } - - const generateUniformName = (displayName: string): string => - displayName - .toLowerCase() - .replaceAll('team ', '') - .replaceAll('æ', 'ae') - .replaceAll('ø', 'oe') - .replaceAll('å', 'aa') - .slice(0, uniformNameLengthLimit) - .trim() - .replaceAll(' ', '-') - - const validateUniformName = (name: string): O.Option => { - const validUniformName = generateUniformName(name) - if (name.length > uniformNameLengthLimit) { - return O.some(`Teknisk navn kan ikke være lengre enn ${uniformNameLengthLimit} tegn`) - } else if (validUniformName !== name) { - return O.some('Teknisk navn er ugyldig') - } else { - return O.none() - } - } - const handleUniformNameInput = (name: string): void => { - O.match(validateUniformName(name), { - onNone: () => { - setUniformNameErrorMsg('') - setUniformName(name) - }, - onSome: (errorMsg: string) => setUniformNameErrorMsg(errorMsg), - }) - } + const renderTeamOwnerCard = (isLoading: boolean) => + isLoading ? ( + + ) : ( + + + {`${Utils.option( + user, + () => 'loading', + (u: User) => u.display_name + )} blir teamansvarlig for dette teamet. Hvis noen andre skal være ansvarlig kan det oppgis i feltet `} + Tilleggsinformasjon. + + + ) - const renderUniformNameField = () => { - const label = ( - - {uniformNameLabel} - - ) - return !overrideUniformName ? ( -
- {label} - -
- ) : ( - - ) - } - - const renderTeamOwnerCard = (isLoading: boolean) => - isLoading ? ( - - ) : ( - - {`${O.getOrElse(userName, () => 'loading')} blir teamansvarlig for dette teamet. Hvis noen andre skal være ansvarlig kan det oppgis nedenfor.`} - + const renderContent = () => ( +
) => e.preventDefault()}> + {displayNameLabel}} + id='display_name' + type='text' + handleChange={(value: string) => { + const uniformName = generateUniformName(value) + setUniformName(uniformName) + setDisplayName(value) + }} + value={displayName} + /> + toggleUniformNameInput(checkboxes)} + orientation='column' + items={[{ label: 'Overstyr teknisk navn?', value: 'override' }]} + /> + {renderUniformNameField()} + {/* NOTE: Changes to `selectedSection`, except from the `onSelect` function, doesn't re-render the component because the component is bugged. */} + {sectionLabel}} + searchable + items={sections} + selectedItem={O.getOrElse(selectedSection, () => undefined)} + onSelect={(section: DisplaySSBSection) => setSelectedSection(O.some(section))} + /> + {'Autonomitetsnivå'}} + selectedItem={selectedAutonomyLevel} + items={teamAutonomyLevels} + onSelect={(autonomyLevel: DisplayAutonomyLevel) => setSelectedAutonomyLevel(autonomyLevel)} + /> +