Implemented dependency injection in all tag related components

This commit is contained in:
Alejandro Celaya 2018-12-18 11:28:15 +01:00
parent 79a0a5e4ea
commit 471322f4db
13 changed files with 75 additions and 81 deletions

View file

@ -20,7 +20,6 @@ import SearchBar from '../short-urls/SearchBar';
import { listShortUrls } from '../short-urls/reducers/shortUrlsList'; import { listShortUrls } from '../short-urls/reducers/shortUrlsList';
import ShortUrlsList from '../short-urls/ShortUrlsList'; import ShortUrlsList from '../short-urls/ShortUrlsList';
import { resetShortUrlParams } from '../short-urls/reducers/shortUrlsListParams'; import { resetShortUrlParams } from '../short-urls/reducers/shortUrlsListParams';
import Tag from '../tags/helpers/Tag';
import { ColorGenerator } from '../utils/ColorGenerator'; import { ColorGenerator } from '../utils/ColorGenerator';
import { Storage } from '../utils/Storage'; import { Storage } from '../utils/Storage';
import ShortUrlsRow from '../short-urls/helpers/ShortUrlsRow'; import ShortUrlsRow from '../short-urls/helpers/ShortUrlsRow';
@ -41,6 +40,11 @@ import { deleteShortUrl, resetDeleteShortUrl, shortUrlDeleted } from '../short-u
import EditTagsModal from '../short-urls/helpers/EditTagsModal'; import EditTagsModal from '../short-urls/helpers/EditTagsModal';
import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../short-urls/reducers/shortUrlTags'; import { editShortUrlTags, resetShortUrlsTags, shortUrlTagsEdited } from '../short-urls/reducers/shortUrlTags';
import buildShlinkApiClient from '../api/ShlinkApiClientBuilder'; import buildShlinkApiClient from '../api/ShlinkApiClientBuilder';
import TagCard from '../tags/TagCard';
import DeleteTagConfirmModal from '../tags/helpers/DeleteTagConfirmModal';
import { deleteTag, tagDeleted } from '../tags/reducers/tagDelete';
import EditTagModal from '../tags/helpers/EditTagModal';
import { editTag, tagEdited } from '../tags/reducers/tagEdit';
const bottle = new Bottle(); const bottle = new Bottle();
const { container } = bottle; const { container } = bottle;
@ -78,7 +82,7 @@ bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateSer
bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter'); bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter');
bottle.decorator('ServersDropdown', connectDecorator([ 'servers', 'selectedServer' ], { listServers, selectServer })); bottle.decorator('ServersDropdown', connectDecorator([ 'servers', 'selectedServer' ], { listServers, selectServer }));
bottle.serviceFactory('TagsList', () => TagsList); bottle.serviceFactory('TagsList', TagsList, 'TagCard');
bottle.decorator('TagsList', connectDecorator([ 'tagsList' ], { forceListTags, filterTags })); bottle.decorator('TagsList', connectDecorator([ 'tagsList' ], { forceListTags, filterTags }));
bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList'); bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList');
@ -86,7 +90,7 @@ bottle.decorator('ShortUrls', connect(
(state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList) (state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList)
)); ));
bottle.serviceFactory('SearchBar', SearchBar, 'Tag'); bottle.serviceFactory('SearchBar', SearchBar, 'ColorGenerator');
bottle.decorator('SearchBar', connectDecorator([ 'shortUrlsListParams' ], { listShortUrls })); bottle.decorator('SearchBar', connectDecorator([ 'shortUrlsListParams' ], { listShortUrls }));
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow'); bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow');
@ -95,13 +99,11 @@ bottle.decorator('ShortUrlsList', connectDecorator(
{ listShortUrls, resetShortUrlParams } { listShortUrls, resetShortUrlParams }
)); ));
bottle.serviceFactory('Tag', Tag, 'ColorGenerator');
bottle.constant('localStorage', global.localStorage); bottle.constant('localStorage', global.localStorage);
bottle.service('Storage', Storage, 'localStorage'); bottle.service('Storage', Storage, 'localStorage');
bottle.service('ColorGenerator', ColorGenerator, 'Storage'); bottle.service('ColorGenerator', ColorGenerator, 'Storage');
bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'Tag', 'ShortUrlsRowMenu'); bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'ShortUrlsRowMenu', 'ColorGenerator');
bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal'); bottle.serviceFactory('ShortUrlsRowMenu', ShortUrlsRowMenu, 'DeleteShortUrlModal', 'EditTagsModal');
@ -151,4 +153,12 @@ bottle.serviceFactory('shortUrlTagsEdited', () => shortUrlTagsEdited);
bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios'); bottle.serviceFactory('buildShlinkApiClient', buildShlinkApiClient, 'axios');
bottle.serviceFactory('TagCard', TagCard, 'DeleteTagConfirmModal', 'EditTagModal', 'ColorGenerator');
bottle.serviceFactory('DeleteTagConfirmModal', () => DeleteTagConfirmModal);
bottle.decorator('DeleteTagConfirmModal', connectDecorator([ 'tagDelete' ], { deleteTag, tagDeleted }));
bottle.serviceFactory('EditTagModal', EditTagModal, 'ColorGenerator');
bottle.decorator('EditTagModal', connectDecorator([ 'tagEdit' ], { editTag, tagEdited }));
export default container; export default container;

View file

@ -4,6 +4,7 @@ import React from 'react';
import { isEmpty } from 'ramda'; import { isEmpty } from 'ramda';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import SearchField from '../utils/SearchField'; import SearchField from '../utils/SearchField';
import Tag from '../tags/helpers/Tag';
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams'; import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
import './SearchBar.scss'; import './SearchBar.scss';
@ -12,7 +13,7 @@ const propTypes = {
shortUrlsListParams: shortUrlsListParamsType, shortUrlsListParams: shortUrlsListParamsType,
}; };
const SearchBar = (Tag) => { const SearchBar = (colorGenerator) => {
const SearchBar = ({ listShortUrls, shortUrlsListParams }) => { const SearchBar = ({ listShortUrls, shortUrlsListParams }) => {
const selectedTags = shortUrlsListParams.tags || []; const selectedTags = shortUrlsListParams.tags || [];
@ -29,6 +30,7 @@ const SearchBar = (Tag) => {
   
{selectedTags.map((tag) => ( {selectedTags.map((tag) => (
<Tag <Tag
colorGenerator={colorGenerator}
key={tag} key={tag}
text={tag} text={tag}
clearable clearable

View file

@ -7,9 +7,10 @@ import { serverType } from '../../servers/prop-types';
import ExternalLink from '../../utils/ExternalLink'; import ExternalLink from '../../utils/ExternalLink';
import { shortUrlType } from '../reducers/shortUrlsList'; import { shortUrlType } from '../reducers/shortUrlsList';
import { stateFlagTimeout } from '../../utils/utils'; import { stateFlagTimeout } from '../../utils/utils';
import Tag from '../../tags/helpers/Tag';
import './ShortUrlsRow.scss'; import './ShortUrlsRow.scss';
const ShortUrlsRow = (Tag, ShortUrlsRowMenu) => class ShortUrlsRow extends React.Component { const ShortUrlsRow = (ShortUrlsRowMenu, colorGenerator) => class ShortUrlsRow extends React.Component {
static propTypes = { static propTypes = {
refreshList: PropTypes.func, refreshList: PropTypes.func,
shortUrlsListParams: shortUrlsListParamsType, shortUrlsListParams: shortUrlsListParamsType,
@ -29,6 +30,7 @@ const ShortUrlsRow = (Tag, ShortUrlsRowMenu) => class ShortUrlsRow extends React
return tags.map((tag) => ( return tags.map((tag) => (
<Tag <Tag
colorGenerator={colorGenerator}
key={tag} key={tag}
text={tag} text={tag}
onClick={() => refreshList({ tags: [ ...selectedTags, tag ] })} onClick={() => refreshList({ tags: [ ...selectedTags, tag ] })}

View file

@ -7,10 +7,8 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import TagBullet from './helpers/TagBullet'; import TagBullet from './helpers/TagBullet';
import './TagCard.scss'; import './TagCard.scss';
import DeleteTagConfirmModal from './helpers/DeleteTagConfirmModal';
import EditTagModal from './helpers/EditTagModal';
export default class TagCard extends React.Component { const TagCard = (DeleteTagConfirmModal, EditTagModal, colorGenerator) => class TagCard extends React.Component {
static propTypes = { static propTypes = {
tag: PropTypes.string, tag: PropTypes.string,
currentServerId: PropTypes.string, currentServerId: PropTypes.string,
@ -35,7 +33,7 @@ export default class TagCard extends React.Component {
<FontAwesomeIcon icon={editIcon} /> <FontAwesomeIcon icon={editIcon} />
</button> </button>
<h5 className="tag-card__tag-title"> <h5 className="tag-card__tag-title">
<TagBullet tag={tag} /> <TagBullet tag={tag} colorGenerator={colorGenerator} />
<Link to={`/server/${currentServerId}/list-short-urls/1?tag=${tag}`}>{tag}</Link> <Link to={`/server/${currentServerId}/list-short-urls/1?tag=${tag}`}>{tag}</Link>
</h5> </h5>
</CardBody> </CardBody>
@ -45,4 +43,6 @@ export default class TagCard extends React.Component {
</Card> </Card>
); );
} }
} };
export default TagCard;

View file

@ -3,12 +3,11 @@ import { splitEvery } from 'ramda';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import MuttedMessage from '../utils/MuttedMessage'; import MuttedMessage from '../utils/MuttedMessage';
import SearchField from '../utils/SearchField'; import SearchField from '../utils/SearchField';
import TagCard from './TagCard';
const { ceil } = Math; const { ceil } = Math;
const TAGS_GROUPS_AMOUNT = 4; const TAGS_GROUPS_AMOUNT = 4;
export default class TagsList extends React.Component { const TagsList = (TagCard) => class TagsList extends React.Component {
static propTypes = { static propTypes = {
filterTags: PropTypes.func, filterTags: PropTypes.func,
forceListTags: PropTypes.func, forceListTags: PropTypes.func,
@ -80,4 +79,6 @@ export default class TagsList extends React.Component {
</div> </div>
); );
} }
} };
export default TagsList;

View file

@ -1,11 +1,9 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux';
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'; import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { pick } from 'ramda'; import { tagDeleteType } from '../reducers/tagDelete';
import { deleteTag, tagDeleted, tagDeleteType } from '../reducers/tagDelete';
export class DeleteTagConfirmModalComponent extends React.Component { export default class DeleteTagConfirmModal extends React.Component {
static propTypes = { static propTypes = {
tag: PropTypes.string.isRequired, tag: PropTypes.string.isRequired,
toggle: PropTypes.func.isRequired, toggle: PropTypes.func.isRequired,
@ -67,10 +65,3 @@ export class DeleteTagConfirmModalComponent extends React.Component {
); );
} }
} }
const DeleteTagConfirmModal = connect(
pick([ 'tagDelete' ]),
{ deleteTag, tagDeleted }
)(DeleteTagConfirmModalComponent);
export default DeleteTagConfirmModal;

View file

@ -1,31 +1,23 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux';
import { Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap'; import { Modal, ModalBody, ModalFooter, ModalHeader, Popover } from 'reactstrap';
import { pick } from 'ramda';
import { ChromePicker } from 'react-color'; import { ChromePicker } from 'react-color';
import colorIcon from '@fortawesome/fontawesome-free-solid/faPalette'; import colorIcon from '@fortawesome/fontawesome-free-solid/faPalette';
import FontAwesomeIcon from '@fortawesome/react-fontawesome'; import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator';
import { editTag, tagEdited } from '../reducers/tagEdit';
import './EditTagModal.scss'; import './EditTagModal.scss';
export class EditTagModalComponent extends React.Component { const EditTagModal = ({ getColorForKey }) => class EditTagModal extends React.Component {
static propTypes = { static propTypes = {
tag: PropTypes.string, tag: PropTypes.string,
editTag: PropTypes.func, editTag: PropTypes.func,
toggle: PropTypes.func, toggle: PropTypes.func,
tagEdited: PropTypes.func, tagEdited: PropTypes.func,
colorGenerator: colorGeneratorType,
isOpen: PropTypes.bool, isOpen: PropTypes.bool,
tagEdit: PropTypes.shape({ tagEdit: PropTypes.shape({
error: PropTypes.bool, error: PropTypes.bool,
editing: PropTypes.bool, editing: PropTypes.bool,
}), }),
}; };
static defaultProps = {
colorGenerator,
};
saveTag = (e) => { saveTag = (e) => {
e.preventDefault(); e.preventDefault();
@ -53,12 +45,12 @@ export class EditTagModalComponent extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const { colorGenerator, tag } = props; const { tag } = props;
this.state = { this.state = {
showColorPicker: false, showColorPicker: false,
tag, tag,
color: colorGenerator.getColorForKey(tag), color: getColorForKey(tag),
}; };
} }
@ -131,8 +123,6 @@ export class EditTagModalComponent extends React.Component {
</Modal> </Modal>
); );
} }
} };
const EditTagModal = connect(pick([ 'tagEdit' ]), { editTag, tagEdited })(EditTagModalComponent);
export default EditTagModal; export default EditTagModal;

View file

@ -1,36 +1,35 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import './Tag.scss'; import './Tag.scss';
import { colorGeneratorType } from '../../utils/ColorGenerator';
const propTypes = { const propTypes = {
text: PropTypes.string, text: PropTypes.string,
children: PropTypes.node, children: PropTypes.node,
clearable: PropTypes.bool, clearable: PropTypes.bool,
colorGenerator: colorGeneratorType,
onClick: PropTypes.func, onClick: PropTypes.func,
onClose: PropTypes.func, onClose: PropTypes.func,
}; };
const Tag = (colorGenerator) => { const Tag = ({
const Tag = ({ text,
text, children,
children, clearable,
clearable, colorGenerator,
onClick = () => {}, onClick = () => {},
onClose = () => {}, onClose = () => {},
}) => ( }) => (
<span <span
className="badge tag" className="badge tag"
style={{ backgroundColor: colorGenerator.getColorForKey(text), cursor: clearable ? 'auto' : 'pointer' }} style={{ backgroundColor: colorGenerator.getColorForKey(text), cursor: clearable ? 'auto' : 'pointer' }}
onClick={onClick} onClick={onClick}
> >
{children || text} {children || text}
{clearable && <span className="close tag__close-selected-tag" onClick={onClose}>&times;</span>} {clearable && <span className="close tag__close-selected-tag" onClick={onClose}>&times;</span>}
</span> </span>
); );
Tag.propTypes = propTypes; Tag.propTypes = propTypes;
return Tag;
};
export default Tag; export default Tag;

View file

@ -1,24 +1,20 @@
import React from 'react'; import React from 'react';
import * as PropTypes from 'prop-types'; import * as PropTypes from 'prop-types';
import colorGenerator, { colorGeneratorType } from '../../utils/ColorGenerator'; import { colorGeneratorType } from '../../utils/ColorGenerator';
import './TagBullet.scss'; import './TagBullet.scss';
const propTypes = { const propTypes = {
tag: PropTypes.string.isRequired, tag: PropTypes.string.isRequired,
colorGenerator: colorGeneratorType, colorGenerator: colorGeneratorType,
}; };
const defaultProps = {
colorGenerator,
};
export default function TagBullet({ tag, colorGenerator }) { const TagBullet = ({ tag, colorGenerator }) => (
return ( <div
<div style={{ backgroundColor: colorGenerator.getColorForKey(tag) }}
style={{ backgroundColor: colorGenerator.getColorForKey(tag) }} className="tag-bullet"
className="tag-bullet" />
/> );
);
}
TagBullet.propTypes = propTypes; TagBullet.propTypes = propTypes;
TagBullet.defaultProps = defaultProps;
export default TagBullet;

View file

@ -54,7 +54,7 @@ const TagsSelector = (colorGenerator) => class TagsSelector extends React.Compon
getSuggestionValue={(suggestion) => suggestion} getSuggestionValue={(suggestion) => suggestion}
renderSuggestion={(suggestion) => ( renderSuggestion={(suggestion) => (
<React.Fragment> <React.Fragment>
<TagBullet tag={suggestion} /> <TagBullet tag={suggestion} colorGenerator={colorGenerator} />
{suggestion} {suggestion}
</React.Fragment> </React.Fragment>
)} )}

View file

@ -3,12 +3,12 @@ import { shallow } from 'enzyme';
import sinon from 'sinon'; import sinon from 'sinon';
import searchBarCreator from '../../src/short-urls/SearchBar'; import searchBarCreator from '../../src/short-urls/SearchBar';
import SearchField from '../../src/utils/SearchField'; import SearchField from '../../src/utils/SearchField';
import Tag from '../../src/tags/helpers/Tag';
describe('<SearchBar />', () => { describe('<SearchBar />', () => {
let wrapper; let wrapper;
const listShortUrlsMock = sinon.spy(); const listShortUrlsMock = sinon.spy();
const Tag = () => ''; const SearchBar = searchBarCreator({});
const SearchBar = searchBarCreator(Tag);
afterEach(() => { afterEach(() => {
listShortUrlsMock.resetHistory(); listShortUrlsMock.resetHistory();

View file

@ -1,13 +1,15 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import TagCard from '../../src/tags/TagCard'; import createTagCard from '../../src/tags/TagCard';
import TagBullet from '../../src/tags/helpers/TagBullet'; import TagBullet from '../../src/tags/helpers/TagBullet';
describe('<TagCard />', () => { describe('<TagCard />', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
const TagCard = createTagCard(() => '', () => '', {});
wrapper = shallow(<TagCard tag="ssr" currentServerId="1" />); wrapper = shallow(<TagCard tag="ssr" currentServerId="1" />);
}); });
afterEach(() => wrapper.unmount()); afterEach(() => wrapper.unmount());

View file

@ -2,16 +2,17 @@ import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { identity, range } from 'ramda'; import { identity, range } from 'ramda';
import * as sinon from 'sinon'; import * as sinon from 'sinon';
import TagsList from '../../src/tags/TagsList'; import createTagsList from '../../src/tags/TagsList';
import MuttedMessage from '../../src/utils/MuttedMessage'; import MuttedMessage from '../../src/utils/MuttedMessage';
import TagCard from '../../src/tags/TagCard';
import SearchField from '../../src/utils/SearchField'; import SearchField from '../../src/utils/SearchField';
describe('<TagsList />', () => { describe('<TagsList />', () => {
let wrapper; let wrapper;
const filterTags = sinon.spy(); const filterTags = sinon.spy();
const TagCard = () => '';
const createWrapper = (tagsList) => { const createWrapper = (tagsList) => {
const params = { serverId: '1' }; const params = { serverId: '1' };
const TagsList = createTagsList(TagCard);
wrapper = shallow( wrapper = shallow(
<TagsList forceListTags={identity} filterTags={filterTags} match={{ params }} tagsList={tagsList} /> <TagsList forceListTags={identity} filterTags={filterTags} match={{ params }} tagsList={tagsList} />