diff --git a/frontend/package.json b/frontend/package.json index 7ba15ffe..891f2261 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,9 +7,9 @@ "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", "bootstrap": "^4.5.0", - "firebase": "^7.15.5", + "firebase": "^7.17.1", + "history": "^5.0.0", "moment-timezone": "^0.5.31", - "history": "^5.0.0" "react": "^16.13.1", "react-bootstrap": "1.0.1", "react-dom": "^16.13.1", diff --git a/frontend/src/components/ViewActivities/editActivity.js b/frontend/src/components/ViewActivities/editActivity.js index 1e645f1b..3b118263 100644 --- a/frontend/src/components/ViewActivities/editActivity.js +++ b/frontend/src/components/ViewActivities/editActivity.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Button, Col, Form, Row } from 'react-bootstrap'; +import { Button, Form } from 'react-bootstrap'; import { getField, writeActivity } from './activityfns.js'; import * as DB from '../../constants/database.js' import { countryList } from '../../constants/countries.js'; @@ -11,8 +11,8 @@ const db = app.firestore(); /** * React component for the form that's used when the user is editing an activity. - * - * @property {Object} props ReactJS props. + * + * @property {Object} props ReactJS props. * @property {ActivityInfo} props.activity The activity to display. * @property {function} props.submitFunction The function to run upon submission. */ @@ -29,7 +29,7 @@ class EditActivity extends React.Component { this.deleteActivity = this.deleteActivity.bind(this); this.timezoneDropdown = this.timezoneDropdown.bind(this); - // References. + // References. this.editTitleRef = React.createRef(); this.editStartDateRef = React.createRef(); this.editEndDateRef = React.createRef(); @@ -41,7 +41,7 @@ class EditActivity extends React.Component { this.startTz = React.createRef(); this.endTz = React.createRef(); } - + /** * Edit an activity in the database upon form submission. * TODO: Update times as well! This only does the text field forms (#64). @@ -72,8 +72,8 @@ class EditActivity extends React.Component { this.props.submitFunction(); } - // "Flip switch" on timezone dropdown so the dropdown's contents update to the - // selected country's timezones. + // "Flip switch" on timezone dropdown so the dropdown's contents update to the + // selected country's timezones. startTimeTzUpdate = () => { this.setState({startTz : !this.state.startTz})}; endTimeTzUpdate = () => { this.setState({endTz : !this.state.endTz})}; @@ -81,10 +81,10 @@ class EditActivity extends React.Component { * Returns a dropdown of all the timezones. * The dropdown's values change based on the corrresponding country dropdown to * reduce scrolling and ensure that the location corresponds to the time zone. - * + * * Tests done manually using UI. - * - * @param {string} st Either 'start' or 'end' depending on whether the + * + * @param {string} st Either 'start' or 'end' depending on whether the * timezone is for the start or end timezone. * @return {HTML} HTML dropdown item. */ @@ -110,13 +110,13 @@ class EditActivity extends React.Component { ) } /** - * Create a dropdown of all the countries. - * This dropdown is linked to the corresponding timezone dropdown, - * so when the country changes here, the values in the timezone dropdown - * change as well. - * + * Create a dropdown of all the countries. + * This dropdown is linked to the corresponding timezone dropdown, + * so when the country changes here, the values in the timezone dropdown + * change as well. + * * @param {ref} ref The reference to attach to the dropdown. - * @param {ref} tzref The corresponding time zone reference field. + * @param {ref} tzref The corresponding time zone reference field. * @return {HTML} HTML dropdown of all the countries with timezones. */ countriesDropdown(ref, tzref) { @@ -133,8 +133,8 @@ class EditActivity extends React.Component { } /** - * Delete this activity. - * + * Delete this activity. + * * @return {boolean} true if the activity was successfully deleted. */ async deleteActivity() { @@ -156,7 +156,7 @@ class EditActivity extends React.Component { {formElements.textElementFormGroup( 'formActivityTitle', // controlId 'Title:', // formLabel - activity[DB.ACTIVITIES_TITLE],// placeHolder + activity[DB.ACTIVITIES_TITLE],// placeHolder this.editTitleRef // ref )} {formElements.locationElementFormGroup( @@ -173,28 +173,28 @@ class EditActivity extends React.Component { 'formActivityStartTime', // controlId 'From:', // formLabel this.editStartDateRef, // dateRef - null, // dateDefault - this.editStartTimeRef, // timeRef, - null, // timeDefault, - this.timezoneDropdown('start') // tzpicker + null, // dateDefault + this.editStartTimeRef, // timeRef, + null, // timeDefault, + this.timezoneDropdown('start') // tzpicker )} {formElements.dateTimeTzFormGroup( 'formActivityEndTime', // controlId 'To:', // formLabel this.editEndDateRef, // dateRef - null, // dateDefault - this.editEndTimeRef, // timeRef, - null, //timeDefault, - this.timezoneDropdown('end') // tzpicker + null, // dateDefault + this.editEndTimeRef, // timeRef, + null, //timeDefault, + this.timezoneDropdown('end') // tzpicker )} {formElements.textElementFormGroup( 'formActivityDescription', // controlId 'Description:', // formLabel - getField(activity, DB.ACTIVITIES_DESCRIPTION, 'Add some details!'), // placeHolder + getField(activity, DB.ACTIVITIES_DESCRIPTION, 'Add some details!'), // placeHolder this.editDescriptionRef // ref )} - diff --git a/frontend/src/components/ViewActivities/editActivityFormElements.js b/frontend/src/components/ViewActivities/editActivityFormElements.js index f53ccaba..8bde0ef6 100644 --- a/frontend/src/components/ViewActivities/editActivityFormElements.js +++ b/frontend/src/components/ViewActivities/editActivityFormElements.js @@ -1,8 +1,8 @@ import React from 'react'; -import { Button, Col, Form, Row } from 'react-bootstrap'; +import { Col, Form, Row } from 'react-bootstrap'; -// This file waas written after #87 was created. -// As a result, some fields and functions may not be used yet. +// This file waas written after #87 was created. +// As a result, some fields and functions may not be used yet. const TITLEWIDTH = 3; const COUNTRYWIDTH = 6; const DATEWIDTH = 4; @@ -10,11 +10,11 @@ const TIMEWIDTH = 2; const TZPICKERWIDTH = 3; /** - * Create a Text element Form Group for the editActivity form. - * + * Create a Text element Form Group for the editActivity form. + * * @param {string} controlId FormGroup's control ID. - * @param {string} formLabel The label of the field for this FormGroup. - * @param {string} placeHolder The input's placeholder. + * @param {string} formLabel The label of the field for this FormGroup. + * @param {string} placeHolder The input's placeholder. * @param {ref} ref The input's reference. * @returns {HTML} A text element form group. */ @@ -24,7 +24,7 @@ export function textElementFormGroup(controlId, formLabel, placeHolder, ref) { {formLabel} @@ -32,11 +32,11 @@ export function textElementFormGroup(controlId, formLabel, placeHolder, ref) { } /** - * Create a Location Dropdown element Form Group for the editActivity form. - * + * Create a Location Dropdown element Form Group for the editActivity form. + * * @param {string} controlId FormGroup's control ID. - * @param {string} formLabel The label of the field for this FormGroup. - * @param {string} dropdown The dropdown. + * @param {string} formLabel The label of the field for this FormGroup. + * @param {string} dropdown The dropdown. * @returns {HTML} a location dropdown form group. */ export function locationElementFormGroup(controlId, formLabel, dropdown) { @@ -49,19 +49,19 @@ export function locationElementFormGroup(controlId, formLabel, dropdown) { } /** - * Create a Form Group for inserting date, time, and timezone for + * Create a Form Group for inserting date, time, and timezone for * the editActivity form.. - * - * @param {string} controlId FormGroup's control ID. - * @param {string} formLabel Label of the field for this FormGroup. - * @param {ref} dateRef Date's reference. - * @param {string} dateDefault Default date. - * @param {ref} timeRef Time's reference. - * @param {ref} timeDefault Default time. - * @param {HTML} tzpicker Timezone picker dropdown. - * @returns {HTML} A FormGroup for date, time, and timezone. + * + * @param {string} controlId FormGroup's control ID. + * @param {string} formLabel Label of the field for this FormGroup. + * @param {ref} dateRef Date's reference. + * @param {string} dateDefault Default date. + * @param {ref} timeRef Time's reference. + * @param {ref} timeDefault Default time. + * @param {HTML} tzpicker Timezone picker dropdown. + * @returns {HTML} A FormGroup for date, time, and timezone. */ -export function dateTimeTzFormGroup(controlId, formLabel, dateRef, +export function dateTimeTzFormGroup(controlId, formLabel, dateRef, dateDefault, timeRef, timeDefault, tzpicker) { return ( @@ -76,4 +76,4 @@ export function dateTimeTzFormGroup(controlId, formLabel, dateRef, {tzpicker} ); -} \ No newline at end of file +} diff --git a/frontend/src/components/ViewTrips/delete-trip-button.js b/frontend/src/components/ViewTrips/delete-trip-button.js new file mode 100644 index 00000000..b801b859 --- /dev/null +++ b/frontend/src/components/ViewTrips/delete-trip-button.js @@ -0,0 +1,119 @@ +import React from 'react'; + +import app from '../Firebase/'; +import Button from 'react-bootstrap/Button'; + +import * as DB from '../../constants/database.js'; + +const db = app.firestore(); +const LIMIT_QUERY_DOCS_RETRIEVED = 5; + +/** + * Component used to delete a Trip. + * + * + * @param {Object} props These are the props for this component: + * - tripId: Document ID for the current Trip document. + * - refreshTripsContainer: Handler that refreshes the TripsContainer + * component upon trip creation (Remove when fix Issue #62). + */ +const DeleteTripsButton = (props) => { + /** + * Deletes documents in query with a batch delete. + * + * This was taken from the delete collection snippets in the documentation + * at https://firebase.google.com/docs/firestore/manage-data/delete-data. + * + * @param {firebase.firestore.Firestore} db Firestore database instance. + * @param {firebase.firestore.Query} query Query containing documents from + * the activities subcollection of a trip documents. + * @param {Function} resolve Resolve function that returns a void Promise. + */ + async function deleteQueryBatch(db, query, resolve) { + const snapshot = await query.get(); + + const batchSize = snapshot.size; + if (batchSize === 0) { + // When there are no documents left, we are done. + resolve(); + return; + } + + // Delete documents in a batch. + const batch = db.batch(); + snapshot.docs.forEach((doc) => { + batch.delete(doc.ref); + }); + await batch.commit(); + + // Recurse on the next process tick, to avoid + // exploding the stack. + process.nextTick(() => { + deleteQueryBatch(db, query, resolve); + }); + } + + /** + * Deletes a trip's subcollection of activities corresponding to the + * `tripId` prop. + * + * This was adapted from the delete collection snippets in the documentation + * at https://firebase.google.com/docs/firestore/manage-data/delete-data. + * + * TODO(Issue #81): Consider deleting data with callable cloud function + * https://firebase.google.com/docs/firestore/solutions/delete-collections. + */ + async function deleteTripActivities() { + const query = db.collection(DB.COLLECTION_TRIPS) + .doc(props.tripId) + .collection(DB.COLLECTION_ACTIVITIES) + .orderBy(DB.ACTIVITIES_TITLE) + .limit(LIMIT_QUERY_DOCS_RETRIEVED); + + return new Promise((resolve, reject) => { + deleteQueryBatch(db, query, resolve).catch(reject); + }); + } + + /** + * Deletes a trip and its subcollection of activities corrsponding to the + * `tripId` prop and then refreshes the TripsContainer component. + * + * TODO(Issue #62): Remove refreshTripsContainer. + */ + async function deleteTrip() { + if (window.confirm('Are you sure you want to delete this trip? This' + + ' action cannot be undone!')) { + await deleteTripActivities() + .then(() => { + console.log("Activity subcollection successfully deleted for trip" + + " with id: ", props.tripId); + }) + .catch(error => { + console.error("Error deleting activities subcollection: ", error); + }); + + db.collection(DB.COLLECTION_TRIPS) + .doc(props.tripId) + .delete() + .then(() => { + console.log("Document successfully deleted with id: ", props.tripId); + }).catch(error => { + console.error("Error removing document: ", error); + }); + + props.refreshTripsContainer(); + } + } + + return ( + + ); +} + +export default DeleteTripsButton; diff --git a/frontend/src/components/ViewTrips/index.js b/frontend/src/components/ViewTrips/index.js index d12d0ef8..08ce2971 100644 --- a/frontend/src/components/ViewTrips/index.js +++ b/frontend/src/components/ViewTrips/index.js @@ -109,6 +109,7 @@ class ViewTrips extends React.Component { diff --git a/frontend/src/components/ViewTrips/trip.js b/frontend/src/components/ViewTrips/trip.js index 92641d92..6e7d87d5 100644 --- a/frontend/src/components/ViewTrips/trip.js +++ b/frontend/src/components/ViewTrips/trip.js @@ -4,6 +4,7 @@ import Button from 'react-bootstrap/Button'; import { timestampToISOString } from '../Utils/time.js'; import { getUserEmailFromUid } from '../Utils/temp-auth-utils.js'; +import DeleteTripButton from './delete-trip-button.js'; import ViewActivitiesButton from './view-activities-button.js'; /** @@ -29,6 +30,8 @@ export function getDateRange(tripData) { } /** + * Return collaborator emails corresponding to the collaborator uid's + * `collaboratorUidArr` in a comma separated string. * * @param {!Array} collaboratorUidArr Array of collaborator uids * stored in trip document. @@ -52,6 +55,8 @@ export function getCollaboratorEmails(collaboratorUidArr) { * - 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. + * - refreshTripsContainer: Handler that refreshes the TripsContainer + * component upon trip creation (Remove when fix Issue #62). * - key: Special React attribute that ensures a new Trip instance is * created whenever this key is updated */ @@ -79,6 +84,10 @@ const Trip = (props) => {

{description}

{collaboratorEmailsStr}

+