Commit c9f69977 authored by Florent Chehab's avatar Florent Chehab Committed by Florent Chehab
Browse files

redesign(form/editor/etc.)

parent 8cb617b8
......@@ -35,6 +35,8 @@ import CountryService from "../../services/data/CountryService";
import CurrencyService from "../../services/data/CurrencyService";
import LanguageService from "../../services/data/LanguageService";
import FilterService from "../../services/FilterService";
import AlertServiceComponent from "../services/AlertServiceComponent";
import AlertService from "../../services/AlertService";
const SERVICES_TO_INITIALIZE = [
UniversityService,
......@@ -66,6 +68,7 @@ function App() {
}}
>
<MainAppFrame>
<AlertServiceComponent ref={AlertService.setComponent} />
<FullScreenDialogServiceComponent
ref={FullScreenDialogService.setComponent}
/>
......
/**
* Class to handle app errors (such as form errors) in a nice wrapped way
* Class to handle app errors (such as _form errors) in a nice wrapped way
* It's for non fatal errors.
*
* @export
......
import React from "react";
import React, { useEffect } from "react";
import PropTypes from "prop-types";
import Alert from "./Alert";
import useDeleteOne from "../../hooks/useDeleteOne";
import AlertService from "../../services/AlertService";
// TODO component is Useless...
function DeleteHandler({ performClose, route, id }) {
const performDelete = useDeleteOne(route);
return (
<Alert
open
info={false}
title="Confirmer la suppression de l'object."
description="Ếtes-vous sûr⋅e ?"
agreeText="Oui"
disagreeText="Non"
handleClose={performClose}
handleResponse={confirmed => {
useEffect(() => {
AlertService.open({
info: false,
title: "Confirmer la suppression de l'object.",
description: "Ếtes-vous sûr⋅e ?",
agreeText: "Oui",
disagreeText: "Non",
handleClose: { performClose },
handleResponse: confirmed => {
if (confirmed) performDelete(id, () => performClose());
else performClose();
}}
/>
);
}
});
}, []);
return <></>;
}
DeleteHandler.propTypes = {
......
......@@ -6,14 +6,13 @@ import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import RequestParams from "../../redux/api/RequestParams";
import getActions from "../../redux/api/getActions";
import Alert from "./Alert";
import APP_ROUTES from "../../config/appRoutes";
import AlertService from "../../services/AlertService";
function clear() {
return {
error: null,
errorInfo: null,
alertOpen: true
errorInfo: null
};
}
......@@ -27,8 +26,7 @@ class ErrorBoundary extends React.Component {
// Catch errors in any components below and re-render with error message
this.setState({
error,
errorInfo,
alertOpen: true
errorInfo
});
const data = "stack" in error ? { componentStack: error.stack } : errorInfo;
......@@ -43,25 +41,23 @@ class ErrorBoundary extends React.Component {
if (errorInfo) {
// eslint-disable-next-line no-console
console.log(error, errorInfo);
return (
<Alert
open={this.state.alertOpen}
info={false}
title={
"Une erreur inconnue c'est produite dans l'application. Nous vous prions de nous en excuser."
}
description={
"Nous vous invitons à recharger la page. Si l'erreur persiste, merci de contacter les administrateurs du site; l'erreur leur a été transmise."
}
agreeText={"C'est noté, je sais que vous faîtes de votre mieux :)"}
disagreeText={"Retourner à l'accueil"}
handleResponse={agreed => {
// May need to click twice, but there seem to be no other ways
if (!agreed) this.props.history.push(APP_ROUTES.base);
}}
handleClose={() => this.setState(clear())}
/>
);
AlertService.open({
info: false,
title:
"Une erreur inconnue c'est produite dans l'application. Nous vous prions de nous en excuser.",
description:
"Nous vous invitons à recharger la page. Si l'erreur persiste, merci de contacter les administrateurs du site; l'erreur leur a été transmise.",
agreeText: "C'est noté, je sais que vous faîtes de votre mieux :)",
disagreeText: "Retourner à l'accueil",
handleResponse: agreed => {
this.setState(clear());
// May need to click twice, but there seem to be no other ways
if (!agreed) this.props.history.push(APP_ROUTES.base);
}
});
return <></>;
}
// Normally, just render children
......
import React from "react";
import AppBar from "@material-ui/core/AppBar/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import IconButton from "@material-ui/core/IconButton";
import CloseIcon from "@material-ui/icons/Close";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/styles";
const useStyles = makeStyles(theme => ({
appBar: {
position: "sticky"
},
flex: {
flex: 1
},
paper: {
padding: theme.spacing(2),
margin: theme.spacing(2)
}
}));
function FullScreenDialogFrame({
children,
rightButton,
handleCloseRequest,
title
}) {
const classes = useStyles();
return (
<>
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton
color="inherit"
onClick={handleCloseRequest}
aria-label="Close"
>
<CloseIcon />
</IconButton>
<Typography variant="h6" color="inherit" className={classes.flex}>
{title}
</Typography>
{rightButton}
</Toolbar>
</AppBar>
<Paper className={classes.paper}>{children}</Paper>
</>
);
}
FullScreenDialogFrame.propTypes = {
children: PropTypes.node.isRequired,
handleCloseRequest: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
rightButton: PropTypes.node
};
FullScreenDialogFrame.defaultProps = {
rightButton: <></>
};
export default FullScreenDialogFrame;
/* eslint-disable react/sort-comp */
import React, { Component } from "react";
import React from "react";
import PropTypes from "prop-types";
import Divider from "@material-ui/core/Divider";
import Button from "@material-ui/core/Button";
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 Alert from "../common/Alert";
// Form is imported only for type hints
// eslint-disable-next-line no-unused-vars
import Form from "../form/Form";
import FormManager from "../../utils/editionRelated/FormManager";
import FullScreenDialogService from "../../services/FullScreenDialogService";
import NotificationService from "../../services/NotificationService";
import LicenseNotice from "../common/LicenseNotice";
import FormContext from "../../contexts/FormContext";
import AlertService from "../../services/AlertService";
import FullScreenDialogFrame from "../common/FullScreenDialogFrame";
import EditorManager from "../../utils/editionRelated/EditorManager";
/**
* Class to handle editions of models on the frontend. It should be extended, eg:
* Class to handle editions of models on the frontend.
*
* class BlablaEditor extends Editor {
* renderForm() {
* return <blablaForm
* modelData={this.props.rawModelData}
* ref={this.formRef}
* />;
* }
* }
*
* @class Editor
* @extends {Component}
* It should be used with the hook useEditor
*/
class Editor extends Component {
// handling of the alert
state = {
alert: {
open: false
}
};
// Store the "saving" notification so that we can performDelete it when done saving.
savingNotification = null;
// reference to the form that the editor should contain
formRef = React.createRef();
class Editor extends React.Component {
/**
* @type {FormManager}
*/
formManager = undefined;
/**
* Extra attributes that should be retrieved in rawModelData when parsing
* I.e. the fields that are not present in the form but still need to be there
*
* @type {Array.<string>}
* @type {EditorManager}
*/
extraFieldMappings = [];
editorManager = undefined;
/**
* Creates an instance of Editor. and subscribes to the module wrapper so that it can access its functions.
......@@ -60,26 +35,25 @@ class Editor extends Component {
constructor(props) {
super(props);
// editors can be used outside of a module wrapper.
this.formManager = new FormManager(
props.rawModelData,
props.formLevelErrors
);
this.editorManager = new EditorManager();
props.subscribeToModuleWrapper(this);
}
/**
* Function that extracts the modelData from the raw one.
*
* Basically we extract all field that can be edited in the form associated with the editor.
* Basically we extract all field that can be edited in the _form associated with the editor.
* Plut the id.
*
* @returns
*/
parseRawModelData() {
const out = {};
this.getForm()
.getFields()
.forEach(({ fieldMapping }) => {
out[fieldMapping] = this.props.rawModelData[fieldMapping];
});
this.extraFieldMappings.forEach(fieldMapping => {
this.formManager.getFields().forEach(({ fieldMapping }) => {
out[fieldMapping] = this.props.rawModelData[fieldMapping];
});
......@@ -91,56 +65,32 @@ class Editor extends Component {
return out;
}
/**
* Returns the form instance associated with the editor
*
* @returns {Form}
*/
getForm() {
return this.formRef.current;
}
// ///////
// Shortcut functions to those of the form instance
getFormError() {
return this.getForm().getError();
}
getFormData() {
return this.getForm().getDataFromFields();
}
formHasChanges() {
return this.getForm().hasChanges();
}
// End of shortcut functions
// //////////////////////
/**
* Function to handle save editor events, eg when clicking on the save button
* This function is not trivial and checks are performed.
*
*/
handleSaveEditorRequest() {
const getFormError = this.getFormError();
if (!getFormError.status) {
handleSaveEditorRequest = () => {
const formErrors = this.formManager.getError();
if (!formErrors.status) {
// no error, we can save if necessary
if (this.formHasChanges()) {
// Copy the model data and copy above the data from the form
if (this.formManager.hasChanges()) {
const formData = this.formManager.getDataFromFields();
// Copy the model data and copy above the data from the _form
// So that we don't forget anything.
this.performSave({
...this.parseRawModelData(this.props.rawModelData),
...this.getFormData()
...formData
});
} else {
this.notifyNoChangesDetected();
this.editorManager.notifyNoChangesDetected();
this.closeEditor(false);
}
} else {
this.notifyFormHasErrors();
this.editorManager.notifyFormHasErrors();
}
}
};
/**
* Function to save the `data` to the server.
......@@ -148,7 +98,7 @@ class Editor extends Component {
* @param {Object} data
*/
performSave(data) {
this.notifyIsSaving();
this.editorManager.notifyIsSaving();
this.props.saveData(data, newData =>
this.handleSaveRequestWasSuccessful(newData)
);
......@@ -178,7 +128,7 @@ class Editor extends Component {
"Les données ont été enregistrées et sont en attentes de modération.";
}
}
this.notifySaveSuccessful(message);
this.editorManager.notifySaveSuccessful(message);
this.closeEditor(true);
}
......@@ -187,207 +137,123 @@ class Editor extends Component {
* It checks if there is data to save or not.
*
*/
handleCloseEditorRequest() {
if (this.formHasChanges()) {
handleCloseEditorRequest = () => {
if (this.formManager.hasChanges()) {
this.alertChangesNotSaved();
} else {
this.closeEditor(false);
}
}
};
/**
* Effectively performClose the editor window and notify if there was something new that was saved
*
* @param {Boolean} somethingWasSaved
*/
closeEditor(somethingWasSaved) {
closeEditor(somethingWasSaved = false) {
FullScreenDialogService.closeDialog();
this.props.closeEditorPanel(somethingWasSaved);
const { onClose } = this.props;
onClose(somethingWasSaved);
}
/**
* This function is extended to handle all the logic such as
* - opening the editor
* - Detecting when there was a successful save
* - etc.
*
*/
componentDidUpdate() {
// Open the editor if needed
// don't check the if prevProps was false before, we need to rerender it anyway so that we can
// display the alert.
if (this.props.open) {
FullScreenDialogService.openDialog(this.renderEditor());
}
// we make to notify if saving to the server has errors
const { savingHasError } = this.props;
if (savingHasError.failed && !this.state.alert.open) {
if (savingHasError.failed) {
this.alertSaveFailed(JSON.stringify(savingHasError.error, null, 2));
}
}
/**
* Function to render the forms elements.
* Basically something that extends the `Form` component
*
* @returns
*/
renderForm() {
throw new Error("The renderForm function must be extended in sub classes");
}
/**
* Main rendering function. Its return value is passed to the
* fullscreen app dialog through redux.
*
* @returns
*/
renderEditor() {
const { classes } = this.props;
render() {
const { Form, license } = this.props;
return (
<>
<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.renderForm()}</Paper>
</>
<FullScreenDialogFrame
handleCloseRequest={this.handleCloseEditorRequest}
title="Mode édition"
rightButton={
<Button color="inherit" onClick={this.handleSaveEditorRequest}>
Enregistrer
</Button>
}
>
<div>
<LicenseNotice variant={license} />
<Divider />
</div>
<FormContext.Provider value={{ formManager: this.formManager }}>
<Form />
</FormContext.Provider>
</FullScreenDialogFrame>
);
}
render() {
return <></>;
}
// /////////////////
// Notification and alert related functions
// Alerts related
alertSaveFailed(error) {
this.removeSavingNotification();
this.setState({
alert: {
open: true,
info: true,
title: "L'enregistrement sur le serveur a échoué.",
description: `Vous pourrez réessayer après avoir fermer cette alerte. Si l'erreur persiste, vérifier votre connexion internet ou contacter les administrateurs du site.\n\n${error}`,
infoText: "J'ai compris",
handleResponse: () => {
this.props.clearSaveError();
this.setState({ alert: { open: false } });
}
this.editorManager.removeSavingNotification();
AlertService.open({
info: true,
title: "L'enregistrement sur le serveur a échoué.",
description: `Vous pourrez réessayer après avoir fermer cette alerte. Si l'erreur persiste, vérifier votre connexion internet ou contacter les administrateurs du site.\n\n${error}`,
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.closeEditor();
}
AlertService.open({
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.closeEditor();
}
}
});
}
handleCloseAlertRequest() {
this.setState({
alert: { open: false }
});
}
// Notifications related
notifyNoChangesDetected() {
NotificationService.info("Aucun changement n'a été repéré.");
}
notifyFormHasErrors() {
NotificationService.error(
"Le formulaire semble incohérent, merci de vérifier son contenu."
);
}
notifyIsSaving() {
this.savingNotification = NotificationService.notify(
"Enregistrement en cours",
{
variant: "info",