diff --git a/api/src/handlers/Tags.php b/api/src/handlers/Tags.php index 9f50528..92546ab 100644 --- a/api/src/handlers/Tags.php +++ b/api/src/handlers/Tags.php @@ -9,6 +9,7 @@ class Tags extends RequestBase { function initHandlers() { $this->addToDB(); $this->getFromDB(); + $this->delete(); } private function addToDB() { @@ -65,4 +66,36 @@ class Tags extends RequestBase { $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 . '"}'); + } + } + }); + } } diff --git a/src/elements/PageTitle/PageTitle.tsx b/src/elements/PageTitle/PageTitle.tsx index b1315d2..9238a2b 100644 --- a/src/elements/PageTitle/PageTitle.tsx +++ b/src/elements/PageTitle/PageTitle.tsx @@ -4,7 +4,7 @@ import GlobalInfos from '../../utils/GlobalInfos'; interface props { title: string; - subtitle: string | null; + subtitle: string | number | null; } /** diff --git a/src/elements/Popups/AddTagPopup/AddTagPopup.test.js b/src/elements/Popups/AddTagPopup/AddTagPopup.test.js index dc869ff..3e1c39c 100644 --- a/src/elements/Popups/AddTagPopup/AddTagPopup.test.js +++ b/src/elements/Popups/AddTagPopup/AddTagPopup.test.js @@ -22,60 +22,14 @@ describe('', function () { }); it('test tag click', function () { - const wrapper = shallow(); - wrapper.instance().addTag = jest.fn(); + const wrapper = shallow(); wrapper.setState({ items: [{tag_id: 1, tag_name: 'test'}] }, () => { wrapper.find('Tag').first().dive().simulate('click'); - expect(wrapper.instance().addTag).toHaveBeenCalledTimes(1); - }); - }); - - it('test addtag', done => { - const wrapper = shallow(); - - 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.onHide).toHaveBeenCalledTimes(1); - - global.fetch.mockClear(); - done(); - }); - }); - - it('test failing addTag', done => { - const wrapper = shallow(); - - 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(); }); }); }); diff --git a/src/elements/Popups/AddTagPopup/AddTagPopup.tsx b/src/elements/Popups/AddTagPopup/AddTagPopup.tsx index 9803fa5..266653d 100644 --- a/src/elements/Popups/AddTagPopup/AddTagPopup.tsx +++ b/src/elements/Popups/AddTagPopup/AddTagPopup.tsx @@ -3,7 +3,6 @@ import Tag from '../../Tag/Tag'; import PopupBase from '../PopupBase'; import {callAPI} from '../../../utils/Api'; import {TagType} from '../../../types/VideoTypes'; -import {GeneralSuccess} from '../../../types/GeneralTypes'; interface props { onHide: () => void; @@ -41,29 +40,13 @@ class AddTagPopup extends React.Component { this.state.items.map((i) => ( { - this.addTag(i.tag_id, i.tag_name); + this.props.submit(i.tag_id, i.tag_name); + this.props.onHide(); }}/> )) : null} ); } - - /** - * 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; diff --git a/src/elements/Popups/SubmitPopup/SubmitPopup.test.js b/src/elements/Popups/SubmitPopup/SubmitPopup.test.js new file mode 100644 index 0000000..c36080b --- /dev/null +++ b/src/elements/Popups/SubmitPopup/SubmitPopup.test.js @@ -0,0 +1,28 @@ +import {shallow} from "enzyme"; +import React from "react"; +import SubmitPopup from "./SubmitPopup"; + +describe('', function () { + it('renders without crashing ', function () { + const wrapper = shallow(); + wrapper.unmount(); + }); + + it('test submit click', function () { + const func = jest.fn(); + const wrapper = shallow( 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( func()}/>); + + wrapper.find('Button').findWhere(p => p.props().title === 'Cancel').simulate('click'); + + expect(func).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/elements/Popups/SubmitPopup/SubmitPopup.tsx b/src/elements/Popups/SubmitPopup/SubmitPopup.tsx new file mode 100644 index 0000000..e6723d1 --- /dev/null +++ b/src/elements/Popups/SubmitPopup/SubmitPopup.tsx @@ -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 ( + + - - - - - - - - - - - {this.state.popupvisible ? - { - 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; diff --git a/src/pages/CategoryPage/CategoryView.test.js b/src/pages/CategoryPage/CategoryView.test.js index 57fe75b..2fbcde2 100644 --- a/src/pages/CategoryPage/CategoryView.test.js +++ b/src/pages/CategoryPage/CategoryView.test.js @@ -4,7 +4,7 @@ import {CategoryView} from './CategoryView'; describe('', function () { function instance() { - return shallow(); + return shallow(); } it('renders without crashing ', function () { @@ -21,4 +21,54 @@ describe('', function () { wrapper.find('button').simulate('click'); 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); + }); }); diff --git a/src/pages/CategoryPage/CategoryView.tsx b/src/pages/CategoryPage/CategoryView.tsx index 6171429..1bb1758 100644 --- a/src/pages/CategoryPage/CategoryView.tsx +++ b/src/pages/CategoryPage/CategoryView.tsx @@ -4,13 +4,18 @@ import VideoContainer from '../../elements/VideoContainer/VideoContainer'; import {callAPI} from '../../utils/Api'; import {withRouter} from 'react-router-dom'; 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 }> { - setSubTitle: (title: string) => void -} +interface CategoryViewProps extends RouteComponentProps<{ id: string }> {} interface CategoryViewState { - loaded: boolean + loaded: boolean; + submitForceDelete: boolean; } /** @@ -23,7 +28,8 @@ export class CategoryView extends React.Component + + + + Default Tags: + + + + + + + + {this.handlePopups()} ); } + private handlePopups(): JSX.Element { + if (this.state.submitForceDelete) { + return ( this.setState({submitForceDelete: false})} + submit={(): void => {this.deleteTag(true);}}/>); + } else { + return <>; + } + } + /** * fetch data for a specific tag from backend * @param id tagid */ - fetchVideoData(id: number): void { + private fetchVideoData(id: number): void { callAPI('video.php', {action: 'getMovies', tag: id}, result => { this.videodata = result; this.setState({loaded: true}); - this.props.setSubTitle(this.videodata.length + ' Videos'); }); } + /** + * delete the current tag + */ + private deleteTag(force: boolean): void { + callAPI('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}); + } + }); + } } /** diff --git a/src/pages/CategoryPage/TagView.test.js b/src/pages/CategoryPage/TagView.test.js index 0d2e1a6..e5d24da 100644 --- a/src/pages/CategoryPage/TagView.test.js +++ b/src/pages/CategoryPage/TagView.test.js @@ -14,4 +14,30 @@ describe('', function () { expect(wrapper.find('TagPreview')).toHaveLength(1); }); + + it('test new tag popup', function () { + const wrapper = shallow(); + + 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(); + + 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(); + wrapper.setState({popupvisible: true}); + + wrapper.find('NewTagPopup').props().onHide(); + + expect(wrapper.find('NewTagPopup')).toHaveLength(0); + }); }); diff --git a/src/pages/CategoryPage/TagView.tsx b/src/pages/CategoryPage/TagView.tsx index 28d00d4..ec384d7 100644 --- a/src/pages/CategoryPage/TagView.tsx +++ b/src/pages/CategoryPage/TagView.tsx @@ -4,20 +4,27 @@ import videocontainerstyle from '../../elements/VideoContainer/VideoContainer.mo import {Link} from 'react-router-dom'; import {TagPreview} from '../../elements/Preview/Preview'; 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 { loadedtags: TagType[]; + popupvisible: boolean; } -interface props { - setSubTitle: (title: string) => void -} +interface props {} class TagView extends React.Component { constructor(props: props) { super(props); - this.state = {loadedtags: []}; + this.state = { + loadedtags: [], + popupvisible: false + }; } componentDidMount(): void { @@ -27,6 +34,23 @@ class TagView extends React.Component { render(): JSX.Element { return ( <> + + + + Default Tags: + + + + + + + +
{this.state.loadedtags ? this.state.loadedtags.map((m) => ( @@ -36,6 +60,7 @@ class TagView extends React.Component { )) : 'loading'}
+ {this.handlePopups()} ); } @@ -46,9 +71,21 @@ class TagView extends React.Component { loadTags(): void { callAPI('tags.php', {action: 'getAllTags'}, result => { this.setState({loadedtags: result}); - this.props.setSubTitle(result.length + ' different Tags'); }); } + + private handlePopups(): JSX.Element { + if (this.state.popupvisible) { + return ( + { + this.setState({popupvisible: false}); + this.loadTags(); + }}/> + ); + } else { + return (<>); + } + } } export default TagView;