Merge branch 'addtag_filterablesearch' into 'master'

Filterbutton on addtag pupup

See merge request lukas/openmediacenter!38
This commit is contained in:
Lukas Heiligenbrunner 2021-03-08 14:11:26 +00:00
commit 162b4efd0e
7 changed files with 234 additions and 101 deletions

View File

@ -0,0 +1,64 @@
import {shallow} from 'enzyme';
import React from 'react';
import FilterButton from './FilterButton';
import RandomPage from "../../pages/RandomPage/RandomPage";
import {callAPI} from "../../utils/Api";
describe('<FilterButton/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
wrapper.unmount();
});
it('test initial render ', function () {
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
expect(wrapper.find('input')).toHaveLength(0);
});
it('test clicking', function () {
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
wrapper.simulate('click');
expect(wrapper.find('input')).toHaveLength(1);
});
it('test call of callback on textfield change', function () {
let val = '';
const func = jest.fn((vali => {val = vali}));
const wrapper = shallow(<FilterButton onFilterChange={func}/>);
wrapper.simulate('click');
wrapper.find('input').simulate('change', {target: {value: 'test'}});
expect(func).toHaveBeenCalledTimes(1);
expect(val).toBe('test')
});
it('test closing on x button click', function () {
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
wrapper.simulate('click');
expect(wrapper.find('input')).toHaveLength(1);
wrapper.find('Button').simulate('click');
expect(wrapper.find('input')).toHaveLength(0);
});
it('test shortkey press', function () {
let events = [];
document.addEventListener = jest.fn((event, cb) => {
events[event] = cb;
});
shallow(<RandomPage/>);
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
expect(wrapper.find('input')).toHaveLength(0);
// trigger the keypress event
events.keyup({key: 'f'});
expect(wrapper.find('input')).toHaveLength(1);
});
});

View File

@ -0,0 +1,99 @@
import React from "react";
import style from "../Popups/AddActorPopup/AddActorPopup.module.css";
import {Button} from "../GPElements/Button";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faFilter, faTimes} from "@fortawesome/free-solid-svg-icons";
import {addKeyHandler, removeKeyHandler} from "../../utils/ShortkeyHandler";
interface props {
onFilterChange: (filter: string) => void
}
interface state {
filtervisible: boolean;
filter: string;
}
class FilterButton extends React.Component<props, state> {
// filterfield anchor, needed to focus after filter btn click
private filterfield: HTMLInputElement | null | undefined;
constructor(props: props) {
super(props);
this.state = {
filtervisible: false,
filter: ''
}
this.keypress = this.keypress.bind(this);
this.enableFilterField = this.enableFilterField.bind(this);
}
componentWillUnmount(): void {
removeKeyHandler(this.keypress);
}
componentDidMount(): void {
addKeyHandler(this.keypress);
}
render(): JSX.Element {
if (this.state.filtervisible) {
return (
<>
<input className={'form-control mr-sm-2 ' + style.searchinput}
type='text' placeholder='Filter' value={this.state.filter}
onChange={(e): void => {
this.props.onFilterChange(e.target.value);
this.setState({filter: e.target.value});
}}
ref={(input): void => {
this.filterfield = input;
}}/>
<Button title={<FontAwesomeIcon style={{
verticalAlign: 'middle',
lineHeight: '130px'
}} icon={faTimes} size='1x'/>} color={{backgroundColor: 'red'}} onClick={(): void => {
this.setState({filter: '', filtervisible: false});
}}/>
</>
);
} else {
return (<Button
title={<span>Filter <FontAwesomeIcon
style={{
verticalAlign: 'middle',
lineHeight: '130px'
}}
icon={faFilter}
size='1x'/></span>}
color={{backgroundColor: 'cornflowerblue', color: 'white'}}
onClick={this.enableFilterField}/>)
}
}
/**
* enable filterfield and focus into searchbar
*/
private enableFilterField(): void {
this.setState({filtervisible: true}, () => {
// focus filterfield after state update
this.filterfield?.focus();
});
}
/**
* key event handling
* @param event keyevent
*/
private keypress(event: KeyboardEvent): void {
// hide if escape is pressed
if (event.key === 'f') {
this.enableFilterField();
}
}
}
export default FilterButton;

View File

@ -6,10 +6,7 @@ import {NewActorPopupContent} from '../NewActorPopup/NewActorPopup';
import {APINode, callAPI} from '../../../utils/Api'; import {APINode, callAPI} from '../../../utils/Api';
import {ActorType} from '../../../types/VideoTypes'; import {ActorType} from '../../../types/VideoTypes';
import {GeneralSuccess} from '../../../types/GeneralTypes'; import {GeneralSuccess} from '../../../types/GeneralTypes';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import FilterButton from "../../FilterButton/FilterButton";
import {faFilter, faTimes} from '@fortawesome/free-solid-svg-icons';
import {Button} from '../../GPElements/Button';
import {addKeyHandler, removeKeyHandler} from '../../../utils/ShortkeyHandler';
interface props { interface props {
onHide: () => void; onHide: () => void;
@ -20,7 +17,6 @@ interface state {
contentDefault: boolean; contentDefault: boolean;
actors: ActorType[]; actors: ActorType[];
filter: string; filter: string;
filtervisible: boolean;
} }
/** /**
@ -36,23 +32,14 @@ class AddActorPopup extends React.Component<props, state> {
this.state = { this.state = {
contentDefault: true, contentDefault: true,
actors: [], actors: [],
filter: '', filter: ''
filtervisible: false
}; };
this.tileClickHandler = this.tileClickHandler.bind(this); this.tileClickHandler = this.tileClickHandler.bind(this);
this.filterSearch = this.filterSearch.bind(this); this.filterSearch = this.filterSearch.bind(this);
this.parentSubmit = this.parentSubmit.bind(this); this.parentSubmit = this.parentSubmit.bind(this);
this.keypress = this.keypress.bind(this);
} }
componentWillUnmount(): void {
removeKeyHandler(this.keypress);
}
componentDidMount(): void { componentDidMount(): void {
addKeyHandler(this.keypress);
// fetch the available actors // fetch the available actors
this.loadActors(); this.loadActors();
} }
@ -94,30 +81,9 @@ class AddActorPopup extends React.Component<props, state> {
return ( return (
<> <>
<div className={style.searchbar}> <div className={style.searchbar}>
{ <FilterButton onFilterChange={(filter): void => {
this.state.filtervisible ? this.setState({filter: filter})
<>
<input className={'form-control mr-sm-2 ' + style.searchinput}
type='text' placeholder='Filter' value={this.state.filter}
onChange={(e): void => {
this.setState({filter: e.target.value});
}}
ref={(input): void => {this.filterfield = input;}}/>
<Button title={<FontAwesomeIcon style={{
verticalAlign: 'middle',
lineHeight: '130px'
}} icon={faTimes} size='1x'/>} color={{backgroundColor: 'red'}} onClick={(): void => {
this.setState({filter: '', filtervisible: false});
}}/> }}/>
</> :
<Button
title={<span>Filter <FontAwesomeIcon style={{
verticalAlign: 'middle',
lineHeight: '130px'
}} icon={faFilter} size='1x'/></span>}
color={{backgroundColor: 'cornflowerblue', color: 'white'}}
onClick={(): void => this.enableFilterField()}/>
}
</div> </div>
{this.state.actors.filter(this.filterSearch).map((el) => (<ActorTile actor={el} onClick={this.tileClickHandler}/>))} {this.state.actors.filter(this.filterSearch).map((el) => (<ActorTile actor={el} onClick={this.tileClickHandler}/>))}
</> </>
@ -155,16 +121,6 @@ class AddActorPopup extends React.Component<props, state> {
}); });
} }
/**
* enable filterfield and focus into searchbar
*/
private enableFilterField(): void {
this.setState({filtervisible: true}, () => {
// focus filterfield after state update
this.filterfield?.focus();
});
}
/** /**
* filter the actor array for search matches * filter the actor array for search matches
* @param actor * @param actor
@ -185,17 +141,6 @@ class AddActorPopup extends React.Component<props, state> {
this.tileClickHandler(filteredList[0]); this.tileClickHandler(filteredList[0]);
} }
} }
/**
* key event handling
* @param event keyevent
*/
private keypress(event: KeyboardEvent): void {
// hide if escape is pressed
if (event.key === 'f') {
this.enableFilterField();
}
}
} }
export default AddActorPopup; export default AddActorPopup;

View File

@ -1,26 +1,3 @@
.popup { .actionbar{
border: 3px #3574fe solid; margin-bottom: 15px;
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

@ -25,11 +25,37 @@ describe('<AddTagPopup/>', function () {
const wrapper = shallow(<AddTagPopup submit={jest.fn()} onHide={jest.fn()}/>); const wrapper = shallow(<AddTagPopup submit={jest.fn()} onHide={jest.fn()}/>);
wrapper.setState({ wrapper.setState({
items: [{tag_id: 1, tag_name: 'test'}] items: [{TagId: 1, TagName: 'test'}]
}, () => { }, () => {
wrapper.find('Tag').first().dive().simulate('click'); wrapper.find('Tag').first().dive().simulate('click');
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);
}); });
}); });
it('test parent submit if one item left', function () {
const onhide = jest.fn();
const submit = jest.fn();
const wrapper = shallow(<AddTagPopup submit={submit} onHide={onhide}/>);
wrapper.setState({
items: [{TagId: 1, TagName: 'test'}]
}, () => {
wrapper.instance().parentSubmit();
expect(onhide).toHaveBeenCalledTimes(1);
expect(submit).toHaveBeenCalledTimes(1);
wrapper.setState({
items: [{TagId: 1, TagName: 'test'}, {TagId: 3, TagName: 'test3'}]
}, () => {
wrapper.instance().parentSubmit();
// expect no submit if there are more than 1 item left...
expect(onhide).toHaveBeenCalledTimes(1);
expect(submit).toHaveBeenCalledTimes(1);
})
});
});
}); });

View File

@ -3,15 +3,17 @@ import Tag from '../../Tag/Tag';
import PopupBase from '../PopupBase'; import PopupBase from '../PopupBase';
import {APINode, callAPI} from '../../../utils/Api'; import {APINode, callAPI} from '../../../utils/Api';
import {TagType} from '../../../types/VideoTypes'; import {TagType} from '../../../types/VideoTypes';
import FilterButton from "../../FilterButton/FilterButton";
import styles from './AddTagPopup.module.css'
interface props { interface props {
onHide: () => void; onHide: () => void;
submit: (tagId: number, tagName: string) => void; submit: (tagId: number, tagName: string) => void;
movie_id: number;
} }
interface state { interface state {
items: TagType[]; items: TagType[];
filter: string;
} }
/** /**
@ -21,7 +23,11 @@ class AddTagPopup extends React.Component<props, state> {
constructor(props: props) { constructor(props: props) {
super(props); super(props);
this.state = {items: []}; this.state = {items: [], filter: ''};
this.tagFilter = this.tagFilter.bind(this);
this.parentSubmit = this.parentSubmit.bind(this);
this.onItemClick = this.onItemClick.bind(this);
} }
componentDidMount(): void { componentDidMount(): void {
@ -34,18 +40,37 @@ class AddTagPopup extends React.Component<props, state> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<PopupBase title='Add a Tag to this Video:' onHide={this.props.onHide}> <PopupBase title='Add a Tag to this Video:' onHide={this.props.onHide} ParentSubmit={this.parentSubmit}>
<div className={styles.actionbar}>
<FilterButton onFilterChange={(filter): void => this.setState({filter: filter})}/>
</div>
{this.state.items ? {this.state.items ?
this.state.items.map((i) => ( this.state.items.filter(this.tagFilter).map((i) => (
<Tag tagInfo={i} <Tag tagInfo={i}
onclick={(): void => { onclick={(): void => this.onItemClick(i)}/>
this.props.submit(i.TagId, i.TagName);
this.props.onHide();
}}/>
)) : null} )) : null}
</PopupBase> </PopupBase>
); );
} }
private onItemClick(tag: TagType): void {
this.props.submit(tag.TagId, tag.TagName);
this.props.onHide();
}
private tagFilter(tag: TagType): boolean {
return tag.TagName.toLowerCase().includes(this.state.filter.toLowerCase());
}
private parentSubmit(): void {
// allow submit only if one item is left in selection
const filteredList = this.state.items.filter(this.tagFilter);
if (filteredList.length === 1) {
// simulate click if parent submit
this.onItemClick(filteredList[0]);
}
}
} }
export default AddTagPopup; export default AddTagPopup;

View File

@ -181,13 +181,10 @@ export class Player extends React.Component<myprops, mystate> {
handlePopOvers(): JSX.Element { handlePopOvers(): JSX.Element {
return ( return (
<> <>
{this.state.popupvisible ? {
<AddTagPopup onHide={(): void => { this.state.popupvisible ?
this.setState({popupvisible: false}); <AddTagPopup onHide={(): void => this.setState({popupvisible: false})}
}} submit={this.quickAddTag}/> : null
submit={this.quickAddTag}
movie_id={this.state.movie_id}/> :
null
} }
{ {
this.state.actorpopupvisible ? this.state.actorpopupvisible ?