Commit c3ddb098 authored by Florent Chehab's avatar Florent Chehab
Browse files

Editor redesign in progress

parent f6c6583c
Pipeline #35649 canceled with stages
......@@ -2,6 +2,14 @@
* This file contains action types for the custom redux behaviors (that are not REST related)
*/
// handling of full screen dialog and notifications in an original manner,
// ie common fot the entire app
export const OPEN_FULL_SCREEN_DIALOG = "OPEN_FULL_SCREEN_DIALOG";
export const CLOSE_FULL_SCREEN_DIALOG = "CLOSE_UNIVERSITY_BEING_VIEWED";
export const CREATE_NOTIFICATION = "CREATE_NOTIFICATION";
export const DELETE_NOTIFICATION = "DELETE_NOTIFICATION";
// Other redux actions
export const SAVE_MAIN_MAP_STATUS = "SAVE_MAIN_MAP_STATUS";
export const SAVE_SELECTED_UNIVERSITIES = "SAVE_SELECTED_UNIVERSITIES";
export const SAVE_FILTER_CONFIG = "SAVE_FILTER_CONFIG";
......
/*
* This file contains the redux actions related to handling the app fullScreenDialog
*/
import {
OPEN_FULL_SCREEN_DIALOG,
CLOSE_FULL_SCREEN_DIALOG,
} from "./action-types";
/**
* Action: Open the full screen dialog and pass attributes.
*
* @export
* @param {Node} innerNodes
* @returns {object}
*/
export function openFullScreenDialog(innerNodes) {
return {
type: OPEN_FULL_SCREEN_DIALOG,
innerNodes
};
}
/**
* Action: Close the full screen dialog
*
* @export
* @returns {object}
*/
export function closeFullScreenDialog() {
return {
type: CLOSE_FULL_SCREEN_DIALOG,
};
}
/*
* This file contains the redux actions related to the handling of notifications in the app
*/
import {
CREATE_NOTIFICATION,
DELETE_NOTIFICATION,
} from "./action-types";
/**
* Action: Create a notifiacation in the app
*
* @export
* @param {object} attrs
* @returns {object}
*/
export function createNotification(attrs) {
return {
type: CREATE_NOTIFICATION,
attrs
};
}
/**
* Action: Delete a notification
* TODO add support for multiple
*
* @export
* @returns {object}
*/
export function deleteNotification() {
return {
type: DELETE_NOTIFICATION,
};
}
......@@ -16,7 +16,9 @@ import Chip from "@material-ui/core/Chip";
import Avatar from "@material-ui/core/Avatar";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import SchoolIcon from "@material-ui/icons/School";
import { mainListItems, secondaryListItems, thirdListItems } from "./template/listItems";
import { mainListItems, secondaryListItems, thirdListItems } from "./appRelated/listItems";
import FullScreenDialog from "./appRelated/FullScreenDialog";
import Notifications from "./appRelated/Notifications";
import { connect } from "react-redux";
import CustomComponentForAPI from "./CustomComponentForAPI";
......@@ -111,6 +113,10 @@ class App extends CustomComponentForAPI {
</Drawer>
<Notifications />
<FullScreenDialog />
<main className={classNames(classes.content, classes.noPaddingTop)}>
<div className={classes.paddingTop}>
<Route path="/app/" exact={true} component={PageHome} />
......@@ -126,6 +132,7 @@ class App extends CustomComponentForAPI {
<div >
<Route path="/app/university/:id" component={PageUniversity} />
</div>
</main>
</div>
......
import React, { Component } from "react";
import Dialog from "@material-ui/core/Dialog";
import Slide from "@material-ui/core/Slide";
import PropTypes from "prop-types";
import { connect } from "react-redux";
/**
* Function to enable the FullScreenDialog to have nice transitions
* @returns
*/
function Transition(props) {
return <Slide direction="up" {...props} />;
}
/**
* Class to display a full screen dialog.
* It is connected to the redux state to have only one of such dialog across the app.
*
* @class FullScreenDialog
* @extends {Component}
*/
class FullScreenDialog extends Component {
render() {
return (
<Dialog
fullScreen
open={this.props.open}
TransitionComponent={Transition}
>
{this.props.innerNodes}
</Dialog>
);
}
}
FullScreenDialog.propTypes = {
open: PropTypes.bool.isRequired,
innerNodes: PropTypes.node.isRequired,
};
// Get the props from the redux store.
const mapStateToProps = (state) => ({ ...state.app.fullScreenDialog });
export default connect(mapStateToProps)(FullScreenDialog);
import React from "react";
import React, { Component } from "react";
import PropTypes from "prop-types";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import Paper from "@material-ui/core/Paper";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import IconButton from "@material-ui/core/IconButton";
import Typography from "@material-ui/core/Typography";
import CloseIcon from "@material-ui/icons/Close";
import Slide from "@material-ui/core/Slide";
import CustomComponentForAPI from "../../CustomComponentForAPI";
import Alert from "./Alert";
import Notification from "./Notification";
// import renderFieldsMixIn from "./editorFunctions/renderFieldsMixIn";
......@@ -20,9 +15,6 @@ import Notification from "./Notification";
// eslint-disable-next-line no-unused-vars
import Form from "./Form";
function Transition(props) {
return <Slide direction="up" {...props} />;
}
/**
* Class to handle editions of models on the frontend. It should be extended, eg:
......@@ -37,10 +29,9 @@ function Transition(props) {
}
*
* @class Editor
* @extends {CustomComponentForAPI}
* @extends React.Component
* @extends {Component}
*/
class Editor extends CustomComponentForAPI {
class Editor extends Component {
state = {
alert: {
......@@ -49,8 +40,6 @@ class Editor extends CustomComponentForAPI {
notification: {
open: false,
},
lastSaveTime: this.props.lastSaveTime,
lastUpdateTimeInModel: this.props.lastUpdateTimeInModel,
}
formRef = React.createRef();
......@@ -114,7 +103,7 @@ class Editor extends CustomComponentForAPI {
);
} else {
this.notifyNoChangesDetected();
this.props.handleCloseEditor();
this.closeEditor(false);
}
} else {
......@@ -127,24 +116,41 @@ class Editor extends CustomComponentForAPI {
if (this.formHasChanges()) {
this.alertChangesNotSaved();
} else {
this.props.handleCloseEditor();
this.notifyNoChangesDetected();
this.closeEditor(false);
}
}
/**
* Effectively close the editor window and notify if there was something new that was saved
*
* @param {Boolean} somethingWasSaved
* @memberof Editor
*/
closeEditor(somethingWasSaved){
this.props.closeFullScreenDialog();
this.props.handleEditorWasClosed(somethingWasSaved);
}
/**
* This function is extended to handle custom behaviors on component update
*
* @memberof Editor
*/
componentDidUpdate(prevProps, prevState, snapshot) {
super.componentDidUpdate(prevProps, prevState, snapshot);
componentDidUpdate(prevProps) {
// Open the editor if needed
if (this.props.open) {
this.props.openFullScreenDialog(this.renderEditor());
}
// TODO is this needed with redesign
// To handle moderation, we use the editor in "hacky" manner
// with the `dataToSave` props, which triggers a save.
const { dataToSave } = this.props;
if (dataToSave) {
this.props.handleCloseEditor();
this.props.handleEditorWasClosed();
this.performSave(dataToSave);
}
// end of hacky behavior.
......@@ -158,10 +164,10 @@ class Editor extends CustomComponentForAPI {
}
//saving data was successful
if (this.state.lastSaveTime < this.props.lastSaveTime) {
if (prevProps.lastSaveTime < this.props.lastSaveTime) {
// We check if data was moderated
let message,
lastUpdateTimeInModel = this.state.lastUpdateTimeInModel;
lastUpdateTimeInModel = prevProps.lastUpdateTimeInModel;
const newUpdateTimeInModel = this.props.lastUpdateTimeInModel;
if (lastUpdateTimeInModel == newUpdateTimeInModel) {
......@@ -173,7 +179,7 @@ class Editor extends CustomComponentForAPI {
this.setState({ lastSaveTime: this.props.lastSaveTime, lastUpdateTimeInModel });
this.notifySaveSuccessful(message);
this.props.handleCloseEditor(true);
this.closeEditor(true);
}
}
......@@ -190,45 +196,46 @@ class Editor extends CustomComponentForAPI {
}
customRender() {
// TODO move dialog, notification and alert to somewhere else if possible
/**
* Main rendering function. Its return value is passed to the
* fullscreen app dialog through redux.
*
* @returns
* @memberof Editor
*/
renderEditor() {
const { classes } = this.props;
return (
<div>
<Notification
{...this.state.notification}
handleClose={() => this.handleCloseNotificationRequest()}
<Alert
{...this.state.alert}
handleClose={() => this.handleCloseAlertRequest()}
/>
<Dialog
fullScreen
open={this.props.open}
TransitionComponent={Transition}
>
<Alert
{...this.state.alert}
handleClose={() => this.handleCloseAlertRequest()}
/>
<AppBar className={classes.appBar} >
<Toolbar>
<IconButton color="inherit" onClick={() => this.handleCloseEditorRequest()} aria-label="Close">
<CloseIcon />
</IconButton>
<Typography variant="h6" color="inherit" className={classes.flex}>
Mode édition
</Typography>
<Button color="inherit" onClick={() => this.handleSaveEditorRequest()}>
Enregistrer
</Button>
</Toolbar>
</AppBar>
<Paper className={classes.paper}>
{this.props.open ? this.renderForm() : <div></div>}
</Paper>
</Dialog>
<AppBar className={classes.appBar} >
<Toolbar>
<IconButton color="inherit" onClick={() => this.handleCloseEditorRequest()} aria-label="Close">
<CloseIcon />
</IconButton>
<Typography variant="h6" color="inherit" className={classes.flex}>
Mode édition
</Typography>
<Button color="inherit" onClick={() => this.handleSaveEditorRequest()}>
Enregistrer
</Button>
</Toolbar>
</AppBar>
<Paper className={classes.paper}>
{this.props.open ? this.renderForm() : <div></div>}
</Paper>
</div>
);
}
render() {
return <div></div>;
}
///////////////////
// Notification and alert related functions
......@@ -251,80 +258,87 @@ class Editor extends CustomComponentForAPI {
// Notifications and alerts from JS
notifyNoChangesDetected() {
this.setState({
notification: {
open: true,
message: "Aucun changement n'a été repéré.",
success: true,
}
});
console.log("no changes detected")
// this.setState({
// notification: {
// open: true,
// message: "Aucun changement n'a été repéré.",
// success: true,
// }
// });
}
notifyFormHasErrors(errors) {
this.setState({
notification: {
open: true,
message: "Le formulaire semble incohérent, merci de vérifier son contenu.",
error: true,
errors,
handleClose: this.handleCloseNotificationRequest()
}
});
console.log("errors in the form")
// this.setState({
// notification: {
// open: true,
// message: "Le formulaire semble incohérent, merci de vérifier son contenu.",
// error: true,
// errors,
// handleClose: this.handleCloseNotificationRequest()
// }
// });
}
notifyIsSaving() {
this.setState({
notification: {
open: true,
message: "Enregistrement en cours",
isLoading: true,
duration: null
}
});
console.log("is saving")
// this.setState({
// notification: {
// open: true,
// message: "Enregistrement en cours",
// isLoading: true,
// duration: null
// }
// });
}
notifySaveSuccessful(message) {
this.setState({
notification: {
open: true,
message,
success: true,
preventClickAway: false
}
});
console.log("save success")
// this.setState({
// notification: {
// open: true,
// message,
// success: true,
// preventClickAway: false
// }
// });
}
alertSaveFailed(error) {
this.setState({
alert: {
open: true,
info: true,
title: "L'enregistrement sur le serveur a échoué.",
description: JSON.stringify(error, null, 2) + "\n \nVous pourrez réessayer après avoir fermer cette alerte. Si l'erreur persiste, vérifier votre connexion internet ou contacter les administrateurs du site.",
infoText: "J'ai compris",
// handleResponse: () => this.props.clearSaveError()
}
});
console.log("Save failed");
// this.setState({
// alert: {
// open: true,
// info: true,
// title: "L'enregistrement sur le serveur a échoué.",
// description: JSON.stringify(error, null, 2) + "\n \nVous pourrez réessayer après avoir fermer cette alerte. Si l'erreur persiste, vérifier votre connexion internet ou contacter les administrateurs du site.",
// infoText: "J'ai compris",
// // handleResponse: () => this.props.clearSaveError()
// }
// });
}
alertChangesNotSaved() {
this.setState({
alert: {
open: true,
info: false,
title: "Modifications non enregistrées !",
description: "Vous avez des modifications qui n'ont pas été sauvegardées. Voulez-vous les enregistrer ?",
agreeText: "Oui, je les enregistre",
disagreeText: "Non",
handleResponse: (agree) => {
if (agree) {
this.handleSaveEditorRequest();
} else {
this.props.handleCloseEditor();
}
},
}
});
console.log("changes not saved");
// this.setState({
// alert: {
// open: true,
// info: false,
// title: "Modifications non enregistrées !",
// description: "Vous avez des modifications qui n'ont pas été sauvegardées. Voulez-vous les enregistrer ?",
// agreeText: "Oui, je les enregistre",
// disagreeText: "Non",
// handleResponse: (agree) => {
// if (agree) {
// this.handleSaveEditorRequest();
// } else {
// this.props.handleEditorWasClosed();
// }
// },
// }
// });
}
}
......@@ -336,15 +350,18 @@ Editor.propTypes = {
classes: PropTypes.object.isRequired,
// clearSaveError: PropTypes.func.isRequired,
savingHasError: PropTypes.object.isRequired,
saveData: PropTypes.func.isRequired,
lastUpdateTimeInModel: PropTypes.string,
lastSaveTime: PropTypes.number,
forceSave: PropTypes.bool.isRequired,
__apiAttr: PropTypes.oneOf([PropTypes.number, PropTypes.string, ""]),
modelData: PropTypes.object.isRequired,
open: PropTypes.bool.isRequired,
handleCloseEditor: PropTypes.func.isRequired,
handleEditorWasClosed: PropTypes.func.isRequired,
dataToSave: PropTypes.object,
// props added in subclasses but are absolutely required to handle redux
openFullScreenDialog: PropTypes.func.isRequired,
closeFullScreenDialog: PropTypes.func.isRequired,
saveData: PropTypes.func.isRequired,
};
Editor.defaultProps = {
......@@ -352,7 +369,7 @@ Editor.defaultProps = {
forceSave: false,
__apiAttr: "",
// eslint-disable-next-line no-console
handleCloseEditor: () => console.error("Dev forgot something...")
handleEditorWasClosed: () => console.error("Dev forgot something...")
};
export default Editor;
......@@ -65,7 +65,7 @@ class GenericModule extends CustomComponentForAPI {
}
};
handleCloseEditor = (somethingWasSaved = false) => {
handleEditorWasClosed = (somethingWasSaved = false) => {
this.setState({ editorOpen: false, dataToSave: null });
if (somethingWasSaved && this.props.moduleInGroupInfos.isInGroup) {
// this.props.moduleInGroupInfos.invalidateGroup();
......@@ -163,7 +163,7 @@ class GenericModule extends CustomComponentForAPI {
/>
<this.props.editor
open={this.state.editorOpen}
handleCloseEditor={this.handleCloseEditor}
handleEditorWasClosed={this.handleEditorWasClosed}
modelData={this.props.parseRawModelData(this.state.rawModelDataForEditor)}
outsideData={this.props.outsideData}
userData={this.props.userData}
......
import getActions from "../../../../api/getActions";
import { openFullScreenDialog, closeFullScreenDialog } from "../../../../actions/fullScreenDialog";
/**
* Function to create the mapDispatchToProps function for editor in a "generic way"
......@@ -11,6 +12,8 @@ export default function getMapDispatchToPropsForEditor(name) {
return (dispatch) => {
return {
saveData: (data) => dispatch(getActions(name).update(data)),
openFullScreenDialog: (innerNodes) => dispatch(openFullScreenDialog(innerNodes)),
closeFullScreenDialog: () => dispatch(closeFullScreenDialog()),
// clearSaveError: () => dispatch(savingHasErrorAction(false))
};
};
......
/*
* This file contains the redux reducers related to the app fullScreenDialog
*/
import React from "react";
import {
OPEN_FULL_SCREEN_DIALOG,
CLOSE_FULL_SCREEN_DIALOG
} from "../actions/action-types";
/**
* Reducer for the fullScreenDialog