From 39628a56013298170dd6022206a4b65a48739cc8 Mon Sep 17 00:00:00 2001 From: Jonas Brunvoll Larsson <59939294+jonasbrunvoll@users.noreply.github.com> Date: Wed, 25 Sep 2024 20:21:56 +0200 Subject: [PATCH] chore: rewrite ticket control to use one state machine for all forms (#353) * Rewrite to use only one state machine for all ticket control forms * Add temporary success message. * Specifies which inputs that should be validated when submitting a form from tricketControll * Update validation for travelGuarantee. --- src/page-modules/contact/index.ts | 11 +- src/page-modules/contact/layouts/index.ts | 5 - .../layouts/ticket-control-page-layout.tsx | 63 --- .../means-of-transport-form-machine.ts | 5 +- .../complaint/complaintFormMachine.ts | 193 --------- .../contact/ticket-control/events.ts | 1 + .../feedback/feedbackformMachine.ts | 169 -------- .../index.tsx => forms/feeComplaintForm.tsx} | 39 +- .../index.tsx => forms/feedbackForm.tsx} | 38 +- .../index.tsx => forms/postponePayment.tsx} | 44 +- .../contact/ticket-control/index.ts | 1 - .../contact/ticket-control/index.tsx | 76 ++++ .../postponePaymentFormMachine.ts | 118 ------ .../ticket-control-form-machine.ts | 395 ++++++++++++++++++ .../travel-guarantee/form-selector.tsx | 2 +- .../travelGuaranteeFormMachine.ts | 5 +- .../validation/commonInputValidator.ts | 8 +- .../travelGuaranteeInputValidator.ts | 6 +- .../ticket-control/complaint/index.tsx | 27 -- .../contact/ticket-control/feedback/index.tsx | 27 -- src/pages/contact/ticket-control/index.tsx | 4 +- .../ticket-control/postpone-payment/index.tsx | 28 -- src/translations/pages/contact.ts | 28 +- 23 files changed, 540 insertions(+), 753 deletions(-) delete mode 100644 src/page-modules/contact/layouts/ticket-control-page-layout.tsx delete mode 100644 src/page-modules/contact/ticket-control/complaint/complaintFormMachine.ts delete mode 100644 src/page-modules/contact/ticket-control/feedback/feedbackformMachine.ts rename src/page-modules/contact/ticket-control/{complaint/index.tsx => forms/feeComplaintForm.tsx} (93%) rename src/page-modules/contact/ticket-control/{feedback/index.tsx => forms/feedbackForm.tsx} (89%) rename src/page-modules/contact/ticket-control/{postpone-paymnet/index.tsx => forms/postponePayment.tsx} (74%) delete mode 100644 src/page-modules/contact/ticket-control/index.ts create mode 100644 src/page-modules/contact/ticket-control/index.tsx delete mode 100644 src/page-modules/contact/ticket-control/postpone-paymnet/postponePaymentFormMachine.ts create mode 100644 src/page-modules/contact/ticket-control/ticket-control-form-machine.ts delete mode 100644 src/pages/contact/ticket-control/complaint/index.tsx delete mode 100644 src/pages/contact/ticket-control/feedback/index.tsx delete mode 100644 src/pages/contact/ticket-control/postpone-payment/index.tsx diff --git a/src/page-modules/contact/index.ts b/src/page-modules/contact/index.ts index e28c6868..de84e695 100644 --- a/src/page-modules/contact/index.ts +++ b/src/page-modules/contact/index.ts @@ -1,14 +1,7 @@ -export { - ContactPageLayout, - type ContactPageLayoutProps, - TicketControlPageLayout, - type TicketControlPageLayoutProps, -} from './layouts'; +export { ContactPageLayout, type ContactPageLayoutProps } from './layouts'; -export { PostponePaymentForm } from './ticket-control'; export { RefundForm } from './travel-guarantee'; -export { FeeComplaintForm } from './ticket-control/complaint'; export { type Line } from './server/journey-planner/validators'; export { shouldShowContactPage } from './utils'; -export { FeedbackForm } from './ticket-control/feedback'; export { MeansOfTransportContent } from './means-of-transport'; +export { default as TicketControlPageContent } from './ticket-control'; diff --git a/src/page-modules/contact/layouts/index.ts b/src/page-modules/contact/layouts/index.ts index 5867b8c5..f15aff2c 100644 --- a/src/page-modules/contact/layouts/index.ts +++ b/src/page-modules/contact/layouts/index.ts @@ -2,8 +2,3 @@ export { default as ContactPageLayout, type ContactPageLayoutProps, } from './contact-page-layout'; - -export { - default as TicketControlPageLayout, - type TicketControlPageLayoutProps, -} from './ticket-control-page-layout'; diff --git a/src/page-modules/contact/layouts/ticket-control-page-layout.tsx b/src/page-modules/contact/layouts/ticket-control-page-layout.tsx deleted file mode 100644 index 401d0790..00000000 --- a/src/page-modules/contact/layouts/ticket-control-page-layout.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { PropsWithChildren } from 'react'; -import { SectionCard } from '../components/section-card'; -import { useRouter } from 'next/router'; -import { PageText, useTranslation } from '@atb/translations'; -import { Checkbox } from '../components/input/checkbox'; -import { Input } from '../components/input'; -import { RadioInput } from '../components/input/radio'; - -export type TicketControlPageLayoutProps = PropsWithChildren<{ - title: string; -}>; - -function TicketControlPageLayout({ children }: TicketControlPageLayoutProps) { - const { t } = useTranslation(); - const router = useRouter(); - - return ( -
- - - - - {children} -
- ); -} - -export default TicketControlPageLayout; diff --git a/src/page-modules/contact/means-of-transport/means-of-transport-form-machine.ts b/src/page-modules/contact/means-of-transport/means-of-transport-form-machine.ts index d9710ee4..af9a5fc1 100644 --- a/src/page-modules/contact/means-of-transport/means-of-transport-form-machine.ts +++ b/src/page-modules/contact/means-of-transport/means-of-transport-form-machine.ts @@ -60,7 +60,10 @@ export const meansOfTransportFormMachine = setup({ events: meansOfTransportFormEvents, }, guards: { - validateInputs: ({ context }) => commonInputValidator(context), + validateInputs: ({ context }) => { + context.errorMessages = commonInputValidator(context); + return Object.keys(context.errorMessages).length > 0 ? false : true; + }, }, actions: { onInputChange: assign(({ context, event }) => { diff --git a/src/page-modules/contact/ticket-control/complaint/complaintFormMachine.ts b/src/page-modules/contact/ticket-control/complaint/complaintFormMachine.ts deleted file mode 100644 index 900893e5..00000000 --- a/src/page-modules/contact/ticket-control/complaint/complaintFormMachine.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { assign, fromPromise, setup } from 'xstate'; -import { commonInputValidator, InputErrorMessages } from '../../validation'; -import { convertFilesToBase64 } from '../../utils'; -import { ticketControlFormEvents } from '../events'; - -type APIParams = { - feeNumber: string; - appPhoneNumber: string | undefined; - customerNumber: string | undefined; - travelCardNumber: string | undefined; - feedback: string; - firstName: string; - lastName: string; - address: string; - postalCode: string; - city: string; - email: string; - phoneNumber: string; - bankAccountNumber: string; - IBAN: string; - SWIFT: string; - attachments?: File[]; -}; - -type ContextProps = { - agreesFirstAgreement: boolean; - agreesSecondAgreement: boolean; - isAppTicketStorageMode: boolean; - hasInternationalBankAccount: boolean; - errorMessages: InputErrorMessages; -} & APIParams; - -export const formMachine = setup({ - types: { - context: {} as ContextProps, - events: ticketControlFormEvents, - }, - guards: { - validateInputs: ({ context }) => commonInputValidator(context), - }, - actions: { - cleanErrorMessages: assign({ - errorMessages: () => ({}), - }), - - onInputChange: assign(({ context, event }) => { - if (event.type === 'ON_INPUT_CHANGE') { - const { inputName, value } = event; - // Remove errorMessages if any - context.errorMessages[inputName] = []; - return { - ...context, - [inputName]: value, - }; - } - return context; - }), - }, - actors: { - callAPI: fromPromise( - async ({ - input: { - feeNumber, - appPhoneNumber, - customerNumber, - travelCardNumber, - feedback, - firstName, - lastName, - address, - postalCode, - city, - email, - phoneNumber, - bankAccountNumber, - IBAN, - SWIFT, - attachments, - }, - }: { - input: APIParams; - }) => { - const base64EncodedAttachments = await convertFilesToBase64( - attachments || [], - ); - return await fetch('/api/contact/ticket-control', { - method: 'POST', - body: JSON.stringify({ - feeNumber: feeNumber, - appPhoneNumber: appPhoneNumber, - customerNumber: customerNumber, - travelCardNumber: travelCardNumber, - additionalInfo: feedback, - firstName: firstName, - lastName: lastName, - address: address, - postalCode: postalCode, - city: city, - email: email, - phoneNumber: phoneNumber, - bankAccountNumber: bankAccountNumber, - IBAN: IBAN, - SWIFT: SWIFT, - attachments: base64EncodedAttachments, - }), - }) - .then((response) => { - // throw an error to force onError - if (!response.ok) throw new Error('Failed to call API'); - return response.ok; - }) - .catch((error) => { - throw error; - }); - }, - ), - }, -}).createMachine({ - id: 'feeComplaintForm', - initial: 'editing', - context: { - feeNumber: '', - appPhoneNumber: undefined, - customerNumber: undefined, - travelCardNumber: undefined, - feedback: '', - firstName: '', - lastName: '', - address: '', - postalCode: '', - city: '', - email: '', - phoneNumber: '', - bankAccountNumber: '', - IBAN: '', - SWIFT: '', - agreesFirstAgreement: false, - agreesSecondAgreement: false, - hasInternationalBankAccount: false, - isAppTicketStorageMode: true, - errorMessages: {}, - }, - states: { - editing: { - entry: 'cleanErrorMessages', - on: { - ON_INPUT_CHANGE: { - actions: 'onInputChange', - }, - VALIDATE: { - guard: 'validateInputs', - target: 'submitting', - }, - }, - }, - - submitting: { - invoke: { - src: 'callAPI', - input: ({ context }) => ({ - feeNumber: context.feeNumber, - appPhoneNumber: context.appPhoneNumber, - customerNumber: context.customerNumber, - travelCardNumber: context.travelCardNumber, - feedback: context.feedback, - firstName: context.firstName, - lastName: context.lastName, - address: context.address, - postalCode: context.postalCode, - city: context.city, - email: context.email, - phoneNumber: context.phoneNumber, - bankAccountNumber: context.bankAccountNumber, - IBAN: context.IBAN, - SWIFT: context.SWIFT, - attachments: context.attachments, - }), - - onDone: { - target: 'success', - }, - - onError: { - target: 'editing', - }, - }, - }, - - success: { - type: 'final', - }, - }, -}); diff --git a/src/page-modules/contact/ticket-control/events.ts b/src/page-modules/contact/ticket-control/events.ts index ee68ad7b..f0f342ca 100644 --- a/src/page-modules/contact/ticket-control/events.ts +++ b/src/page-modules/contact/ticket-control/events.ts @@ -3,6 +3,7 @@ import { commonEvents } from '../commoneEvents'; const ticketControlSpecificFormEvents = {} as { type: 'ON_INPUT_CHANGE'; inputName: + | 'formType' | 'feeNumber' | 'invoiceNumber' | 'appPhoneNumber' diff --git a/src/page-modules/contact/ticket-control/feedback/feedbackformMachine.ts b/src/page-modules/contact/ticket-control/feedback/feedbackformMachine.ts deleted file mode 100644 index bd89555b..00000000 --- a/src/page-modules/contact/ticket-control/feedback/feedbackformMachine.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { TransportModeType } from '@atb-as/config-specs'; -import { Line } from '../../server/journey-planner/validators'; -import { assign, fromPromise, setup } from 'xstate'; -import { commonInputValidator, InputErrorMessages } from '../../validation'; -import { - convertFilesToBase64, - getCurrentDateString, - getCurrentTimeString, -} from '../../utils'; -import { ticketControlFormEvents } from '../events'; - -type APIParams = { - transportMode: TransportModeType | undefined; - line: Line | undefined; - fromStop: Line['quays'][0] | undefined; - toStop: Line['quays'][0] | undefined; - date: string; - plannedDepartureTime: string; - feedback: string; - firstName: string; - lastName: string; - email: string; - attachments?: File[]; -}; - -type ContextProps = { - errorMessages: InputErrorMessages; -} & APIParams; - -export const formMachine = setup({ - types: { - context: {} as ContextProps, - events: ticketControlFormEvents, - }, - guards: { - validateInputs: ({ context }) => commonInputValidator(context), - }, - actions: { - onInputChange: assign(({ context, event }) => { - if (event.type === 'ON_INPUT_CHANGE') { - const { inputName, value } = event; - // Remove errorMessages if any - context.errorMessages[inputName] = []; - return { - ...context, - [inputName]: value, - }; - } - return context; - }), - - cleanErrorMessages: assign({ - errorMessages: () => ({}), - }), - }, - actors: { - callAPI: fromPromise( - async ({ - input: { - transportMode, - line, - fromStop, - toStop, - date, - plannedDepartureTime, - feedback, - firstName, - lastName, - email, - attachments, - }, - }: { - input: APIParams; - }) => { - const base64EncodedAttachments = await convertFilesToBase64( - attachments || [], - ); - return await fetch('/api/contact/ticket-control', { - method: 'POST', - body: JSON.stringify({ - transportMode: transportMode, - line: line?.id, - fromStop: fromStop?.id, - toStop: toStop?.id, - date: date, - plannedDepartureTime: plannedDepartureTime, - feedback: feedback, - firstName: firstName, - lastName: lastName, - email: email, - attachments: base64EncodedAttachments, - }), - }) - .then((response) => { - // throw an error to force onError - if (!response.ok) throw new Error('Failed to call API'); - return response.ok; - }) - .catch((error) => { - console.log(error); - throw error; - }); - }, - ), - }, -}).createMachine({ - /** @xstate-layout N4IgpgJg5mDOIC5QDMyQEYEMDGBrAYgPYBOAtgHQAuxmAbmADYCyhEYAxLGJQCo33NWYANoAGALqJQAB0KwAlpXmEAdlJAAPRAFoAjADYATOV0BOAKwBmXYfMAaEAE9Eu8+QDsugCwAOL5fdzAF8gh1QMHAISCgZ5FQ4uSgAZOJEJdVkFJVV1LQRbHw9Rd1FrWwdnfN1yfVFzL30fQJCwtAgsPCIycjZpTGJKAFdiMCTCbExslU5uABEwPoHh0fHJ5RUxSSQQTMV13J1dS0sTUVcjeycXdy9yUtNRC5aQcPbIrop+4nlaTAYxiZTGaUACCxG+v3+qymmwycj2OW2eW0lh8hVM3ke5SuCEshn0RTKwWeKiE8G2rw6UTIcKy+yROn8hWOpj8AUulW0ng8PnM51sz0p72iVH4jBYbFpCLUDIQKIe5BZbMCFRcbk8vn8zVCLzaVI+5Fi8SlUwOCH0pnIPn0eJ8Ng5iEM1Vq9Ua2taEU6It6-SGIwBa0RMnhptlhlElt5pnZqoQBgJNh8DyeOqFXu6Xx+fwDoeDdKDmh0ln0CYxXixDrjPlEivdus91IosEG6FIikoJvpoGR1nIvlEDSala8DUVfPMKZCQA */ - id: 'feedbackForm', - initial: 'editing', - context: { - transportMode: undefined, - line: undefined, - fromStop: undefined, - toStop: undefined, - date: getCurrentDateString(), - plannedDepartureTime: getCurrentTimeString(), - feedback: '', - firstName: '', - lastName: '', - email: '', - errorMessages: {}, - }, - - states: { - editing: { - entry: 'cleanErrorMessages', - on: { - ON_INPUT_CHANGE: { - actions: 'onInputChange', - }, - VALIDATE: { - guard: 'validateInputs', - target: 'submitting', - }, - }, - }, - - submitting: { - invoke: { - src: 'callAPI', - input: ({ context }) => ({ - transportMode: context.transportMode, - line: context.line, - fromStop: context.fromStop, - toStop: context.toStop, - date: context.date, - plannedDepartureTime: context.plannedDepartureTime, - feedback: context.feedback, - firstName: context.firstName, - lastName: context.lastName, - email: context.email, - attachments: context.attachments, - }), - - onDone: { - target: 'success', - }, - - onError: { - target: 'editing', - }, - }, - }, - - success: { - type: 'final', - }, - }, -}); diff --git a/src/page-modules/contact/ticket-control/complaint/index.tsx b/src/page-modules/contact/ticket-control/forms/feeComplaintForm.tsx similarity index 93% rename from src/page-modules/contact/ticket-control/complaint/index.tsx rename to src/page-modules/contact/ticket-control/forms/feeComplaintForm.tsx index 23327611..00acc161 100644 --- a/src/page-modules/contact/ticket-control/complaint/index.tsx +++ b/src/page-modules/contact/ticket-control/forms/feeComplaintForm.tsx @@ -1,35 +1,23 @@ -import { FormEventHandler, useState } from 'react'; import style from '../../contact.module.css'; -import { Button } from '@atb/components/button'; import { Input } from '../../components/input'; import { SectionCard } from '../../components/section-card'; import { PageText, TranslatedString, useTranslation } from '@atb/translations'; -import { useMachine } from '@xstate/react'; -import { formMachine } from './complaintFormMachine'; import { Checkbox } from '../../components/input/checkbox'; import { Typo } from '@atb/components/typography'; import { RadioInput } from '../../components/input/radio'; import { Textarea } from '../../components/input/textarea'; import ErrorMessage from '../../components/input/error-message'; import { FileInput } from '../../components/input/file'; -import { input } from '@testing-library/user-event/dist/cjs/event/input.js'; +import { ContextProps } from '../ticket-control-form-machine'; +import { ticketControlFormEvents } from '../events'; -export const FeeComplaintForm = () => { - const { t } = useTranslation(); - const [state, send] = useMachine(formMachine); - - // Local state to force re-render to display errors. - const [forceRerender, setForceRerender] = useState(false); - - const onSubmit: FormEventHandler = async (e) => { - e.preventDefault(); - send({ type: 'VALIDATE' }); +type FeeComplaintFormProps = { + state: { context: ContextProps }; + send: (event: typeof ticketControlFormEvents) => void; +}; - // Force a re-render with dummy state. - if (Object.keys(state.context.errorMessages).length > 0) { - setForceRerender(!forceRerender); - } - }; +export const FeeComplaintForm = ({ state, send }: FeeComplaintFormProps) => { + const { t } = useTranslation(); const FirstAgreement = () => { return ( @@ -119,7 +107,7 @@ export const FeeComplaintForm = () => { {state.context.agreesFirstAgreement && } {state.context.agreesSecondAgreement && ( -
+
@@ -243,7 +231,7 @@ export const FeeComplaintForm = () => {