diff --git a/src/actions/experiences.js b/src/actions/experiences.js index a703993..e68cff6 100644 --- a/src/actions/experiences.js +++ b/src/actions/experiences.js @@ -8,12 +8,14 @@ import { call, put, takeLatest } from 'redux-saga/effects'; import { EXPERIENCES_FETCH_FOR_USER, EXPERIENCES_CREATE, - EXPERIENCES_EDIT + EXPERIENCES_EDIT, + EXPERIENCES_DELETE } from '../constants'; import { experiencesFetchForUser as getExperiencesForUser, experiencesCreate as postExperiences, - experiencesEdit as patchExperiences + experiencesEdit as patchExperiences, + experiencesRemove as removeExperiences } from '../lib/api'; import actionGenerator from '../lib/actionGenerator'; @@ -125,8 +127,37 @@ export function* experiencesEdit({ ); } +/** + * Deletes an existing experience. + * + * @param {object} payload - Payload for this saga action. + * @param {string} payload.id - Id of the experience to be removed + * @param {function} payload.successHandler + * Function to be executed if/when this action succeeds. + * @param {object} payload.user - Object containing user data. + * @param {object} payload.user.authentication - Object containing auth data. + * @param {string} payload.user.authentication.accessToken + * Access token for the current user. + * @param {string} payload.user.authentication.csrfToken + * CSRF token for the current user. + */ +export function* experiencesDelete({ user, id, successHandler = () => {} }) { + yield* actionGenerator( + EXPERIENCES_DELETE, + function* experienceRemoveHandler() { + yield call(removeExperiences, id, user); + yield put({ + type: `${EXPERIENCES_DELETE}_SUCCESS`, + payload: { id } + }); + }, + successHandler + ); +} + export function* watchExperiencesActions() { yield takeLatest(EXPERIENCES_FETCH_FOR_USER, experiencesFetchForUser); yield takeLatest(EXPERIENCES_CREATE, experiencesCreate); yield takeLatest(EXPERIENCES_EDIT, experiencesEdit); + yield takeLatest(EXPERIENCES_DELETE, experiencesDelete); } diff --git a/src/actions/openExperience.js b/src/actions/openExperience.js index 1d28bf8..12dce30 100644 --- a/src/actions/openExperience.js +++ b/src/actions/openExperience.js @@ -9,6 +9,7 @@ import { OPEN_EXPERIENCE_FETCH_FOR_USER, OPEN_EXPERIENCE_SCENE_CREATE, OPEN_EXPERIENCE_SCENE_EDIT, + OPEN_EXPERIENCE_SCENE_DELETE, OPEN_EXPERIENCE_COMPONENT_CREATE, OPEN_EXPERIENCE_COMPONENT_DELETE, OPEN_EXPERIENCE_COMPONENT_EDIT, @@ -21,6 +22,7 @@ import { fileCreate, sceneCreate, sceneEdit, + sceneRemove, componentEdit, componentCreate, componentRemove, @@ -169,6 +171,45 @@ export function* openExperienceSceneEdit({ ); } +/** + * Dispatches an action that deletes a scene in the current openExperience. + * + * @param {object} payload + * Payload for this saga action. + * @param {string} payload.id + * ID of this scene. + * @param {string} payload.sceneSlug + * Slug of this scene. + * @param {object} payload.user + * Object containing user data. + * @param {object} payload.user.authentication + * Object containing auth data. + * @param {string} payload.user.authentication.accessToken + * Access token for the current user. + * @param {string} payload.user.authentication.csrfToken + * CSRF token for the current user. + * @param {function} payload.successHandler + * Function to be executed if/when this action succeeds. + */ +export function* openExperienceSceneDelete({ + id, + sceneSlug, + user, + successHandler = () => {} +}) { + yield* actionGenerator( + OPEN_EXPERIENCE_SCENE_DELETE, + function* openExperienceSceneDeleteHandler() { + yield call(sceneRemove, id, user); + yield put({ + type: `${OPEN_EXPERIENCE_SCENE_DELETE}_SUCCESS`, + payload: { sceneSlug } + }); + }, + successHandler + ); +} + /** * Dispatches an action that creates a new component. * @@ -347,6 +388,7 @@ export function* watchOpenExperienceActions() { yield takeLatest(OPEN_EXPERIENCE_FETCH_FOR_USER, openExperienceFetchForUser); yield takeLatest(OPEN_EXPERIENCE_SCENE_CREATE, openExperienceSceneCreate); yield takeLatest(OPEN_EXPERIENCE_SCENE_EDIT, openExperienceSceneEdit); + yield takeLatest(OPEN_EXPERIENCE_SCENE_DELETE, openExperienceSceneDelete); yield takeLatest( OPEN_EXPERIENCE_COMPONENT_CREATE, openExperienceComponentCreate diff --git a/src/components/SceneCards/SceneCards.js b/src/components/SceneCards/SceneCards.js index c9f760f..50c34ae 100644 --- a/src/components/SceneCards/SceneCards.js +++ b/src/components/SceneCards/SceneCards.js @@ -4,7 +4,7 @@ * experience that's currently open. */ -import React from 'react'; +import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import { Link } from 'react-router-dom'; @@ -19,137 +19,201 @@ import { Tooltip, TextField } from '@material-ui/core'; -import { OpenInBrowser, Edit } from '@material-ui/icons'; +import { OpenInBrowser, Edit, DeleteForever } from '@material-ui/icons'; import SceneCardsStyles from './SceneCards.style'; -import { MODE_SCENE_EDIT } from '../../constants'; +import { + MODE_SCENE_EDIT, + FORM_MESSAGE_DELETE_SCENE_CONFIRM, + OPEN_EXPERIENCE_SCENE_DELETE +} from '../../constants'; import parseSkyFromScene from '../../lib/parseSkyFromScene'; -const SceneCards = ({ - experience: { item: experience }, - user: { username }, - classes, - history: { - location: { pathname: location } - }, - match: { - params: { sceneSlug } - } -}) => ( -
- {experience.scenes && - Object.entries(experience.scenes).map(([id, scene]) => { - const { title, body, field_slug: slug } = scene; - const openPath = `/experience/vreditor/${ - experience.field_experience_path - }/${slug}`; - const editPath = `/experience/vreditor/${ - experience.field_experience_path - }/${slug}/${MODE_SCENE_EDIT}`; - const sharePath = `${window.location.origin}/experience/${username}/${ - experience.field_experience_path - }/${slug}`; - const sky = parseSkyFromScene(scene, true); - - return ( - - {sky && ( - - )} - - - {title} - - - - - - - - - - - - - - ); - })} -
-); - -SceneCards.propTypes = { - classes: PropTypes.shape({}).isRequired, - user: PropTypes.shape({ - username: PropTypes.string.isRequired - }).isRequired, - experience: PropTypes.shape({ - item: PropTypes.shape({ - scenes: PropTypes.objectOf( - PropTypes.shape({ - id: PropTypes.string, - title: PropTypes.string, - field_slug: PropTypes.string, - field_photosphere: PropTypes.shape({ - meta: PropTypes.shape({ - derivatives: PropTypes.shape({ - sc: PropTypes.string +class SceneCards extends Component { + static propTypes = { + classes: PropTypes.shape({}).isRequired, + user: PropTypes.shape({ + username: PropTypes.string.isRequired + }).isRequired, + experience: PropTypes.shape({ + item: PropTypes.shape({ + scenes: PropTypes.objectOf( + PropTypes.shape({ + id: PropTypes.string, + title: PropTypes.string, + field_slug: PropTypes.string, + field_photosphere: PropTypes.shape({ + meta: PropTypes.shape({ + derivatives: PropTypes.shape({ + sc: PropTypes.string + }) }) }) }) - }) - ) - }) - }).isRequired, - history: PropTypes.shape({ - location: PropTypes.shape({ - pathname: PropTypes.string.isRequired - }).isRequired - }).isRequired, - match: PropTypes.shape({ - params: PropTypes.shape({ - experienceSlug: PropTypes.string.isRequired, - sceneSlug: PropTypes.string - }).isRequired - }).isRequired -}; + ) + }) + }).isRequired, + history: PropTypes.shape({ + location: PropTypes.shape({ + pathname: PropTypes.string.isRequired + }).isRequired + }).isRequired, + match: PropTypes.shape({ + params: PropTypes.shape({ + experienceSlug: PropTypes.string.isRequired, + sceneSlug: PropTypes.string + }).isRequired + }).isRequired, + dispatch: PropTypes.func.isRequired + }; + + /** + * Dispatches an action to delete the specified scene. + */ + removeScene = (id, sceneSlug) => { + const { + dispatch, + user, + experience: { item: experience }, + history: { + replace, + location: { pathname: location } + } + } = this.props; + + dispatch({ + type: OPEN_EXPERIENCE_SCENE_DELETE, + id, + sceneSlug, + user, + successHandler: () => { + const experiencePath = `/experience/vreditor/${ + experience.field_experience_path + }`; + const scenePath = `${experiencePath}/${sceneSlug}`; + + // Unload scene on sucessfull delete if loaded. + if (location === scenePath) { + replace(experiencePath); + } + } + }); + }; + + /** + * {@inheretdoc} + */ + render() { + const { + experience: { item: experience }, + user: { username }, + classes, + history: { + location: { pathname: location } + }, + match: { + params: { sceneSlug } + } + } = this.props; + + return ( +
+ {experience.scenes && + Object.entries(experience.scenes).map(([id, scene]) => { + const { title, body, field_slug: slug } = scene; + const openPath = `/experience/vreditor/${ + experience.field_experience_path + }/${slug}`; + const editPath = `/experience/vreditor/${ + experience.field_experience_path + }/${slug}/${MODE_SCENE_EDIT}`; + const sharePath = `${ + window.location.origin + }/experience/${username}/${ + experience.field_experience_path + }/${slug}`; + const sky = parseSkyFromScene(scene, true); + + return ( + + {sky && ( + + )} + + + {title} + + + + + + + + + + + + + + + + + ); + })} +
+ ); + } +} export default withStyles(SceneCardsStyles)(SceneCards); diff --git a/src/components/SceneCards/__snapshots__/SceneCards.test.js.snap b/src/components/SceneCards/__snapshots__/SceneCards.test.js.snap index fd073a1..b687412 100644 --- a/src/components/SceneCards/__snapshots__/SceneCards.test.js.snap +++ b/src/components/SceneCards/__snapshots__/SceneCards.test.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` Matches its snapshot 1`] = `"

test

This is my test description

"`; +exports[` Matches its snapshot 1`] = `"

test

This is my test description

"`; diff --git a/src/constants/experiences.js b/src/constants/experiences.js index 0cd9884..9e5a9e1 100644 --- a/src/constants/experiences.js +++ b/src/constants/experiences.js @@ -6,3 +6,4 @@ export const EXPERIENCES_FETCH_FOR_USER = 'EXPERIENCES_FETCH_FOR_USER'; export const EXPERIENCES_CREATE = 'EXPERIENCES_CREATE'; export const EXPERIENCES_EDIT = 'EXPERIENCES_EDIT'; +export const EXPERIENCES_DELETE = 'EXPERIENCES_DELETE'; diff --git a/src/constants/form.js b/src/constants/form.js index c391b83..c510ab7 100644 --- a/src/constants/form.js +++ b/src/constants/form.js @@ -7,6 +7,10 @@ export const FORM_BUTTON_INSERT_UPDATE = 'Save'; export const FORM_BUTTON_DELETE = 'Delete'; export const FORM_MESSAGE_DELETE_CONFIRM = 'Are you sure you want to delete this component?'; +export const FORM_MESSAGE_DELETE_SCENE_CONFIRM = + 'Are you sure you want to delete this scene? \nAll components it contains will be permanently removed. This action cannot be undone!'; +export const FORM_MESSAGE_DELETE_EXPERIENCE_CONFIRM = + 'Are you sure you want to delete this entire experience? \nAll scenes, components it contains will be permanently removed. This action cannot be undone!'; export const FORM_BUTTON_REGISTER = 'Register'; export const FORM_BUTTON_RESET_PASSWORD = 'Reset Password'; export const FORM_BUTTON_FORGOT_PASSWORD = 'Forgot Password'; diff --git a/src/constants/openExperience.js b/src/constants/openExperience.js index 67fbd97..c895c86 100644 --- a/src/constants/openExperience.js +++ b/src/constants/openExperience.js @@ -6,6 +6,7 @@ export const OPEN_EXPERIENCE_FETCH_FOR_USER = 'OPEN_EXPERIENCE_FETCH_FOR_USER'; export const OPEN_EXPERIENCE_SCENE_CREATE = 'OPEN_EXPERIENCE_SCENE_CREATE'; export const OPEN_EXPERIENCE_SCENE_EDIT = 'OPEN_EXPERIENCE_SCENE_EDIT'; +export const OPEN_EXPERIENCE_SCENE_DELETE = 'OPEN_EXPERIENCE_SCENE_DELETE'; export const OPEN_EXPERIENCE_SCENE_FIELD_PRESAVE = 'OPEN_EXPERIENCE_SCENE_FIELD_PRESAVE'; export const OPEN_EXPERIENCE_COMPONENT_FIELD_PRESAVE = diff --git a/src/lib/api/experiences.js b/src/lib/api/experiences.js index 28123a4..2383fd8 100644 --- a/src/lib/api/experiences.js +++ b/src/lib/api/experiences.js @@ -94,3 +94,20 @@ export const experiencesEdit = async ( } } }); + +/** + * Delete a given experience via the API. + * + * @param {string} id + * ID of experience. + * @param {object} user + * Object containing information about the current user. + * @param {object} user.authentication + * Object containing auth data. + * @param {string} user.authentication.accessToken + * Access token for the current user. + * @param {string} user.authentication.csrfToken + * CSRF token for the current user. + */ +export const experiencesRemove = async (id, { authentication }) => + axiosInstance(authentication).delete(`${API_ENDPOINT_EXPERIENCE}/${id}`); diff --git a/src/lib/api/experiences.test.js b/src/lib/api/experiences.test.js index d81eed7..bb43815 100644 --- a/src/lib/api/experiences.test.js +++ b/src/lib/api/experiences.test.js @@ -9,6 +9,7 @@ import mockAxios from 'jest-mock-axios'; import { experiencesFetchForUser, experiencesCreate, + experiencesRemove, experiencesEdit } from './experiences'; import { clientId } from '../../config'; @@ -70,6 +71,20 @@ describe('api->experiences', () => { }); }); + it('experiences->experiencesRemove()', () => { + const id = '42'; + experiencesRemove(id, { + authentication: { + accessToken: 'test', + csrfToken: 'test' + } + }); + + expect(mockAxios.delete).toHaveBeenCalledWith( + `${API_ENDPOINT_EXPERIENCE}/${id}` + ); + }); + it('experiences->experiencesEdit()', () => { const id = '10'; const title = 'test'; diff --git a/src/lib/api/index.js b/src/lib/api/index.js index 1b93bba..602ec82 100644 --- a/src/lib/api/index.js +++ b/src/lib/api/index.js @@ -13,9 +13,15 @@ import { import { experiencesFetchForUser, experiencesCreate, - experiencesEdit + experiencesEdit, + experiencesRemove } from './experiences'; -import { sceneCreate, sceneEdit, sceneAttachComponent } from './scene'; +import { + sceneCreate, + sceneEdit, + sceneRemove, + sceneAttachComponent +} from './scene'; import { fileImageCreate, fileVideoCreate, fileCreate } from './file'; import { componentEdit, componentCreate, componentRemove } from './component'; @@ -32,8 +38,10 @@ export { resetUserPassword, experiencesFetchForUser, experiencesCreate, + experiencesRemove, sceneCreate, sceneEdit, + sceneRemove, sceneAttachComponent, componentCreate, componentEdit, diff --git a/src/lib/api/scene.js b/src/lib/api/scene.js index e82daf1..856c4c7 100644 --- a/src/lib/api/scene.js +++ b/src/lib/api/scene.js @@ -121,6 +121,23 @@ export const sceneEdit = async (id, fields, { authentication }) => { }); }; +/** + * Delete a given scene via the API. + * + * @param {string} id + * ID of scene. + * @param {object} user + * Object containing information about the current user. + * @param {object} user.authentication + * Object containing auth data. + * @param {string} user.authentication.accessToken + * Access token for the current user. + * @param {string} user.authentication.csrfToken + * CSRF token for the current user. + */ +export const sceneRemove = async (id, { authentication }) => + axiosInstance(authentication).delete(`${API_ENDPOINT_SCENE}/${id}`); + /** * Forms a relationship between a given component and scene. * diff --git a/src/pages/Dashboard/Dashboard.js b/src/pages/Dashboard/Dashboard.js index 2069f3f..5fc3ee0 100644 --- a/src/pages/Dashboard/Dashboard.js +++ b/src/pages/Dashboard/Dashboard.js @@ -15,11 +15,15 @@ import { Tooltip, withStyles } from '@material-ui/core'; -import { AddBox, Edit, OpenInBrowser } from '@material-ui/icons'; +import { AddBox, Edit, OpenInBrowser, DeleteForever } from '@material-ui/icons'; import { DashboardLayout } from '../../layouts'; import { Message } from '../../components'; -import { EXPERIENCES_FETCH_FOR_USER } from '../../constants'; +import { + EXPERIENCES_FETCH_FOR_USER, + EXPERIENCES_DELETE, + FORM_MESSAGE_DELETE_EXPERIENCE_CONFIRM +} from '../../constants'; import DashboardStyles from './Dashboard.style'; class Dashboard extends Component { @@ -68,6 +72,19 @@ class Dashboard extends Component { }); } + /** + * Dispatches an action to delete the specified experience. + */ + removeExperience = experienceID => { + const { dispatch, user } = this.props; + + dispatch({ + type: EXPERIENCES_DELETE, + id: experienceID, + user + }); + }; + /** * {@inheretdoc} */ @@ -80,8 +97,9 @@ class Dashboard extends Component { return ( - On this page you can create an experience, or open one of your - existing experiences for editing by clicking the Open button. + On this page you can create an experience, open one of your existing + experiences for editing by clicking the Open button, or delete an + experience by clicking the delete button.
+ + + ) diff --git a/src/pages/Dashboard/__snapshots__/Dashboard.test.js.snap b/src/pages/Dashboard/__snapshots__/Dashboard.test.js.snap index b42c808..76f31aa 100644 --- a/src/pages/Dashboard/__snapshots__/Dashboard.test.js.snap +++ b/src/pages/Dashboard/__snapshots__/Dashboard.test.js.snap @@ -1,3 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` Matches its snapshot 1`] = `"
\\"EditVR

Experiences

On this page you can create an experience, or open one of your existing experiences for editing by clicking the Open button.

test experience

this is a test experience

"`; +exports[` Matches its snapshot 1`] = `"
\\"EditVR

Experiences

On this page you can create an experience, open one of your existing experiences for editing by clicking the Open button, or delete an experience by clicking the delete button.

test experience

this is a test experience

"`; diff --git a/src/reducers/experiences.js b/src/reducers/experiences.js index d6bd751..8d96812 100644 --- a/src/reducers/experiences.js +++ b/src/reducers/experiences.js @@ -6,7 +6,8 @@ import { EXPERIENCES_FETCH_FOR_USER, EXPERIENCES_CREATE, - EXPERIENCES_EDIT + EXPERIENCES_EDIT, + EXPERIENCES_DELETE } from '../constants'; /** @@ -132,6 +133,28 @@ export default function experiences(state = defaultState, action) { }; } + /** + * Reducer that handles experience deletion success actions. + */ + case `${EXPERIENCES_DELETE}_SUCCESS`: { + const { id } = action.payload; + + // Filter out the deleted experience. + const newItems = [...state.items].filter(item => { + if (item.id === id) { + return false; + } + + return true; + }); + + return { + loading: false, + error: null, + items: newItems + }; + } + /** * Reducer that handles experience edit failure actions. */ diff --git a/src/reducers/openExperience.js b/src/reducers/openExperience.js index 33d4347..1266b68 100644 --- a/src/reducers/openExperience.js +++ b/src/reducers/openExperience.js @@ -8,6 +8,7 @@ import { OPEN_EXPERIENCE_FETCH_FOR_USER, OPEN_EXPERIENCE_SCENE_CREATE, OPEN_EXPERIENCE_SCENE_EDIT, + OPEN_EXPERIENCE_SCENE_DELETE, OPEN_EXPERIENCE_COMPONENT_FIELD_PRESAVE, OPEN_EXPERIENCE_SCENE_FIELD_PRESAVE, OPEN_EXPERIENCE_COMPONENT_EDIT, @@ -132,6 +133,23 @@ export default function openExperiences(state = defaultState, action) { }; } + /** + * Reducer that handles scene deletion success actions. + */ + case `${OPEN_EXPERIENCE_SCENE_DELETE}_SUCCESS`: { + const { sceneSlug } = action.payload; + + // Filter out the deleted scene. + const newItem = Object.assign({}, state.item); + delete newItem.scenes[sceneSlug]; + + return { + loading: false, + error: null, + item: newItem + }; + } + /** * Reducer that handles scene edit loading actions. */