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
class CourseFeedbackSerializer(EssentialModuleSerializer):
course_code = serializers.SerializerMethodField() # needed for the front
course_title = serializers.SerializerMethodField() # needed for the front
def get_course_code(self, obj):
return obj.course.code
def get_course_title(self, obj):
return obj.course.title
def update(self, instance, validated_data):
instance.untouched = False
return super().update(instance, validated_data)
......@@ -21,6 +25,7 @@ class CourseFeedbackSerializer(EssentialModuleSerializer):
model = CourseFeedback
fields = EssentialModuleSerializer.Meta.fields + (
"course_code",
"course_title",
"language",
"comment",
"adequation",
......
......@@ -60,9 +60,14 @@ class UnlinkedPartners extends CustomComponentForAPI {
nUnlinked > 0 ?
<>
<Typography variant={"caption"} color={"primary"}>
⚠&nbsp;{unlinkedPartners.length} universités partenaires de l'UTC
ne sont pas encore pleinement disponible(s) sur la plateforme.
Plus d'informations&nbsp;
⚠&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}>
<Typography variant={"caption"} color={"primary"}>
ici
......
......@@ -32,8 +32,8 @@ function SimplePopupMenu(props) {
onClose={handleClose}
>
{
props.items.map(({label, onClick}, key) => (
<MenuItem key={key} onClick={() => {
props.items.map(({label, onClick, disabled}, key) => (
<MenuItem key={key} disabled={disabled} onClick={() => {
onClick();
handleClose();
}}>
......@@ -51,6 +51,7 @@ SimplePopupMenu.propTypes = {
items: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
disabled: PropTypes.bool.isRequired,
})).isRequired,
renderHolder: PropTypes.func.isRequired,
};
......
......@@ -152,6 +152,8 @@ class Form extends Component {
// we need to compare objects (ie JSON objects) differently
if (typeof cmp1 === "object") {
return !isEqual(cmp1, cmp2);
} else if ((typeof cmp1 === "number") || (typeof cmp2 === "number")) {
return cmp1 != cmp2; // allow 93 == "93.00"
} else {
return cmp1 !== cmp2;
}
......
......@@ -134,7 +134,7 @@ Field.propTypes = {
required: PropTypes.bool.isRequired, // is the field required ?
label: PropTypes.string, // text to go along the field
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
fieldMapping: PropTypes.string.isRequired, // name of the field in the data
};
......
......@@ -39,8 +39,11 @@ class NumberField extends Field {
}
serializeFromField() {
const value = super.serializeFromField();
return parseFloat(value);
const value = super.serializeFromField(),
parsed = parseFloat(value);
if (isNaN(parsed)) return null;
else return parsed;
}
handleChangeValue(val) {
......
......@@ -45,7 +45,7 @@ class PageEditExchangeFeedbacks extends CustomComponentForAPI {
if (id) {
return (
<>
<Typography variant={"h3"}>
<Typography variant={"h4"}>
{
univId ? <CustomLink to={APP_ROUTES.forUniversity(univId)}>{univName}</CustomLink>
:
......
......@@ -103,6 +103,7 @@ class SelectListSubPage extends CustomComponentForAPI {
<SimplePopupMenu
items={[
{
disabled: false,
label: "Confirmer",
onClick: () => this.props.deleteList(list.id, () => this.props.invalidateReadAll())
},
......
......@@ -452,6 +452,7 @@ class View extends React.Component {
<SimplePopupMenu
items={[
{
disabled: false,
label: "Confirmer",
onClick: () => this.deleteBlock()
},
......@@ -466,8 +467,8 @@ class View extends React.Component {
<SimplePopupMenu
items={[
{label: "Markdown", onClick: () => this.addBlock("text-block")},
{label: "Université", onClick: () => this.addBlock("univ-block")},
{label: "Markdown", onClick: () => this.addBlock("text-block"), disabled: false,},
{label: "Université", onClick: () => this.addBlock("univ-block"), disabled: false,},
]}
renderHolder={({onClick}) => (
<Button variant={"contained"}
......
......@@ -9,7 +9,6 @@ import Divider from "@material-ui/core/Divider";
import Button from "@material-ui/core/Button";
import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft";
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
import SwipeableViews from "react-swipeable-views";
import range from "../../utils/range";
import {APP_ROUTES} from "../../config/appRoutes";
import isEqual from "lodash/isEqual";
......@@ -69,35 +68,24 @@ class UnivList extends React.Component {
return (
<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
steps={maxSteps}
position="static"
variant={"progress"}
activeStep={activeStep}
className={classes.mobileStepper}
nextButton={
......
......@@ -58,7 +58,7 @@ class UniversityTemplate extends Component {
{value} = this.state;
let scroll = true;
if (isWidthUp("lg", width)) {
if (isWidthUp("sm", width)) {
scroll = false;
}
......
......@@ -32,12 +32,14 @@ class UniversityGeneralForm extends Form {
{...this.getReferenceAndValue("website")}
maxLength={300}
isUrl={true}/>
<TextField label={"Logo de l'université"}
{...this.getReferenceAndValue("logo")}
maxLength={300}
isUrl={true}
urlExtensions={["jpg", "png", "svg"]}/>
{/* Logo feature is not ready yet */}
{this.renderHiddenField({fieldMapping: "logo"})}
{/*<TextField label={"Logo de l'université"}*/}
{/* {...this.getReferenceAndValue("logo")}*/}
{/* required={false}*/}
{/* maxLength={300}*/}
{/* isUrl={true}*/}
{/* urlExtensions={["jpg", "png", "svg"]}/>*/}
</>
);
}
......
......@@ -122,9 +122,6 @@ const styles = theme => ({
// marginBottom: theme.spacing(2),
flexGrow: 1,
},
green: {
color: green.A200,
},
button: {
width: theme.spacing(5),
height: theme.spacing(5),
......
import React, { Component } 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 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 renderUsefulLinks from "./moduleWrapperFunctions/renderUsefulLinks";
import renderFirstRow from "./moduleWrapperFunctions/renderFirstRow";
import renderTitle from "./moduleWrapperFunctions/renderTitle";
import ModuleTitle from "./subComponents/ModuleTitle";
import History from "./History";
import PendingModeration from "./PendingModeration";
import ModuleFirstRow from "./subComponents/ModuleFirstRow";
import UsefulLinks from "./subComponents/UsefulLinks";
/**
......@@ -41,7 +36,7 @@ class ModuleWrapper extends Component {
historyOpen: false,
pendingModerationOpen: false,
rawModelDataForEditor: this.props.rawModelData,
alert: { open: false }
alert: {open: false}
};
/**
......@@ -53,7 +48,7 @@ class ModuleWrapper extends Component {
*/
openEditorPanel(ignorePendingModeration = false) {
if (ignorePendingModeration || !this.props.rawModelData.has_pending_moderation) {
this.setState({ editorOpen: true });
this.setState({editorOpen: true});
} else {
this.alertThereIsSomethingPendingModeration();
}
......@@ -67,7 +62,7 @@ class ModuleWrapper extends Component {
* @memberof ModuleWrapper
*/
closeEditorPanel(somethingWasSaved = false) {
this.setState({ editorOpen: false });
this.setState({editorOpen: false});
if (somethingWasSaved && this.props.moduleInGroupInfos.isInGroup) {
this.props.moduleInGroupInfos.invalidateGroup();
}
......@@ -134,7 +129,7 @@ class ModuleWrapper extends Component {
* @memberof ModuleWrapper
*/
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 {
return (
<>
{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 {
* @memberof ModuleWrapper
*/
openPendingModerationPanel() {
this.setState({ pendingModerationOpen: true });
this.setState({pendingModerationOpen: true});
}
/**
......@@ -169,7 +164,7 @@ class ModuleWrapper extends Component {
* @memberof ModuleWrapper
*/
closePendingModerationPanel() {
this.setState({ pendingModerationOpen: false });
this.setState({pendingModerationOpen: false});
}
/**
......@@ -178,7 +173,7 @@ class ModuleWrapper extends Component {
* @memberof ModuleWrapper
*/
closeHistoryPanel() {
this.setState({ historyOpen: false });
this.setState({historyOpen: false});
}
/**
......@@ -189,7 +184,7 @@ class ModuleWrapper extends Component {
* @memberof ModuleWrapper
*/
render() {
const { classes, rawModelData } = this.props;
const {classes, rawModelData} = this.props;
return (
<>
......@@ -203,7 +198,7 @@ class ModuleWrapper extends Component {
{this.state.alert.open ?
<Alert
{...this.state.alert}
handleClose={() => this.setState({ alert: { open: false } })}
handleClose={() => this.setState({alert: {open: false}})}
/>
:
<></>
......@@ -211,7 +206,7 @@ class ModuleWrapper extends Component {
{this.state.historyOpen ?
<History
renderer={this}
modelInfo={{ contentTypeId: rawModelData.content_type_id, id: rawModelData.id }}
modelInfo={{contentTypeId: rawModelData.content_type_id, id: rawModelData.id}}
closeHistoryPanel={() => this.closeHistoryPanel()}
editFromVersion={(modelData) => this.editFromVersion(modelData)}
/>
......@@ -221,7 +216,7 @@ class ModuleWrapper extends Component {
{this.state.pendingModerationOpen ?
<PendingModeration
renderer={this}
modelInfo={{ contentTypeId: rawModelData.content_type_id, id: rawModelData.id }}
modelInfo={{contentTypeId: rawModelData.content_type_id, id: rawModelData.id}}
closePendingModerationPanel={() => this.closePendingModerationPanel()}
editFromPendingModeration={(pendingModelData) => this.editFromPendingModeration(pendingModelData)}
moderatePendingModeration={(modelData) => this.moderatePendingModeration(modelData)}
......@@ -231,7 +226,12 @@ class ModuleWrapper extends Component {
<></>
}
<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)}
</Paper>
</>
......@@ -273,21 +273,23 @@ class ModuleWrapper extends Component {
ModuleWrapper.defaultProps = {
buildTitle: () => null,
moduleInGroupInfos: { isInGroup: false, invalidateGroup: () => null },
moduleInGroupInfos: {isInGroup: false, invalidateGroup: () => null},
coreClasses: {}, // REFACTO: not needed with hooks ?
Icon: undefined,
};
ModuleWrapper.propTypes = {
classes: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
rawModelData: PropTypes.object.isRequired,
buildTitle: PropTypes.func.isRequired,
renderCore: PropTypes.func.isRequired,
coreClasses: PropTypes.object.isRequired,
outsideData: 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 => ({
padding: theme.spacing(1),
// 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(
withStyles(styles, { withTheme: true }),
withStyles(styles),
)(ModuleWrapper);
......@@ -13,7 +13,7 @@ export default function getModerationTooltipAndClass(hasPendingModeration, userC
};
} else {
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"
};
}
......
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)()}
</Grid>
<Grid item xs={4} style={{textAlign: "right"}}>
<Tooltip title={moderTooltip} placement="top">
<div
style={{display: "inline-block"}}> {/* Needed to fire events for the tooltip when below is disabled! when below is disabled!! */}
<MyBadge badgeContent={hasPendingModeration ? 1 : 0} color="secondary" minNumber={1}>
<IconButton aria-label="Modération" disabled={moderClass === "disabled" || moderClass === "green"}
onClick={() => this.openPendingModerationPanel()} className={classes.button}>
<VerifiedUserIcon className={classes[moderClass]}/>
</IconButton>
</MyBadge>
</div>
</Tooltip>
<Tooltip title={editTooltip} placement="top">
<div style={{display: "inline-block"}}> {/* Needed to fire events for the tooltip when below is disabled!! */}
<IconButton aria-label="Éditer" className={classes.button} disabled={editClass === "disabled"}
onClick={() => this.editCurrent()}>
<CreateIcon className={classes[editClass]}/>
</IconButton>
</div>
</Tooltip>
{
versioned ?
<Tooltip title={versionTooltip} placement="top">
<div
style={{display: "inline-block"}}> {/* Needed to fire events for the tooltip when below is disabled!! */}
<MyBadge badgeContent={nbVersions} color="secondary" minNumber={2}>
<IconButton aria-label="Restorer" disabled={versionClass === "disabled"} className={classes.button}
onClick={() => this.setState({historyOpen: true})}>
<SettingsBackRestoreIcon className={classes[versionClass]}/>
</IconButton>
</MyBadge>
</div>
</Tooltip>
:
<></>
}
</Grid>
</Grid>
);
}
import React from "react";
import ModuleTitle from "./ModuleTitle";
import getEditTooltipAndClass from "../moduleWrapperFunctions/getEditTooltipAndClass";
import getVersionTooltipAndClass from "../moduleWrapperFunctions/getVersionTooltipAndClass";
import getModerationTooltipAndClass from "../moduleWrapperFunctions/getModerationTooltipAndClass";
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 MoreIcon from "@material-ui/icons/MoreHoriz";
import SettingsBackRestoreIcon from "@material-ui/icons/SettingsBackupRestore";
import Tooltip from "@material-ui/core/Tooltip";
import PropTypes from "prop-types";
import {makeStyles} from "@material-ui/styles";
import green from "@material-ui/core/colors/green";
import orange from "@material-ui/core/colors/orange";
import red from "@material-ui/core/colors/red";
import UpdateInfo from "./UpdateInfo";
import SimplePopupMenu from "../../../../common/SimplePopupMenu";
const useStyles = makeStyles(theme => {
const onDesktops = "@media (min-width:450px)";
const onMobiles = "@media (max-width:450px)";
return {
button: {
width: theme.spacing(5),
height: theme.spacing(5),
// margin: theme.spacing(0.5),
},
green: {
color: green.A700,
},
orange: {
color: orange.A700,
},
red: {
color: red.A700
},
container: {
display: "flex",
justifyContent: "space-between"
},
itemLeft: {
paddingBottom: theme.spacing(1)
},
itemRight: {
textAlign: "right",
[onDesktops]: {
minWidth: 130,
},
[onMobiles]: {
minWidth: 45,
}
},
desktopsOnly: {
[onMobiles]: {
display: "none",
}
},
mobilesOnly: {
[onDesktops]: {
display: "none",
}
}
};
});
function ModuleFirstRow(props) {
const classes = useStyles();
const {rawModelData, Icon, buildTitle} = 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);
const moderDisabled = moderClass === "disabled" || moderClass === "green",
editDisabled = editClass === "disabled",
versionDisabled = (!versioned) || versionClass === "disabled";
const menuItems = [
{
disabled: moderDisabled,
label: "En attente de modération",
onClick: () => props.openPendingModerationPanel(),
},
{
disabled: editDisabled,
label: "Éditer l'élément",
onClick: () => props.editCurrent(),
},
{
disabled: versionDisabled,
label: "Précédente(s) version(s)",
onClick: () => props.openHistoryPanel(),
}
];
return (
<div className={classes.container}>
<div className={classes.itemLeft}>
<ModuleTitle rawModelData={rawModelData} buildTitle={buildTitle} Icon={Icon}/>
<UpdateInfo rawModelData={rawModelData}/>
</div>
<div className={classes.itemRight}>
<div className={classes.desktopsOnly}>
<Tooltip title={moderTooltip} placement="top">
<div
style={{display: "inline-block"}}> {/* Needed to fire events for the tooltip when below is disabled! when below is disabled!! */}
<MyBadge badgeContent={hasPendingModeration ? 1 : 0} color="secondary" minNumber={1}>
<IconButton aria-label="Modération" disabled={moderDisabled}
onClick={() => props.openPendingModerationPanel()} className={classes.button}>
<VerifiedUserIcon className={classes[moderClass]}/>