From 95cb52fe2e1ce35a9dcee7105c68c98d98460170 Mon Sep 17 00:00:00 2001 From: Albin Henriksson <albhe428@student.liu.se> Date: Mon, 19 Apr 2021 08:34:35 +0000 Subject: [PATCH] Resolve "Use component api in editor" --- client/src/actions/editor.ts | 161 +----------------- client/src/actions/types.ts | 1 + client/src/enum/ComponentTypes.ts | 2 +- client/src/interfaces/ApiModels.ts | 12 +- client/src/interfaces/ApiRichModels.ts | 3 +- .../PresentationEditorPage.test.tsx | 2 +- .../PresentationEditorPage.tsx | 25 ++- .../components/CompetitionSettings.tsx | 17 +- .../components/SlideEditor.tsx | 43 +++-- .../components/SlideSettings.tsx | 69 ++++---- .../components/TextComponentDisplay.test.tsx | 8 +- .../components/TextComponentDisplay.tsx | 29 +++- client/src/reducers/editorReducer.ts | 9 +- server/app/apis/components.py | 3 +- 14 files changed, 143 insertions(+), 241 deletions(-) diff --git a/client/src/actions/editor.ts b/client/src/actions/editor.ts index d5e6fede..ac26dbdb 100644 --- a/client/src/actions/editor.ts +++ b/client/src/actions/editor.ts @@ -8,162 +8,17 @@ export const getEditorCompetition = (id: string) => async (dispatch: AppDispatch .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, - }, - ], - }, + payload: res.data, }) }) .catch((err) => { console.log(err) }) } + +export const setEditorSlideId = (id: number) => (dispatch: AppDispatch) => { + dispatch({ + type: Types.SET_EDITOR_SLIDE_ID, + payload: id, + }) +} diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index 848d41f0..a417f6af 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -16,6 +16,7 @@ export default { SET_COMPETITIONS_TOTAL: 'SET_COMPETITIONS_TOTAL', SET_COMPETITIONS_COUNT: 'SET_COMPETITIONS_COUNT', SET_EDITOR_COMPETITION: 'SET_EDITOR_COMPETITION', + SET_EDITOR_SLIDE_ID: 'SET_EDITOR_SLIDE_ID', SET_PRESENTATION_COMPETITION: 'SET_PRESENTATION_COMPETITION', SET_PRESENTATION_SLIDE: 'SET_PRESENTATION_SLIDE', SET_PRESENTATION_SLIDE_PREVIOUS: 'SET_PRESENTATION_SLIDE_PREVIOUS', diff --git a/client/src/enum/ComponentTypes.ts b/client/src/enum/ComponentTypes.ts index c8d194c0..8acb2fd9 100644 --- a/client/src/enum/ComponentTypes.ts +++ b/client/src/enum/ComponentTypes.ts @@ -1,5 +1,5 @@ export enum ComponentTypes { - Text, + Text = 1, Checkbox, Image, } diff --git a/client/src/interfaces/ApiModels.ts b/client/src/interfaces/ApiModels.ts index 3c72e99f..1937428e 100644 --- a/client/src/interfaces/ApiModels.ts +++ b/client/src/interfaces/ApiModels.ts @@ -68,16 +68,20 @@ export interface Component { y: number w: number h: number - type: number + type_id: number } export interface ImageComponent extends Component { - media_id: number + data: { + media_id: number + } } export interface TextComponent extends Component { - text: string - font: string + data: { + text: string + font: string + } } export interface QuestionAlternativeComponent extends Component { diff --git a/client/src/interfaces/ApiRichModels.ts b/client/src/interfaces/ApiRichModels.ts index e21ae5fa..893749a3 100644 --- a/client/src/interfaces/ApiRichModels.ts +++ b/client/src/interfaces/ApiRichModels.ts @@ -1,5 +1,4 @@ -import { Component } from 'react' -import { Media, QuestionAlternative, QuestionAnswer, QuestionType } from './ApiModels' +import { Component, Media, QuestionAlternative, QuestionAnswer, QuestionType } from './ApiModels' export interface RichCompetition { name: string diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx index ebc96d73..956d0b72 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.test.tsx @@ -13,7 +13,7 @@ it('renders presentation editor', () => { id: 0, year: 0, city_id: 0, - slides: [], + slides: [{ id: 5 }], teams: [], }, } diff --git a/client/src/pages/presentationEditor/PresentationEditorPage.tsx b/client/src/pages/presentationEditor/PresentationEditorPage.tsx index 2993b12b..0bfea0c4 100644 --- a/client/src/pages/presentationEditor/PresentationEditorPage.tsx +++ b/client/src/pages/presentationEditor/PresentationEditorPage.tsx @@ -8,7 +8,7 @@ import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' import React, { useEffect } from 'react' import { useParams } from 'react-router-dom' import { getCities } from '../../actions/cities' -import { getEditorCompetition } from '../../actions/editor' +import { getEditorCompetition, setEditorSlideId } from '../../actions/editor' import { useAppDispatch, useAppSelector } from '../../hooks' import { Content } from '../views/styled' import SettingsPanel from './components/SettingsPanel' @@ -64,12 +64,18 @@ const PresentationEditorPage: React.FC = () => { const classes = useStyles() const { id }: CompetitionParams = useParams() const dispatch = useAppDispatch() + const activeSlideId = useAppSelector((state) => state.editor.activeSlideId) const competition = useAppSelector((state) => state.editor.competition) // TODO: wait for dispatch to finish useEffect(() => { dispatch(getEditorCompetition(id)) dispatch(getCities()) }, []) + + const setActiveSlideId = (id: number) => { + dispatch(setEditorSlideId(id)) + } + return ( <PresentationEditorContainer> <CssBaseline /> @@ -102,11 +108,18 @@ const PresentationEditorPage: React.FC = () => { <div className={classes.toolbar} /> <Divider /> <List> - {competition.slides.map((slide) => ( - <SlideListItem divider button key={slide.title}> - <ListItemText primary={slide.title} /> - </SlideListItem> - ))} + {competition.slides && + competition.slides.map((slide) => ( + <SlideListItem + divider + button + key={slide.id} + selected={slide.id === activeSlideId} + onClick={() => setActiveSlideId(slide.id)} + > + <ListItemText primary={slide.title} /> + </SlideListItem> + ))} </List> </Drawer> <div className={classes.toolbar} /> diff --git a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx index 2c61633f..75f6ed13 100644 --- a/client/src/pages/presentationEditor/components/CompetitionSettings.tsx +++ b/client/src/pages/presentationEditor/components/CompetitionSettings.tsx @@ -134,14 +134,15 @@ const CompetitionSettings: React.FC = () => { <ListItem> <ListItemText className={classes.textCenter} primary="Lag" /> </ListItem> - {competition.teams.map((team) => ( - <div key={team.id}> - <ListItem divider button> - <ListItemText primary={team.name} /> - <CloseIcon onClick={() => handleClick(team.id)} /> - </ListItem> - </div> - ))} + {competition.teams && + competition.teams.map((team) => ( + <div key={team.id}> + <ListItem divider button> + <ListItemText primary={team.name} /> + <CloseIcon onClick={() => handleClick(team.id)} /> + </ListItem> + </div> + ))} <ListItem className={classes.center} button> <Button>Lägg till lag</Button> </ListItem> diff --git a/client/src/pages/presentationEditor/components/SlideEditor.tsx b/client/src/pages/presentationEditor/components/SlideEditor.tsx index 22a73ef3..9baa9791 100644 --- a/client/src/pages/presentationEditor/components/SlideEditor.tsx +++ b/client/src/pages/presentationEditor/components/SlideEditor.tsx @@ -1,37 +1,32 @@ import React from 'react' import { ComponentTypes } from '../../../enum/ComponentTypes' +import { useAppSelector } from '../../../hooks' +import { ImageComponent, TextComponent } from '../../../interfaces/ApiModels' import CheckboxComponent from './CheckboxComponent' import ImageComponentDisplay from './ImageComponentDisplay' import { SlideEditorContainer } from './styled' import TextComponentDisplay from './TextComponentDisplay' const SlideEditor: React.FC = () => { - // const components = useAppSelector(state => state.editor.slide.components) // get the current RichSlide - const components: any[] = [ - { id: 0, x: 15, y: 150, w: 200, h: 300, type: ComponentTypes.Checkbox }, - { id: 1, x: 15, y: 250, w: 200, h: 300, type: ComponentTypes.Checkbox }, - { id: 2, x: 15, y: 350, w: 200, h: 300, type: ComponentTypes.Checkbox }, - { id: 3, x: 300, y: 500, w: 100, h: 300, type: ComponentTypes.Text, text: 'text component', font: 'arial' }, - { id: 4, x: 250, y: 100, w: 200, h: 300, type: ComponentTypes.Image }, - { id: 5, x: 350, y: 100, w: 200, h: 300, type: ComponentTypes.Image }, - ] + const components = useAppSelector( + (state) => + state.editor.competition.slides.find((slide) => slide && slide.id === state.editor.activeSlideId)?.components + ) return ( <SlideEditorContainer> - {components.map((component) => { - switch (component.type) { - case ComponentTypes.Checkbox: - return <CheckboxComponent key={component.id} component={component} /> - break - case ComponentTypes.Text: - return <TextComponentDisplay key={component.id} component={component} /> - break - case ComponentTypes.Image: - return <ImageComponentDisplay key={component.id} component={component} /> - break - default: - break - } - })} + {components && + components.map((component) => { + switch (component.type_id) { + case ComponentTypes.Checkbox: + return <CheckboxComponent key={component.id} component={component} /> + case ComponentTypes.Text: + return <TextComponentDisplay key={component.id} component={component as TextComponent} /> + case ComponentTypes.Image: + return <ImageComponentDisplay key={component.id} component={component as ImageComponent} /> + default: + break + } + })} </SlideEditorContainer> ) } diff --git a/client/src/pages/presentationEditor/components/SlideSettings.tsx b/client/src/pages/presentationEditor/components/SlideSettings.tsx index cd611060..c61d478e 100644 --- a/client/src/pages/presentationEditor/components/SlideSettings.tsx +++ b/client/src/pages/presentationEditor/components/SlideSettings.tsx @@ -20,6 +20,7 @@ import React, { 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 { HiddenInput } from './styled' const useStyles = makeStyles((theme: Theme) => @@ -93,14 +94,12 @@ const SlideSettings: React.FC = () => { .catch(console.log) } - const textList = [ - { id: 'text1', name: 'Text 1' }, - { id: 'text2', name: 'Text 2' }, - ] - const handleCloseTextClick = (id: string) => { - setTexts(texts.filter((item) => item.id !== id)) //Will not be done like this when api is used - } - const [texts, setTexts] = useState(textList) + const texts = useAppSelector( + (state) => + state.editor.competition.slides + .find((slide) => slide.id === state.editor.activeSlideId) + ?.components.filter((component) => component.type_id === 1) as TextComponent[] + ) const pictureList = [ { id: 'picture1', name: 'Picture1.jpeg' }, @@ -151,7 +150,7 @@ const SlideSettings: React.FC = () => { const handleAddText = async () => { console.log('Add text component') // TODO: post the new text] - setTexts([...texts, { id: 'newText', name: 'New Text' }]) + // setTexts([...texts, { id: 'newText', name: 'New Text' }]) } const GreenCheckbox = withStyles({ @@ -206,21 +205,24 @@ const SlideSettings: React.FC = () => { secondary="(Fyll i rutan höger om textfältet för att markera korrekt svar)" /> </ListItem> - {(currentSlide?.questions[0].question_alternatives || []).map((alt) => ( - <div key={alt.id}> - <ListItem divider> - <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> - ))} + {currentSlide && + currentSlide.questions[0] && + currentSlide.questions[0].question_alternatives && + currentSlide.questions[0].question_alternatives.map((alt) => ( + <div key={alt.id}> + <ListItem divider> + <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> + ))} <ListItem className={classes.center} button> <Button>Lägg till svarsalternativ</Button> </ListItem> @@ -230,15 +232,16 @@ const SlideSettings: React.FC = () => { <ListItem divider> <ListItemText className={classes.textCenter} primary="Text" /> </ListItem> - {texts.map((text) => ( - <div key={text.id}> - <ListItem divider> - <TextField className={classes.textInput} label={text.name} variant="outlined" /> - <MoreHorizOutlinedIcon className={classes.clickableIcon} /> - <CloseIcon className={classes.clickableIcon} onClick={() => handleCloseTextClick(text.id)} /> - </ListItem> - </div> - ))} + {texts && + texts.map((text) => ( + <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> </ListItem> diff --git a/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx b/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx index 8f61ee36..c4489878 100644 --- a/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx +++ b/client/src/pages/presentationEditor/components/TextComponentDisplay.test.tsx @@ -1,12 +1,18 @@ import { Editor } from '@tinymce/tinymce-react' import { mount } from 'enzyme' import React from 'react' +import { Provider } from 'react-redux' +import store from '../../../store' import TextComponentDisplay from './TextComponentDisplay' it('renders text component display', () => { const testText = 'TEST' const container = mount( - <TextComponentDisplay component={{ id: 0, x: 0, y: 0, w: 0, h: 0, text: testText, type: 2, font: '123123' }} /> + <Provider store={store}> + <TextComponentDisplay + component={{ id: 0, x: 0, y: 0, w: 0, h: 0, data: { text: testText, font: '123123' }, type_id: 2 }} + /> + </Provider> ) expect(container.find(Editor).prop('initialValue')).toBe(testText) }) diff --git a/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx b/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx index 30426b9d..4fb34650 100644 --- a/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx +++ b/client/src/pages/presentationEditor/components/TextComponentDisplay.tsx @@ -1,6 +1,8 @@ import { Editor } from '@tinymce/tinymce-react' +import axios from 'axios' import React, { useState } from 'react' import { Rnd } from 'react-rnd' +import { useAppSelector } from '../../../hooks' import { TextComponent } from '../../../interfaces/ApiModels' import { Position, Size } from '../../../interfaces/Components' @@ -11,9 +13,26 @@ type ImageComponentProps = { const TextComponentDisplay = ({ component }: ImageComponentProps) => { const [currentPos, setCurrentPos] = useState<Position>({ x: component.x, y: component.y }) const [currentSize, setCurrentSize] = useState<Size>({ w: component.w, h: component.h }) + const competitionId = useAppSelector((state) => state.editor.competition.id) + const slideId = useAppSelector((state) => state.editor.activeSlideId) + if (component.id === 1) console.log(component) const handleEditorChange = (e: any) => { console.log('Content was updated:', e.target.getContent()) - //TODO: axios.post + axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, { + data: { ...component.data, text: e.target.getContent() }, + }) + } + const handleUpdatePos = (pos: Position) => { + axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, { + x: pos.x, + y: pos.y, + }) + } + const handleUpdateSize = () => { + axios.put(`/competitions/${competitionId}/slides/${slideId}/components/${component.id}`, { + w: currentSize.w, + h: currentSize.h, + }) } return ( <Rnd @@ -22,6 +41,7 @@ const TextComponentDisplay = ({ component }: ImageComponentProps) => { bounds="parent" onDragStop={(e, d) => { setCurrentPos({ x: d.x, y: d.y }) + handleUpdatePos(d) }} size={{ width: currentSize.w, height: currentSize.h }} position={{ x: currentPos.x, y: currentPos.y }} @@ -32,15 +52,12 @@ const TextComponentDisplay = ({ component }: ImageComponentProps) => { }) setCurrentPos(position) }} - onResizeStop={() => { - console.log('skickar till server') - }} + onResizeStop={handleUpdateSize} > <div style={{ height: '100%', width: '100%' }}> <Editor - initialValue={component.text} + initialValue={component.data.text} init={{ - body_class: 'mceBlackBody', height: '100%', menubar: false, plugins: [ diff --git a/client/src/reducers/editorReducer.ts b/client/src/reducers/editorReducer.ts index 606f5d69..627c1658 100644 --- a/client/src/reducers/editorReducer.ts +++ b/client/src/reducers/editorReducer.ts @@ -4,6 +4,7 @@ import { RichCompetition } from '../interfaces/ApiRichModels' interface EditorState { competition: RichCompetition + activeSlideId: number } const initialState: EditorState = { @@ -15,14 +16,20 @@ const initialState: EditorState = { slides: [], teams: [], }, + activeSlideId: 0, } 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, + } + case Types.SET_EDITOR_SLIDE_ID: + return { + ...state, + activeSlideId: action.payload as number, } default: return state diff --git a/server/app/apis/components.py b/server/app/apis/components.py index f88e1e2d..d301ff53 100644 --- a/server/app/apis/components.py +++ b/server/app/apis/components.py @@ -24,7 +24,8 @@ class ComponentByID(Resource): @jwt_required def put(self, CID, SOrder, component_id): args = component_parser.parse_args() - item = dbc.edit.component(**args) + item = dbc.get.one(Component, component_id) + item = dbc.edit.component(item,**args) return item_response(schema.dump(item)) @jwt_required -- GitLab