Commit 5eb5805d authored by Maxime Chaillet's avatar Maxime Chaillet

Refactor TagList. Write tests for TagLabel. Fix existing tests on NewlyAvailableEventsDialogue.

parent 2c07a995
This diff is collapsed.
......@@ -155,7 +155,6 @@ class ClosedDataPage extends React.Component {
</div>
);
}
}
class OpenDataPage extends React.Component {
......
......@@ -5,7 +5,7 @@ import { Label, Glyphicon } from 'react-bootstrap';
/** React component which renders a tag label */
class TagLabel extends React.Component {
render() {
let { context, tag, showDeleteButton } = this.props;
let { tag, showDeleteButton } = this.props;
return (<div style={{ display: 'inline-block', marginRight: '4px' }}>
<Label
......@@ -42,7 +42,7 @@ class TagLabel extends React.Component {
color: '#FFFFFF',
verticalAlign: 'bottom'
}}
onClick={() => this.props.removeTag(tag)}
onClick={() => this.props.onDeleteTagClicked(tag)}
/>
</Label> : null}
</div>)
......@@ -50,16 +50,12 @@ class TagLabel extends React.Component {
}
TagLabel.propTypes = {
/* the context the tag is rendered */
context: PropTypes.string.isRequired,
/* callback function which show that page to edit the tag */
editTag: PropTypes.func,
/* the tag to render */
tag: PropTypes.object.isRequired,
/* Callback function used to remove a tag from the selection. Used in EVENT context only */
removeTag: PropTypes.func,
onDeleteTagClicked: PropTypes.func,
/* Whether the delete button is displayed or not */
showDeleteButton: PropTypes.bool
showDeleteButton: PropTypes.bool,
/* the tag to render */
tag: PropTypes.object.isRequired,
}
TagLabel.defaultProps = {
......
import React from 'react';
import Proptype from 'prop-types';
import TagLabel from './TagLabel';
import { EVENTLIST_CONTEXT } from '../../../constants/EventTypes';
import ScrollMenu from 'react-horizontal-scrolling-menu';
/** React component which renders tags in line */
class TagListInLine extends React.Component {
render() {
let tagViewerList = [];
if (this.props.tags && this.props.tags instanceof Array && this.props.tags.length !== null) {
return this.props.tags.map((tag) => { return (<TagLabel key={tag._id} context={EVENTLIST_CONTEXT} tag={tag} />); })
}
let { hasHorizontalScrolls, tags } = this.props;
if (tags && tags instanceof Array && tags.length !== null) {
let tagLabels = tags.map((tag) => {
return (<TagLabel
key={tag._id}
onDeleteTagClicked={this.props.onDeleteTagClicked ? this.props.onDeleteTagClicked : null}
showDeleteButton={this.props.showDeleteButton === true ? true : false}
tag={tag} />);
})
if (hasHorizontalScrolls === true) {
return (
<ScrollMenu
data={tagLabels}
arrowLeft={<div className='arrow-prev'> &#60; </div>}
arrowRight={<div className='arrow-next'> &#62; </div>}
wrapperClass='customWrapperClass'
innerWrapperClass='customInnerWrapperClass'
/>)
} else {
return tagLabels;
}
}
return null;
}
}
TagListInLine.proptype = {
/** whether an horizontal scroll is displayed */
hasHorizontalScrolls: Proptype.bool,
/** Callback function triggered when the user clicks on the delete button of a tag. */
onDeleteTagClicked: Proptype.func,
/** Whether or not the delete button is shown for the tags. */
showDeleteButton: Proptype.bool,
/** all tag objects to be rendered */
tags: Proptype.array
}
TagListInLine.defaultProps = {
hasHorizontalScrolls: false,
}
export default TagListInLine;
\ No newline at end of file
......@@ -2,12 +2,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { Grid, Row, Col } from 'react-bootstrap';
import { TAG_MANAGER_CONTEXT, BASIC_EVENT_CONTEXT, EDIT_EVENT_CONTEXT, INFO_MESSAGE_TYPE } from '../../../constants/EventTypes';
import TagLabel from './TagLabel';
import CreatableSelect from 'react-select/lib/Creatable';
import _ from 'lodash';
import { GUI_CONFIG } from '../../../config/gui.config';
import UserMessage from '../../UserMessage';
import ScrollMenu from 'react-horizontal-scrolling-menu';
import './TagList.css';
import TagListTableLine from './TagListTableLine';
......@@ -15,86 +13,17 @@ import TagListTableLine from './TagListTableLine';
* React component which displays a list of tags
* @param {*} props tag as provided by the server
*/
class TagList extends React.Component {
class TagListPanel extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
render() {
let { availableTags, context } = this.props;
if (context === EDIT_EVENT_CONTEXT) {
// show tags inside a detailed event
const customStyles = {
dropdownIndicator: () => ({
display: 'none'
}),
indicatorSeparator: () => ({
display: 'none'
}),
control: (provided) => ({
...provided,
maxWidth: 140,
minWidth: 100,
minHeight: 25,
}),
container: (provided) => ({
...provided,
}),
valueContainer: (provided) => ({
...provided,
paddingTop: '0px',
paddingBottom: '0px',
}),
menu: (provided) => ({
...provided,
bottom: '100%',
top: 'unset',
})
};
let selectedTags = this.props.selectedTags ?
this.props.selectedTags.map((tag) => { return (<TagLabel key={tag._id} context={EDIT_EVENT_CONTEXT} tag={tag} removeTag={this.props.removeTagFromSelection} />); })
: null;
let availableTagsForSelect = availableTags ?
availableTags.map((tag) => ({ value: tag._id, label: tag.name }))
: null;
return (
<div style={{ display: 'flex' }}>
<div style={{ flex: '0 0 148px' }}>
<CreatableSelect
isClearable={false}
noOptionsMessage={() => 'No existing tag'}
onChange={this.onChange}
styles={customStyles}
options={availableTagsForSelect}
isSearchable={true} />
</div>
<div style={{ flex: '1 1 100px', overflow: 'hidden' }}>
<ScrollMenu
data={selectedTags}
arrowLeft={<div className='arrow-prev'> &#60; </div>}
arrowRight={<div className='arrow-next'> &#62; </div>}
wrapperClass='customWrapperClass'
innerWrapperClass='customInnerWrapperClass'
/>
</div>
</div>
);
} else if (context === TAG_MANAGER_CONTEXT) {
if (context === TAG_MANAGER_CONTEXT) {
// show tags for management
if (!availableTags || availableTags.length === 0) {
return (
......@@ -140,29 +69,10 @@ class TagList extends React.Component {
return null;
}
/**
* Callback function triggered when the input has changed. A new tag is being created
* @param {*} value the new tag as provided by the react-select component
*/
onChange(option, action) {
if (option && action) {
if (action.action === 'create-option') {
let newTag = {
color: GUI_CONFIG().DEFAULT_TAG_COLOR,
description: null,
name: option.label,
investigationId: this.props.investigationId
};
this.props.createNewTag(newTag);
} else if (action.action === 'select-option') {
this.props.addTagToSelection(_.find(this.props.availableTags, (tag) => { return tag._id === option.value; }));
}
}
}
}
TagList.propTypes = {
TagListPanel.propTypes = {
/* List of available tags (can be null when there is no available tags yet)*/
availableTags: PropTypes.array,
/* Context defining how tag list is rendered */
......@@ -181,4 +91,4 @@ TagList.propTypes = {
removeTagFromSelection: PropTypes.func,
};
export default TagList;
export default TagListPanel;
import React from 'react';
import Proptypes from 'prop-types';
import _ from 'lodash';
import CreatableSelect from 'react-select/lib/Creatable';
import { GUI_CONFIG } from '../../../config/gui.config';
class TagSelector extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
render() {
let { availableTags } = this.props;
// show tags inside a detailed event
const customStyles = {
dropdownIndicator: () => ({
display: 'none'
}),
indicatorSeparator: () => ({
display: 'none'
}),
control: (provided) => ({
...provided,
maxWidth: 140,
minWidth: 100,
minHeight: 25,
}),
container: (provided) => ({
...provided,
}),
valueContainer: (provided) => ({
...provided,
paddingTop: '0px',
paddingBottom: '0px',
}),
menu: (provided) => ({
...provided,
bottom: '100%',
top: 'unset',
})
};
let availableTagsForSelect = availableTags ? availableTags.map((tag) => ({ value: tag._id, label: tag.name })) : null;
return (
<CreatableSelect
isClearable={false}
noOptionsMessage={() => 'No existing tag'}
onChange={this.onChange}
styles={customStyles}
options={availableTagsForSelect}
isSearchable={true} />);
}
/**
* Callback function triggered when the input has changed. A new tag is being created
* @param {*} value the new tag as provided by the react-select component
*/
onChange(option, action) {
if (option && action) {
if (action.action === 'create-option') {
let newTag = {
color: GUI_CONFIG().DEFAULT_TAG_COLOR,
description: null,
name: option.label,
investigationId: this.props.investigationId
};
this.props.onTagCreatedInSelect(newTag);
} else if (action.action === 'select-option') {
this.props.onTagSelectedInSelect(_.find(this.props.availableTags, (tag) => { return tag._id === option.value; }));
}
}
}
}
export default TagSelector;
TagSelector.proptype = {
/** list of avialable tags */
availableTags: Proptypes.array.isRequired,
/** callback function triggered when a tag is created in the select element */
onTagCreatedInSelect: Proptypes.func,
/** Callback funnction triggered when a tag is selelcted in the select element */
onTagSelectedInSelect: Proptypes.func
}
......@@ -68,7 +68,6 @@ export class LogbookContainer extends React.Component {
}
render() {
debugger;
const { investigationId, user } = this.props;
const selectionFilter = getSelectionFiltersForMongoQuery(this.state.userSearchCriteria, this.state.sortingFilter, this.state.view);
......
......@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import axios from 'axios';
import { connect } from 'react-redux';
import TagList from '../../components/Logbook/Tag/TagList';
import TagListPanel from '../../components/Logbook/Tag/TagListPanel';
import { createTagsByInvestigationId, updateTagsByInvestigationId } from '../../api/icat/icatPlus';
import _ from 'lodash';
import { INFO_MESSAGE_TYPE, ERROR_MESSAGE_TYPE, TAG_MANAGER_CONTEXT, BASIC_EVENT_CONTEXT, NEW_EVENT_CONTEXT, EDIT_EVENT_CONTEXT, FETCHED_STATUS, FETCHING_STATUS, EDIT_TAG_CONTEXT, NEW_TAG_CONTEXT, LOGBOOK_CONTAINER_CONTEXT } from '../../constants/EventTypes';
......@@ -12,6 +12,8 @@ import UserMessage from '../../components/UserMessage';
import { SUCCESS_MESSAGE_TYPE } from '../../constants/UserMessages';
import { setAvailableTagAction } from '../../actions/logbook';
import { getTagsByInvestigationId } from './IORequests';
import TagSelector from '../../components/Logbook/Tag/TagSelector';
import TagListInLine from '../../components/Logbook/Tag/TagListInLine';
/**
* The event tag container in charge of managing event tags.
......@@ -69,16 +71,18 @@ class TagContainer extends React.Component {
if (this.state.context === EDIT_EVENT_CONTEXT || this.state.context === NEW_EVENT_CONTEXT) {
return (
<TagList
availableTags={this.props.availableTags}
addTagToSelection={this.addTagToSelection}
context={EDIT_EVENT_CONTEXT}
createNewTag={this.createNewTag}
investigationId={investigationId}
removeTagFromSelection={this.removeTagFromSelection}
selectedTags={this.state.selectedTags}
/>
);
<div style={{ display: 'flex' }}>
<div style={{ flex: '0 0 148px' }}>
<TagSelector
availableTags={this.props.availableTags}
onTagCreatedInSelect={this.createNewTag}
onTagSelectedInSelect={this.addTagToSelection} />
</div>
<div style={{ flex: '1 1 100px', overflow: 'hidden' }}>
<TagListInLine tags={this.state.selectedTags} hasHorizontalScrolls={true} onDeleteTagClicked={this.removeTagFromSelection} showDeleteButton={true} />
</div>
</div>);
}
// no event is provided.
......@@ -86,7 +90,7 @@ class TagContainer extends React.Component {
return (<div>
{userMessage}
<TagListMenu onNewTagButtonClicked={this.onNewTagButtonClicked} />
<TagList
<TagListPanel
availableTags={this.props.availableTags}
context={TAG_MANAGER_CONTEXT}
editTag={this.onEditTagButtonClicked}
......
......@@ -31,7 +31,7 @@ const store = createStore(
const persistor = persistStore(store)
// Extracted from reat-persist-crosstab https://github.com/rt2zz/redux-persist-crosstab/blob/v4.0.0-0/index.js
//Extracted from redux-persist-crosstab https://github.com/rt2zz/redux-persist-crosstab/blob/v4.0.0-0/index.js
function crosstabSync(store, persistConfig, crosstabConfig = {}) {
const blacklist = crosstabConfig.blacklist || null
const whitelist = crosstabConfig.whitelist || null
......
import React from 'react';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { createStore, applyMiddleware } from 'redux'
import { persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import logger from 'redux-logger'
import thunk from 'redux-thunk'
import reducer from '../../src/reducers'
import promise from "redux-promise-middleware"
import { createStore, applyMiddleware } from 'redux';
import { persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import reducer from '../../src/reducers';
import promise from "redux-promise-middleware";
import { LOGGED_IN } from '../../src/constants/ActionTypes';
import { GUI_CONFIG } from '../../src/config/gui.config';
jest.mock("../../src/config/gui.config");
......@@ -21,7 +21,10 @@ beforeEach(() => {
})
describe("NewlyAvailableEventsDialogue integration tests", () => {
// mock and reimplement getTagsByInvestigationId()
IORequestModule.getTagsByInvestigationId = jest.fn((sessionId, investigationId, onSuccess, onError) => {
onSuccess({ data: [] });
});
function getWrapper() {
/* overrides default configuration */
GUI_CONFIG.mockReturnValue({
......@@ -43,10 +46,7 @@ describe("NewlyAvailableEventsDialogue integration tests", () => {
applyMiddleware(...middleware, logger, promise(), thunk)
)
return Enzyme.mount(<LogbookContainer
investigationId="testInvestigationId"
store={store}
/>)
return Enzyme.mount(<LogbookContainer investigationId="testInvestigationId" />, { context: { store } })
};
it('calls getEvents when the user clicks the refresh icon on the NewlyAvailableEventsDialogue', (done) => {
......@@ -77,7 +77,6 @@ describe("NewlyAvailableEventsDialogue integration tests", () => {
mockedGetEvents.mockRestore();
done()
}, 500);
})
it('refreshes the event list when the user clicks the refresh icon on the NewlyAvailableEventsDialogue', () => {
......
import React from 'react';
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import TagLabel from '../../src/components/Logbook/Tag/TagLabel';
let resources = require('./resources/TagLabel.resource');
beforeEach(() => {
Enzyme.configure({ adapter: new Adapter() })
})
describe("TagLabel unit tests", () => {
function getShallowWrapper(tag, showDeleteButton, onDeleteTagClicked) {
return Enzyme.shallow(<TagLabel
onDeleteTagClicked={onDeleteTagClicked}
showDeleteButton={showDeleteButton}
tag={tag} />
);
}
describe('rendering', () => {
let resource = resources.rendering;
it('renders a label with no deletebutton by default', () => {
expect(getShallowWrapper(resource.tag, null, null).find('Label').length).toBe(1);
expect(getShallowWrapper(resource.tag, null, null).find('Label').exists('Glyphicon')).toEqual(false);
})
it('renders a label with no deletebutton when showDeleteButton prop is false', () => {
expect(getShallowWrapper(resource.tag, false, null).find('Label').length).toBe(1);
expect(getShallowWrapper(resource.tag, false, null).find('Label').exists('Glyphicon')).toEqual(false);
})
it('renders a label with no deletebutton when showDeleteButton prop is true', () => {
expect(getShallowWrapper(resource.tag, true, null).find('Label').length).toBe(2);
expect(getShallowWrapper(resource.tag, true, null).find('Label').exists('Glyphicon')).toEqual(true);
})
});
describe('callback', () => {
it('onDeleteButton is called when user clicks on the delete button', () => {
let resource = resources.rendering;
let mockedOnDeleteTagClicked = jest.fn();
let wrapper = getShallowWrapper(resource.tag, true, mockedOnDeleteTagClicked);
expect(mockedOnDeleteTagClicked).toHaveBeenCalledTimes(0);
wrapper.find('Glyphicon').simulate('click');
expect(mockedOnDeleteTagClicked).toHaveBeenCalledTimes(1);
});
})
});
module.exports = {
rendering: {
tag: {
_id: "5ca2095754907c0012c898d5",
name: 'testName',
description: 'testDescription',
color: '#000000',
investigationId: 123456,
createdAt: "2019-04-01T12:51:35.883Z",
updatedAt: "2019-07-02T13:18:25.107Z"
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment