Merge branch 'tat_remove' into 'master'

make tags deleteable

See merge request lukas/openmediacenter!30
This commit is contained in:
Lukas Heiligenbrunner 2021-01-26 19:14:57 +00:00
commit f06da8044f
12 changed files with 266 additions and 183 deletions

View File

@ -9,6 +9,7 @@ class Tags extends RequestBase {
function initHandlers() { function initHandlers() {
$this->addToDB(); $this->addToDB();
$this->getFromDB(); $this->getFromDB();
$this->delete();
} }
private function addToDB() { private function addToDB() {
@ -65,4 +66,36 @@ class Tags extends RequestBase {
$this->commitMessage(json_encode($rows)); $this->commitMessage(json_encode($rows));
}); });
} }
private function delete() {
/**
* delete a Tag with specified id
*/
$this->addActionHandler("deleteTag", function () {
$tag_id = $_POST['tagId'];
$force = $_POST['force'];
// delete key constraints first
if ($force === "true") {
$query = "DELETE FROM video_tags WHERE tag_id=$tag_id";
if ($this->conn->query($query) !== TRUE) {
$this->commitMessage('{"result":"' . $this->conn->error . '"}');
}
}
$query = "DELETE FROM tags WHERE tag_id=$tag_id";
if ($this->conn->query($query) === TRUE) {
$this->commitMessage('{"result":"success"}');
} else {
// check if error is a constraint error
if (preg_match('/^.*a foreign key constraint fails.*$/i', $this->conn->error)) {
$this->commitMessage('{"result":"not empty tag"}');
} else {
$this->commitMessage('{"result":"' . $this->conn->eror . '"}');
}
}
});
}
} }

View File

@ -4,7 +4,7 @@ import GlobalInfos from '../../utils/GlobalInfos';
interface props { interface props {
title: string; title: string;
subtitle: string | null; subtitle: string | number | null;
} }
/** /**

View File

@ -22,60 +22,14 @@ describe('<AddTagPopup/>', function () {
}); });
it('test tag click', function () { it('test tag click', function () {
const wrapper = shallow(<AddTagPopup/>); const wrapper = shallow(<AddTagPopup submit={jest.fn()} onHide={jest.fn()}/>);
wrapper.instance().addTag = jest.fn();
wrapper.setState({ wrapper.setState({
items: [{tag_id: 1, tag_name: 'test'}] items: [{tag_id: 1, tag_name: 'test'}]
}, () => { }, () => {
wrapper.find('Tag').first().dive().simulate('click'); wrapper.find('Tag').first().dive().simulate('click');
expect(wrapper.instance().addTag).toHaveBeenCalledTimes(1);
});
});
it('test addtag', done => {
const wrapper = shallow(<AddTagPopup movie_id={1}/>);
global.fetch = prepareFetchApi({result: 'success'});
wrapper.setProps({
submit: jest.fn(() => {}),
onHide: jest.fn()
}, () => {
wrapper.instance().addTag(1, 'test');
expect(global.fetch).toHaveBeenCalledTimes(1);
});
process.nextTick(() => {
expect(wrapper.instance().props.submit).toHaveBeenCalledTimes(1); expect(wrapper.instance().props.submit).toHaveBeenCalledTimes(1);
expect(wrapper.instance().props.onHide).toHaveBeenCalledTimes(1); expect(wrapper.instance().props.onHide).toHaveBeenCalledTimes(1);
global.fetch.mockClear();
done();
});
});
it('test failing addTag', done => {
const wrapper = shallow(<AddTagPopup movie_id={1}/>);
global.fetch = prepareFetchApi({result: 'fail'});
wrapper.setProps({
submit: jest.fn(() => {}),
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();
done();
}); });
}); });
}); });

View File

@ -3,7 +3,6 @@ import Tag from '../../Tag/Tag';
import PopupBase from '../PopupBase'; import PopupBase from '../PopupBase';
import {callAPI} from '../../../utils/Api'; import {callAPI} from '../../../utils/Api';
import {TagType} from '../../../types/VideoTypes'; import {TagType} from '../../../types/VideoTypes';
import {GeneralSuccess} from '../../../types/GeneralTypes';
interface props { interface props {
onHide: () => void; onHide: () => void;
@ -41,29 +40,13 @@ class AddTagPopup extends React.Component<props, state> {
this.state.items.map((i) => ( this.state.items.map((i) => (
<Tag tagInfo={i} <Tag tagInfo={i}
onclick={(): void => { onclick={(): void => {
this.addTag(i.tag_id, i.tag_name); this.props.submit(i.tag_id, i.tag_name);
this.props.onHide();
}}/> }}/>
)) : null} )) : null}
</PopupBase> </PopupBase>
); );
} }
/**
* add a new tag to this video
* @param tagid tag id to add
* @param tagname tag name to add
*/
addTag(tagid: number, tagname: string): void {
callAPI('tags.php', {action: 'addTag', id: tagid, movieid: this.props.movie_id}, (result: GeneralSuccess) => {
if (result.result !== 'success') {
console.log('error occured while writing to db -- todo error handling');
console.log(result.result);
} else {
this.props.submit(tagid, tagname);
}
this.props.onHide();
});
}
} }
export default AddTagPopup; export default AddTagPopup;

View File

@ -0,0 +1,28 @@
import {shallow} from "enzyme";
import React from "react";
import SubmitPopup from "./SubmitPopup";
describe('<SubmitPopup/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<SubmitPopup/>);
wrapper.unmount();
});
it('test submit click', function () {
const func = jest.fn();
const wrapper = shallow(<SubmitPopup submit={() => func()}/>);
wrapper.find('Button').findWhere(p => p.props().title === 'Submit').simulate('click');
expect(func).toHaveBeenCalledTimes(1);
});
it('test cancel click', function () {
const func = jest.fn();
const wrapper = shallow(<SubmitPopup onHide={() => func()}/>);
wrapper.find('Button').findWhere(p => p.props().title === 'Cancel').simulate('click');
expect(func).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,18 @@
import React from 'react';
import PopupBase from '../PopupBase';
import {Button} from '../../GPElements/Button';
interface props {
onHide: (_: void) => void;
submit: (_: void) => void;
}
export default function SubmitPopup(props: props): JSX.Element {
return (
<PopupBase title='Are you sure?' onHide={props.onHide} height='160px' width='300px'>
<Button title='Submit' color={{backgroundColor: 'green'}} onClick={(): void => props.submit()}/>
<Button title='Cancel' color={{backgroundColor: 'red'}} onClick={(): void => props.onHide()}/>
</PopupBase>
);
}

View File

@ -7,41 +7,4 @@ describe('<CategoryPage/>', function () {
const wrapper = shallow(<CategoryPage/>); const wrapper = shallow(<CategoryPage/>);
wrapper.unmount(); wrapper.unmount();
}); });
it('test new tag popup', function () {
const wrapper = shallow(<CategoryPage/>);
expect(wrapper.find('NewTagPopup')).toHaveLength(0);
wrapper.find('[data-testid="btnaddtag"]').simulate('click');
// newtagpopup should be showing now
expect(wrapper.find('NewTagPopup')).toHaveLength(1);
});
it('test add popup', function () {
const wrapper = shallow(<CategoryPage/>);
expect(wrapper.find('NewTagPopup')).toHaveLength(0);
wrapper.setState({popupvisible: true});
expect(wrapper.find('NewTagPopup')).toHaveLength(1);
});
it('test hiding of popup', function () {
const wrapper = shallow(<CategoryPage/>);
wrapper.setState({popupvisible: true});
wrapper.find('NewTagPopup').props().onHide();
expect(wrapper.find('NewTagPopup')).toHaveLength(0);
});
it('test setting of subtitle', function () {
const wrapper = shallow(<CategoryPage/>);
expect(wrapper.find('PageTitle').props().subtitle).not.toBe('testtitle');
wrapper.instance().setSubTitle('testtitle');
// test if prop of title is set correctly
expect(wrapper.find('PageTitle').props().subtitle).toBe('testtitle');
});
}); });

View File

@ -1,82 +1,25 @@
import React from 'react'; import React from 'react';
import SideBar, {SideBarTitle} from '../../elements/SideBar/SideBar';
import Tag from '../../elements/Tag/Tag';
import NewTagPopup from '../../elements/Popups/NewTagPopup/NewTagPopup';
import PageTitle, {Line} from '../../elements/PageTitle/PageTitle';
import {Route, Switch} from 'react-router-dom'; import {Route, Switch} from 'react-router-dom';
import {DefaultTags} from '../../types/GeneralTypes';
import {CategoryViewWR} from './CategoryView'; import {CategoryViewWR} from './CategoryView';
import TagView from './TagView'; import TagView from './TagView';
interface CategoryPageState {
popupvisible: boolean;
subtitle: string;
}
/** /**
* Component for Category Page * Component for Category Page
* Contains a Tag Overview and loads specific Tag videos in VideoContainer * Contains a Tag Overview and loads specific Tag videos in VideoContainer
*/ */
class CategoryPage extends React.Component<{}, CategoryPageState> { class CategoryPage extends React.Component {
constructor(props: {}) {
super(props);
this.state = {
popupvisible: false,
subtitle: ''
};
this.setSubTitle = this.setSubTitle.bind(this);
}
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <Switch>
<PageTitle <Route path='/categories/:id'>
title='Categories' <CategoryViewWR/>
subtitle={this.state.subtitle}/> </Route>
<Route path='/categories'>
<SideBar> <TagView/>
<SideBarTitle>Default Tags:</SideBarTitle> </Route>
<Tag tagInfo={DefaultTags.all}/> </Switch>
<Tag tagInfo={DefaultTags.fullhd}/>
<Tag tagInfo={DefaultTags.hd}/>
<Tag tagInfo={DefaultTags.lowq}/>
<Line/>
<button data-testid='btnaddtag' className='btn btn-success' onClick={(): void => {
this.setState({popupvisible: true});
}}>Add a new Tag!
</button>
</SideBar>
<Switch>
<Route path='/categories/:id'>
<CategoryViewWR setSubTitle={this.setSubTitle}/>
</Route>
<Route path='/categories'>
<TagView setSubTitle={this.setSubTitle}/>
</Route>
</Switch>
{this.state.popupvisible ?
<NewTagPopup onHide={(): void => {
this.setState({popupvisible: false});
// this.loadTags();
}}/> :
null
}
</>
); );
} }
/**
* set the subtitle of this page
* @param subtitle string as subtitle
*/
setSubTitle(subtitle: string): void {
this.setState({subtitle: subtitle});
}
} }
export default CategoryPage; export default CategoryPage;

View File

@ -4,7 +4,7 @@ import {CategoryView} from './CategoryView';
describe('<CategoryView/>', function () { describe('<CategoryView/>', function () {
function instance() { function instance() {
return shallow(<CategoryView match={{params: {id: 10}}}/>); return shallow(<CategoryView match={{params: {id: 10}}} history={{push: jest.fn()}}/>);
} }
it('renders without crashing ', function () { it('renders without crashing ', function () {
@ -21,4 +21,54 @@ describe('<CategoryView/>', function () {
wrapper.find('button').simulate('click'); wrapper.find('button').simulate('click');
expect(func).toHaveBeenCalledTimes(1); expect(func).toHaveBeenCalledTimes(1);
}); });
it('test delete of tag', function () {
const wrapper = instance();
callAPIMock({result: 'success'});
// simulate button click
wrapper.find('Button').props().onClick();
expect(wrapper.instance().props.history.push).toHaveBeenCalledTimes(1);
});
it('test delete of non empty tag', function () {
const wrapper = instance();
callAPIMock({result: 'not empty tag'});
// simulate button click
wrapper.find('Button').props().onClick();
// expect SubmitPopup showing
expect(wrapper.find('SubmitPopup')).toHaveLength(1);
// mock deleteTag function
wrapper.instance().deleteTag = jest.fn((v) => {});
// simulate submit
wrapper.find('SubmitPopup').props().submit();
// expect deleteTag function to have been called with force parameter
expect(wrapper.instance().deleteTag).toHaveBeenCalledWith(true);
});
it('test cancel of ', function () {
const wrapper = instance();
callAPIMock({result: 'not empty tag'});
// simulate button click
wrapper.find('Button').props().onClick();
// expect SubmitPopup showing
expect(wrapper.find('SubmitPopup')).toHaveLength(1);
// mock deleteTag function
wrapper.instance().deleteTag = jest.fn((v) => {});
// simulate submit
wrapper.find('SubmitPopup').props().onHide();
// expect deleteTag function to have been called with force parameter
expect(wrapper.instance().deleteTag).toHaveBeenCalledTimes(0);
});
}); });

View File

@ -4,13 +4,18 @@ import VideoContainer from '../../elements/VideoContainer/VideoContainer';
import {callAPI} from '../../utils/Api'; import {callAPI} from '../../utils/Api';
import {withRouter} from 'react-router-dom'; import {withRouter} from 'react-router-dom';
import {VideoTypes} from '../../types/ApiTypes'; import {VideoTypes} from '../../types/ApiTypes';
import PageTitle, {Line} from '../../elements/PageTitle/PageTitle';
import SideBar, {SideBarTitle} from '../../elements/SideBar/SideBar';
import Tag from '../../elements/Tag/Tag';
import {DefaultTags, GeneralSuccess} from '../../types/GeneralTypes';
import {Button} from '../../elements/GPElements/Button';
import SubmitPopup from '../../elements/Popups/SubmitPopup/SubmitPopup';
interface CategoryViewProps extends RouteComponentProps<{ id: string }> { interface CategoryViewProps extends RouteComponentProps<{ id: string }> {}
setSubTitle: (title: string) => void
}
interface CategoryViewState { interface CategoryViewState {
loaded: boolean loaded: boolean;
submitForceDelete: boolean;
} }
/** /**
@ -23,7 +28,8 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
super(props); super(props);
this.state = { this.state = {
loaded: false loaded: false,
submitForceDelete: false
}; };
} }
@ -42,6 +48,20 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<PageTitle
title='Categories'
subtitle={this.videodata.length + ' Videos'}/>
<SideBar>
<SideBarTitle>Default Tags:</SideBarTitle>
<Tag tagInfo={DefaultTags.all}/>
<Tag tagInfo={DefaultTags.fullhd}/>
<Tag tagInfo={DefaultTags.hd}/>
<Tag tagInfo={DefaultTags.lowq}/>
<Line/>
<Button title='Delete Tag' onClick={(): void => {this.deleteTag(false);}} color={{backgroundColor: 'red'}}/>
</SideBar>
{this.state.loaded ? {this.state.loaded ?
<VideoContainer <VideoContainer
data={this.videodata}/> : null} data={this.videodata}/> : null}
@ -51,22 +71,50 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
this.props.history.push('/categories'); this.props.history.push('/categories');
}}>Back to Categories }}>Back to Categories
</button> </button>
{this.handlePopups()}
</> </>
); );
} }
private handlePopups(): JSX.Element {
if (this.state.submitForceDelete) {
return (<SubmitPopup
onHide={(): void => this.setState({submitForceDelete: false})}
submit={(): void => {this.deleteTag(true);}}/>);
} else {
return <></>;
}
}
/** /**
* fetch data for a specific tag from backend * fetch data for a specific tag from backend
* @param id tagid * @param id tagid
*/ */
fetchVideoData(id: number): void { private fetchVideoData(id: number): void {
callAPI<VideoTypes.VideoUnloadedType[]>('video.php', {action: 'getMovies', tag: id}, result => { callAPI<VideoTypes.VideoUnloadedType[]>('video.php', {action: 'getMovies', tag: id}, result => {
this.videodata = result; this.videodata = result;
this.setState({loaded: true}); this.setState({loaded: true});
this.props.setSubTitle(this.videodata.length + ' Videos');
}); });
} }
/**
* delete the current tag
*/
private deleteTag(force: boolean): void {
callAPI<GeneralSuccess>('tags.php', {
action: 'deleteTag',
tagId: parseInt(this.props.match.params.id),
force: force
}, result => {
console.log(result.result);
if (result.result === 'success') {
this.props.history.push('/categories');
} else if (result.result === 'not empty tag') {
// show submisison tag to ask if really delete
this.setState({submitForceDelete: true});
}
});
}
} }
/** /**

View File

@ -14,4 +14,30 @@ describe('<TagView/>', function () {
expect(wrapper.find('TagPreview')).toHaveLength(1); expect(wrapper.find('TagPreview')).toHaveLength(1);
}); });
it('test new tag popup', function () {
const wrapper = shallow(<TagView/>);
expect(wrapper.find('NewTagPopup')).toHaveLength(0);
wrapper.find('[data-testid="btnaddtag"]').simulate('click');
// newtagpopup should be showing now
expect(wrapper.find('NewTagPopup')).toHaveLength(1);
});
it('test add popup', function () {
const wrapper = shallow(<TagView/>);
expect(wrapper.find('NewTagPopup')).toHaveLength(0);
wrapper.setState({popupvisible: true});
expect(wrapper.find('NewTagPopup')).toHaveLength(1);
});
it('test hiding of popup', function () {
const wrapper = shallow(<TagView/>);
wrapper.setState({popupvisible: true});
wrapper.find('NewTagPopup').props().onHide();
expect(wrapper.find('NewTagPopup')).toHaveLength(0);
});
}); });

View File

@ -4,20 +4,27 @@ import videocontainerstyle from '../../elements/VideoContainer/VideoContainer.mo
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import {TagPreview} from '../../elements/Preview/Preview'; import {TagPreview} from '../../elements/Preview/Preview';
import {callAPI} from '../../utils/Api'; import {callAPI} from '../../utils/Api';
import PageTitle, {Line} from '../../elements/PageTitle/PageTitle';
import SideBar, {SideBarTitle} from '../../elements/SideBar/SideBar';
import Tag from '../../elements/Tag/Tag';
import {DefaultTags} from '../../types/GeneralTypes';
import NewTagPopup from '../../elements/Popups/NewTagPopup/NewTagPopup';
interface TagViewState { interface TagViewState {
loadedtags: TagType[]; loadedtags: TagType[];
popupvisible: boolean;
} }
interface props { interface props {}
setSubTitle: (title: string) => void
}
class TagView extends React.Component<props, TagViewState> { class TagView extends React.Component<props, TagViewState> {
constructor(props: props) { constructor(props: props) {
super(props); super(props);
this.state = {loadedtags: []}; this.state = {
loadedtags: [],
popupvisible: false
};
} }
componentDidMount(): void { componentDidMount(): void {
@ -27,6 +34,23 @@ class TagView extends React.Component<props, TagViewState> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<PageTitle
title='Categories'
subtitle={this.state.loadedtags.length + ' different Tags'}/>
<SideBar>
<SideBarTitle>Default Tags:</SideBarTitle>
<Tag tagInfo={DefaultTags.all}/>
<Tag tagInfo={DefaultTags.fullhd}/>
<Tag tagInfo={DefaultTags.hd}/>
<Tag tagInfo={DefaultTags.lowq}/>
<Line/>
<button data-testid='btnaddtag' className='btn btn-success' onClick={(): void => {
this.setState({popupvisible: true});
}}>Add a new Tag!
</button>
</SideBar>
<div className={videocontainerstyle.maincontent}> <div className={videocontainerstyle.maincontent}>
{this.state.loadedtags ? {this.state.loadedtags ?
this.state.loadedtags.map((m) => ( this.state.loadedtags.map((m) => (
@ -36,6 +60,7 @@ class TagView extends React.Component<props, TagViewState> {
)) : )) :
'loading'} 'loading'}
</div> </div>
{this.handlePopups()}
</> </>
); );
} }
@ -46,9 +71,21 @@ class TagView extends React.Component<props, TagViewState> {
loadTags(): void { loadTags(): void {
callAPI<TagType[]>('tags.php', {action: 'getAllTags'}, result => { callAPI<TagType[]>('tags.php', {action: 'getAllTags'}, result => {
this.setState({loadedtags: result}); this.setState({loadedtags: result});
this.props.setSubTitle(result.length + ' different Tags');
}); });
} }
private handlePopups(): JSX.Element {
if (this.state.popupvisible) {
return (
<NewTagPopup onHide={(): void => {
this.setState({popupvisible: false});
this.loadTags();
}}/>
);
} else {
return (<></>);
}
}
} }
export default TagView; export default TagView;