Merge branch 'tutorial-builder' into tutorial

This commit is contained in:
Delucse 2020-09-30 15:41:52 +02:00
commit f97ab10a53
10 changed files with 223 additions and 150 deletions

View File

@ -85,10 +85,18 @@ export const removeErrorStep = (index) => (dispatch, getState) => {
});
};
export const changeContent = (index, property, content) => (dispatch, getState) => {
export const changeContent = (content, index, property1, property2) => (dispatch, getState) => {
var steps = getState().builder.steps;
var step = steps[index];
step[property] = content;
if(property2){
if(step[property1] && step[property1][property2]){
step[property1][property2] = content;
} else {
step[property1] = {[property2]: content};
}
} else {
step[property1] = content;
}
dispatch({
type: BUILDER_CHANGE_STEP,
payload: steps
@ -96,10 +104,16 @@ export const changeContent = (index, property, content) => (dispatch, getState)
dispatch(changeTutorialBuilder());
};
export const deleteProperty = (index, property) => (dispatch, getState) => {
export const deleteProperty = (index, property1, property2) => (dispatch, getState) => {
var steps = getState().builder.steps;
var step = steps[index];
delete step[property];
if(property2){
if(step[property1] && step[property1][property2]){
delete step[property1][property2];
}
} else {
delete step[property1];
}
dispatch({
type: BUILDER_DELETE_PROPERTY,
payload: steps
@ -170,14 +184,14 @@ export const setSubmitError = () => (dispatch, getState) => {
dispatch(setError(undefined, 'title'));
}
var type = builder.steps.map((step, i) => {
// picture and xml are directly checked for errors in their components and
// media and xml are directly checked for errors in their components and
// therefore do not have to be checked again
step.id = i+1;
if(i === 0){
if(step.requirements && step.requirements.length > 0){
var requirements = step.requirements.filter(requirement => typeof(requirement)==='number');
if(requirements.length < step.requirements.length){
dispatch(changeContent(i, 'requirements', requirements));
dispatch(changeContent(requirements, i, 'requirements'));
}
}
if(step.hardware === undefined || step.hardware.length < 1){
@ -187,7 +201,7 @@ export const setSubmitError = () => (dispatch, getState) => {
var hardwareIds = data.map(hardware => hardware.id);
var hardware = step.hardware.filter(hardware => hardwareIds.includes(hardware));
if(hardware.length < step.hardware.length){
dispatch(changeContent(i, 'hardware', hardware));
dispatch(changeContent(hardware, i, 'hardware'));
}
}
}
@ -272,8 +286,14 @@ export const readJSON = (json) => (dispatch, getState) => {
if(step.xml){
object.xml = step.xml;
}
if(step.picture && step.type === 'instruction'){
object.picture = step.picture;
if(step.media && step.type === 'instruction'){
object.media = {};
if(step.media.picture){
object.media.picture = step.media.picture;
}
else if(step.media.youtube){
object.media.youtube = step.media.youtube;
}
}
return object;
});

View File

@ -107,7 +107,7 @@ class BlocklyExample extends Component {
setXml = () => {
var xml = this.props.xml;
this.props.changeContent(this.props.index, 'xml', xml);
this.props.changeContent(xml, this.props.index, 'xml');
this.setState({input: moment(Date.now()).format('LTS')});
}

View File

@ -56,7 +56,7 @@ class Requirements extends Component {
this.props.deleteError(this.props.index, 'hardware');
}
}
this.props.changeContent(this.props.index, 'hardware', hardwareArray);
this.props.changeContent(hardwareArray, this.props.index, 'hardware');
if(hardwareArray.length === 0){
this.props.setError(this.props.index, 'hardware');
}

View File

@ -0,0 +1,178 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { changeContent, deleteProperty, setError, deleteError } from '../../../actions/tutorialBuilderActions';
import Textfield from './Textfield';
import { withStyles } from '@material-ui/core/styles';
import Switch from '@material-ui/core/Switch';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormHelperText from '@material-ui/core/FormHelperText';
import Radio from '@material-ui/core/Radio';
import RadioGroup from '@material-ui/core/RadioGroup';
import Button from '@material-ui/core/Button';
const styles = (theme) => ({
errorColor: {
color: theme.palette.error.dark
},
errorBorder: {
border: `1px solid ${theme.palette.error.dark}`
},
errorButton: {
marginTop: '5px',
height: '40px',
backgroundColor: theme.palette.error.dark,
'&:hover':{
backgroundColor: theme.palette.error.dark
}
}
});
class Media extends Component {
constructor(props){
super(props);
this.state={
checked: props.value ? true : false,
error: false,
radioValue: !props.picture && !props.youtube ? 'picture' : props.picture ? 'picture' : 'youtube'
};
}
componentDidUpdate(props){
if(props.value !== this.props.value){
this.setState({ checked: this.props.value ? true : false });
}
}
onChangeSwitch = (value) => {
var oldValue = this.state.checked;
this.setState({checked: value});
if(oldValue !== value){
if(value){
this.props.setError(this.props.index, 'media');
} else {
this.props.deleteError(this.props.index, 'media');
this.props.deleteProperty(this.props.index, 'media');
this.props.deleteProperty(this.props.index, 'url');
this.setState({ error: false});
}
}
}
onChangeRadio = (value) => {
this.props.setError(this.props.index, 'media');
var oldValue = this.state.radioValue;
this.setState({radioValue: value, error: false});
// delete property 'oldValue', so that all old media files are reset
this.props.deleteProperty(this.props.index, 'media', oldValue);
if(oldValue === 'picture'){
this.props.deleteProperty(this.props.index, 'url');
}
}
uploadPicture = (pic) => {
if(!(/^image\/.*/.test(pic.type))){
this.props.setError(this.props.index, 'media');
this.setState({ error: true });
this.props.deleteProperty(this.props.index, 'url');
}
else {
this.props.deleteError(this.props.index, 'media');
this.setState({ error: false });
this.props.changeContent(URL.createObjectURL(pic), this.props.index, 'url');
}
this.props.changeContent(pic.name, this.props.index, 'media', 'picture');
}
render() {
return (
<div style={{marginBottom: '10px', padding: '18.5px 14px', borderRadius: '25px', border: '1px solid lightgrey', width: 'calc(100% - 28px)'}}>
<FormControlLabel
labelPlacement="end"
label={"Medien"}
control={
<Switch
checked={this.state.checked}
onChange={(e) => this.onChangeSwitch(e.target.checked)}
color="primary"
/>
}
/>
{this.state.checked ?
<div>
<RadioGroup row value={this.state.radioValue} onChange={(e) => {this.onChangeRadio(e.target.value);}}>
<FormControlLabel style={{color: 'black'}}
value="picture"
control={<Radio color="primary" />}
label="Bild"
labelPlacement="end"
/>
<FormControlLabel style={{color: 'black'}}
value="youtube"
control={<Radio color="primary" />}
label="Youtube-Video"
labelPlacement="end"
/>
</RadioGroup>
{this.state.radioValue === 'picture' ?
<div>
{!this.props.error ?
<div>
<FormHelperText style={{lineHeight: 'initial', marginBottom: '10px'}}>{`Beachte, dass das Foto zusätzlich in den Ordner public/media/tutorial unter dem Namen '${this.props.picture}' abgespeichert werden muss.`}</FormHelperText>
<img src={this.props.url ? this.props.url : `/media/tutorial/${this.props.picture}`} alt={this.props.url ? '' : `Das Bild '${this.props.picture}' konnte nicht im Ordner public/media/tutorial gefunden werden und kann daher nicht angezeigt werden.`} style={{maxHeight: '180px', maxWidth: '360px', marginBottom: '5px'}}/>
</div>
: <div
style={{height: '150px', maxWidth: '250px', marginBottom: '5px', justifyContent: "center", alignItems: "center", display:"flex", padding: '20px'}}
className={this.props.error ? this.props.classes.errorBorder : null} >
{this.state.error ?
<FormHelperText style={{lineHeight: 'initial', textAlign: 'center'}} className={this.props.classes.errorColor}>{`Die übergebene Datei entspricht nicht dem geforderten Bild-Format. Überprüfe, ob es sich um ein Bild handelt und versuche es nochmal.`}</FormHelperText>
: <FormHelperText style={{lineHeight: 'initial', textAlign: 'center'}} className={this.props.classes.errorColor}>{`Wähle ein Bild aus.`}</FormHelperText>
}
</div>}
{/*upload picture*/}
<div>
<input
style={{display: 'none'}}
accept="image/*"
onChange={(e) => {this.uploadPicture(e.target.files[0]);}}
id={`picture ${this.props.index}`}
type="file"
/>
<label htmlFor={`picture ${this.props.index}`}>
<Button component="span" className={this.props.error ? this.props.classes.errorButton : null} style={{marginRight: '10px', marginBottom: '10px'}} variant='contained' color='primary'>Bild auswählen</Button>
</label>
</div>
</div>
:
/*youtube-video*/
<div>
<Textfield value={this.props.value && this.props.value.youtube} property={'media'} property2={'youtube'} label={'Youtube-ID'} index={this.props.index} error={this.props.error} errorText={`Gib eine Youtube-ID ein.`}/>
{this.props.youtube && !this.props.error ?
<div>
<FormHelperText style={{lineHeight: 'initial', margin: '0 25px 10px 25px'}}>{`Stelle sicher, dass das unten angezeigte Youtube-Video funktioniert, andernfalls überprüfe die Youtube-ID.`}</FormHelperText>
<div style={{position: 'relative', paddingBottom: '56.25%', height: 0}}>
<iframe title={this.props.youtube} style={{borderRadius: '25px', position: 'absolute', top: '0', left: '0', width: '100%', height: '100%'}} src={`https://www.youtube.com/embed/${this.props.youtube}`} frameBorder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowFullScreen />
</div>
</div>
: null}
</div>
}
</div>
: null}
</div>
);
};
}
Media.propTypes = {
changeContent: PropTypes.func.isRequired,
deleteProperty: PropTypes.func.isRequired,
setError: PropTypes.func.isRequired,
deleteError: PropTypes.func.isRequired,
};
export default connect(null, { changeContent, deleteProperty, setError, deleteError })(withStyles(styles, {withTheme: true})(Media));

View File

@ -1,132 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { changeContent, deleteProperty, setError, deleteError } from '../../../actions/tutorialBuilderActions';
import { withStyles } from '@material-ui/core/styles';
import Switch from '@material-ui/core/Switch';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormHelperText from '@material-ui/core/FormHelperText';
import Button from '@material-ui/core/Button';
const styles = (theme) => ({
errorColor: {
color: theme.palette.error.dark
},
errorBorder: {
border: `1px solid ${theme.palette.error.dark}`
},
errorButton: {
marginTop: '5px',
height: '40px',
backgroundColor: theme.palette.error.dark,
'&:hover':{
backgroundColor: theme.palette.error.dark
}
}
});
class Picture extends Component {
constructor(props){
super(props);
this.state={
checked: props.value ? true : false,
error: false
};
}
componentDidUpdate(props){
if(props.value !== this.props.value){
this.setState({ checked: this.props.value ? true : false });
}
}
onChange = (value) => {
var oldValue = this.state.checked;
this.setState({checked: value});
if(oldValue !== value){
if(value){
this.props.setError(this.props.index, 'picture');
} else {
this.props.deleteError(this.props.index, 'picture');
this.props.deleteProperty(this.props.index, 'picture');
this.props.deleteProperty(this.props.index, 'url');
this.setState({ error: false});
}
}
}
uploadPicture = (pic) => {
if(!(/^image\/.*/.test(pic.type))){
this.props.setError(this.props.index, 'picture');
this.setState({ error: true });
this.props.deleteProperty(this.props.index, 'url');
}
else {
this.props.deleteError(this.props.index, 'picture');
this.setState({ error: false });
this.props.changeContent(this.props.index, 'url', URL.createObjectURL(pic));
}
this.props.changeContent(this.props.index, 'picture', pic.name);
}
render() {
return (
<div style={{marginBottom: '10px', padding: '18.5px 14px', borderRadius: '25px', border: '1px solid lightgrey', width: 'calc(100% - 28px)'}}>
<FormControlLabel
labelPlacement="end"
label={"Bild"}
control={
<Switch
checked={this.state.checked}
onChange={(e) => this.onChange(e.target.checked)}
color="primary"
/>
}
/>
{this.state.checked ?
<div>
{!this.props.error ?
<div>
<FormHelperText style={{lineHeight: 'initial', marginBottom: '10px'}}>{`Beachte, dass das Foto zusätzlich in den Ordner public/media/tutorial unter dem Namen '${this.props.value}' abgespeichert werden muss.`}</FormHelperText>
<img src={this.props.url ? this.props.url : `/media/tutorial/${this.props.value}`} alt={this.props.url ? '' : `Das Bild '${this.props.value}' konnte nicht im Ordner public/media/tutorial gefunden werden und kann daher nicht angezeigt werden.`} style={{maxHeight: '180px', maxWidth: '360px', marginBottom: '5px'}}/>
</div>
: <div
style={{height: '150px', maxWidth: '250px', marginBottom: '5px', justifyContent: "center", alignItems: "center", display:"flex", padding: '20px'}}
className={this.props.error ? this.props.classes.errorBorder : null} >
{this.props.error ?
this.state.error ?
<FormHelperText style={{lineHeight: 'initial', textAlign: 'center'}} className={this.props.classes.errorColor}>{`Die übergebene Datei entspricht nicht dem geforderten Bild-Format. Überprüfe, ob es sich um ein Bild handelt und versuche es nochmal.`}</FormHelperText>
: <FormHelperText style={{lineHeight: 'initial', textAlign: 'center'}} className={this.props.classes.errorColor}>{`Wähle ein Bild aus.`}</FormHelperText>
: null}
</div>}
{/*upload picture*/}
<div ref={this.inputRef}>
<input
style={{display: 'none'}}
accept="image/*"
onChange={(e) => {this.uploadPicture(e.target.files[0])}}
id="picture"
type="file"
/>
<label htmlFor="picture">
<Button component="span" className={this.props.error ? this.props.classes.errorButton : null} style={{marginRight: '10px', marginBottom: '10px'}} variant='contained' color='primary'>Bild auswählen</Button>
</label>
</div>
</div>
: null}
</div>
);
};
}
Picture.propTypes = {
changeContent: PropTypes.func.isRequired,
deleteProperty: PropTypes.func.isRequired,
setError: PropTypes.func.isRequired,
deleteError: PropTypes.func.isRequired,
};
export default connect(null, { changeContent, deleteProperty, setError, deleteError })(withStyles(styles, {withTheme: true})(Picture));

View File

@ -23,7 +23,7 @@ class Requirements extends Component {
else {
requirements = requirements.filter(requirement => requirement !== value);
}
this.props.changeContent(this.props.index, 'requirements', requirements);
this.props.changeContent(requirements, this.props.index, 'requirements');
}
render() {

View File

@ -10,7 +10,7 @@ import StepType from './StepType';
import BlocklyExample from './BlocklyExample';
import Requirements from './Requirements';
import Hardware from './Hardware';
import Picture from './Picture';
import Media from './Media';
import { withStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
@ -105,7 +105,7 @@ class Step extends Component {
</div>
: null}
{this.props.step.type === 'instruction' ?
<Picture value={this.props.step.picture} url={this.props.step.url} index={index} error={this.props.error.steps[index].picture} />
<Media value={this.props.step.media} picture={this.props.step.media && this.props.step.media.picture} youtube={this.props.step.media && this.props.step.media.youtube} url={this.props.step.url} index={index} error={this.props.error.steps[index].media} />
: null}
<BlocklyExample value={this.props.step.xml} index={index} task={this.props.step.type === 'task'} error={this.props.error.steps[index].xml ? true : false}/>
</div>

View File

@ -10,7 +10,7 @@ import FormControlLabel from '@material-ui/core/FormControlLabel';
class StepType extends Component {
onChange = (value) => {
this.props.changeContent(this.props.index, 'type', value);
this.props.changeContent(value, this.props.index, 'type');
// delete property 'xml', so that all used blocks are reset
this.props.deleteProperty(this.props.index, 'xml');
if(value === 'task'){

View File

@ -28,7 +28,9 @@ class Textfield extends Component {
componentDidMount(){
if(this.props.error){
this.props.deleteError(this.props.index, this.props.property);
if(this.props.property !== 'media'){
this.props.deleteError(this.props.index, this.props.property);
}
}
}
@ -41,7 +43,7 @@ class Textfield extends Component {
this.props.jsonString(value);
}
else {
this.props.changeContent(this.props.index, this.props.property, value);
this.props.changeContent(value, this.props.index, this.props.property, this.props.property2);
}
if(value.replace(/\s/g,'') === ''){
this.props.setError(this.props.index, this.props.property);

View File

@ -57,6 +57,9 @@
"type": "instruction",
"headline": "Programmierung",
"text": "Man benötigt folgenden Block:",
"media": {
"picture": "block_en.svg"
},
"xml": "<xml xmlns='https://developers.google.com/blockly/xml'><block type='sensebox_wifi' id='-!X.Ay]z1ACt!f5+Vfr8'><field name='SSID'>SSID</field><field name='Password'>Password</field></block></xml>"
},
{
@ -64,7 +67,9 @@
"type": "instruction",
"headline": "Block richtig einbinden",
"text": "Dies ist ein Test.",
"picture": "block_en.svg"
"media": {
"youtube": "Q41MPMBCPto"
}
},
{
"id": 4,