Skip to content

Commit

Permalink
Make address form submittable
Browse files Browse the repository at this point in the history
This fixes validation and field names to make sure
the form submits to the application correctly.

https://phabricator.wikimedia.org/T345528
  • Loading branch information
Abban committed Sep 15, 2023
1 parent 02902f1 commit bc097da
Show file tree
Hide file tree
Showing 14 changed files with 149 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@

<RadioField
v-model="addressType"
name="addressType"
name="addressTypeSelector"
:options="[
{ value: AddressTypeModel.PERSON, label: $t( 'donation_form_addresstype_option_private_addresstype' ) },
{ value: AddressTypeModel.COMPANY, label: $t( 'donation_form_addresstype_option_company_addresstype' ) },
{ value: AddressTypeModel.COMPANY_WITH_CONTACT, label: $t( 'donation_form_addresstype_option_company_addresstype' ) },
]"
:label="$t( 'donation_form_address_choice_title_addresstype_basic' )"
:show-error="showAddressTypeError"
:error-message="$t( 'donation_form_section_address_error' )"
/>

<TextField
v-if="addressType === AddressTypeModel.COMPANY"
name="company-name"
v-if="addressType === AddressTypeModel.COMPANY_WITH_CONTACT"
name="companyName"
input-id="company-name"
v-model="formData.companyName.value"
:show-error="showError.companyName"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<template>
<div class="name-section">

<RadioField
name="salutationInternal"
name="salutation"
v-model="formData.salutation.value"
:label="$t( 'donation_form_salutation_label' )"
:options="salutations"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Store } from 'vuex';
import { trackFormSubmission } from '@src/tracking';
import { action } from '@src/store/util';
import { NS_ADDRESS, NS_BANKDATA, NS_PAYMENT } from '@src/store/namespaces';
import { validateAddress, validateAddressType, validateEmail } from '@src/store/address/actionTypes';
import { AddressTypeModel } from '@src/view_models/AddressTypeModel';
import { markEmptyValuesAsInvalid } from '@src/store/bankdata/actionTypes';
import { waitForServerValidationToFinish } from '@src/wait_for_server_validation';
import { discardInitialization } from '@src/store/payment/actionTypes';
import { ComputedRef } from 'vue';

const scrollToFirstError = () => {
document.getElementsByClassName( 'help is-danger' )[ 0 ]
.scrollIntoView( { behavior: 'smooth', block: 'center', inline: 'nearest' } );
};

type ReturnType = {
submit: ( e: Event ) => Promise<void>,
previousPage: () => void,
}

export function useAddressFormEventHandlers(
store: Store<any>,
emit: ( eventName: string ) => void,
isDirectDebit: ComputedRef<any>,
validateAddressUrl: string,
validateEmailUrl: string
): ReturnType {
const submit = async ( e: Event ) => {
e.preventDefault();

await store.dispatch( action( NS_ADDRESS, validateAddressType ), {
type: store.state.address.addressType,
disallowed: [ AddressTypeModel.UNSET ],
} );
await store.dispatch( action( NS_ADDRESS, validateAddress ), validateAddressUrl );
await store.dispatch( action( NS_ADDRESS, validateEmail ), validateEmailUrl );

if ( isDirectDebit.value ) {
await store.dispatch( action( NS_BANKDATA, markEmptyValuesAsInvalid ) );
}

// We need to wait for the asynchronous bank data validation, that might still be going on
await waitForServerValidationToFinish( store );

if ( !store.getters[ NS_ADDRESS + '/requiredFieldsAreValid' ] ) {
scrollToFirstError();
return;
}

if ( isDirectDebit.value && !store.getters[ NS_BANKDATA + '/bankDataIsValid' ] ) {
scrollToFirstError();
return;
}

const form = e.target as HTMLFormElement;

trackFormSubmission( form );
form.submit();
};

const previousPage = async () => {
await store.dispatch( action( NS_PAYMENT, discardInitialization ) );
emit( 'previous-page' );
};

return {
submit,
previousPage,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import { Store } from 'vuex';
import { addressTypeName as getAddressTypeName } from '@src/view_models/AddressTypeModel';

type ReturnType = {
addressType: ComputedRef<string>,
addressType: ComputedRef<number>,
addressTypeName: ComputedRef<string>,
};

export function useAddressType( store: Store<any> ): ReturnType {
const addressType = computed<string>( () => store.getters[ 'address/addressType' ] );
const addressType = computed<number>( () => store.getters[ 'address/addressType' ] );
const addressTypeName = computed<string>( (): string => getAddressTypeName( store.state.address.addressType ) );

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ComputedRef, Ref, ref, watch } from 'vue';
import { Store } from 'vuex';
import { action } from '@src/store/util';
import { NS_ADDRESS } from '@src/store/namespaces';
import { setAddressType as setAddressTypeActionType } from '@src/store/address/actionTypes';
import { AddressTypeModel } from '@src/view_models/AddressTypeModel';

export function useAddressTypeFromReceiptSetter( receiptModel: Ref<boolean | null>, addressType: ComputedRef<number>, store: Store<any> ): void {
const lastAddressType = ref<number>( addressType.value );

if ( !receiptModel.value ) {
store.dispatch( action( NS_ADDRESS, setAddressTypeActionType ), AddressTypeModel.EMAIL );
} else if ( ![ AddressTypeModel.PERSON, AddressTypeModel.COMPANY_WITH_CONTACT ].includes( addressType.value ) ) {
store.dispatch( action( NS_ADDRESS, setAddressTypeActionType ), AddressTypeModel.UNSET );
}

const setAddressTypeFromReceipt = ( receipt: boolean | null ): void => {
if ( !receipt ) {
lastAddressType.value = addressType.value;
store.dispatch( action( NS_ADDRESS, setAddressTypeActionType ), AddressTypeModel.EMAIL );
} else {
store.dispatch( action( NS_ADDRESS, setAddressTypeActionType ), lastAddressType.value );
lastAddressType.value = AddressTypeModel.UNSET;
}
};

watch( receiptModel, setAddressTypeFromReceipt );
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<template>
<div class="address-page">
<h1 v-if="!paymentWasInitialized" class="title is-size-1">{{ $t( 'donation_form_section_headline' ) }}</h1>

<PaymentSummary
v-if="paymentWasInitialized"
:amount="paymentSummary.amount"
Expand All @@ -9,7 +10,7 @@
@previous-page="previousPage">
</PaymentSummary>

<form @submit="evt => evt.preventDefault()">
<form @submit="submit" action="/donation/add" method="post">
<AutofillHandler @autofill="onAutofill">

<PaymentBankData
Expand Down Expand Up @@ -43,10 +44,10 @@

<RadioField
v-model="receiptModel"
name="receipt"
name="donationReceipt"
:options="[
{ value: true, label: $t( 'yes' ) },
{ value: false, label: $t( 'no' ) },
{ value: true, label: $t( 'yes' ) },
]"
:label="$t( 'donation_confirmation_cta_title_alt' )"
@field-changed="onFieldChange"
Expand Down Expand Up @@ -88,7 +89,8 @@
<FunButton
id="submit-btn"
:class="[ 'level-item is-primary is-main', { 'is-loading' : store.getters.isValidating } ]"
@click="submit">
button-type="submit"
>
{{ $t( 'donation_form_finalize' ) }}
</FunButton>
</div>
Expand All @@ -101,7 +103,7 @@
</div>
</div>

<input v-if="!receiptModel" type="hidden" name="addressType" :value="AddressTypeModel.EMAIL">
<input type="hidden" name="addressType" :value="addressTypeName">
<input type="hidden" name="paymentType" :value="paymentType">
<input type="hidden" name="interval" :value="interval">
<input type="hidden" name="amount" :value="amount">
Expand All @@ -127,7 +129,7 @@ import { Salutation } from '@src/view_models/Salutation';
import { StoreKey } from '@src/store/donation_store';
import { TrackingData } from '@src/view_models/TrackingData';
import { trackDynamicForm } from '@src/tracking';
import { useAddressFormEventHandlers } from '@src/components/pages/donation_form/useAddressFormEventHandlers';
import { useAddressFormEventHandlers } from '@src/components/pages/donation_form/DonationReceipt/useAddressFormEventHandlers';
import { useAddressSummary } from '@src/components/pages/donation_form/useAddressSummary';
import { usePaymentFunctions } from '@src/components/pages/donation_form/usePaymentFunctions';
import NameFields from '@src/components/pages/donation_form/DonationReceipt/NameFields.vue';
Expand All @@ -143,8 +145,8 @@ import { useAddressType } from '@src/components/pages/donation_form/DonationRece
import { useStore } from 'vuex';
import AutofillHandler from '@src/components/shared/AutofillHandler.vue';
import { Validity } from '@src/view_models/Validity';
import { AddressTypeModel } from '@src/view_models/AddressTypeModel';
import { usePaymentValues } from '@src/components/pages/donation_form/DonationReceipt/usePaymentValues';
import { useAddressTypeFromReceiptSetter } from '@src/components/pages/donation_form/DonationReceipt/useAddressTypeFromReceiptSetter';
interface Props {
assetsPath: string;
Expand Down Expand Up @@ -186,12 +188,15 @@ const {
paymentWasInitialized,
} = usePaymentFunctions( store );
const { submit, previousPage } = useAddressFormEventHandlers( store, emit, addressType, isDirectDebit, props.validateAddressUrl, props.validateEmailUrl );
const { submit, previousPage } = useAddressFormEventHandlers( store, emit, isDirectDebit, props.validateAddressUrl, props.validateEmailUrl );
useAddressTypeFromReceiptSetter( receiptModel, addressType, store );
onBeforeMount( () => {
countryWasRestored.value = store.state.address.validity.country === Validity.RESTORED;
initializeDataFromStore();
} );
onMounted( trackDynamicForm );
</script>
22 changes: 12 additions & 10 deletions src/components/shared/Postal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,18 @@
/>
</div>

<CountryAutocompleteField
v-model="formData.country.value"
:countries="countries"
:was-restored="countryWasRestored"
:show-error="showError.country"
:error-message="$t('donation_form_country_error')"
:label="$t( 'donation_form_country_label' )"
:placeholder="$t( 'form_for_example', { example: countries[0].countryFullName } )"
@field-changed="onCountryFieldChanged"
/>
<div style="margin-top: 36px;">
<CountryAutocompleteField
v-model="formData.country.value"
:countries="countries"
:was-restored="countryWasRestored"
:show-error="showError.country"
:error-message="$t('donation_form_country_error')"
:label="$t( 'donation_form_country_label' )"
:placeholder="$t( 'form_for_example', { example: countries[0].countryFullName } )"
@field-changed="onCountryFieldChanged"
/>
</div>

</div>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
@blur="onBlur"
/>
<transition name="fade">
<div class="dropdown-menu" v-show="autocompleteIsActive">
<div class="dropdown-menu" v-show="autocompleteIsActive && cities.length > 0">
<div class="dropdown-content">
<template v-for="city in cities">
<a class="dropdown-item" role="button" tabindex="0" @click.stop="onSelectItem( city )">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div class="form-field form-field-autocomplete" :class="{ 'is-invalid': showError }">
<label for="country" class="form-field-label">{{ label }}</label>
<div class="autocomplete control">
<div class="form-field-autocomplete-container">
<TextFormInput
v-model="countryName"
input-type="text"
Expand Down
2 changes: 1 addition & 1 deletion src/components/shared/form_fields/MailingListField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="form-field form-field-checkbox">
<CheckboxSingleFormInput
v-model="fieldModel"
name="newsletter"
name="info"
input-id="newsletter"
@update:modelValue="onUpdateModel"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
>
<input
v-model="inputModel"
:value="inputModel"
type="checkbox"
ref="inputRef"
:name="name"
Expand Down
5 changes: 4 additions & 1 deletion src/scss/components/form_field.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
&-select,
&-email,
&-autocomplete {
max-width: map.get( forms.$input, 'max-width' );
.control {
max-width: map.get( forms.$input, 'max-width' );
}
}

&-autocomplete-container {
max-width: map.get( forms.$input, 'max-width' );
position: relative;
}

Expand Down
2 changes: 2 additions & 0 deletions src/store/address/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface AddressRequirements {
export const REQUIRED_FIELDS: AddressRequirements = {
[ AddressTypeModel.PERSON ]: [ 'salutation', 'firstName', 'lastName', 'street', 'postcode', 'city', 'country', 'email', 'addressType' ],
[ AddressTypeModel.COMPANY ]: [ 'companyName', 'street', 'postcode', 'city', 'country', 'email', 'addressType' ],
[ AddressTypeModel.COMPANY_WITH_CONTACT ]: [ 'salutation', 'firstName', 'lastName', 'companyName', 'street', 'postcode', 'city', 'country', 'email', 'addressType' ],
[ AddressTypeModel.EMAIL ]: [ 'email', 'salutation', 'firstName', 'lastName' ],
[ AddressTypeModel.ANON ]: [],
[ AddressTypeModel.UNSET ]: [ 'addressType' ],
Expand All @@ -16,4 +17,5 @@ export const REQUIRED_FIELDS: AddressRequirements = {
export const REQUIRED_FIELDS_ADDRESS_UPDATE: AddressRequirements = {
[ AddressTypeModel.PERSON ]: [ 'salutation', 'firstName', 'lastName', 'street', 'postcode', 'city', 'country' ],
[ AddressTypeModel.COMPANY ]: [ 'companyName', 'street', 'postcode', 'city', 'country' ],
[ AddressTypeModel.COMPANY_WITH_CONTACT ]: [ 'salutation', 'firstName', 'lastName', 'companyName', 'street', 'postcode', 'city', 'country' ],
};
11 changes: 7 additions & 4 deletions src/view_models/AddressTypeModel.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
export enum AddressTypeModel {
PERSON,
COMPANY,
COMPANY_WITH_CONTACT,
EMAIL,
ANON,
UNSET,
}

export const AddressTypeNames = new Map<number, string>( [
[ AddressTypeModel.ANON, 'anonym' ],
[ AddressTypeModel.EMAIL, 'email' ],
[ AddressTypeModel.PERSON, 'person' ],
[ AddressTypeModel.COMPANY, 'firma' ],
[ AddressTypeModel.COMPANY_WITH_CONTACT, 'company_with_contact' ],
[ AddressTypeModel.EMAIL, 'email' ],
[ AddressTypeModel.ANON, 'anonym' ],
[ AddressTypeModel.UNSET, 'unset' ],
] );

export const AddressTypes = new Map<string, number>( [
[ 'anonym', AddressTypeModel.ANON ],
[ 'email', AddressTypeModel.EMAIL ],
[ 'person', AddressTypeModel.PERSON ],
[ 'firma', AddressTypeModel.COMPANY ],
[ 'company_with_contact', AddressTypeModel.COMPANY_WITH_CONTACT ],
[ 'email', AddressTypeModel.EMAIL ],
[ 'anonym', AddressTypeModel.ANON ],
[ 'unset', AddressTypeModel.UNSET ],
] );

Expand Down

0 comments on commit bc097da

Please sign in to comment.