Commit dc264b86 authored by Florent Chehab's avatar Florent Chehab

hotFix(front crash on missing course code) & refacto(moduleHeader as component) & Tweaks

Hot fix:
* Front was crashing on course sort when a course didn't have a code.

Refacto:
* Changed module header subfunctions to concrete components,

Tweaks:
* tabs on the university page are now centered on smaller devices,
* Removed react swipeable from search for performance,
* Changed search pagination to progress for better long list support,
* Smaller univ name on the edit feedback page,
* Fixed wording unlinked partners for singular vs plural,
* Allow disabled items in SimpleFormMenu,

Other:
* Removed univ logo from edit form, not supported yet,
parent 8bd21217
Pipeline #43284 passed with stages
in 3 minutes and 52 seconds
...@@ -9,10 +9,14 @@ from backend_app.models.exchange import Exchange ...@@ -9,10 +9,14 @@ from backend_app.models.exchange import Exchange
class CourseFeedbackSerializer(EssentialModuleSerializer): class CourseFeedbackSerializer(EssentialModuleSerializer):
course_code = serializers.SerializerMethodField() # needed for the front course_code = serializers.SerializerMethodField() # needed for the front
course_title = serializers.SerializerMethodField() # needed for the front
def get_course_code(self, obj): def get_course_code(self, obj):
return obj.course.code return obj.course.code
def get_course_title(self, obj):
return obj.course.title
def update(self, instance, validated_data): def update(self, instance, validated_data):
instance.untouched = False instance.untouched = False
return super().update(instance, validated_data) return super().update(instance, validated_data)
...@@ -21,6 +25,7 @@ class CourseFeedbackSerializer(EssentialModuleSerializer): ...@@ -21,6 +25,7 @@ class CourseFeedbackSerializer(EssentialModuleSerializer):
model = CourseFeedback model = CourseFeedback
fields = EssentialModuleSerializer.Meta.fields + ( fields = EssentialModuleSerializer.Meta.fields + (
"course_code", "course_code",
"course_title",
"language", "language",
"comment", "comment",
"adequation", "adequation",
......
...@@ -60,9 +60,14 @@ class UnlinkedPartners extends CustomComponentForAPI { ...@@ -60,9 +60,14 @@ class UnlinkedPartners extends CustomComponentForAPI {
nUnlinked > 0 ? nUnlinked > 0 ?
<> <>
<Typography variant={"caption"} color={"primary"}> <Typography variant={"caption"} color={"primary"}>
⚠&nbsp;{unlinkedPartners.length} universités partenaires de l'UTC ⚠&nbsp;
ne sont pas encore pleinement disponible(s) sur la plateforme. {
Plus d'informations&nbsp; nUnlinked === 1 ?
"1 université partenaire de l'UTC n'est pas encore pleinement disponible"
:
`${nUnlinked} universités partenaires de l'UTC ne sont pas encore pleinement disponibles`
}
sur la plateforme. Plus d'informations&nbsp;
<CustomLink to={APP_ROUTES.aboutUnlinkedPartners}> <CustomLink to={APP_ROUTES.aboutUnlinkedPartners}>
<Typography variant={"caption"} color={"primary"}> <Typography variant={"caption"} color={"primary"}>
ici ici
......
...@@ -32,8 +32,8 @@ function SimplePopupMenu(props) { ...@@ -32,8 +32,8 @@ function SimplePopupMenu(props) {
onClose={handleClose} onClose={handleClose}
> >
{ {
props.items.map(({label, onClick}, key) => ( props.items.map(({label, onClick, disabled}, key) => (
<MenuItem key={key} onClick={() => { <MenuItem key={key} disabled={disabled} onClick={() => {
onClick(); onClick();
handleClose(); handleClose();
}}> }}>
...@@ -51,6 +51,7 @@ SimplePopupMenu.propTypes = { ...@@ -51,6 +51,7 @@ SimplePopupMenu.propTypes = {
items: PropTypes.arrayOf(PropTypes.shape({ items: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
})).isRequired, })).isRequired,
renderHolder: PropTypes.func.isRequired, renderHolder: PropTypes.func.isRequired,
}; };
......
...@@ -152,6 +152,8 @@ class Form extends Component { ...@@ -152,6 +152,8 @@ class Form extends Component {
// we need to compare objects (ie JSON objects) differently // we need to compare objects (ie JSON objects) differently
if (typeof cmp1 === "object") { if (typeof cmp1 === "object") {
return !isEqual(cmp1, cmp2); return !isEqual(cmp1, cmp2);
} else if ((typeof cmp1 === "number") || (typeof cmp2 === "number")) {
return cmp1 != cmp2; // allow 93 == "93.00"
} else { } else {
return cmp1 !== cmp2; return cmp1 !== cmp2;
} }
......
...@@ -134,7 +134,7 @@ Field.propTypes = { ...@@ -134,7 +134,7 @@ Field.propTypes = {
required: PropTypes.bool.isRequired, // is the field required ? required: PropTypes.bool.isRequired, // is the field required ?
label: PropTypes.string, // text to go along the field label: PropTypes.string, // text to go along the field
comment: PropTypes.string, // text to give more information on what is expected comment: PropTypes.string, // text to give more information on what is expected
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, // value of the field value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // value of the field
form: PropTypes.object, // required in constructor (reference of to the) form containing the field form: PropTypes.object, // required in constructor (reference of to the) form containing the field
fieldMapping: PropTypes.string.isRequired, // name of the field in the data fieldMapping: PropTypes.string.isRequired, // name of the field in the data
}; };
......
...@@ -39,8 +39,11 @@ class NumberField extends Field { ...@@ -39,8 +39,11 @@ class NumberField extends Field {
} }
serializeFromField() { serializeFromField() {
const value = super.serializeFromField(); const value = super.serializeFromField(),
return parseFloat(value); parsed = parseFloat(value);
if (isNaN(parsed)) return null;
else return parsed;
} }
handleChangeValue(val) { handleChangeValue(val) {
......
...@@ -45,7 +45,7 @@ class PageEditExchangeFeedbacks extends CustomComponentForAPI { ...@@ -45,7 +45,7 @@ class PageEditExchangeFeedbacks extends CustomComponentForAPI {
if (id) { if (id) {
return ( return (
<> <>
<Typography variant={"h3"}> <Typography variant={"h4"}>
{ {
univId ? <CustomLink to={APP_ROUTES.forUniversity(univId)}>{univName}</CustomLink> univId ? <CustomLink to={APP_ROUTES.forUniversity(univId)}>{univName}</CustomLink>
: :
......
...@@ -103,6 +103,7 @@ class SelectListSubPage extends CustomComponentForAPI { ...@@ -103,6 +103,7 @@ class SelectListSubPage extends CustomComponentForAPI {
<SimplePopupMenu <SimplePopupMenu
items={[ items={[
{ {
disabled: false,
label: "Confirmer", label: "Confirmer",
onClick: () => this.props.deleteList(list.id, () => this.props.invalidateReadAll()) onClick: () => this.props.deleteList(list.id, () => this.props.invalidateReadAll())
}, },
......
...@@ -452,6 +452,7 @@ class View extends React.Component { ...@@ -452,6 +452,7 @@ class View extends React.Component {
<SimplePopupMenu <SimplePopupMenu
items={[ items={[
{ {
disabled: false,
label: "Confirmer", label: "Confirmer",
onClick: () => this.deleteBlock() onClick: () => this.deleteBlock()
}, },
...@@ -466,8 +467,8 @@ class View extends React.Component { ...@@ -466,8 +467,8 @@ class View extends React.Component {
<SimplePopupMenu <SimplePopupMenu
items={[ items={[
{label: "Markdown", onClick: () => this.addBlock("text-block")}, {label: "Markdown", onClick: () => this.addBlock("text-block"), disabled: false,},
{label: "Université", onClick: () => this.addBlock("univ-block")}, {label: "Université", onClick: () => this.addBlock("univ-block"), disabled: false,},
]} ]}
renderHolder={({onClick}) => ( renderHolder={({onClick}) => (
<Button variant={"contained"} <Button variant={"contained"}
......
...@@ -9,7 +9,6 @@ import Divider from "@material-ui/core/Divider"; ...@@ -9,7 +9,6 @@ import Divider from "@material-ui/core/Divider";
import Button from "@material-ui/core/Button"; import Button from "@material-ui/core/Button";
import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft"; import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft";
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight"; import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
import SwipeableViews from "react-swipeable-views";
import range from "../../utils/range"; import range from "../../utils/range";
import {APP_ROUTES} from "../../config/appRoutes"; import {APP_ROUTES} from "../../config/appRoutes";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
...@@ -69,35 +68,24 @@ class UnivList extends React.Component { ...@@ -69,35 +68,24 @@ class UnivList extends React.Component {
return ( return (
<div className={classes.root}> <div className={classes.root}>
<SwipeableViews
axis={theme.direction === "rtl" ? "x-reverse" : "x"}
index={this.state.activeStep}
onChangeIndex={(step) => this.handleStepChange(step)}
enableMouseEvents
>
{range(maxSteps).map(page => (
<div key={page}>
<List component="nav">
<Divider/>
{getIndexesOnPage(page).map(
univIdx => (
<CustomNavLink to={APP_ROUTES.forUniversity(universitiesToList[univIdx].id)} key={univIdx}>
<ListItem button divider={true} key={univIdx}>
<ListItemText primary={universitiesToList[univIdx].name}/>
</ListItem>
</CustomNavLink>
)
)}
</List>
</div>
))}
</SwipeableViews>
<List component="nav">
<Divider/>
{getIndexesOnPage(activeStep).map(
univIdx => (
<CustomNavLink to={APP_ROUTES.forUniversity(universitiesToList[univIdx].id)} key={univIdx}>
<ListItem button divider={true} key={univIdx}>
<ListItemText primary={universitiesToList[univIdx].name}/>
</ListItem>
</CustomNavLink>
)
)}
</List>
<MobileStepper <MobileStepper
steps={maxSteps} steps={maxSteps}
position="static" position="static"
variant={"progress"}
activeStep={activeStep} activeStep={activeStep}
className={classes.mobileStepper} className={classes.mobileStepper}
nextButton={ nextButton={
......
...@@ -58,7 +58,7 @@ class UniversityTemplate extends Component { ...@@ -58,7 +58,7 @@ class UniversityTemplate extends Component {
{value} = this.state; {value} = this.state;
let scroll = true; let scroll = true;
if (isWidthUp("lg", width)) { if (isWidthUp("sm", width)) {
scroll = false; scroll = false;
} }
......
...@@ -32,12 +32,14 @@ class UniversityGeneralForm extends Form { ...@@ -32,12 +32,14 @@ class UniversityGeneralForm extends Form {
{...this.getReferenceAndValue("website")} {...this.getReferenceAndValue("website")}
maxLength={300} maxLength={300}
isUrl={true}/> isUrl={true}/>
{/* Logo feature is not ready yet */}
<TextField label={"Logo de l'université"} {this.renderHiddenField({fieldMapping: "logo"})}
{...this.getReferenceAndValue("logo")} {/*<TextField label={"Logo de l'université"}*/}
maxLength={300} {/* {...this.getReferenceAndValue("logo")}*/}
isUrl={true} {/* required={false}*/}
urlExtensions={["jpg", "png", "svg"]}/> {/* maxLength={300}*/}
{/* isUrl={true}*/}
{/* urlExtensions={["jpg", "png", "svg"]}/>*/}
</> </>
); );
} }
......
...@@ -122,9 +122,6 @@ const styles = theme => ({ ...@@ -122,9 +122,6 @@ const styles = theme => ({
// marginBottom: theme.spacing(2), // marginBottom: theme.spacing(2),
flexGrow: 1, flexGrow: 1,
}, },
green: {
color: green.A200,
},
button: { button: {
width: theme.spacing(5), width: theme.spacing(5),
height: theme.spacing(5), height: theme.spacing(5),
......
import React, { Component } from "react"; import React, {Component} from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import withStyles from "@material-ui/core/styles/withStyles"; import withStyles from "@material-ui/core/styles/withStyles";
import compose from "recompose/compose"; import compose from "recompose/compose";
import Paper from "@material-ui/core/Paper"; 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 Alert from "../../../common/Alert"; import Alert from "../../../common/Alert";
import ModuleTitle from "./subComponents/ModuleTitle";
import renderUsefulLinks from "./moduleWrapperFunctions/renderUsefulLinks";
import renderFirstRow from "./moduleWrapperFunctions/renderFirstRow";
import renderTitle from "./moduleWrapperFunctions/renderTitle";
import History from "./History"; import History from "./History";
import PendingModeration from "./PendingModeration"; import PendingModeration from "./PendingModeration";
import ModuleFirstRow from "./subComponents/ModuleFirstRow";
import UsefulLinks from "./subComponents/UsefulLinks";
/** /**
...@@ -41,7 +36,7 @@ class ModuleWrapper extends Component { ...@@ -41,7 +36,7 @@ class ModuleWrapper extends Component {
historyOpen: false, historyOpen: false,
pendingModerationOpen: false, pendingModerationOpen: false,
rawModelDataForEditor: this.props.rawModelData, rawModelDataForEditor: this.props.rawModelData,
alert: { open: false } alert: {open: false}
}; };
/** /**
...@@ -53,7 +48,7 @@ class ModuleWrapper extends Component { ...@@ -53,7 +48,7 @@ class ModuleWrapper extends Component {
*/ */
openEditorPanel(ignorePendingModeration = false) { openEditorPanel(ignorePendingModeration = false) {
if (ignorePendingModeration || !this.props.rawModelData.has_pending_moderation) { if (ignorePendingModeration || !this.props.rawModelData.has_pending_moderation) {
this.setState({ editorOpen: true }); this.setState({editorOpen: true});
} else { } else {
this.alertThereIsSomethingPendingModeration(); this.alertThereIsSomethingPendingModeration();
} }
...@@ -67,7 +62,7 @@ class ModuleWrapper extends Component { ...@@ -67,7 +62,7 @@ class ModuleWrapper extends Component {
* @memberof ModuleWrapper * @memberof ModuleWrapper
*/ */
closeEditorPanel(somethingWasSaved = false) { closeEditorPanel(somethingWasSaved = false) {
this.setState({ editorOpen: false }); this.setState({editorOpen: false});
if (somethingWasSaved && this.props.moduleInGroupInfos.isInGroup) { if (somethingWasSaved && this.props.moduleInGroupInfos.isInGroup) {
this.props.moduleInGroupInfos.invalidateGroup(); this.props.moduleInGroupInfos.invalidateGroup();
} }
...@@ -134,7 +129,7 @@ class ModuleWrapper extends Component { ...@@ -134,7 +129,7 @@ class ModuleWrapper extends Component {
* @memberof ModuleWrapper * @memberof ModuleWrapper
*/ */
renderTitle(rawModelData) { renderTitle(rawModelData) {
return renderTitle(rawModelData, this.props.classes, this.props.buildTitle); return <ModuleTitle rawModelData={rawModelData} buildTitle={this.props.buildTitle}/>;
} }
/** /**
...@@ -148,7 +143,7 @@ class ModuleWrapper extends Component { ...@@ -148,7 +143,7 @@ class ModuleWrapper extends Component {
return ( return (
<> <>
{this.props.renderCore(rawModelData, this.props.coreClasses, this.props.outsideData)} {this.props.renderCore(rawModelData, this.props.coreClasses, this.props.outsideData)}
{renderUsefulLinks(rawModelData, this.props.classes, this.props.theme)} <UsefulLinks usefulLinks={rawModelData.useful_links}/>
</> </>
); );
} }
...@@ -160,7 +155,7 @@ class ModuleWrapper extends Component { ...@@ -160,7 +155,7 @@ class ModuleWrapper extends Component {
* @memberof ModuleWrapper * @memberof ModuleWrapper
*/ */
openPendingModerationPanel() { openPendingModerationPanel() {
this.setState({ pendingModerationOpen: true }); this.setState({pendingModerationOpen: true});
} }
/** /**
...@@ -169,7 +164,7 @@ class ModuleWrapper extends Component { ...@@ -169,7 +164,7 @@ class ModuleWrapper extends Component {
* @memberof ModuleWrapper * @memberof ModuleWrapper
*/ */
closePendingModerationPanel() { closePendingModerationPanel() {
this.setState({ pendingModerationOpen: false }); this.setState({pendingModerationOpen: false});
} }
/** /**
...@@ -178,7 +173,7 @@ class ModuleWrapper extends Component { ...@@ -178,7 +173,7 @@ class ModuleWrapper extends Component {
* @memberof ModuleWrapper * @memberof ModuleWrapper
*/ */
closeHistoryPanel() { closeHistoryPanel() {
this.setState({ historyOpen: false }); this.setState({historyOpen: false});
} }
/** /**
...@@ -189,7 +184,7 @@ class ModuleWrapper extends Component { ...@@ -189,7 +184,7 @@ class ModuleWrapper extends Component {
* @memberof ModuleWrapper * @memberof ModuleWrapper
*/ */
render() { render() {
const { classes, rawModelData } = this.props; const {classes, rawModelData} = this.props;
return ( return (
<> <>
...@@ -203,7 +198,7 @@ class ModuleWrapper extends Component { ...@@ -203,7 +198,7 @@ class ModuleWrapper extends Component {
{this.state.alert.open ? {this.state.alert.open ?
<Alert <Alert
{...this.state.alert} {...this.state.alert}
handleClose={() => this.setState({ alert: { open: false } })} handleClose={() => this.setState({alert: {open: false}})}
/> />
: :
<></> <></>
...@@ -211,7 +206,7 @@ class ModuleWrapper extends Component { ...@@ -211,7 +206,7 @@ class ModuleWrapper extends Component {
{this.state.historyOpen ? {this.state.historyOpen ?
<History <History
renderer={this} renderer={this}
modelInfo={{ contentTypeId: rawModelData.content_type_id, id: rawModelData.id }} modelInfo={{contentTypeId: rawModelData.content_type_id, id: rawModelData.id}}
closeHistoryPanel={() => this.closeHistoryPanel()} closeHistoryPanel={() => this.closeHistoryPanel()}
editFromVersion={(modelData) => this.editFromVersion(modelData)} editFromVersion={(modelData) => this.editFromVersion(modelData)}
/> />
...@@ -221,7 +216,7 @@ class ModuleWrapper extends Component { ...@@ -221,7 +216,7 @@ class ModuleWrapper extends Component {
{this.state.pendingModerationOpen ? {this.state.pendingModerationOpen ?
<PendingModeration <PendingModeration
renderer={this} renderer={this}
modelInfo={{ contentTypeId: rawModelData.content_type_id, id: rawModelData.id }} modelInfo={{contentTypeId: rawModelData.content_type_id, id: rawModelData.id}}
closePendingModerationPanel={() => this.closePendingModerationPanel()} closePendingModerationPanel={() => this.closePendingModerationPanel()}
editFromPendingModeration={(pendingModelData) => this.editFromPendingModeration(pendingModelData)} editFromPendingModeration={(pendingModelData) => this.editFromPendingModeration(pendingModelData)}
moderatePendingModeration={(modelData) => this.moderatePendingModeration(modelData)} moderatePendingModeration={(modelData) => this.moderatePendingModeration(modelData)}
...@@ -231,7 +226,12 @@ class ModuleWrapper extends Component { ...@@ -231,7 +226,12 @@ class ModuleWrapper extends Component {
<></> <></>
} }
<Paper className={classes.root} square={true}> <Paper className={classes.root} square={true}>
{renderFirstRow.bind(this)()} <ModuleFirstRow editCurrent={() => this.editCurrent()}
openHistoryPanel={() => this.setState({historyOpen: true})}
openPendingModerationPanel={() => this.openPendingModerationPanel()}
rawModelData={rawModelData}
buildTitle={this.props.buildTitle}
Icon={this.props.Icon}/>
{this.renderCore(this.props.rawModelData)} {this.renderCore(this.props.rawModelData)}
</Paper> </Paper>
</> </>
...@@ -273,21 +273,23 @@ class ModuleWrapper extends Component { ...@@ -273,21 +273,23 @@ class ModuleWrapper extends Component {
ModuleWrapper.defaultProps = { ModuleWrapper.defaultProps = {
buildTitle: () => null, buildTitle: () => null,
moduleInGroupInfos: { isInGroup: false, invalidateGroup: () => null }, moduleInGroupInfos: {isInGroup: false, invalidateGroup: () => null},
coreClasses: {}, // REFACTO: not needed with hooks ? coreClasses: {}, // REFACTO: not needed with hooks ?
Icon: undefined, Icon: undefined,
}; };
ModuleWrapper.propTypes = { ModuleWrapper.propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
rawModelData: PropTypes.object.isRequired, rawModelData: PropTypes.object.isRequired,
buildTitle: PropTypes.func.isRequired, buildTitle: PropTypes.func.isRequired,
renderCore: PropTypes.func.isRequired, renderCore: PropTypes.func.isRequired,
coreClasses: PropTypes.object.isRequired, coreClasses: PropTypes.object.isRequired,
outsideData: PropTypes.object, outsideData: PropTypes.object,
Icon: PropTypes.object, Icon: PropTypes.object,
moduleInGroupInfos: PropTypes.shape({ isInGroup: PropTypes.bool.isRequired, invalidateGroup: PropTypes.func }).isRequired, moduleInGroupInfos: PropTypes.shape({
isInGroup: PropTypes.bool.isRequired,
invalidateGroup: PropTypes.func
}).isRequired,
}; };
...@@ -297,42 +299,9 @@ const styles = theme => ({ ...@@ -297,42 +299,9 @@ const styles = theme => ({
padding: theme.spacing(1), padding: theme.spacing(1),
// marginBottom: theme.spacing(2) // marginBottom: theme.spacing(2)
}, },
rootLinks: {
display: "flex",
justifyContent: "flex-start",
flexWrap: "wrap",
},
green: {
color: green.A700,
},
orange: {
color: orange.A700,
},
red: {
color: red.A700
},
disabled: {
color: theme.palette.action.disabled
},
button: {
width: theme.spacing(5),
height: theme.spacing(5),
// margin: theme.spacing(0.5),
},
chip: {
margin: theme.spacing(1),
},
titleContainer: {
display: "flex",
alignItems: "center",
},
titleIcon: {
paddingRight: theme.spacing(1),
...theme.typography.h4,
}
}); });
export default compose( export default compose(
withStyles(styles, { withTheme: true }), withStyles(styles),
)(ModuleWrapper); )(ModuleWrapper);
...@@ -13,7 +13,7 @@ export default function getModerationTooltipAndClass(hasPendingModeration, userC ...@@ -13,7 +13,7 @@ export default function getModerationTooltipAndClass(hasPendingModeration, userC
}; };
} else { } else {
return { return {
moderTooltip: "Aucune mise-à-jour de ce module est en attente de modération pour ce module.", moderTooltip: "Aucune mise-à-jour de ce module est en attente de modération.",
moderClass: "green" moderClass: "green"
}; };
} }
......
import React from "react";
import renderTitle from "./renderTitle";
import renderUpdateInfo from "./renderUpdateInfo";
import getEditTooltipAndClass from "./getEditTooltipAndClass";
import getVersionTooltipAndClass from "./getVersionTooltipAndClass";
import getModerationTooltipAndClass from "./getModerationTooltipAndClass";
import Grid from "@material-ui/core/Grid";
import MyBadge from "../../../../common/MyBadge";
import IconButton from "@material-ui/core/IconButton";
import VerifiedUserIcon from "@material-ui/icons/VerifiedUser";
import CreateIcon from "@material-ui/icons/Create";
import SettingsBackRestoreIcon from "@material-ui/icons/SettingsBackupRestore";
import Tooltip from "@material-ui/core/Tooltip";
export default function renderFirstRow() {
const {classes, theme, rawModelData, Icon} = this.props,
nbVersions = Math.max(0, rawModelData.nb_versions),
hasPendingModeration = rawModelData.has_pending_moderation;
const {user_can_edit: userCanEdit, user_can_moderate: userCanModerate, versioned} = rawModelData.obj_info,
{versionTooltip, versionClass} = getVersionTooltipAndClass(nbVersions),
{moderTooltip, moderClass} = getModerationTooltipAndClass(hasPendingModeration, userCanEdit),
{editTooltip, editClass} = getEditTooltipAndClass(userCanEdit, userCanModerate);
return (
<Grid container spacing={1}>
<Grid item xs style={{paddingBottom: theme.spacing(1)}}>
{renderTitle(this.props.rawModelData, this.props.classes, this.props.buildTitle, Icon)}
{renderUpdateInfo.bind(this)()}