Merge branch 'addTagPopover' into 'master'

build custom popup for addtag in player

See merge request lukas/openmediacenter!16
This commit is contained in:
Lukas Heiligenbrunner 2020-10-09 14:00:51 +00:00
commit b21d2a29cc
8 changed files with 235 additions and 92 deletions

View File

@ -1,27 +1,36 @@
import React from "react"; import React from "react";
import Modal from 'react-bootstrap/Modal' import ReactDom from 'react-dom';
import Dropdown from "react-bootstrap/Dropdown"; import style from './AddTagPopup.module.css'
import DropdownButton from "react-bootstrap/DropdownButton"; import Tag from "../Tag/Tag";
import {Line} from "../PageTitle/PageTitle";
import GlobalInfos from "../../GlobalInfos";
/** /**
* component creates overlay to add a new tag to a video * component creates overlay to add a new tag to a video
*/ */
class AddTagPopup extends React.Component { class AddTagPopup extends React.Component {
/// instance of root element
element;
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this.state = {items: []};
selection: { this.handleClickOutside = this.handleClickOutside.bind(this);
name: "nothing selected", this.keypress = this.keypress.bind(this);
id: -1
},
items: []
};
this.props = props; this.props = props;
} }
componentDidMount() { componentDidMount() {
document.addEventListener('click', this.handleClickOutside);
document.addEventListener('keyup', this.keypress);
// add element drag drop events
if (this.element != null) {
this.dragElement();
}
const updateRequest = new FormData(); const updateRequest = new FormData();
updateRequest.append('action', 'getAllTags'); updateRequest.append('action', 'getAllTags');
@ -34,50 +43,62 @@ class AddTagPopup extends React.Component {
}); });
} }
componentWillUnmount() {
// remove the appended listeners
document.removeEventListener('click', this.handleClickOutside);
document.removeEventListener('keyup', this.keypress);
}
render() { render() {
const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<> <div className={[style.popup, themeStyle.thirdbackground].join(' ')} ref={el => this.element = el}>
<Modal <div className={[style.header, themeStyle.textcolor].join(' ')}>Add a Tag to this Video:</div>
show={this.props.show} <Line/>
onHide={this.props.onHide} <div className={style.content}>
size="lg"
aria-labelledby="contained-modal-title-vcenter"
centered>
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title-vcenter">
Add to Tag
</Modal.Title>
</Modal.Header>
<Modal.Body>
<h4>Select a Tag:</h4>
<DropdownButton id="dropdown-basic-button" title={this.state.selection.name}>
{this.state.items ? {this.state.items ?
this.state.items.map((i) => ( this.state.items.map((i) => (
<Dropdown.Item key={i.tag_name} onClick={() => { <Tag onclick={() => {
this.setState({selection: {name: i.tag_name, id: i.tag_id}}) this.addTag(i.tag_id, i.tag_name);
}}>{i.tag_name}</Dropdown.Item> }}>{i.tag_name}</Tag>
)) : )) : null}
<Dropdown.Item>loading tags...</Dropdown.Item>} </div>
</DropdownButton> </div>
</Modal.Body>
<Modal.Footer>
<button className='btn btn-primary' onClick={() => {
this.storeselection();
}}>Add
</button>
</Modal.Footer>
</Modal>
</>
); );
} }
/** /**
* store the filled in form to the backend * Alert if clicked on outside of element
*/ */
storeselection() { handleClickOutside(event) {
const domNode = ReactDom.findDOMNode(this);
if (!domNode || !domNode.contains(event.target)) {
this.props.onHide();
}
}
/**
* key event handling
* @param event keyevent
*/
keypress(event) {
// hide if escape is pressed
if (event.key === "Escape") {
this.props.onHide();
}
}
/**
* add a new tag to this video
* @param tagid tag id to add
* @param tagname tag name to add
*/
addTag(tagid, tagname) {
console.log(this.props)
const updateRequest = new FormData(); const updateRequest = new FormData();
updateRequest.append('action', 'addTag'); updateRequest.append('action', 'addTag');
updateRequest.append('id', this.state.selection.id); updateRequest.append('id', tagid);
updateRequest.append('movieid', this.props.movie_id); updateRequest.append('movieid', this.props.movie_id);
fetch('/api/tags.php', {method: 'POST', body: updateRequest}) fetch('/api/tags.php', {method: 'POST', body: updateRequest})
@ -86,10 +107,51 @@ class AddTagPopup extends React.Component {
if (result.result !== "success") { if (result.result !== "success") {
console.log("error occured while writing to db -- todo error handling"); console.log("error occured while writing to db -- todo error handling");
console.log(result.result); console.log(result.result);
} else {
this.props.submit(tagid, tagname);
} }
this.props.onHide(); this.props.onHide();
})); }));
} }
/**
* make the element drag and droppable
*/
dragElement() {
let xOld = 0, yOld = 0;
const elmnt = this.element;
elmnt.firstChild.onmousedown = dragMouseDown;
function dragMouseDown(e) {
e.preventDefault();
// get the mouse cursor position at startup:
xOld = e.clientX;
yOld = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e.preventDefault();
// calculate the new cursor position:
const dx = xOld - e.clientX;
const dy = yOld - e.clientY;
xOld = e.clientX;
yOld = e.clientY;
// set the element's new position:
elmnt.style.top = (elmnt.offsetTop - dy) + "px";
elmnt.style.left = (elmnt.offsetLeft - dx) + "px";
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
}
}
} }
export default AddTagPopup; export default AddTagPopup;

View File

@ -0,0 +1,26 @@
.popup {
border: 3px #3574fe solid;
border-radius: 18px;
height: 80%;
left: 20%;
opacity: 0.95;
position: absolute;
top: 10%;
width: 60%;
z-index: 2;
}
.header {
cursor: move;
font-size: x-large;
margin-left: 15px;
margin-top: 10px;
opacity: 1;
}
.content {
margin-left: 20px;
margin-right: 20px;
margin-top: 10px;
opacity: 1;
}

View File

@ -11,47 +11,68 @@ describe('<AddTagPopup/>', function () {
wrapper.unmount(); wrapper.unmount();
}); });
it('test dropdown insertion', function () { it('test tag insertion', function () {
const wrapper = shallow(<AddTagPopup/>); const wrapper = shallow(<AddTagPopup/>);
wrapper.setState({items: ["test1", "test2", "test3"]}); wrapper.setState({
expect(wrapper.find('DropdownItem')).toHaveLength(3); items: [{tag_id: 1, tag_name: 'test'}, {tag_id: 2, tag_name: "ee"}]
}, () => {
expect(wrapper.find('Tag')).toHaveLength(2);
expect(wrapper.find('Tag').first().dive().text()).toBe("test");
});
}); });
it('test storeseletion click event', done => { it('test tag click', function () {
const mockSuccessResponse = {};
const mockJsonPromise = Promise.resolve(mockSuccessResponse);
const mockFetchPromise = Promise.resolve({
json: () => mockJsonPromise,
});
global.fetch = jest.fn().mockImplementation(() => mockFetchPromise);
const func = jest.fn();
const wrapper = shallow(<AddTagPopup/>); const wrapper = shallow(<AddTagPopup/>);
wrapper.setProps({ wrapper.instance().addTag = jest.fn();
onHide: () => {
func()
}
});
wrapper.setState({ wrapper.setState({
items: ["test1", "test2", "test3"], items: [{tag_id: 1, tag_name: 'test'}]
selection: { }, () => {
name: "test1", wrapper.find('Tag').first().dive().simulate('click');
id: 42 expect(wrapper.instance().addTag).toHaveBeenCalledTimes(1);
} });
}); });
// first call of fetch is getting of available tags it('test addtag', done => {
expect(global.fetch).toHaveBeenCalledTimes(1); const wrapper = shallow(<AddTagPopup/>);
wrapper.find('ModalFooter').find('button').simulate('click');
// now called 2 times global.fetch = prepareFetchApi({result: "success"});
expect(global.fetch).toHaveBeenCalledTimes(2);
wrapper.setProps({
submit: jest.fn((arg1, arg2) => {}),
onHide: jest.fn()
}, () => {
wrapper.instance().addTag(1, "test");
expect(global.fetch).toHaveBeenCalledTimes(1);
});
process.nextTick(() => { process.nextTick(() => {
//callback to close window should have called expect(wrapper.instance().props.submit).toHaveBeenCalledTimes(1);
expect(func).toHaveBeenCalledTimes(1); expect(wrapper.instance().props.onHide).toHaveBeenCalledTimes(1);
global.fetch.mockClear();
done();
});
});
it('test failing addTag', done => {
const wrapper = shallow(<AddTagPopup/>);
global.fetch = prepareFetchApi({result: "fail"});
wrapper.setProps({
submit: jest.fn((arg1, arg2) => {}),
onHide: jest.fn()
}, () => {
wrapper.instance().addTag(1, "test");
expect(global.fetch).toHaveBeenCalledTimes(1);
});
process.nextTick(() => {
expect(wrapper.instance().props.submit).toHaveBeenCalledTimes(0);
expect(wrapper.instance().props.onHide).toHaveBeenCalledTimes(1);
global.fetch.mockClear(); global.fetch.mockClear();
done(); done();

View File

@ -40,7 +40,8 @@ class Preview extends React.Component {
render() { render() {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<div className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview} onClick={() => this.itemClick()}> <div className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}
onClick={() => this.itemClick()}>
<div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.state.name}</div> <div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.state.name}</div>
<div className={style.previewpic}> <div className={style.previewpic}>
{this.state.previewpicture !== null ? {this.state.previewpicture !== null ?
@ -77,7 +78,9 @@ export class TagPreview extends React.Component {
render() { render() {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<div className={style.videopreview + ' ' + style.tagpreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview} onClick={() => this.itemClick()}> <div
className={style.videopreview + ' ' + style.tagpreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}
onClick={() => this.itemClick()}>
<div className={style.tagpreviewtitle + ' ' + themeStyle.lighttextcolor}> <div className={style.tagpreviewtitle + ' ' + themeStyle.lighttextcolor}>
{this.props.name} {this.props.name}
</div> </div>

View File

@ -33,7 +33,8 @@ export class SideBarItem extends React.Component {
render() { render() {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<div className={style.sidebarinfo + ' ' + themeStyle.thirdbackground + ' ' + themeStyle.lighttextcolor}>{this.props.children}</div> <div
className={style.sidebarinfo + ' ' + themeStyle.thirdbackground + ' ' + themeStyle.lighttextcolor}>{this.props.children}</div>
); );
} }
} }

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import SideBar from "./SideBar"; import SideBar, {SideBarItem, SideBarTitle} from "./SideBar";
import "@testing-library/jest-dom" import "@testing-library/jest-dom"
import {shallow} from "enzyme"; import {shallow} from "enzyme";
@ -14,4 +14,14 @@ describe('<SideBar/>', function () {
const wrapper = shallow(<SideBar>test</SideBar>); const wrapper = shallow(<SideBar>test</SideBar>);
expect(wrapper.children().text()).toBe("test"); expect(wrapper.children().text()).toBe("test");
}); });
it('sidebar Item renders without crashing', function () {
const wrapper = shallow(<SideBarItem>Test</SideBarItem>);
expect(wrapper.children().text()).toBe("Test");
});
it('renderes sidebartitle correctly', function () {
const wrapper = shallow(<SideBarTitle>Test</SideBarTitle>);
expect(wrapper.children().text()).toBe("Test");
});
}); });

View File

@ -44,6 +44,8 @@ class Player extends React.Component {
suggesttag: [], suggesttag: [],
popupvisible: false popupvisible: false
}; };
this.quickAddTag = this.quickAddTag.bind(this);
} }
componentDidMount() { componentDidMount() {
@ -56,7 +58,6 @@ class Player extends React.Component {
* @param tag_name name of tag to add * @param tag_name name of tag to add
*/ */
quickAddTag(tag_id, tag_name) { quickAddTag(tag_id, tag_name) {
// save the tag
const updateRequest = new FormData(); const updateRequest = new FormData();
updateRequest.append('action', 'addTag'); updateRequest.append('action', 'addTag');
updateRequest.append('id', tag_id); updateRequest.append('id', tag_id);
@ -75,6 +76,7 @@ class Player extends React.Component {
return e.tag_id; return e.tag_id;
}).indexOf(tag_id); }).indexOf(tag_id);
// check if tag is available in quickadds
if (index !== -1) { if (index !== -1) {
array.splice(index, 1); array.splice(index, 1);
@ -82,11 +84,35 @@ class Player extends React.Component {
tags: [...this.state.tags, {tag_name: tag_name}], tags: [...this.state.tags, {tag_name: tag_name}],
suggesttag: array suggesttag: array
}); });
} else {
this.setState({
tags: [...this.state.tags, {tag_name: tag_name}]
});
} }
} }
})); }));
} }
/**
* handle the popovers generated according to state changes
* @returns {JSX.Element}
*/
handlePopOvers() {
return (
<>
{this.state.popupvisible ?
<AddTagPopup show={this.state.popupvisible}
onHide={() => {
this.setState({popupvisible: false});
}}
submit={this.quickAddTag}
movie_id={this.state.movie_id}/> :
null
}
</>
);
}
/** /**
* generate sidebar with all items * generate sidebar with all items
*/ */
@ -143,19 +169,13 @@ class Player extends React.Component {
<button className='btn btn-primary' onClick={() => this.likebtn()}>Like this Video!</button> <button className='btn btn-primary' onClick={() => this.likebtn()}>Like this Video!</button>
<button className='btn btn-info' onClick={() => this.setState({popupvisible: true})}>Give this Video a Tag</button> <button className='btn btn-info' onClick={() => this.setState({popupvisible: true})}>Give this Video a Tag</button>
<button className='btn btn-danger' onClick={() =>{this.deleteVideo()}}>Delete Video</button> <button className='btn btn-danger' onClick={() =>{this.deleteVideo()}}>Delete Video</button>
{this.state.popupvisible ?
<AddTagPopup show={this.state.popupvisible}
onHide={() => {
this.setState({popupvisible: false});
this.fetchMovieData();
}}
movie_id={this.state.movie_id}/> :
null
}
</div> </div>
</div> </div>
<button className={style.closebutton} onClick={() => this.closebtn()}>Close</button> <button className={style.closebutton} onClick={() => this.closebtn()}>Close</button>
{
// handle the popovers switched on and off according to state changes
this.handlePopOvers()
}
</div> </div>
); );
} }