Jaime Ramírez
2020-06-11 d4efcf556bee5599b87a18da9420df2143e1c757
commit | author | age
eeab6d 1 import React, {FormEvent} from "react";
J 2 import {Animal} from "../Models/Animal";
3 import {AnimalService} from "../Services/AnimalService";
bae328 4 import {
JR 5     ActionGroup,
eeab6d 6     Alert,
J 7     AlertActionCloseButton,
bae328 8     Button,
JR 9     ButtonType,
eeab6d 10     Checkbox,
J 11     Form,
12     FormGroup,
13     FormSelect,
14     FormSelectOption,
15     TextInput
bae328 16 } from "@patternfly/react-core";
eeab6d 17 import {Residency} from "../Models/Residency";
J 18 import {ApproximateSize} from "../Models/ApproximateSize";
bae328 19 import BullseyeSpinner from "./BullseyeSpinner";
eeab6d 20 import {RESTConnectionError} from "../Services/RESTService";
3e95dd 21 import {ShelterService} from "../Services/ShelterService";
J 22 import {Shelter} from "../Models/Shelter";
23 import LoadingData from "./LoadingData";
bae328 24
JR 25 type AnimalCreateViewProps = {
26     animalService: AnimalService;
3e95dd 27     shelterService: ShelterService;
bae328 28 }
JR 29
30 type AnimalCreateFormState = {
31     showInvalidFormAlert: boolean;
32     showSubmitSucessAlert: boolean;
33     showSubmitErrorAlert: boolean;
34     submitError: {
35         title: string,
36         description: string
37     }
38     isSubmitting: boolean;
39     animal: Animal
3e95dd 40     shelters: Shelter[],
J 41     loading: boolean,
42     error: {
43         isActive: boolean,
44         header: string,
45         message: string
46     }
bae328 47 }
JR 48
49 export default class AnimalCreateForm
50     extends React.Component<AnimalCreateViewProps, AnimalCreateFormState> {
51
52     constructor(props: AnimalCreateViewProps) {
53         super(props);
54         this.state = {
55             showInvalidFormAlert: false,
56             showSubmitSucessAlert: false,
57             showSubmitErrorAlert: false,
58             submitError: {
59                 title: "",
60                 description: ""
61             },
62             isSubmitting: false,
3e95dd 63             loading: false,
J 64             animal: this.getEmptyFields(),
65             shelters: [],
66             error: {
67                 isActive: false,
68                 header: "",
69                 message: ""
70             }
bae328 71         };
JR 72     }
3e95dd 73
J 74     // TODO refactor into common class
75     public async componentDidMount() {
76         this.setState({loading: true});
77
78         try {
79             const shelters = await this.props.shelterService.getAll();
80             this.setState({shelters});
d4efcf 81             // Set default shelter as first option.
JR 82             //If we do not do this the form will not know which one is selected by default
e72b62 83             if (shelters[0].shelterId) {
d4efcf 84                 this.handleShelterIdChange(shelters[0].shelterId);
e72b62 85             }
3e95dd 86         } catch (error) {
J 87             if (error instanceof RESTConnectionError) {
88                 this.showConnectionError(error);
89             }
90         } finally {
91             this.setState({loading: false});
92         }
93     }
94
95
96     private showConnectionError(error: RESTConnectionError) {
97         this.setState({
98             error: {
99                 isActive: true,
100                 header: error.message,
101                 message: error.description,
102             }
103         });
104     }
105
106     private closeErrorAlert = () => {
107         this.setState({
108             error: {
109                 isActive: false,
110                 message: "",
111                 header: ""
112             }
113         });
114     }
115
bae328 116
JR 117     private handleNameChange(animalName: string) {
0736f9 118         // Immutability: instead of modifying the state,
bae328 119         // we make a copy with the new value, and then
JR 120         // set the new state
3e95dd 121         const animal = {...this.state.animal, animalName};
bae328 122         this.setState({
JR 123             animal
124         });
125     }
126
127     private handleShelterIdChange(shelterId: string) {
0736f9 128         // Immutability: instead of modifying the state,
bae328 129         // we make a copy with the new value, and then
JR 130         // set the new state
3e95dd 131         const animal = {...this.state.animal, shelterId};
bae328 132         this.setState({
JR 133             animal
134         });
135     }
136
137     private handleBreedChange(breed: string) {
0736f9 138         // Immutability: instead of modifying the state,
bae328 139         // we make a copy with the new value, and then
JR 140         // set the new state
3e95dd 141         const animal = {...this.state.animal, breed};
bae328 142         this.setState({
JR 143             animal
144         });
145     }
146
147     private handleApproximateSizeChange(approximateSize: string) {
0736f9 148         // Immutability: instead of modifying the state,
bae328 149         // we make a copy with the new value, and then
JR 150         // set the new state
3e95dd 151         const animal = {...this.state.animal, approximateSize};
bae328 152         this.setState({
JR 153             animal
154         });
155     }
156
157     private handleResidencyRequiredChange(residencyRequired: string) {
0736f9 158         // Immutability: instead of modifying the state,
bae328 159         // we make a copy with the new value, and then
JR 160         // set the new state
3e95dd 161         const animal = {...this.state.animal, residencyRequired};
bae328 162         this.setState({
JR 163             animal
164         });
165     }
166
167     private handleWeightChange(weight: string) {
0736f9 168         // Immutability: instead of modifying the state,
bae328 169         // we make a copy with the new value, and then
JR 170         // set the new state
171         const animal = {
172             ...this.state.animal,
173             weight: parseInt(weight)
174         };
175         this.setState({
176             animal
177         });
178     }
179
180     private handleSquareFootageOfHomeChange(squareFootageOfHome: string) {
0736f9 181         // Immutability: instead of modifying the state,
bae328 182         // we make a copy with the new value, and then
JR 183         // set the new state
184         const animal = {
185             ...this.state.animal,
186             squareFootageOfHome: parseInt(squareFootageOfHome)
187         };
188         this.setState({
189             animal
190         });
191     }
192
193     private handleChildSafeChange(childSafe: boolean) {
0736f9 194         // Immutability: instead of modifying the state,
bae328 195         // we make a copy with the new value, and then
JR 196         // set the new state
3e95dd 197         const animal = {...this.state.animal, childSafe};
bae328 198         this.setState({
JR 199             animal
200         });
201     }
202
203     private handleOtherDogSafeChange(otherDogSafe: boolean) {
0736f9 204         // Immutability: instead of modifying the state,
bae328 205         // we make a copy with the new value, and then
JR 206         // set the new state
3e95dd 207         const animal = {...this.state.animal, otherDogSafe};
bae328 208         this.setState({
JR 209             animal
210         });
211     }
212
213     private handleAdoptableChange(adoptable: boolean) {
0736f9 214         // Immutability: instead of modifying the state,
bae328 215         // we make a copy with the new value, and then
JR 216         // set the new state
3e95dd 217         const animal = {...this.state.animal, adoptable};
bae328 218         this.setState({
JR 219             animal
220         });
221     }
222
223     private async handleFormSubmit(event: FormEvent) {
224         if (this.isFormValid()) {
3e95dd 225             this.setState({isSubmitting: true});
bae328 226             try {
JR 227                 await this.props.animalService.create(this.state.animal);
228                 // const animalId = await this.props.animalService.create(this.state.animal);
229                 // TODO photo input and then write file to server
230                 // Maybe have some embedded database like SQLite?
231
232                 this.showSuccessAlert();
233                 this.resetFormFields();
234             } catch (error) {
235                 this.showErrorAlert(error);
236             } finally {
3e95dd 237                 this.setState({isSubmitting: false});
bae328 238             }
JR 239         } else {
3e95dd 240             this.setState({showInvalidFormAlert: true});
bae328 241         }
JR 242         event.preventDefault();
243     }
244
245     private isFormValid() {
3e95dd 246         const {animal} = this.state;
d4efcf 247         const fieldIsEmpty = (field: string) => { return animal[field as keyof Animal] === ""; };
bae328 248
JR 249         const hasEmptyFields = Object
250             .keys(animal)
251             .some(fieldIsEmpty);
252         return !hasEmptyFields;
253     }
254
255     private showSuccessAlert() {
256         this.setState({
257             showSubmitSucessAlert: true,
258             showSubmitErrorAlert: false,
259             showInvalidFormAlert: false
260         });
261         this.hideAlertsAfter(3000);
262     }
263
264     private showErrorAlert(error: Error) {
265         let submitError = {
266             title: error.name,
267             description: error.message
268         };
269         if (error instanceof RESTConnectionError) {
270             submitError.description = error.description;
271         }
272         this.setState({
273             showSubmitErrorAlert: true,
274             submitError,
275             showSubmitSucessAlert: false,
276             showInvalidFormAlert: false
277         });
278         this.hideAlertsAfter(3000);
279     }
280
281     private hideAlertsAfter(millis: number) {
282         setTimeout(() => {
283             this.setState({
284                 showSubmitSucessAlert: false,
285                 showSubmitErrorAlert: false,
286                 submitError: {
287                     title: "",
288                     description: ""
289                 }
290             });
291         }, millis);
292     }
293
294     private handleCloseInvalidFormAlert() {
3e95dd 295         this.setState({showInvalidFormAlert: false});
bae328 296     }
JR 297
298     private getEmptyFields(): Animal {
299         return {
300             animalName: "",
301             shelterId: "",
302             breed: "",
303             approximateSize: ApproximateSize.M,
304             residencyRequired: Residency.APARTMENT,
305             weight: 0,
306             adoptable: true,
307             squareFootageOfHome: 0,
308             childSafe: false,
ec0b6f 309             otherDogSafe: false,
bae328 310         };
JR 311     }
312
313     private resetFormFields() {
314         this.setState({
315             animal: this.getEmptyFields()
316         });
317     }
318
319     public render() {
320         return (
321             <React.Fragment>
322                 {this.state.isSubmitting ? this.renderLoader() : this.renderForm()}
323             </React.Fragment>
324         );
325     }
326
327     public renderLoader() {
3e95dd 328         return <BullseyeSpinner/>;
bae328 329     }
JR 330
331     public renderForm() {
3e95dd 332         let state = this.state;
J 333         const {animal, showInvalidFormAlert} = state;
bae328 334         return (
JR 335             <Form onSubmit={this.handleFormSubmit.bind(this)}>
336                 {this.renderCreationSuccessAlert()}
337                 {this.renderCreationErrorAlert()}
338                 {showInvalidFormAlert &&
3e95dd 339                 <Alert
J 340                     id="myalert"
341                     className="popup"
342                     variant="danger"
343                     title="Invalid form"
344                     action={<AlertActionCloseButton
345                         onClose={this.handleCloseInvalidFormAlert.bind(this)}
346                     />}>
347                     Please complete required fields
348                 </Alert>}
bae328 349                 <FormGroup
JR 350                     label="Name"
351                     isRequired
352                     fieldId="animal-form-name"
353                     helperText="Please provide the animal name"
354                 >
355                     <TextInput
356                         isRequired
357                         type="text"
358                         id="animal-form-name"
359                         name="animal-form-name"
360                         aria-describedby="animal-form-name-helper"
361                         value={animal.animalName}
362                         onChange={this.handleNameChange.bind(this)}
363                     />
364                 </FormGroup>
3e95dd 365                 <LoadingData
J 366                     showLoader={state.loading}
367                     showError={state.error.isActive}
368                     errorTitle={state.error.header}
369                     errorDescription={state.error.message}
370                     onErrorClosed={this.closeErrorAlert}>
371                     <FormGroup
372                         label="Shelter"
bae328 373                         isRequired
3e95dd 374                         fieldId="animal-form-shelter-id"
e72b62 375
J 376                         helperText="Please provide the shelter"
3e95dd 377                     >
J 378                         <FormSelect
379                             value={animal.shelterId}
380                             onChange={this.handleShelterIdChange.bind(this)}
381                             aria-label="Select Shelter">
382                             {state.shelters.map(shelter => {
383                                 return <FormSelectOption
384                                     key={shelter.shelterId}
385                                     value={shelter.shelterId}
386                                     label={shelter.shelterName}
d4efcf 387                                 />;
3e95dd 388                             })}
J 389                         </FormSelect>
390                     </FormGroup>
391                 </LoadingData>
bae328 392                 <FormGroup
JR 393                     label="Breed"
394                     isRequired
395                     fieldId="animal-form-breed"
396                     helperText="Please provide the breed"
397                 >
398                     <TextInput
399                         isRequired
400                         type="text"
401                         id="animal-form-breed"
402                         name="animal-form-breed"
403                         aria-describedby="animal-form-breed-helper"
404                         value={animal.breed}
405                         onChange={this.handleBreedChange.bind(this)}
406                     />
407                 </FormGroup>
408                 <FormGroup
409                     label="Adoptable"
410                     isRequired
411                     fieldId="animal-form-adoptable"
412                     helperText="Please indicate if animal is adoptable"
413                 >
414                     <Checkbox
415                         label="Adoptable?"
416                         id="animal-form-adoptable"
417                         name="animal-form-adoptable"
418                         aria-label="Adoptable?"
419                         isChecked={animal.adoptable}
3e95dd 420                         onChange={this.handleAdoptableChange.bind(this)}/>
bae328 421                 </FormGroup>
JR 422                 <FormGroup
423                     label="Residency"
424                     isRequired
425                     fieldId="animal-form-residency"
426                     helperText="Which type of residency is required">
427                     <FormSelect
428                         value={animal.residencyRequired}
429                         onChange={this.handleResidencyRequiredChange.bind(this)}
430                         aria-label="Select Residency">
431                         <FormSelectOption
432                             key={Residency.HOUSE}
433                             value={Residency.HOUSE}
434                             label={"House"}
435                         />
436                         <FormSelectOption
437                             key={Residency.APARTMENT}
438                             value={Residency.APARTMENT}
439                             label={"Apartment"}
440                         />
441                     </FormSelect>
442                 </FormGroup>
443                 <FormGroup
444                     label="Approximate Size"
445                     isRequired
446                     fieldId="animal-form-approximate-size"
447                     helperText="Please provide approximate size"
448                 >
449                     <FormSelect
450                         value={animal.approximateSize}
451                         onChange={this.handleApproximateSizeChange.bind(this)}
452                         aria-label="Select approximate size">
0736f9 453                         {Object.keys(ApproximateSize).map((approximateSize) => {
J 454                             return <FormSelectOption
455                                 key={approximateSize}
456                                 value={approximateSize}
457                                 label={approximateSize}
d4efcf 458                             />;
0736f9 459                         })}
bae328 460                     </FormSelect>
JR 461                 </FormGroup>
462                 <FormGroup
463                     label="Square Footage of Home Required"
464                     isRequired fieldId="animal-form-footage"
465                 >
466                     <TextInput
467                         isRequired
468                         type="number"
469                         id="animal-form-footage"
470                         name="animal-form-footage"
471                         value={animal.squareFootageOfHome}
472                         onChange={this.handleSquareFootageOfHomeChange.bind(this)}
473                     />
474                 </FormGroup>
475                 <FormGroup label="Animal Weight" isRequired fieldId="animal-form-weight">
476                     <TextInput
477                         isRequired
478                         type="number"
479                         id="animal-form-weight"
480                         name="animal-form-weight"
481                         value={animal.weight}
482                         onChange={this.handleWeightChange.bind(this)}
483                     />
484                 </FormGroup>
485                 <FormGroup
486                     label="Safe with kids"
487                     isRequired
488                     fieldId="animal-form-kid-safe"
489                     helperText="Please indicate if animal is safe with children under 16"
490                 >
491                     <Checkbox
492                         label="Safe with Kids?"
493                         id="animal-form-kid-safe"
494                         name="animal-form-kid-safe"
495                         aria-label="Safe with Kids?"
496                         isChecked={animal.childSafe}
3e95dd 497                         onChange={this.handleChildSafeChange.bind(this)}/>
bae328 498                 </FormGroup>
JR 499                 <FormGroup
500                     label="Safe with other animals"
501                     isRequired
502                     fieldId="animal-form-animal-safe"
503                     helperText="Please indicate if animal is safe with other animals"
504                 >
505                     <Checkbox
506                         label="Safe with other Animals?"
507                         id="animal-form-animal-safe"
508                         name="animal-form-animal-safe"
509                         aria-label="Safe with other Animals?"
510                         isChecked={animal.otherDogSafe}
3e95dd 511                         onChange={this.handleOtherDogSafeChange.bind(this)}/>
bae328 512                 </FormGroup>
JR 513                 <ActionGroup>
514                     <Button variant="primary" type={ButtonType.submit}>Create Animal</Button>
515                 </ActionGroup>
516             </Form>
517         );
518     }
519
520
521     private renderCreationErrorAlert(): React.ReactNode | null {
522         if (this.state.showSubmitErrorAlert) {
523             return (
524                 <Alert
525                     variant="danger"
526                     title={`Error creating animal: ${this.state.submitError.title}`}
527                 >
528                     {this.state.submitError.description}
529                 </Alert>
530             );
531         }
532         return null;
533     }
534
535     private renderCreationSuccessAlert(): React.ReactNode | null {
536         if (this.state.showSubmitSucessAlert) {
537             return <Alert
538                 variant="success"
539                 title="Animal created"
540             />;
541         }
542         return null;
543     }
544 }