diff --git a/frontend/src/components/Utils/filter-input.js b/frontend/src/components/Utils/filter-input.js index e32419cd..be16a313 100644 --- a/frontend/src/components/Utils/filter-input.js +++ b/frontend/src/components/Utils/filter-input.js @@ -48,9 +48,9 @@ export function getCollaboratorUidArray(collaboratorEmailArr) { * attacks. Thus, no sanitization is needed for text inputs besides providing a * default value in a Trip field where applicable. * - * @param {Object} rawTripObj A JS Object containing the raw form data from the + * @param {!Object} rawTripObj A JS Object containing the raw form data from the * add trip form. - * @return {Object} Formatted/cleaned version of `rawTripObj` holding the data + * @return {!Object} Formatted/cleaned version of `rawTripObj` holding the data * for the new Trip document that is to be created. */ export function formatTripData(rawTripObj) { diff --git a/frontend/src/components/Utils/temp-auth-utils.js b/frontend/src/components/Utils/temp-auth-utils.js index 787bfdc5..a9ba28e0 100644 --- a/frontend/src/components/Utils/temp-auth-utils.js +++ b/frontend/src/components/Utils/temp-auth-utils.js @@ -2,7 +2,7 @@ * @fileoverview This is a temporary file that is used to implement 'fake' * versions of the Auth utility functions used in the ViewTrips components. * - * TODO(Issue 55): Remove this whole file function and replace any imports to + * TODO(Issue #55): Remove this whole file function and replace any imports to * this file with Auth utils. */ @@ -10,7 +10,7 @@ /** * Temporary hardcoded function that returns the current users email. * - * @return Hardcoded user email string. + * @return {string} Hardcoded user email string. */ export function getCurUserEmail() { return 'matt.murdock'; @@ -19,7 +19,7 @@ export function getCurUserEmail() { /** * Temporary hardcoded function that returns the current users uid. * - * @return Hardcoded user uid string. + * @return {string} Hardcoded user uid string. */ export function getCurUserUid() { return getUserUidFromUserEmail(getCurUserEmail()); diff --git a/frontend/src/components/Utils/time.js b/frontend/src/components/Utils/time.js index fe354c26..6b71e246 100644 --- a/frontend/src/components/Utils/time.js +++ b/frontend/src/components/Utils/time.js @@ -61,7 +61,7 @@ export function timestampToFormatted(msTimestamp, timezone = "America/New_York") /** * Return a Firestore Timestamp corresponding to the date in `dateStr`. * - * @param {string} dateStr String containing a date in the form 'yyyy-mm-dd'. + * @param {string} dateStr String containing a date in the form 'YYYY-MM-DD'. * @return {firebase.firestore.Timestamp} Firestore timestamp object created. */ export function getTimestampFromDateString(dateStr) { @@ -73,3 +73,13 @@ export function getTimestampFromDateString(dateStr) { const date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); return firebase.firestore.Timestamp.fromDate(date); } + +/** + * Formats a Firestore timestamp into a date string in ISO format. + * + * @param {firebase.firestore.Timestamp} timestamp Firestore timestamp object. + * @return {string} ISO formatted date string: "YYYY-MM-DD or 2020-05-12". + */ +export function timestampToISOString(timestamp) { + return timestamp.toDate().toISOString().substring(0,10); +} diff --git a/frontend/src/components/ViewTrips/index.js b/frontend/src/components/ViewTrips/index.js index 33f425ff..d12d0ef8 100644 --- a/frontend/src/components/ViewTrips/index.js +++ b/frontend/src/components/ViewTrips/index.js @@ -19,14 +19,7 @@ class ViewTrips extends React.Component { refreshTripsContainer: false, refreshSaveTripModal: false, tripId: null, - placeholderObj: { - name: null, - description: null, - destination: null, - startDate: null, - endDate: null, - collaborators: [] - } + defaultFormObj: null, }; } @@ -75,14 +68,7 @@ class ViewTrips extends React.Component { showAddTripModal = () => { this.setState({ tripId: null, - placeholderObj: { - name: 'Enter Trip Name', - description: 'Enter Trip Description', - destination: 'Enter Trip Destination', - startDate: '', - endDate: '', - collaborators: ['person@email.xyz'] - } + defaultFormObj: null, }); this.showSaveTripModal(); } @@ -94,19 +80,11 @@ class ViewTrips extends React.Component { * to ensure the modal has the visual characteristics of an "edit trip" modal * and overwrites and existing Trip document in the database. * - * TODO(Issue #69): Get individual tripId and trip data for placeholderObj. */ - showEditTripModal = () => { + showEditTripModal = (tripId, tripData) => { this.setState({ - tripId: null, - placeholderObj: { - name: null, - description: null, - destination: null, - startDate: null, - endDate: null, - collaborators: [] - } + tripId: tripId, + defaultFormObj: tripData }); this.showSaveTripModal(); } @@ -121,7 +99,7 @@ class ViewTrips extends React.Component { handleClose={this.hideSaveTripModal} refreshTripsContainer={this.refreshTripsContainer} tripId={this.state.tripId} - placeholderObj={this.state.placeholderObj} + defaultFormObj={this.state.defaultFormObj} key={this.state.refreshSaveTripModal} />
@@ -130,6 +108,7 @@ class ViewTrips extends React.Component {
diff --git a/frontend/src/components/ViewTrips/save-trip-form-elements.js b/frontend/src/components/ViewTrips/save-trip-form-elements.js new file mode 100644 index 00000000..e327c91f --- /dev/null +++ b/frontend/src/components/ViewTrips/save-trip-form-elements.js @@ -0,0 +1,143 @@ +import React from 'react'; + +import { Form } from 'react-bootstrap'; + +/** + * Returns a Form.Control element with input type 'text' and other props + * specified by the function parameters. + * + * @param {React.RefObject} ref Ref attached to the value inputted in the form. + * @param {string} placeholder Placeholder text value in the form input. + * @param {?string} defaultText Optional default text value in the form input. + * Null if no default text. + * @return {JSX.Element} The Form.Control element. + */ +function createTextFormControl(ref, placeholder, defaultText) { + return ( + + ); +} + +/** + * Returns a Form.Control element with input type 'date' and other props + * specified by the function parameters. + * + * @param {React.RefObject} ref Ref attached to the date inputted in the form. + * @param {?string} defaultDate Optional default ISO date string placed in the + * form input. Null if no default date. + * @return {JSX.Element} The Form.Control element. + */ +function createDateFormControl(ref, defaultDate) { + return ( + + ); +} + +/** + * Returns a Form.Control element with input type 'email' and other props + * specified by the function parameters. + * + * @param {React.RefObject} ref Ref attached to the value inputted in the form. + * @param {number} idx Index of the email Form.Control used for key prop. + * @param {string} placeholder Placeholder text value in the form input. + * @param {?Array} defaultEmailArr Array of the emails to be displayed + * in the default form fields. Null if no default emails. + * @return {JSX.Element} The Form.Control element. + */ +function createEmailFormControl(ref, idx, placeholder, defaultEmailArr) { + if (defaultEmailArr === null) { + return ( + + ); + } + return ( + + ); +} + +/** + * Returns multiple Form.Control elements with input type 'email' and other + * props specified by the function parameters. + * + * One is added to the index of the emails show in order to display all + * collaborators except the current user. + * + * TODO(Issue #67): Email verification before submitting the form. + * + * TODO(Issue #72): More intuitive remove collaborator when !`isAddTripForm`. + * + * @param {!Array} refArr Array of refs attached to the + * emails inputted in the form. + * @param {boolean} isAddTripForm True if form is adding new trip, false if + * form is editting existing trip. + * @param {string} placeholder Placeholder text value in the form input. + * @param {?Array} defaultEmailArr Array of the emails to be displayed + * in the default form fields. + * @return {JSX.Element} The Form.Control elements. + */ +function createMultiFormControl(refArr, placeholder, defaultEmailArr) { + return ( + <> + {refArr.map((ref, idx) => + createEmailFormControl(ref, idx, placeholder, defaultEmailArr) + )} + + ); +} + +/** + * Returns a Form.Group element with components specified by the input args. + * + * @param {string} controlId Prop that accessibly wires the nested label and + * input prop. + * @param {string} formLabel Label/title for the form input. + * @param {string} inputType Input type of the form. + * @param {!React.RefObject} ref Ref attached to the values inputted in the form. + * @param {string} placeholder Placeholder text value in the form input. + * @param {?string|?Array} defaultVal Default value in the form input. + * @return {JSX.Element} The Form.Group element. + */ +export function createFormGroup(controlId, formLabel, inputType, + ref, placeholder, defaultVal) { + let formControl; + switch(inputType) { + case 'text': + formControl = createTextFormControl(ref, placeholder, defaultVal); + break; + case 'date': + formControl = createDateFormControl(ref, defaultVal); + break; + case 'emails': + formControl = createMultiFormControl(ref, placeholder, defaultVal); + break; + default: + console.error('There should be no other input type') + } + + return ( + + {formLabel} + {formControl} + + ) +} diff --git a/frontend/src/components/ViewTrips/save-trip-modal.js b/frontend/src/components/ViewTrips/save-trip-modal.js index 769985d7..49a81324 100644 --- a/frontend/src/components/ViewTrips/save-trip-modal.js +++ b/frontend/src/components/ViewTrips/save-trip-modal.js @@ -3,127 +3,32 @@ import React from 'react'; import app from '../Firebase'; import { Button, Modal, Form } from 'react-bootstrap'; -import { COLLECTION_TRIPS } from '../../constants/database.js'; +import * as DB from '../../constants/database.js'; import { formatTripData } from '../Utils/filter-input.js'; +import { createFormGroup } from './save-trip-form-elements.js'; const db = app.firestore(); -/** - * Returns a Form.Control element with input type 'text' and other fields - * specified by the function parameters. - * - * @param {string} placeholder Text placehold in the form input - * @param {React.RefObject} ref Ref attached to the value inputted in the form. - * @return {JSX.Element} The Form.Control element. - */ -function createTextFormControl(placeholder, ref) { - return ( - - ); -} - -/** - * Returns a Form.Control element with input type 'date' and other fields - * specified by the function parameters. - * - * @param {React.RefObject} refArr The list of refs attached to the emails - * inputted in the form. - * @return {JSX.Element} The Form.Control element. - */ -function createDateFormControl(defaultValue, ref) { - return ( - - ); -} - -/** - * Returns a Form.Control element with input type 'email' and other fields - * specified by the function parameters. - * - * TODO(Issue #67): Email verification before submitting the form. - * - * @param {string} placeholder Text placehold in the form input - * @param {React.RefObject} refArr The list of refs attached to the emails - * inputted in the form. - * @return {JSX.Element} The Form.Control element. - */ -function createMultiFormControl(placeholder, refArr) { - return ( - <> - {refArr.map((ref, idx) => { - return ( - - ); - })} - - ); -} - -/** - * Returns a Form.Group element with components specified by the input args. - * - * @param {string} controlId Prop that accessibly wires the nested label and - * input prop. - * @param {string} formLabel Label/title for the form input. - * @param {string} inputType Input type of the form. - * @param {string} placeholder Text placeholder in the form input. - * @param {React.RefObject} ref Ref attached to the values inputted in the form. - * @param {string} subFormText Subtext instructions under a form input. - * @return {JSX.Element} The Form.Group element. - */ -function createFormGroup(controlId, formLabel, inputType, placeholder, ref) { - let formControl; - switch(inputType) { - case 'text': - formControl = createTextFormControl(placeholder, ref); - break; - case 'date': - formControl = createDateFormControl(placeholder, ref); - break; - case 'emails': - formControl = createMultiFormControl(placeholder, ref); - break; - default: - console.error('There should be no other input type') - } - - return ( - - {formLabel} - {formControl} - - ) -} /** * Component corresponding to the save trips modal. * - * This component "acts" as a parent of the (non-existent) AddTripModal and + * This component acts as a 'pseudo-parent' of the AddTripModal and * EditTripModal components. The only differences in the implementation between - * the two fake components are dervied from the props `tripid` and - * `placeholderObj` (see below). + * the two fake components are dervied from the props `tripid` and + * `defaultFormObj` (see below). The primary difference between the add and + * edit trip modals is the former displays placeholder values in the empty form + * fields whereas the latter displays the current values of the trip in the + * respective form fields. * * @param {Object} props These are the props for this component: - * - show: Boolean that determines if the save trips modal should be displayed. - * - handleClose: Handler that closes the save trips modal upon calling. - * - refreshTripsContainer: Function that handles refreshing the TripsContainer - * component upon trip saving (Remove when fix Issue #62). + * - show: Boolean that determines if the add trips modal should be displayed. + * - handleClose: Event handler responsible for closing the add trips modal. + * - refreshTripsContainer: Handler that refreshes the TripsContainer + * component upon trip creation (Remove when fix Issue #62). * - tripId: For adding a new trip, this will be null. For editting an existing * trip, this will the document id associated with the trip. - * - placeholderObj: Object containing the placeholder/default values for the + * - defaultFormObj: Object containing the placeholder/default values for the * form input text boxes. * - key: Special React attribute that ensures a new AddTripModal instance is * created whenever this key is updated @@ -144,11 +49,19 @@ class SaveTripModal extends React.Component { this.isAddTripForm = this.props.tripId === null; - // Create the number of collaborator input box refs as number of - // collaborators specified in the placeholderObj + // For edit trips, create the number of collaborator input box refs as one + // less than the number of collaborators specified in prop `defaultFormObj` + // (do not include current user in list). + // + // TODO(Issue #71): Give user option to remove themself as a collaborator + // from current trip. const collaboratorsRefArr = []; - for (let i = 0; i < this.props.placeholderObj.collaborators.length; i++) { - collaboratorsRefArr.push(React.createRef()) + if (this.isAddTripForm) { + collaboratorsRefArr.push(React.createRef()); + } else { + for (let i = 1; i < this.props.defaultFormObj.collaborators.length; i++) { + collaboratorsRefArr.push(React.createRef()) + } } this.state = { collaboratorsRefArr: collaboratorsRefArr } } @@ -163,34 +76,34 @@ class SaveTripModal extends React.Component { /** * Creates a new Trip document in firestore with data in `tripData`. * - * @param {Object} tripData Data the new trip document will contain. + * @param {!Object} tripData Data the new trip document will contain. */ addNewTrip(tripData) { - db.collection(COLLECTION_TRIPS) + db.collection(DB.COLLECTION_TRIPS) .add(tripData) .then(docRef => { - console.log("Document written with ID: ", docRef.id); + console.log('Document written with ID: ', docRef.id); }) .catch(error => { - console.error("Error adding document: ", error); + console.error('Error adding document: ', error); }); } /** * Updates an existing Trip document in firestore with data in `tripData`. * - * @param {string} tripId The document ID of the trip that is updated. - * @param {Object} tripData Data the new trip document will contain. + * @param {!string} tripId The document ID of the trip that is updated. + * @param {!Object} tripData Data the new trip document will contain. */ updateExistingTrip(tripId, tripData) { - db.collection(COLLECTION_TRIPS) + db.collection(DB.COLLECTION_TRIPS) .doc(tripId) .set(tripData) .then(() => { - console.log("Document written with ID: ", tripId); + console.log('Document written with ID: ', tripId); }) .catch(error => { - console.error("Error adding document: ", error); + console.error('Error adding document: ', error); }); } @@ -238,6 +151,20 @@ class SaveTripModal extends React.Component { return 'Edit Trip'; } + /** Returns the default form value for the trip field specified by `field`. + * + * @param {!string} field A trip document field + * (the constants in `database.js`). + * @return {?string} Default form value for edit trip modal or null for + * add trip modals. + */ + getDefaultFormField = (field) => { + if (this.isAddTripForm) { + return null; + } + return this.props.defaultFormObj[field]; + } + /** @inheritdoc */ render() { return ( @@ -248,19 +175,54 @@ class SaveTripModal extends React.Component {
- {createFormGroup('tripNameGroup', 'Trip Name', 'text', - this.props.placeholderObj.name, this.nameRef)} - {createFormGroup('tripDescGroup', 'Trip Description', 'text', - this.props.placeholderObj.description, this.descriptionRef)} - {createFormGroup('tripDestGroup', 'Trip Destination', 'text', - this.props.placeholderObj.destination, this.destinationRef)} - {createFormGroup('tripStartDateGroup', 'Start Date', 'date', - this.props.placeholderObj.startDate, this.startDateRef)} - {createFormGroup('tripEndDateGroup', 'End Date', 'date', - this.props.placeholderObj.endDate, this.endDateRef)} - {createFormGroup('tripCollabsGroup', 'Trip Collaborators', 'emails', - this.props.placeholderObj.collaborators, - this.state.collaboratorsRefArr)} + {createFormGroup( + 'tripNameGroup', // controlId + 'Trip Name', // formLabel + 'text', // inputType + this.nameRef, // ref + 'Enter Trip Name', // placeholder + this.getDefaultFormField(DB.TRIPS_NAME) // defaultVal + )} + {createFormGroup( + 'tripDescGroup', // controlId + 'Trip Description', // formLabel + 'text', // inputType + this.descriptionRef, // ref + 'Enter Trip Description', // placeholder + this.getDefaultFormField(DB.TRIPS_DESCRIPTION) // defaultVal + )} + {createFormGroup( + 'tripDestGroup', // controlId + 'Trip Destination', // formLabel + 'text', // inputType + this.destinationRef, // ref + 'Enter Trip Destination', // placeholder + this.getDefaultFormField(DB.TRIPS_DESTINATION) // defaultVal + )} + {createFormGroup( + 'tripStartDateGroup', // controlId + 'Start Date', // formLabel + 'date', // inputType + this.startDateRef, // ref + '', // placeholder + this.getDefaultFormField(DB.TRIPS_START_DATE) // defaultVal + )} + {createFormGroup( + 'tripEndDateGroup', // controlId + 'End Date', // formLabel + 'date', // inputType + this.endDateRef, // ref + '', // placeholder + this.getDefaultFormField(DB.TRIPS_END_DATE) // defaultVal + )} + {createFormGroup( + 'tripCollabsGroup', // controlId + 'Trip Collaborators', // formLabel + 'emails', // inputType + this.state.collaboratorsRefArr, // ref + 'person@email.xyz', // placeholder + this.getDefaultFormField(DB.TRIPS_COLLABORATORS) // defaultVal + )} diff --git a/frontend/src/components/ViewTrips/trip.js b/frontend/src/components/ViewTrips/trip.js index bc8db4d9..92641d92 100644 --- a/frontend/src/components/ViewTrips/trip.js +++ b/frontend/src/components/ViewTrips/trip.js @@ -2,7 +2,8 @@ import React from 'react'; import Button from 'react-bootstrap/Button'; -import { getUserEmailFromUid } from '../Utils/temp-auth-utils.js' +import { timestampToISOString } from '../Utils/time.js'; +import { getUserEmailFromUid } from '../Utils/temp-auth-utils.js'; import ViewActivitiesButton from './view-activities-button.js'; /** @@ -15,13 +16,13 @@ import ViewActivitiesButton from './view-activities-button.js'; * dates, the months are 0 indexed rather than 1 indexed so they must be * incremented by 1 in order for the month to be correct. * - * @param {firebase.firestore.DocumentData} tripObj Object containing the fields - * and values for a Trip document. + * @param {!firebase.firestore.DocumentData} tripData Object containing the + * fields and values for a Trip document. * @return {string} Date range of the trip. */ -export function getDateRange(tripObj) { - const startDate = tripObj.start_date.toDate(); - const endDate = tripObj.end_date.toDate(); +export function getDateRange(tripData) { + const startDate = tripData.start_date.toDate(); + const endDate = tripData.end_date.toDate(); return `${startDate.getMonth() + 1}/${startDate.getDate()}/` + `${startDate.getFullYear()} - ${endDate.getMonth() + 1}/` + `${endDate.getDate()}/${endDate.getFullYear()}`; @@ -29,7 +30,7 @@ export function getDateRange(tripObj) { /** * - * @param {!Array} collaboratorUidArr Array of collaborator uids + * @param {!Array} collaboratorUidArr Array of collaborator uids * stored in trip document. * @returns {string} Collaborator emails in comma separated string. * Ex: "person1@email.com, person2@email.com". @@ -48,20 +49,43 @@ export function getCollaboratorEmails(collaboratorUidArr) { * on the 'display' side. * * @param {Object} props These are the props for this component: - * - tripObj: JS object holding a Trip document fields and corresponding values. + * - tripData: Object holding a Trip document fields and corresponding values. * - tripId: Document ID for the current Trip document. + * - handleEditTrip: Handler that displays the edit trip modal. + * - key: Special React attribute that ensures a new Trip instance is + * created whenever this key is updated */ const Trip = (props) => { + const name = props.tripData.name; + const description = props.tripData.description; + const destination = props.tripData.destination; + const collaboratorEmailsStr = + getCollaboratorEmails(props.tripData.collaborators); + + const formattedTripData = { + name: name, + description: description, + destination: destination, + start_date: timestampToISOString(props.tripData.start_date), + end_date: timestampToISOString(props.tripData.end_date), + collaborators: collaboratorEmailsStr.split(', ') + }; + return (
-

{props.tripObj.name}

-

{props.tripObj.destination}

-

{getDateRange(props.tripObj)}

-

{props.tripObj.description}

-

{getCollaboratorEmails(props.tripObj.collaborators)}

+

{name}

+

{destination}

+

{getDateRange(props.tripData)}

+

{description}

+

{collaboratorEmailsStr}

- {/* TODO(Issue 15): Add edit trip page. */} - +
); diff --git a/frontend/src/components/ViewTrips/trips-container.js b/frontend/src/components/ViewTrips/trips-container.js index 7fd851b6..0f61d8ac 100644 --- a/frontend/src/components/ViewTrips/trips-container.js +++ b/frontend/src/components/ViewTrips/trips-container.js @@ -30,14 +30,22 @@ function queryUserTrips(db) { * * @param {Promise} querySnapshot Promise * object containing the query results with zero or more Trip documents. + * @param {EventHandler} handleEditTrip Displays the edit trip modal. * @return {Promise>} Promise object containing an array * of Trip React/HTML elements corresponding to the Trip documents included * in `querySnapshot`. */ -function serveTrips(querySnapshot) { +function serveTrips(querySnapshot, handleEditTrip) { return new Promise(function(resolve) { const tripsContainer = querySnapshot.docs.map(doc => - ( )); + ( + ) + ); resolve(tripsContainer); }); } @@ -45,7 +53,7 @@ function serveTrips(querySnapshot) { /** * Returns a `
` element with the specified error message. * - * @param {string} error Error message in `componentDidMount()` catch statement. + * @param {string} error Error message in `componentDidMount` catch statement. * @return {Promise} Promise object containing a `
` element * with the error message `error` inside. */ @@ -61,6 +69,7 @@ function getErrorElement(error) { * props * * @param {Object} props These are the props for this component: + * - handleEditTrip: Handler that displays the edit trip modal. * - key: Special React attribute that ensures a new TripsContainer instance is * created whenever this key is updated (Remove when fix Issue #62). * @extends React.Component @@ -76,7 +85,8 @@ class TripsContainer extends React.Component { async componentDidMount() { try { const querySnapshot = await queryUserTrips(db); - let tripsContainer = await serveTrips(querySnapshot); + let tripsContainer = await serveTrips(querySnapshot, + this.props.handleEditTrip); this.setState({ trips: tripsContainer }); } catch (error) {