Commit 5665fb60 authored by Florent Chehab's avatar Florent Chehab

Cleaned module wrapper / editor

Removed hacks
Tweaks
And changes coherent with last commit

Group Module broken again !
parent 1bae06b2
......@@ -146,6 +146,7 @@ const mapStateToProps = (state) => {
return {
countries: state.api.countriesAll,
currencies: state.api.currenciesAll,
serverModerationStatus: state.api.serverModerationStatusSpecific,
};
};
......@@ -154,6 +155,7 @@ const mapDispatchToProps = (dispatch) => {
api: {
countries: () => dispatch(getActions("countries").readAll()),
currencies: () => dispatch(getActions("currencies").readAll()),
serverModerationStatus: () => dispatch(getActions("serverModerationStatus").readSpecific("")), // not needed for server moderation status
},
};
};
......
......@@ -17,8 +17,7 @@ import TextField from "./fields/TextField";
import MultiSelectField from "./fields/MultiSelectField";
import NumberField from "./fields/NumberField";
import store from "../../redux/store";
import { getLatestRead } from "../../redux/api/utils";
import { getLatestReadDataFromStore } from "../../redux/api/utils";
export default {
/**
......@@ -33,7 +32,7 @@ export default {
renderObjModerationLevelField() {
// hack to access directly the store and get the value we need.
const userData = getLatestRead(store.getState().api.userDataSpecific).data,
const userData = getLatestReadDataFromStore("userDataSpecific"),
possibleObjModeration = getObjModerationLevel(userData.owner_level, true);
if (possibleObjModeration.length > 1) {
return (
......
......@@ -54,6 +54,17 @@ class Editor extends Component {
*/
extraFieldMappings = []
/**
* Creates an instance of Editor. and subscribes to the module wrapper so that it can access its functions.
*
* @param {object} props
* @memberof Editor
*/
constructor(props) {
super(props);
props.subscribeToModuleWrapper(this);
}
/**
* Function that extracts the modelData from the raw one.
*
......@@ -116,7 +127,8 @@ class Editor extends Component {
handleSaveEditorRequest() {
const getFormError = this.getFormError();
if (!getFormError.status) { // no error, we can save if necessary
if (this.props.forceSave || this.formHasChanges()) {
if (this.formHasChanges()) {
// Copy the model data and copy above the data from the form
// So that we don't forget anything.
this.performSave(
......@@ -183,7 +195,6 @@ class Editor extends Component {
if (this.formHasChanges()) {
this.alertChangesNotSaved();
} else {
this.notifyNoChangesDetected();
this.closeEditor(false);
}
}
......@@ -196,7 +207,7 @@ class Editor extends Component {
*/
closeEditor(somethingWasSaved) {
this.props.closeFullScreenDialog();
this.props.handleEditorWasClosed(somethingWasSaved);
this.props.closeEditorPanel(somethingWasSaved);
}
......@@ -217,16 +228,6 @@ class Editor extends Component {
this.props.openFullScreenDialog(this.renderEditor());
}
// 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.handleEditorWasClosed();
this.performSave(dataToSave);
}
// end of hacky behavior.
// we make to notify if saving to the server has errors
const { savingHasError } = this.props;
if (savingHasError.failed && !this.state.alert.open) {
......@@ -364,17 +365,15 @@ class Editor extends Component {
Editor.propTypes = {
classes: PropTypes.object.isRequired,
savingHasError: PropTypes.object.isRequired,
clearSaveError: PropTypes.func.isRequired,
lastUpdateTimeInModel: PropTypes.string,
lastSaveTime: PropTypes.number,
forceSave: PropTypes.bool.isRequired,
open: PropTypes.bool.isRequired, // should the editor be opened
subscribeToModuleWrapper: PropTypes.func.isRequired,
__apiAttr: PropTypes.oneOf([PropTypes.number, PropTypes.string, ""]),
rawModelData: PropTypes.object.isRequired,
open: PropTypes.bool.isRequired, // should the editor be opened
handleEditorWasClosed: PropTypes.func.isRequired,
dataToSave: PropTypes.object,
closeEditorPanel: PropTypes.func.isRequired,
// props added in subclasses but are absolutely required to handle redux
savingHasError: PropTypes.object.isRequired,
clearSaveError: PropTypes.func.isRequired,
lastUpdateTimeInModel: PropTypes.string,
openFullScreenDialog: PropTypes.func.isRequired,
closeFullScreenDialog: PropTypes.func.isRequired,
saveData: PropTypes.func.isRequired,
......@@ -385,10 +384,9 @@ Editor.propTypes = {
Editor.defaultProps = {
open: false,
forceSave: false,
__apiAttr: "",
// eslint-disable-next-line no-console
handleEditorWasClosed: () => console.error("Dev forgot something...")
closeEditorPanel: () => console.error("Dev forgot something...")
};
export default Editor;
......@@ -62,8 +62,7 @@ class History extends Component {
return [];
} else {
return this.props.versions.readSucceeded.data
.map((el) => { return el.data; }) // get the data inside the version
.slice(1); // the first (ie 0) version is the one already displayed
.map((el) => { return el.data; }); // get the data inside the version
}
}
......@@ -100,21 +99,28 @@ class History extends Component {
&& lastVersion.id == id;
}
/**
* As soon as the History is added to the DOM, we make sure to update it,
* To force the call to `componentDidUpdate`.
*
* @memberof Editor
*/
componentDidMount() {
this.forceUpdate();
}
/**
* Extends function to open panel in redux and load Data
*
* @memberof History
*/
componentDidUpdate() {
if (this.props.open) {
// already open the panel
this.props.openFullScreenDialog(this.renderPanel());
// Load the data as necessary
if (!this.dataIsReady() && !this.props.versions.isLoading) {
const { contentTypeId, id } = this.getContentTypeAndId();
this.props.readVersions(contentTypeId, id);
}
// Load the data as necessary
if (!this.dataIsReady() && !this.props.versions.isLoading) {
const { contentTypeId, id } = this.getContentTypeAndId();
this.props.readVersions(contentTypeId, id);
}
this.props.openFullScreenDialog(this.renderPanel());
}
/**
......@@ -230,7 +236,7 @@ class History extends Component {
variant='outlined'
color="primary"
className={this.props.classes.editButton}
onClick={() => this.props.handleEditFromVersion(rawModelData)}
onClick={() => this.props.editFromVersion(rawModelData)}
>
Éditer à partir de cette version
</Button>
......@@ -247,11 +253,10 @@ class History extends Component {
History.propTypes = {
classes: PropTypes.object.isRequired,
handleEditFromVersion: PropTypes.func.isRequired,
editFromVersion: PropTypes.func.isRequired,
readVersions: PropTypes.func.isRequired,
closeHistoryPanel: PropTypes.func.isRequired,
resetVersions: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
versions: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
openFullScreenDialog: PropTypes.func.isRequired,
......@@ -261,7 +266,6 @@ History.propTypes = {
};
History.defaultProps = {
open: false,
// eslint-disable-next-line no-console
closeHistoryPanel: () => console.error("Dev forgot something..."),
};
......
import React from "react";
import React, { Component } from "react";
import PropTypes from "prop-types";
import withStyles from "@material-ui/core/styles/withStyles";
import compose from "recompose/compose";
import { connect } from "react-redux";
import { getLatestReadDataFromStore } from "../../../../redux/api/utils";
import Typography from "@material-ui/core/Typography";
import Paper from "@material-ui/core/Paper";
......@@ -14,10 +14,8 @@ import Divider from "@material-ui/core/Divider";
import AddIcon from "@material-ui/icons/Add";
import getActions from "../../../../redux/api/getActions";
import green from "@material-ui/core/colors/green";
import CustomComponentForAPI from "../../../common/CustomComponentForAPI";
const styles = theme => ({
root: {
......@@ -47,7 +45,7 @@ const styles = theme => ({
* @extends {CustomComponentForAPI}
* @extends React.Component
*/
class ModuleGroupWrapper extends CustomComponentForAPI {
class ModuleGroupWrapper extends Component {
state = {
editorOpen: false,
};
......@@ -56,25 +54,24 @@ class ModuleGroupWrapper extends CustomComponentForAPI {
this.setState({ editorOpen: true });
}
handleEditorWasClosed = (somethingWasSaved = false) => {
closeEditorPanel = (somethingWasSaved = false) => {
this.setState({ editorOpen: false });
if (somethingWasSaved) {
this.props.invalidateGroup();
}
};
customRender() {
render() {
const { classes, groupTitle, endPoint } = this.props,
userCanPostTo = this.getReadData("userData").owner_can_post_to,
userCanPostTo = getLatestReadDataFromStore("userDataSpecific").owner_can_post_to,
disabled = userCanPostTo.indexOf(endPoint) < 0;
return <></>; // TODO NOW remove
return (
<Paper className={classes.root}>
<this.props.editor
{...this.props.propsForEditor}
open={this.state.editorOpen}
handleEditorWasClosed={this.handleEditorWasClosed}
userData={this.props.userData}
closeEditorPanel={this.closeEditorPanel}
/>
<Grid container spacing={8} alignItems='center'>
......@@ -114,26 +111,10 @@ ModuleGroupWrapper.propTypes = {
classes: PropTypes.object.isRequired,
propsForEditor: PropTypes.object.isRequired,
children: PropTypes.node.isRequired,
userData: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => {
return {
userData: state.api.userDataSpecific
};
};
const mapDispatchToProps = (dispatch) => {
return {
api: {
userData: () => dispatch(getActions("userData").readSpecific("")), // Id not required for user data
}
};
};
export default compose(
withStyles(styles, { withTheme: true }),
connect(mapStateToProps, mapDispatchToProps)
)(ModuleGroupWrapper);
import React from "react";
import React, { Component } from "react";
import PropTypes from "prop-types";
import withStyles from "@material-ui/core/styles/withStyles";
import compose from "recompose/compose";
import { connect } from "react-redux";
import Paper from "@material-ui/core/Paper";
import red from "@material-ui/core/colors/red";
import orange from "@material-ui/core/colors/orange";
import green from "@material-ui/core/colors/green";
import CustomComponentForAPI from "../../../common/CustomComponentForAPI";
import isModerationRequired from "../../../../utils/isModerationRequired";
import Alert from "../../../common/Alert";
import getActions from "../../../../redux/api/getActions";
import renderUsefulLinks from "./moduleWrapperFunctions/renderUsefulLinks";
import renderFirstRow from "./moduleWrapperFunctions/renderFirstRow";
......@@ -23,125 +19,155 @@ import renderTitle from "./moduleWrapperFunctions/renderTitle";
import History from "./History";
import PendingModeration from "./PendingModeration";
import { getLatestReadDataFromStore } from "../../../../redux/api/utils";
/**
* Wrapper around modules to handle in a standard way the edition/moderation/history version;
* displaying of certain attributes, etc.
*
* @class ModuleWrapper
* @extends {CustomComponentForAPI}
* @extends React.Component
*/
class ModuleWrapper extends CustomComponentForAPI {
class ModuleWrapper extends Component {
/**
* Stores a reference to the editor linked to the this module wrapper so that we can manipulate it directly.
*
* @memberof ModuleWrapper
*/
editorInstance = null;
state = {
editorOpen: false,
dataToSave: null,
historyOpen: false,
pendingModerationOpen: false,
rawModelDataForEditor: this.props.rawModelData,
forceSave: false,
alert: { open: false }
};
openEditorPanel = (force = false) => {
if (force || this.props.rawModelData.pending_moderation.length == 0) {
/**
* Function to open the editor panels.
* It also checks that there is no model pending moderation
*
* @param {boolean} [ignorePendingModeration=false]
* @memberof ModuleWrapper
*/
openEditorPanel(ignorePendingModeration = false) {
if (ignorePendingModeration || this.props.rawModelData.pending_moderation.length == 0) {
this.setState({ editorOpen: true });
} else {
this.setState({
alert: {
open: true,
info: false,
title: "Une version est en attente de modération",
description: "Vous vous apprêter à éditer le module tel qu'il est présenté sur la page principale. Toutefois, il conseillé d'éditer le module à partir de la version en attente de modération car cette dernière sera écrasée par votre contribution.",
agreeText: "Ok, je vais voir la version en attente de modération",
disagreeText: "Je ne veux pas me baser sur le travail en attente de modération",
handleResponse: (agree) => {
if (agree) {
this.handleOpenPendingModeration();
} else {
this.openEditorPanel(true);
}
},
multilineButtons: true,
}
});
this.alertThereIsSomethingPendingModeration();
}
};
}
handleEditorWasClosed = (somethingWasSaved = false) => {
this.setState({ editorOpen: false, dataToSave: null });
/**
* Function to close the editor panel
* If something was saved and we are in a group module, we need to refresh every thing.
*
* @param {boolean} [somethingWasSaved=false]
* @memberof ModuleWrapper
*/
closeEditorPanel(somethingWasSaved = false) {
this.setState({ editorOpen: false });
if (somethingWasSaved && this.props.moduleInGroupInfos.isInGroup) {
this.props.moduleInGroupInfos.invalidateGroup();
}
};
handleCloseAlert = () => {
this.setState({ alert: { open: false } });
}
closeHistoryPanel = () => {
this.setState({ historyOpen: false });
};
handleOpenHistory = () => {
this.setState({ historyOpen: true });
};
handleOpenPendingModeration = () => {
this.setState({ pendingModerationOpen: true });
}
closePendingModerationPanel = () => {
this.setState({ pendingModerationOpen: false });
}
handleEditPendingModeration = (rawModelData) => {
/**
* Function to edit the data that is pending moderation
*
* @param {object} pendingModelData
* @memberof ModuleWrapper
*/
editFromPendingModeration(pendingModelData) {
this.setState({
rawModelDataForEditor: rawModelData,
forceSave: this.userCanModerate(),
rawModelDataForEditor: pendingModelData,
});
this.closePendingModerationPanel();
this.openEditorPanel(true);
}
handleEditCurrent = () => {
/**
* Function to edit the current data
*
* @memberof ModuleWrapper
*/
editCurrent() {
this.setState({
rawModelDataForEditor: this.props.rawModelData,
forceSave: false,
});
this.openEditorPanel();
}
handleEditFromVersion = (rawModelData) => {
/**
* Function to edit from a version
*
* @param {object} versionModelData
* @memberof ModuleWrapper
*/
editFromVersion(versionModelData) {
this.setState({
rawModelDataForEditor: rawModelData,
forceSave: true,
rawModelDataForEditor: versionModelData,
});
this.closeHistoryPanel();
this.openEditorPanel();
}
handleApproveModeration = (modelDataInModeration) => {
this.setState({ dataToSave: modelDataInModeration });
/**
* Function to moderate a pendingModeration model
*
* @param {object} modelDataInModeration
* @memberof ModuleWrapper
*/
moderatePendingModeration(modelDataInModeration) {
this.editorInstance.performSave(modelDataInModeration);
this.closePendingModerationPanel();
}
renderTitle = (rawModelData) => (
renderTitle(rawModelData, this.props.classes, this.props.buildTitle)
)
/**
* Renders the tiltle of the model
*
* @param {object} rawModelData
* @returns
* @memberof ModuleWrapper
*/
renderTitle(rawModelData) {
return renderTitle(rawModelData, this.props.classes, this.props.buildTitle);
}
/**
* Renders the core of the model
*
* @param {object} rawModelData
* @returns
* @memberof ModuleWrapper
*/
renderCore(rawModelData) {
return (
<>
{this.props.renderCore(rawModelData, this.props.coreClasses, this.props.outsideData)}
{renderUsefulLinks(rawModelData, this.props.classes, this.props.theme)}
</>
);
}
renderCore = (rawModelData) => (
<>
{this.props.renderCore(rawModelData, this.props.coreClasses, this.props.outsideData)}
{renderUsefulLinks(rawModelData, this.props.classes, this.props.theme)}
</>
)
/**
* Can the user moderate the pending moderation object ?
*
* @returns
* @memberof ModuleWrapper
*/
userCanModerate() {
const userData = this.getReadData("userData");
const serverModerationStatus = this.getReadData("serverModerationStatus");
const { rawModelData } = this.props;
const userData = getLatestReadDataFromStore("userDataSpecific"),
serverModerationStatus = getLatestReadDataFromStore("serverModerationStatusSpecific"),
{ rawModelData } = this.props;
return !isModerationRequired(
serverModerationStatus.activated,
serverModerationStatus.moderator_level,
......@@ -151,42 +177,84 @@ class ModuleWrapper extends CustomComponentForAPI {
);
}
customRender() {
/**
* Open the pending moderation panel
*
* @memberof ModuleWrapper
*/
openPendingModerationPanel() {
this.setState({ pendingModerationOpen: true });
}
/**
* Close the pending moderation panel
*
* @memberof ModuleWrapper
*/
closePendingModerationPanel() {
this.setState({ pendingModerationOpen: false });
}
/**
* Close the history panel
*
* @memberof ModuleWrapper
*/
closeHistoryPanel() {
this.setState({ historyOpen: false });
}
/**
* FYI
* The editor is always rendered whereas the pending Moderation, History and Alert and rendered only when needed.
*
* @returns
* @memberof ModuleWrapper
*/
render() {
const { classes, rawModelData } = this.props,
userCanModerate = this.userCanModerate();
return (
<>
<Alert
{...this.state.alert}
handleClose={this.handleCloseAlert}
/>
<this.props.editor
open={this.state.editorOpen}
handleEditorWasClosed={this.handleEditorWasClosed}
closeEditorPanel={(somethingWasSaved) => this.closeEditorPanel(somethingWasSaved)}
subscribeToModuleWrapper={(editorInstance) => this.editorInstance = editorInstance}
rawModelData={this.state.rawModelDataForEditor}
outsideData={this.props.outsideData}
userData={this.props.userData}
forceSave={this.state.forceSave} // don't take into account that the form hasn't change, it's normal
dataToSave={this.state.dataToSave}
__apiAttr={this.props.__apiAttr}
/>
<History
renderer={this}
modelInfo={{ contentTypeId: rawModelData.content_type_id, id: rawModelData.id }}
open={this.state.historyOpen}
closeHistoryPanel={this.closeHistoryPanel}
handleEditFromVersion={this.handleEditFromVersion}
/>
<PendingModeration
renderer={this}
open={this.state.pendingModerationOpen}
closePendingModerationPanel={this.closePendingModerationPanel}
handleEditPendingModeration={this.handleEditPendingModeration}
handleApproveModeration={this.handleApproveModeration}
userCanModerate={userCanModerate}
/>
{this.state.alert.open ?
<Alert
{...this.state.alert}
handleClose={() => this.setState({ alert: { open: false } })}
/>
:
<></>
}
{this.state.historyOpen ?
<History
renderer={this}
modelInfo={{ contentTypeId: rawModelData.content_type_id, id: rawModelData.id }}
closeHistoryPanel={() => this.closeHistoryPanel()}
editFromVersion={(modelData) => this.editFromVersion(modelData)}
/>
:
<></>
}
{this.state.pendingModerationOpen ?
<PendingModeration
renderer={this}
closePendingModerationPanel={() => this.closePendingModerationPanel()}
editFromPendingModeration={(pendingModelData) => this.editFromPendingModeration(pendingModelData)}
moderatePendingModeration={(modelData) => this.moderatePendingModeration(modelData)}
userCanModerate={userCanModerate}
/>
:
<></>
}
<Paper className={classes.root} square={true}>
{renderFirstRow.bind(this)(userCanModerate)}
{this.renderCore(this.props.rawModelData)}
......@@ -194,6 +262,37 @@ class ModuleWrapper extends CustomComponentForAPI {
</>
);
}
/**
*
* Alert functions
*
*
*
*/
alertThereIsSomethingPendingModeration() {
this.setState({
alert: {
open: true,
info: false,
title: "Une version est en attente de modération",
description: "Vous vous apprêter à éditer le module tel qu'il est présenté sur la page principale. Toutefois, il conseillé d'éditer le module à partir de la version en attente de modération car cette dernière sera écrasée par votre contribution.",
agreeText: "Ok, je vais voir la version en attente de modération",
disagreeText: "Je ne veux pas me baser sur le travail en attente de modération",
handleResponse: (agree) => {
if (agree) {
this.openPendingModerationPanel();
} else {
this.openEditorPanel(true);
}
},
multilineButtons: true,
}
});
}
}
......@@ -211,30 +310,11 @@ ModuleWrapper.propTypes = {
renderCore: PropTypes.func.isRequired,
coreClasses: PropTypes.object.isRequired,
outsideData: PropTypes.object,
userData: PropTypes.object.isRequired,
__apiAttr: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
moduleInGroupInfos: PropTypes.shape({ isInGroup: PropTypes.bool.isRequired, invalidateGroup: PropTypes.func }).isRequired,