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

Resolve "Minor editor fixes/adds"

parent cb9aa8f4
No related branches found
No related tags found
1 merge request!86Resolve "Minor editor fixes/adds"
Pipeline #41765 passed with warnings
client/public/t8-circled.png

42.4 KiB

import axios from 'axios'
import { AppDispatch } from './../store'
import { AppDispatch, RootState } from './../store'
import Types from './types'
export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch) => {
export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch, getState: () => RootState) => {
await axios
.get(`/competitions/${id}`)
.then((res) => {
......@@ -10,6 +10,9 @@ export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch
type: Types.SET_EDITOR_COMPETITION,
payload: res.data,
})
if (getState().editor.activeSlideId === -1 && res.data.slides[0]) {
setEditorSlideId(res.data.slides[0].id)(dispatch)
}
})
.catch((err) => {
console.log(err)
......
......@@ -61,7 +61,7 @@ export interface Question extends NameID {
export interface QuestionAlternative {
id: number
text: string
value: boolean
value: number
question_id: number
}
export interface QuestionAnswer {
......
......@@ -34,5 +34,5 @@ export interface RichQuestion {
total_score: number
question_type: QuestionType
type_id: number
question_alternatives: QuestionAlternative[]
alternatives: QuestionAlternative[]
}
import { Button, CircularProgress, Divider, Menu, MenuItem, Typography } from '@material-ui/core'
import { Button, Checkbox, CircularProgress, Divider, Menu, MenuItem, Typography } from '@material-ui/core'
import AppBar from '@material-ui/core/AppBar'
import { CheckboxProps } from '@material-ui/core/Checkbox'
import CssBaseline from '@material-ui/core/CssBaseline'
import Drawer from '@material-ui/core/Drawer'
import ListItemText from '@material-ui/core/ListItemText'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import { createStyles, makeStyles, Theme, withStyles } from '@material-ui/core/styles'
import AddOutlinedIcon from '@material-ui/icons/AddOutlined'
import BuildOutlinedIcon from '@material-ui/icons/BuildOutlined'
import CreateOutlinedIcon from '@material-ui/icons/CreateOutlined'
import DnsOutlinedIcon from '@material-ui/icons/DnsOutlined'
import InfoOutlinedIcon from '@material-ui/icons/InfoOutlined'
import axios from 'axios'
import React, { useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
import { getCities } from '../../actions/cities'
import { getEditorCompetition, setEditorSlideId } from '../../actions/editor'
......@@ -72,6 +73,12 @@ const useStyles = makeStyles((theme: Theme) =>
backgroundColor: theme.palette.background.default,
padding: theme.spacing(3),
},
alignCheckboxText: {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
paddingRight: 20,
},
})
)
......@@ -86,7 +93,6 @@ const PresentationEditorPage: React.FC = () => {
const activeSlideId = useAppSelector((state) => state.editor.activeSlideId)
const competition = useAppSelector((state) => state.editor.competition)
const competitionLoading = useAppSelector((state) => state.editor.loading)
// TODO: wait for dispatch to finish
useEffect(() => {
dispatch(getEditorCompetition(id))
dispatch(getCities())
......@@ -134,18 +140,31 @@ const PresentationEditorPage: React.FC = () => {
}
const renderSlideIcon = (slide: RichSlide) => {
switch (slide.questions && slide.questions[0].type_id) {
case 0:
return <InfoOutlinedIcon></InfoOutlinedIcon> // information slide
case 1:
return <CreateOutlinedIcon></CreateOutlinedIcon> // text question
case 2:
return <BuildOutlinedIcon></BuildOutlinedIcon> // practical qustion
case 3:
return <DnsOutlinedIcon></DnsOutlinedIcon> // multiple choice question
if (slide.questions && slide.questions[0] && slide.questions[0].type_id) {
switch (slide.questions[0].type_id) {
case 1:
return <CreateOutlinedIcon /> // text question
case 2:
return <BuildOutlinedIcon /> // practical qustion
case 3:
return <DnsOutlinedIcon /> // multiple choice question
}
} else {
return <InfoOutlinedIcon /> // information slide
}
}
const GreenCheckbox = withStyles({
root: {
color: '#FFFFFF',
'&$checked': {
color: '#FFFFFF',
},
},
checked: {},
})((props: CheckboxProps) => <Checkbox color="default" {...props} />)
const [checkbox, setCheckbox] = useState(false)
return (
<PresentationEditorContainer>
<CssBaseline />
......@@ -157,7 +176,12 @@ const PresentationEditorPage: React.FC = () => {
<Typography variant="h6" noWrap>
{competition.name}
</Typography>
<ViewButtonGroup>
<GreenCheckbox checked={checkbox} onChange={(event) => setCheckbox(event.target.checked)} />
<Typography className={classes.alignCheckboxText} variant="button">
Applicera ändringar på samtliga vyer
</Typography>
<ViewButton variant="contained" color="secondary">
Åskådarvy
</ViewButton>
......
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Divider,
FormControl,
InputLabel,
......@@ -9,11 +14,12 @@ import {
MenuItem,
Select,
TextField,
Typography,
} from '@material-ui/core'
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'
import CloseIcon from '@material-ui/icons/Close'
import axios from 'axios'
import React from 'react'
import React, { useState } from 'react'
import { useParams } from 'react-router-dom'
import { getEditorCompetition } from '../../../actions/editor'
import { useAppDispatch, useAppSelector } from '../../../hooks'
......@@ -35,7 +41,6 @@ const useStyles = makeStyles((theme: Theme) =>
},
textCenter: {
textAlign: 'center',
background: 'white',
},
center: {
display: 'flex',
......@@ -52,6 +57,12 @@ const useStyles = makeStyles((theme: Theme) =>
width: '87%',
background: 'white',
},
addButtons: {
padding: 5,
},
panelList: {
padding: 0,
},
})
)
......@@ -73,15 +84,6 @@ const CompetitionSettings: React.FC = () => {
.catch(console.log)
}
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
......@@ -100,6 +102,36 @@ const CompetitionSettings: React.FC = () => {
})
}
const removeTeam = async (tid: number) => {
await axios
.delete(`/competitions/${id}/teams/${tid}`)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
const addTeam = async () => {
setAddTeamOpen(false)
await axios
.post(`/competitions/${id}/teams`, { name: selectedTeamName })
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
// For "add team" dialog
const [addTeamOpen, setAddTeamOpen] = useState(false)
const openAddTeam = () => {
setAddTeamOpen(true)
}
const closeAddTeam = () => {
setAddTeamOpen(false)
}
let selectedTeamName = ''
const updateSelectedTeamName = (event: React.ChangeEvent<{ value: string }>) => {
selectedTeamName = event.target.value
}
return (
<div className={classes.textInputContainer}>
<form noValidate autoComplete="off">
......@@ -113,8 +145,7 @@ const CompetitionSettings: React.FC = () => {
/>
<Divider />
<FormControl variant="outlined" className={classes.dropDown}>
<InputLabel id="region-selection-label">Region</InputLabel>
{/*TODO: fixa så cities laddar in i statet likt i CompetitionManager*/}
<InputLabel>Region</InputLabel>
<Select
value={cities.find((city) => city.id === competition.city_id)?.name || ''}
label="Region"
......@@ -129,7 +160,7 @@ const CompetitionSettings: React.FC = () => {
</FormControl>
</form>
<List>
<List className={classes.panelList}>
<ListItem>
<ListItemText className={classes.textCenter} primary="Lag" />
</ListItem>
......@@ -138,13 +169,31 @@ const CompetitionSettings: React.FC = () => {
<div key={team.id}>
<ListItem divider button>
<ListItemText primary={team.name} />
<CloseIcon onClick={() => handleClick(team.id)} />
<CloseIcon onClick={() => removeTeam(team.id)} />
</ListItem>
</div>
))}
<ListItem className={classes.center} button>
<Button>Lägg till lag</Button>
<ListItem className={classes.center} button onClick={openAddTeam}>
<Typography className={classes.addButtons} variant="button">
Lägg till lag
</Typography>
</ListItem>
<Dialog open={addTeamOpen} onClose={closeAddTeam}>
<DialogTitle className={classes.center}>Lägg till lag</DialogTitle>
<DialogContent>
<DialogContentText>Skriv namnet på laget och klicka sedan på bekräfta.</DialogContentText>
<TextField autoFocus margin="dense" label="Lagnamn" fullWidth onChange={updateSelectedTeamName} />
</DialogContent>
<DialogActions>
<Button onClick={closeAddTeam} color="secondary">
Avbryt
</Button>
<Button onClick={addTeam} color="primary">
Bekräfta
</Button>
</DialogActions>
</Dialog>
</List>
<ListItem button>
......@@ -153,7 +202,7 @@ const CompetitionSettings: React.FC = () => {
src="https://i1.wp.com/stickoutmedia.se/wp-content/uploads/2021/01/placeholder-3.png?ssl=1"
className={classes.importedImage}
/>
<ListItemText className={classes.textCenter} primary="Välj bakgrundsbild ..." />
<ListItemText className={classes.textCenter}>Välj bakgrundsbild ...</ListItemText>
</ListItem>
</div>
)
......
import {
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
FormControl,
InputLabel,
List,
......@@ -9,18 +14,18 @@ import {
MenuItem,
Select,
TextField,
Typography,
} from '@material-ui/core'
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 React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { getEditorCompetition } from '../../../actions/editor'
import { useAppDispatch, useAppSelector } from '../../../hooks'
import { TextComponent } from '../../../interfaces/ApiModels'
import { QuestionAlternative, TextComponent } from '../../../interfaces/ApiModels'
import { HiddenInput } from './styled'
const useStyles = makeStyles((theme: Theme) =>
......@@ -39,7 +44,6 @@ const useStyles = makeStyles((theme: Theme) =>
},
textCenter: {
textAlign: 'center',
background: 'white',
},
center: {
display: 'flex',
......@@ -50,6 +54,7 @@ const useStyles = makeStyles((theme: Theme) =>
margin: theme.spacing(2),
width: '87%',
background: 'white',
padding: 0,
},
clickableIcon: {
cursor: 'pointer',
......@@ -63,6 +68,16 @@ const useStyles = makeStyles((theme: Theme) =>
whiteBackground: {
background: 'white',
},
addButtons: {
padding: 5,
},
panelList: {
padding: 0,
},
addImageButton: {
padding: 5,
cursor: 'pointer',
},
})
)
......@@ -75,23 +90,22 @@ const SlideSettings: React.FC = () => {
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 activeSlideId = useAppSelector((state) => state.editor.activeSlideId)
const activeSlide = useAppSelector((state) =>
state.editor.competition.slides.find((slide) => slide && slide.id === state.editor.activeSlideId)
)
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 handleCloseAnswerClick = async (alternative_id: number) => {
if (activeSlide && activeSlide.questions[0]) {
await axios
.delete(
`/competitions/${id}/slides/${activeSlideId}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}`
)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
}
const texts = useAppSelector(
......@@ -110,25 +124,97 @@ 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 updateSlideType = async () => {
closeSlideTypeDialog()
if (activeSlide) {
if (activeSlide.questions[0] && activeSlide.questions[0].type_id !== selectedSlideType) {
if (selectedSlideType === 0) {
// Change slide type from a question type to information
await axios
.delete(`/competitions/${id}/slides/${activeSlide.order}/questions/${activeSlide.questions[0].id}`)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
} else {
// Change slide type from question type to another question type
await axios
.delete(`/competitions/${id}/slides/${activeSlide.order}/questions/${activeSlide.questions[0].id}`)
.catch(console.log)
await axios
.post(`/competitions/${id}/slides/${activeSlide.order}/questions`, {
name: 'Ny fråga',
total_score: 0,
type_id: selectedSlideType,
slide_id: activeSlide.id,
})
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
} else if (selectedSlideType !== 0) {
// Change slide type from information to a question type
await axios
.post(`/competitions/${id}/slides/${activeSlide.order}/questions`, {
name: 'Ny fråga',
total_score: 0,
type_id: selectedSlideType,
slide_id: activeSlide.id,
})
.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 updateAlternativeValue = async (alternative: QuestionAlternative) => {
if (activeSlide && activeSlide.questions[0]) {
let newValue: number
if (alternative.value === 0) {
newValue = 1
} else newValue = 0
console.log('newValue: ' + newValue)
await axios
.put(
`/competitions/${id}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative.id}`,
{ value: newValue }
)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
}
const updateAlternativeText = async (alternative_id: number, newText: string) => {
if (activeSlide && activeSlide.questions[0]) {
await axios
.put(
`/competitions/${id}/slides/${activeSlide?.id}/questions/${activeSlide?.questions[0].id}/alternatives/${alternative_id}`,
{ text: newText }
)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
}
const addAlternative = async () => {
if (activeSlide && activeSlide.questions[0]) {
await axios
.post(
`/competitions/${id}/slides/${activeSlide?.order}/questions/${activeSlide?.questions[0].id}/alternatives`,
{ text: '', value: 0 }
)
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
}
const handleFileSelected = (e: React.ChangeEvent<HTMLInputElement>): void => {
......@@ -163,27 +249,86 @@ const SlideSettings: React.FC = () => {
checked: {},
})((props: CheckboxProps) => <Checkbox color="default" {...props} />)
const updateTimer = async (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
setTimer(+event.target.value)
if (activeSlide) {
await axios
.put(`/competitions/${id}/slides/${activeSlide.order}`, { timer: event.target.value })
.then(() => {
dispatch(getEditorCompetition(id))
})
.catch(console.log)
}
}
const [timer, setTimer] = useState<number | undefined>(0)
useEffect(() => {
setTimer(activeSlide?.timer)
}, [activeSlide])
// For "slide type" dialog
const [selectedSlideType, setSelectedSlideType] = useState(0)
const [slideTypeDialog, setSlideTypeDialog] = useState(false)
const openSlideTypeDialog = (type_id: number) => {
setSelectedSlideType(type_id)
setSlideTypeDialog(true)
}
const closeSlideTypeDialog = () => {
setSlideTypeDialog(false)
}
const numberToBool = (num: number) => {
if (num === 0) return false
else return true
}
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={currentSlide?.questions[0].type_id || 0} label="Sidtyp" onChange={updateSlideType}>
<InputLabel>Sidtyp</InputLabel>
<Select value={activeSlide?.questions[0]?.type_id || 0} label="Sidtyp" className={classes.panelList}>
<MenuItem value={0}>
<Button>Informationssida</Button>
<Typography variant="button" onClick={() => openSlideTypeDialog(0)}>
Informationssida
</Typography>
</MenuItem>
<MenuItem value={1}>
<Button>Skriftlig fråga</Button>
<Typography variant="button" onClick={() => openSlideTypeDialog(1)}>
Skriftlig fråga
</Typography>
</MenuItem>
<MenuItem value={2}>
<Button>Praktisk fråga</Button>
<Typography variant="button" onClick={() => openSlideTypeDialog(2)}>
Praktisk fråga
</Typography>
</MenuItem>
<MenuItem value={3}>
<Button>Flervalsfråga</Button>
<Typography variant="button" onClick={() => openSlideTypeDialog(3)}>
Flervalsfråga
</Typography>
</MenuItem>
</Select>
</FormControl>
</div>
<Dialog open={slideTypeDialog} onClose={closeSlideTypeDialog}>
<DialogTitle className={classes.center} color="secondary">
Varning!
</DialogTitle>
<DialogContent>
<DialogContentText>
Om du ändrar sidtypen kommer eventuella frågeinställningar gå förlorade. Det inkluderar: frågans namn, poäng
och svarsalternativ.{' '}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={closeSlideTypeDialog} color="secondary">
Avbryt
</Button>
<Button onClick={updateSlideType} color="primary">
Bekräfta
</Button>
</DialogActions>
</Dialog>
<ListItem>
<TextField
......@@ -193,11 +338,13 @@ const SlideSettings: React.FC = () => {
helperText="Lämna blank för att inte använda timerfunktionen"
label="Timer"
type="number"
value={currentSlide?.timer}
defaultValue={activeSlide?.timer || 0}
onChange={updateTimer}
value={timer}
/>
</ListItem>
<List>
<List className={classes.panelList}>
<ListItem divider>
<ListItemText
className={classes.textCenter}
......@@ -205,30 +352,32 @@ const SlideSettings: React.FC = () => {
secondary="(Fyll i rutan höger om textfältet för att markera korrekt svar)"
/>
</ListItem>
{currentSlide &&
currentSlide.questions[0] &&
currentSlide.questions[0].question_alternatives &&
currentSlide.questions[0].question_alternatives.map((alt) => (
{activeSlide &&
activeSlide.questions[0] &&
activeSlide.questions[0].alternatives &&
activeSlide.questions[0].alternatives.map((alt) => (
<div key={alt.id}>
<ListItem divider>
<TextField
className={classes.textInput}
id="outlined-basic"
label={`Svar ${alt.id}`}
value={alt.text}
defaultValue={alt.text}
onChange={(event) => updateAlternativeText(alt.id, event.target.value)}
variant="outlined"
/>
<GreenCheckbox checked={alt.value} onChange={updateAlternativeValue} />
<GreenCheckbox checked={numberToBool(alt.value)} onChange={() => updateAlternativeValue(alt)} />
<CloseIcon className={classes.clickableIcon} onClick={() => handleCloseAnswerClick(alt.id)} />
</ListItem>
</div>
))}
<ListItem className={classes.center} button>
<Button>Lägg till svarsalternativ</Button>
<ListItem className={classes.center} button onClick={addAlternative}>
<Typography className={classes.addButtons} variant="button">
Lägg till svarsalternativ
</Typography>
</ListItem>
</List>
<List>
<List className={classes.panelList}>
<ListItem divider>
<ListItemText className={classes.textCenter} primary="Text" />
</ListItem>
......@@ -237,17 +386,18 @@ const SlideSettings: React.FC = () => {
<div key={text.id}>
<ListItem divider>
<TextField className={classes.textInput} label={text.data.text} variant="outlined" />
<MoreHorizOutlinedIcon className={classes.clickableIcon} />
<CloseIcon className={classes.clickableIcon} />
</ListItem>
</div>
))}
<ListItem className={classes.center} button onClick={handleAddText}>
<Button>Lägg till text</Button>
<Typography className={classes.addButtons} variant="button">
Lägg till text
</Typography>
</ListItem>
</List>
<List>
<List className={classes.panelList}>
<ListItem divider>
<ListItemText className={classes.textCenter} primary="Bilder" />
</ListItem>
......@@ -267,8 +417,8 @@ const SlideSettings: React.FC = () => {
<ListItem className={classes.center} 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 className={classes.addImageButton} htmlFor="contained-button-file">
<Typography variant="button">Lägg till bild</Typography>
</label>
</ListItem>
</List>
......
......@@ -17,7 +17,7 @@ const initialState: EditorState = {
slides: [],
teams: [],
},
activeSlideId: 0,
activeSlideId: -1,
loading: true,
}
......@@ -25,8 +25,8 @@ export default function (state = initialState, action: AnyAction) {
switch (action.type) {
case Types.SET_EDITOR_COMPETITION:
return {
...state,
competition: action.payload as RichCompetition,
activeSlideId: action.payload.slides[0].id as number,
loading: false,
}
case Types.SET_EDITOR_SLIDE_ID:
......
......@@ -24,7 +24,6 @@ class SlidesList(Resource):
def post(self, CID):
item_comp = dbc.get.one(Competition, CID)
item_slide = dbc.add.slide(item_comp)
dbc.add.question(f"Fråga {item_slide.order + 1}", 10, 0, item_slide)
dbc.utils.refresh(item_comp)
return list_response(list_schema.dump(item_comp.slides))
......
......@@ -82,13 +82,12 @@ def _add_items():
h = random.randrange(150, 400)
dbc.add.component(1, item_slide, {"text": f"hej{k}"}, x, y, w, h)
# TODO: Remove comments when slide without questions is fixed
# item_slide = dbc.add.slide(item_comp)
# item_slide.title = f"Slide {len(item_comp.slides)}"
# item_slide.body = f"Body {len(item_comp.slides)}"
# item_slide.timer = 100 + j
# # item_slide.settings = "{}"
# dbc.utils.commit_and_refresh(item_slide)
item_slide = dbc.add.slide(item_comp)
item_slide.title = f"Slide {len(item_comp.slides)}"
item_slide.body = f"Body {len(item_comp.slides)}"
item_slide.timer = 100 + j
# item_slide.settings = "{}"
dbc.utils.commit_and_refresh(item_slide)
# Add teams
for name in 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