Skip to content
Snippets Groups Projects
Commit edb97f85 authored by Emil Wahlqvist's avatar Emil Wahlqvist
Browse files

Resolve "Use data from database in editor"

parent 281c5be8
No related branches found
No related tags found
1 merge request!62Resolve "Use data from database in editor"
Pipeline #40551 passed with warnings
Showing
with 494 additions and 72 deletions
import axios from 'axios'
import { AppDispatch } from './../store'
import Types from './types'
export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch) => {
await axios
.get(`/competitions/${id}`)
.then((res) => {
dispatch({
type: Types.SET_EDITOR_COMPETITION,
//res.data,
payload: {
name: 'Tävling 1 (Hårdkodad)',
id: 1,
year: 1337,
city_id: 1,
slides: [
{
competition_id: 1,
id: 1,
order: 1,
timer: 10,
title: 'Sida 1',
questions: [
{
id: 1,
slide_id: 1,
name: 'Fråga 1 namn',
title: 'Fråga 1 titel',
total_score: 5,
type_id: 3,
question_answers: [
{
id: 1,
question_id: 1,
team_id: 1,
data: 'question answer data 1',
score: 1,
},
{
id: 2,
question_id: 1,
team_id: 2,
data: 'question answer data 2',
score: 3,
},
],
alternatives: [
{
id: 1,
text: '1',
value: true,
question_id: 1,
},
{
id: 2,
text: '0',
value: false,
question_id: 1,
},
],
},
],
body: 'Slide body 1',
settings: 'Slide settings 1',
},
{
competition_id: 1,
id: 2,
order: 2,
timer: 15,
title: 'Sida 2',
questions: [
{
id: 2,
slide_id: 2,
name: 'Fråga 2 namn',
title: 'Fråga 2 titel',
total_score: 6,
type_id: 3,
question_answers: [
{
id: 3,
question_id: 2,
team_id: 1,
data: 'question answer data 1',
score: 1,
},
{
id: 4,
question_id: 2,
team_id: 2,
data: 'question answer data 2',
score: 4,
},
],
alternatives: [
{
id: 1,
text: '5',
value: true,
question_id: 2,
},
{
id: 2,
text: 'abc',
value: false,
question_id: 2,
},
],
},
],
body: 'Slide body 2',
settings: 'Slide settings 2',
},
],
teams: [
{
id: 1,
name: 'Örkelljunga IK',
question_answers: [
{
id: 1,
question_id: 1,
team_id: 1,
data: 'question answer data 1',
score: 1,
},
{
id: 3,
question_id: 2,
team_id: 1,
data: 'question answer data 1',
score: 1,
},
],
competition_id: 1,
},
{
id: 2,
name: 'Vadstena OK',
question_answers: [
{
id: 2,
question_id: 1,
team_id: 2,
data: 'question answer data 2',
score: 3,
},
{
id: 4,
question_id: 2,
team_id: 2,
data: 'question answer data 2',
score: 4,
},
],
competition_id: 1,
},
],
},
})
})
.catch((err) => {
console.log(err)
})
}
......@@ -15,6 +15,7 @@ export default {
SET_COMPETITIONS_FILTER_PARAMS: 'SET_COMPETITIONS_FILTER_PARAMS',
SET_COMPETITIONS_TOTAL: 'SET_COMPETITIONS_TOTAL',
SET_COMPETITIONS_COUNT: 'SET_COMPETITIONS_COUNT',
SET_EDITOR_COMPETITION: 'SET_EDITOR_COMPETITION',
SET_PRESENTATION_COMPETITION: 'SET_PRESENTATION_COMPETITION',
SET_PRESENTATION_SLIDE: 'SET_PRESENTATION_SLIDE',
SET_PRESENTATION_SLIDE_PREVIOUS: 'SET_PRESENTATION_SLIDE_PREVIOUS',
......
......@@ -3,8 +3,9 @@ export interface NameID {
name: string
}
export interface City extends NameID {}
export interface Role extends NameID {}
export interface MediaType extends NameID {}
export interface QuestionType extends NameID {}
export interface ComponentType extends NameID {}
export interface ViewType extends NameID {}
......@@ -23,6 +24,8 @@ export interface Media {
user_id: number
}
export interface MediaType extends NameID {}
export interface User extends NameID {
email: string
role_id: number
......@@ -54,7 +57,7 @@ export interface QuestionAlternative {
export interface QuestionAnswer {
id: number
question_id: number
team_id: string
team_id: number
data: string
score: number
}
......
import { City, Component, Media, QuestionAnswer, QuestionType } from './ApiModels'
import { Component } from 'react'
import { Media, QuestionAlternative, QuestionAnswer, QuestionType } from './ApiModels'
export interface RichCompetition {
name: string
id: number
year: number
city: City
city_id: number
slides: RichSlide[]
teams: RichTeam[]
}
......@@ -15,9 +16,9 @@ export interface RichSlide {
timer: number
title: string
competition_id: number
question: RichQuestion[]
components: Component[]
medias: Media[]
questions: RichQuestion[]
}
export interface RichTeam {
......@@ -34,4 +35,6 @@ export interface RichQuestion {
title: string
total_score: number
question_type: QuestionType
type_id: number
question_alternatives: QuestionAlternative[]
}
import { render } from '@testing-library/react'
import mockedAxios from 'axios'
import React from 'react'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import store from '../../store'
import PresentationEditorPage from './PresentationEditorPage'
it('renders presentation editor', () => {
const competitionRes: any = {
data: {
name: '',
id: 0,
year: 0,
city_id: 0,
slides: [],
teams: [],
},
}
const citiesRes: any = {
data: {
items: [
{
name: '',
city_id: 0,
},
],
},
}
;(mockedAxios.get as jest.Mock).mockImplementation((path: string, params?: any) => {
if (path.startsWith('/competitions')) return Promise.resolve(competitionRes)
return Promise.resolve(citiesRes)
})
render(
<BrowserRouter>
<PresentationEditorPage />
</BrowserRouter>
<Provider store={store}>
<BrowserRouter>
<PresentationEditorPage />
</BrowserRouter>
</Provider>
)
})
......@@ -5,8 +5,11 @@ import Drawer from '@material-ui/core/Drawer'
import List from '@material-ui/core/List'
import ListItemText from '@material-ui/core/ListItemText'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import React from 'react'
import React, { useEffect } from 'react'
import { useParams } from 'react-router-dom'
import { getCities } from '../../actions/cities'
import { getEditorCompetition } from '../../actions/editor'
import { useAppDispatch, useAppSelector } from '../../hooks'
import { Content } from '../views/styled'
import SettingsPanel from './components/SettingsPanel'
import SlideEditor from './components/SlideEditor'
......@@ -16,15 +19,6 @@ function createSlide(name: string) {
return { name }
}
const slides = [
createSlide('Sida 1'),
createSlide('Sida 2'),
createSlide('Sida 3'),
createSlide('Sida 4'),
createSlide('Sida 5'),
createSlide('Sida 6'),
createSlide('Sida 7'),
]
const leftDrawerWidth = 150
const rightDrawerWidth = 390
......@@ -68,14 +62,21 @@ interface CompetitionParams {
const PresentationEditorPage: React.FC = () => {
const classes = useStyles()
const params: CompetitionParams = useParams()
const { id }: CompetitionParams = useParams()
const dispatch = useAppDispatch()
const competition = useAppSelector((state) => state.editor.competition)
// TODO: wait for dispatch to finish
useEffect(() => {
dispatch(getEditorCompetition(id))
dispatch(getCities())
}, [])
return (
<PresentationEditorContainer>
<CssBaseline />
<AppBar position="fixed" className={classes.appBar}>
<ToolBarContainer>
<Typography variant="h6" noWrap>
Tävling nr: {params.id}
Tävlingsnamn: {competition.name}
</Typography>
<ViewButtonGroup>
<ViewButton variant="contained" color="secondary">
......@@ -101,9 +102,9 @@ const PresentationEditorPage: React.FC = () => {
<div className={classes.toolbar} />
<Divider />
<List>
{slides.map((slide) => (
<SlideListItem divider button key={slide.name}>
<ListItemText primary={slide.name} />
{competition.slides.map((slide) => (
<SlideListItem divider button key={slide.title}>
<ListItemText primary={slide.title} />
</SlideListItem>
))}
</List>
......
import { render } from '@testing-library/react'
import React from 'react'
import { BrowserRouter } from 'react-router-dom'
import ImageComponentDisplay from './ImageComponentDisplay'
it('renders image component display', () => {
render(<ImageComponentDisplay component={{ id: 0, x: 0, y: 0, w: 0, h: 0, type: 0, media_id: 0 }} />)
render(
<ImageComponentDisplay component={{ id: 0, x: 0, y: 0, w: 0, h: 0, type: 0, media_id: 0 }} />
)
})
import { Button, Divider, List, ListItem, ListItemText, TextField } from '@material-ui/core'
import {
Button,
Divider,
FormControl,
InputLabel,
List,
ListItem,
ListItemText,
MenuItem,
Select,
TextField,
} from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import CloseIcon from '@material-ui/icons/Close'
import React, { useState } from 'react'
import axios from 'axios'
import React from 'react'
import { useParams } from 'react-router-dom'
import { getEditorCompetition } from '../../../actions/editor'
import { useAppDispatch, useAppSelector } from '../../../hooks'
import { City } from '../../../interfaces/ApiModels'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
......@@ -31,33 +47,94 @@ const useStyles = makeStyles((theme: Theme) =>
height: 50,
background: 'white',
},
dropDown: {
margin: theme.spacing(2),
width: '87%',
background: 'white',
},
})
)
interface CompetitionParams {
id: string
}
const CompetitionSettings: React.FC = () => {
const classes = useStyles()
const initialList = [
{ id: '1', name: 'Lag1' },
{ id: '2', name: 'Lag2' },
{ id: '3', name: 'Lag3' },
]
const handleClick = (id: string) => {
setTeams(teams.filter((item) => item.id !== id)) //Will not be done like this when api is used
const { id }: CompetitionParams = useParams()
const dispatch = useAppDispatch()
const competition = useAppSelector((state) => state.editor.competition)
const updateCompetitionName = async (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
await axios
.put(`/competitions/${id}`, { name: event.target.value })
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
const [teams, setTeams] = useState(initialList)
const handleClick = async (tid: number) => {
await axios
.delete(`/competitions/${id}/teams/${tid}`)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
const cities = useAppSelector((state) => state.cities.cities)
const updateCompetitionCity = async (city: City) => {
await axios
.put(`/competitions/${id}`, { city_id: city.id })
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
cities.forEach((city) => {
if (event.target.value === city.name) {
updateCompetitionCity(city)
}
})
}
return (
<div className={classes.textInputContainer}>
<form noValidate autoComplete="off">
<TextField className={classes.textInput} label="Tävlingsnamn" variant="outlined" />
<TextField
className={classes.textInput}
id="outlined-basic"
label={'Tävlingsnamn'}
defaultValue={competition.name}
onChange={updateCompetitionName}
variant="outlined"
/>
<Divider />
<TextField className={classes.textInput} label="Stad" variant="outlined" />
<FormControl variant="outlined" className={classes.dropDown}>
<InputLabel id="region-selection-label">Region</InputLabel>
{/*TODO: fixa så cities laddar in i statet likt i CompetitionManager*/}
<Select
value={cities.find((city) => city.id === competition.city_id)?.name || ''}
label="RegionSelect"
onChange={handleChange}
>
{cities.map((city) => (
<MenuItem value={city.name} key={city.name}>
<Button>{city.name}</Button>
</MenuItem>
))}
</Select>
</FormControl>
</form>
<List>
<ListItem>
<ListItemText className={classes.textCenter} primary="Lag" />
</ListItem>
{teams.map((team) => (
{competition.teams.map((team) => (
<div key={team.id}>
<ListItem divider button>
<ListItemText primary={team.name} />
......
import { render } from '@testing-library/react'
import { mount } from 'enzyme'
import React from 'react'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import store from '../../../store'
import CompetitionSettings from './CompetitionSettings'
import SettingsPanel from './SettingsPanel'
it('renders settings panel', () => {
render(<SettingsPanel />)
render(
<Provider store={store}>
<BrowserRouter>
<SettingsPanel />
</BrowserRouter>
</Provider>
)
})
it('renders slide settings tab', () => {
const wrapper = mount(<SettingsPanel />)
const wrapper = mount(
<Provider store={store}>
<BrowserRouter>
<SettingsPanel />
</BrowserRouter>
</Provider>
)
const tabs = wrapper.find('.MuiTabs-flexContainer')
expect(wrapper.find(CompetitionSettings).length).toEqual(1)
tabs.children().at(1).simulate('click')
......
import { render } from '@testing-library/react'
import React from 'react'
import { Provider } from 'react-redux'
import { BrowserRouter } from 'react-router-dom'
import store from '../../../store'
import SlideSettings from './SlideSettings'
it('renders slide settings', () => {
render(<SlideSettings />)
render(
<Provider store={store}>
<BrowserRouter>
<SlideSettings />
</BrowserRouter>
</Provider>
)
})
......@@ -10,10 +10,17 @@ import {
Select,
TextField,
} from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { CheckboxProps } from '@material-ui/core/Checkbox'
import { green, grey } from '@material-ui/core/colors'
import { createStyles, makeStyles, Theme, withStyles } from '@material-ui/core/styles'
import CloseIcon from '@material-ui/icons/Close'
import MoreHorizOutlinedIcon from '@material-ui/icons/MoreHorizOutlined'
import axios from 'axios'
import React, { useState } from 'react'
import { useParams } from 'react-router-dom'
import { getEditorCompetition } from '../../../actions/editor'
import { useAppDispatch, useAppSelector } from '../../../hooks'
import { HiddenInput } from './styled'
const useStyles = makeStyles((theme: Theme) =>
createStyles({
......@@ -58,22 +65,33 @@ const useStyles = makeStyles((theme: Theme) =>
})
)
interface CompetitionParams {
id: string
}
const SlideSettings: React.FC = () => {
const classes = useStyles()
const [slideTypeSelected, selectSlideType] = React.useState('')
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
selectSlideType(event.target.value as string)
const { id }: CompetitionParams = useParams()
const dispatch = useAppDispatch()
const competition = useAppSelector((state) => state.editor.competition)
let currentSlide = competition.slides[0]
// Init currentSlide if slides are not in order
for (const slide of competition.slides) {
if (slide.order === 1) {
currentSlide = slide
break
}
}
const answerList = [
{ id: 'answer1', name: 'Svar 1' },
{ id: 'answer2', name: 'Svar 2' },
]
const handleCloseAnswerClick = (id: string) => {
setAnswers(answers.filter((item) => item.id !== id)) //Will not be done like this when api is used
const handleCloseAnswerClick = async (alternative: number) => {
await axios
// TODO: implementera API för att kunnata bort svarsalternativ
.delete(`/competitions/${id}/slide/question/alternative/${alternative}`)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
const [answers, setAnswers] = useState(answerList)
const textList = [
{ id: 'text1', name: 'Text 1' },
......@@ -93,22 +111,75 @@ const SlideSettings: React.FC = () => {
}
const [pictures, setPictures] = useState(pictureList)
const updateSlideType = async (event: React.ChangeEvent<{ value: unknown }>) => {
await axios
// TODO: implementera API för att kunna ändra i questions->type_id
.put(`/competitions/${id}/slides/${currentSlide?.id}`, { type_id: event.target.value })
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
const updateAlternativeValue = async (event: React.ChangeEvent<HTMLInputElement>) => {
// Wheter the alternative is true or false
await axios
// TODO: implementera API för att kunna ändra i alternatives->value
.put(`/competitions/${id}/slides/${currentSlide?.id}`, { value: event.target.value })
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>): void => {
if (e.target.files !== null && e.target.files[0]) {
const files = Array.from(e.target.files)
const file = files[0]
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = function () {
console.log(reader.result)
// TODO: Send image to back-end (remove console.log)
}
reader.onerror = function (error) {
console.log('Error: ', error)
}
}
}
const handleAddText = async () => {
console.log('Add text component')
// TODO: post the new text]
setTexts([...texts, { id: 'newText', name: 'New Text' }])
}
const GreenCheckbox = withStyles({
root: {
color: grey[900],
'&$checked': {
color: green[600],
},
},
checked: {},
})((props: CheckboxProps) => <Checkbox color="default" {...props} />)
return (
<div className={classes.textInputContainer}>
<div className={classes.whiteBackground}>
<FormControl variant="outlined" className={classes.dropDown}>
<InputLabel id="slide-type-selection-label">Sidtyp</InputLabel>
<Select value={slideTypeSelected} label="Sidtyp" defaultValue="informationSlide" onChange={handleChange}>
<MenuItem value="informationSlide">
<Select value={currentSlide?.questions[0].type_id || 0} label="Sidtyp" onChange={updateSlideType}>
<MenuItem value={0}>
<Button>Informationssida</Button>
</MenuItem>
<MenuItem value="textQuestion">
<MenuItem value={1}>
<Button>Skriftlig fråga</Button>
</MenuItem>
<MenuItem value="practicalQuestion">
<MenuItem value={2}>
<Button>Praktisk fråga</Button>
</MenuItem>
<MenuItem value="multipleChoiceQuestion">
<MenuItem value={3}>
<Button>Flervalsfråga</Button>
</MenuItem>
</Select>
......@@ -123,6 +194,7 @@ const SlideSettings: React.FC = () => {
helperText="Lämna blank för att inte använda timerfunktionen"
label="Timer"
type="number"
value={currentSlide?.timer}
/>
</ListItem>
......@@ -134,12 +206,18 @@ const SlideSettings: React.FC = () => {
secondary="(Fyll i rutan höger om textfältet för att markera korrekt svar)"
/>
</ListItem>
{answers.map((answer) => (
<div key={answer.id}>
{(currentSlide?.questions[0].question_alternatives || []).map((alt) => (
<div key={alt.id}>
<ListItem divider>
<TextField className={classes.textInput} label={answer.name} variant="outlined" />
<Checkbox color="default" />
<CloseIcon className={classes.clickableIcon} onClick={() => handleCloseAnswerClick(answer.id)} />
<TextField
className={classes.textInput}
id="outlined-basic"
label={`Svar ${alt.id}`}
value={alt.text}
variant="outlined"
/>
<GreenCheckbox checked={alt.value} onChange={updateAlternativeValue} />
<CloseIcon className={classes.clickableIcon} onClick={() => handleCloseAnswerClick(alt.id)} />
</ListItem>
</div>
))}
......@@ -161,7 +239,7 @@ const SlideSettings: React.FC = () => {
</ListItem>
</div>
))}
<ListItem className={classes.center} button>
<ListItem className={classes.center} button onClick={handleAddText}>
<Button>Lägg till text</Button>
</ListItem>
</List>
......@@ -184,7 +262,11 @@ const SlideSettings: React.FC = () => {
</div>
))}
<ListItem className={classes.center} button>
<Button>Lägg till bild</Button>
<HiddenInput accept="image/*" id="contained-button-file" multiple type="file" onChange={handleFileSelected} />
<label htmlFor="contained-button-file">
<Button component="span">Lägg till bild</Button>
</label>
</ListItem>
</List>
......
......@@ -13,3 +13,7 @@ export const SlideEditorContainer = styled.div`
width: 100%;
height: 100%;
`
export const HiddenInput = styled.input`
display: none;
`
......@@ -3,6 +3,7 @@
import { combineReducers } from 'redux'
import citiesReducer from './citiesReducer'
import competitionsReducer from './competitionsReducer'
import editorReducer from './editorReducer'
import presentationReducer from './presentationReducer'
import rolesReducer from './rolesReducer'
import searchUserReducer from './searchUserReducer'
......@@ -16,6 +17,7 @@ const allReducers = combineReducers({
UI: uiReducer,
competitions: competitionsReducer,
cities: citiesReducer,
editor: editorReducer,
presentation: presentationReducer,
roles: rolesReducer,
searchUsers: searchUserReducer,
......
import { AnyAction } from 'redux'
import Types from '../actions/types'
import { RichCompetition } from '../interfaces/ApiRichModels'
interface EditorState {
competition: RichCompetition
}
const initialState: EditorState = {
competition: {
name: '',
id: 0,
year: 0,
city_id: 1,
slides: [],
teams: [],
},
}
export default function (state = initialState, action: AnyAction) {
switch (action.type) {
case Types.SET_EDITOR_COMPETITION:
return {
...state,
competition: action.payload as RichCompetition,
}
default:
return state
}
}
......@@ -7,10 +7,7 @@ const initialState = {
competition: {
name: '',
id: 0,
city: {
id: 0,
name: '',
},
city_id: 0,
slides: [],
year: 0,
teams: [],
......
......@@ -14,10 +14,7 @@ const initialState: PresentationState = {
competition: {
name: '',
id: 0,
city: {
id: 0,
name: '',
},
city_id: 0,
slides: [],
year: 0,
teams: [],
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment