Migrated a lot more components to new DI system

This commit is contained in:
Alejandro Celaya 2018-12-17 22:18:47 +01:00
parent 5e6ad14a85
commit 5616d045ab
18 changed files with 237 additions and 213 deletions

View file

@ -6,9 +6,8 @@ import React from 'react';
import { NavLink } from 'react-router-dom';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import DeleteServerButton from '../servers/DeleteServerButton';
import './AsideMenu.scss';
import { serverType } from '../servers/prop-types';
import './AsideMenu.scss';
const defaultProps = {
className: '',
@ -20,7 +19,8 @@ const propTypes = {
showOnMobile: PropTypes.bool,
};
export default function AsideMenu({ selectedServer, className, showOnMobile }) {
const AsideMenu = (DeleteServerButton) => {
const AsideMenu = ({ selectedServer, className, showOnMobile }) => {
const serverId = selectedServer ? selectedServer.id : '';
const asideClass = classnames('aside-menu', className, {
'aside-menu--hidden': !showOnMobile,
@ -64,7 +64,12 @@ export default function AsideMenu({ selectedServer, className, showOnMobile }) {
</nav>
</aside>
);
}
};
AsideMenu.defaultProps = defaultProps;
AsideMenu.propTypes = propTypes;
AsideMenu.defaultProps = defaultProps;
AsideMenu.propTypes = propTypes;
return AsideMenu;
};
export default AsideMenu;

View file

@ -6,11 +6,10 @@ import { Link } from 'react-router-dom';
import { Collapse, Nav, Navbar, NavbarBrand, NavbarToggler, NavItem, NavLink } from 'reactstrap';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import ServersDropdown from '../servers/ServersDropdown';
import './MainHeader.scss';
import shlinkLogo from './shlink-logo-white.png';
import './MainHeader.scss';
const MainHeader = () => class MainHeaderComponent extends React.Component {
const MainHeader = (ServersDropdown) => class MainHeader extends React.Component {
static propTypes = {
location: PropTypes.object,
};

View file

@ -7,13 +7,10 @@ import classnames from 'classnames';
import * as PropTypes from 'prop-types';
import ShortUrlsVisits from '../visits/ShortUrlVisits';
import CreateShortUrl from '../short-urls/CreateShortUrl';
import ShortUrls from '../short-urls/ShortUrls';
import './MenuLayout.scss';
import TagsList from '../tags/TagsList';
import { serverType } from '../servers/prop-types';
import AsideMenu from './AsideMenu';
export default class MenuLayout extends React.Component {
const MenuLayout = (TagsList, ShortUrls, AsideMenu) => class MenuLayout extends React.Component {
static propTypes = {
match: PropTypes.object,
selectServer: PropTypes.func,
@ -99,4 +96,6 @@ export default class MenuLayout extends React.Component {
</React.Fragment>
);
}
}
};
export default MenuLayout;

View file

@ -2,37 +2,110 @@ import Bottle from 'bottlejs';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { pick } from 'ramda';
import { assoc, pick } from 'ramda';
import csvjson from 'csvjson';
import axios from 'axios';
import App from '../App';
import ScrollToTop from '../common/ScrollToTop';
import MainHeader from '../common/MainHeader';
import { resetSelectedServer, selectServer } from '../servers/reducers/selectedServer';
import Home from '../common/Home';
import MenuLayout from '../common/MenuLayout';
import { createServer } from '../servers/reducers/server';
import { createServer, createServers, deleteServer, listServers } from '../servers/reducers/server';
import CreateServer from '../servers/CreateServer';
import store from './store';
import ServersDropdown from '../servers/ServersDropdown';
import TagsList from '../tags/TagsList';
import { filterTags, forceListTags } from '../tags/reducers/tagsList';
import ShortUrls from '../short-urls/ShortUrls';
import SearchBar from '../short-urls/SearchBar';
import { listShortUrls } from '../short-urls/reducers/shortUrlsList';
import ShortUrlsList from '../short-urls/ShortUrlsList';
import { resetShortUrlParams } from '../short-urls/reducers/shortUrlsListParams';
import Tag from '../tags/helpers/Tag';
import { ColorGenerator } from '../utils/ColorGenerator';
import { Storage } from '../utils/Storage';
import ShortUrlsRow from '../short-urls/helpers/ShortUrlsRow';
import ShortUrlsRowMenu from '../short-urls/helpers/ShortUrlsRowMenu';
import { ShlinkApiClient } from '../api/ShlinkApiClient';
import DeleteServerModal from '../servers/DeleteServerModal';
import DeleteServerButton from '../servers/DeleteServerButton';
import AsideMenu from '../common/AsideMenu';
import ImportServersBtn from '../servers/helpers/ImportServersBtn';
import { ServersImporter } from '../servers/services/ServersImporter';
import { ServersExporter } from '../servers/services/ServersExporter';
import { ServersService } from '../servers/services/ServersService';
const bottle = new Bottle();
bottle.constant('store', store);
bottle.serviceFactory('ScrollToTop', () => withRouter(ScrollToTop));
bottle.serviceFactory('MainHeader', () => withRouter(MainHeader()));
bottle.serviceFactory('Home', () => connect(pick([ 'servers' ]), { resetSelectedServer })(Home));
bottle.serviceFactory(
bottle.constant('ScrollToTop', ScrollToTop);
bottle.decorator('ScrollToTop', withRouter);
bottle.serviceFactory('MainHeader', MainHeader, 'ServersDropdown');
bottle.decorator('MainHeader', withRouter);
bottle.serviceFactory('Home', () => Home);
bottle.decorator('Home', connect(pick([ 'servers' ]), { resetSelectedServer }));
bottle.serviceFactory('MenuLayout', MenuLayout, 'TagsList', 'ShortUrls', 'AsideMenu');
bottle.decorator(
'MenuLayout',
() => compose(
compose(
connect(pick([ 'selectedServer', 'shortUrlsListParams' ]), { selectServer }),
withRouter
)(MenuLayout)
);
bottle.serviceFactory(
'CreateServer',
() => connect(
pick([ 'selectedServer' ]),
{ createServer, resetSelectedServer }
)(CreateServer)
)
);
bottle.serviceFactory('CreateServer', CreateServer, 'ImportServersBtn');
bottle.decorator('CreateServer', connect(pick([ 'selectedServer' ]), { createServer, resetSelectedServer }));
bottle.serviceFactory('App', App, 'MainHeader', 'Home', 'MenuLayout', 'CreateServer');
bottle.serviceFactory('ServersDropdown', ServersDropdown, 'ServersExporter');
bottle.decorator('ServersDropdown', connect(pick([ 'servers', 'selectedServer' ]), { listServers, selectServer }));
bottle.serviceFactory('TagsList', () => TagsList);
bottle.decorator('TagsList', connect(pick([ 'tagsList' ]), { forceListTags, filterTags }));
bottle.serviceFactory('ShortUrls', ShortUrls, 'SearchBar', 'ShortUrlsList');
bottle.decorator('ShortUrls', connect(
(state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList)
));
bottle.serviceFactory('SearchBar', SearchBar, 'Tag');
bottle.decorator('SearchBar', connect(pick([ 'shortUrlsListParams' ]), { listShortUrls }));
bottle.serviceFactory('ShortUrlsList', ShortUrlsList, 'ShortUrlsRow');
bottle.decorator('ShortUrlsList', connect(
pick([ 'selectedServer', 'shortUrlsListParams' ]),
{ listShortUrls, resetShortUrlParams }
));
bottle.serviceFactory('Tag', Tag, 'ColorGenerator');
bottle.constant('localStorage', global.localStorage);
bottle.service('Storage', Storage, 'localStorage');
bottle.service('ColorGenerator', ColorGenerator, 'Storage');
bottle.serviceFactory('ShortUrlsRow', ShortUrlsRow, 'Tag', 'ShortUrlsRowMenu');
bottle.serviceFactory('ShortUrlsRowMenu', () => ShortUrlsRowMenu);
bottle.constant('axios', axios);
bottle.service('ShlinkApiClient', ShlinkApiClient, 'axios');
bottle.serviceFactory('DeleteServerModal', () => DeleteServerModal);
bottle.decorator('DeleteServerModal', compose(withRouter, connect(null, { deleteServer })));
bottle.serviceFactory('DeleteServerButton', DeleteServerButton, 'DeleteServerModal');
bottle.serviceFactory('AsideMenu', AsideMenu, 'DeleteServerButton');
bottle.serviceFactory('ImportServersBtn', ImportServersBtn, 'ServersImporter');
bottle.decorator('ImportServersBtn', connect(null, { createServers }));
bottle.constant('csvjson', csvjson);
bottle.constant('window', global.window);
bottle.service('ServersImporter', ServersImporter, 'csvjson');
bottle.service('ServersService', ServersService, 'Storage');
bottle.service('ServersExporter', ServersExporter, 'ServersService', 'window', 'csvjson');
export default bottle.container;

View file

@ -6,11 +6,12 @@ import { BrowserRouter } from 'react-router-dom';
import { homepage } from '../package.json';
import registerServiceWorker from './registerServiceWorker';
import container from './container';
import store from './container/store';
import '../node_modules/react-datepicker/dist/react-datepicker.css';
import './common/react-tagsinput.scss';
import './index.scss';
const { App, ScrollToTop, store } = container;
const { App, ScrollToTop } = container;
render(
<Provider store={store}>

View file

@ -4,11 +4,10 @@ import { v4 as uuid } from 'uuid';
import PropTypes from 'prop-types';
import { stateFlagTimeout } from '../utils/utils';
import './CreateServer.scss';
import ImportServersBtn from './helpers/ImportServersBtn';
const SHOW_IMPORT_MSG_TIME = 4000;
export default class CreateServer extends React.Component {
const CreateServer = (ImportServersBtn) => class CreateServer extends React.Component {
static propTypes = {
createServer: PropTypes.func,
history: PropTypes.shape({
@ -88,4 +87,6 @@ export default class CreateServer extends React.Component {
</div>
);
}
}
};
export default CreateServer;

View file

@ -2,10 +2,9 @@ import deleteIcon from '@fortawesome/fontawesome-free-solid/faMinusCircle';
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import React from 'react';
import PropTypes from 'prop-types';
import DeleteServerModal from './DeleteServerModal';
import { serverType } from './prop-types';
export default class DeleteServerButton extends React.Component {
const DeleteServerButton = (DeleteServerModal) => class DeleteServerButton extends React.Component {
static propTypes = {
server: serverType,
className: PropTypes.string,
@ -36,4 +35,6 @@ export default class DeleteServerButton extends React.Component {
</React.Fragment>
);
}
}
};
export default DeleteServerButton;

View file

@ -1,10 +1,6 @@
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { compose } from 'redux';
import { deleteServer } from './reducers/server';
import { serverType } from './prop-types';
const propTypes = {
@ -17,7 +13,7 @@ const propTypes = {
}),
};
export const DeleteServerModalComponent = ({ server, toggle, isOpen, deleteServer, history }) => {
const DeleteServerModal = ({ server, toggle, isOpen, deleteServer, history }) => {
const closeModal = () => {
deleteServer(server);
toggle();
@ -42,11 +38,6 @@ export const DeleteServerModalComponent = ({ server, toggle, isOpen, deleteServe
);
};
DeleteServerModalComponent.propTypes = propTypes;
const DeleteServerModal = compose(
withRouter,
connect(null, { deleteServer })
)(DeleteServerModalComponent);
DeleteServerModal.propTypes = propTypes;
export default DeleteServerModal;

View file

@ -1,30 +1,20 @@
import { isEmpty, pick, values } from 'ramda';
import { isEmpty, values } from 'ramda';
import React from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap';
import PropTypes from 'prop-types';
import { selectServer } from '../servers/reducers/selectedServer';
import serversExporter from '../servers/services/ServersExporter';
import { listServers } from './reducers/server';
import { serverType } from './prop-types';
export class ServersDropdownComponent extends React.Component {
static defaultProps = {
serversExporter,
};
const ServersDropdown = (serversExporter) => class ServersDropdown extends React.Component {
static propTypes = {
servers: PropTypes.object,
serversExporter: PropTypes.shape({
exportServers: PropTypes.func,
}),
selectedServer: serverType,
selectServer: PropTypes.func,
listServers: PropTypes.func,
};
renderServers = () => {
const { servers, selectedServer, selectServer, serversExporter } = this.props;
const { servers, selectedServer, selectServer } = this.props;
if (isEmpty(servers)) {
return <DropdownItem disabled><i>Add a server first...</i></DropdownItem>;
@ -68,11 +58,6 @@ export class ServersDropdownComponent extends React.Component {
</UncontrolledDropdown>
);
}
}
const ServersDropdown = connect(
pick([ 'servers', 'selectedServer' ]),
{ listServers, selectServer }
)(ServersDropdownComponent);
};
export default ServersDropdown;

View file

@ -1,20 +1,15 @@
import React from 'react';
import { connect } from 'react-redux';
import { UncontrolledTooltip } from 'reactstrap';
import { assoc } from 'ramda';
import { v4 as uuid } from 'uuid';
import PropTypes from 'prop-types';
import { createServers } from '../reducers/server';
import serversImporter, { serversImporterType } from '../services/ServersImporter';
export class ImportServersBtnComponent extends React.Component {
const ImportServersBtn = (serversImporter) => class ImportServersBtn extends React.Component {
static defaultProps = {
serversImporter,
onImport: () => ({}),
};
static propTypes = {
onImport: PropTypes.func,
serversImporter: serversImporterType,
createServers: PropTypes.func,
fileRef: PropTypes.oneOfType([ PropTypes.object, PropTypes.node ]),
};
@ -25,7 +20,8 @@ export class ImportServersBtnComponent extends React.Component {
}
render() {
const { serversImporter: { importServersFromFile }, onImport, createServers } = this.props;
const { importServersFromFile } = serversImporter;
const { onImport, createServers } = this.props;
const onChange = (e) =>
importServersFromFile(e.target.files[0])
.then((servers) => servers.map((server) => assoc('id', uuid(), server)))
@ -56,8 +52,6 @@ export class ImportServersBtnComponent extends React.Component {
</React.Fragment>
);
}
}
const ImportServersBtn = connect(null, { createServers })(ImportServersBtnComponent);
};
export default ImportServersBtn;

View file

@ -1,21 +1,19 @@
import tagsIcon from '@fortawesome/fontawesome-free-solid/faTags';
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import React from 'react';
import { connect } from 'react-redux';
import { isEmpty, pick } from 'ramda';
import { isEmpty } from 'ramda';
import PropTypes from 'prop-types';
import Tag from '../tags/helpers/Tag';
import SearchField from '../utils/SearchField';
import { listShortUrls } from './reducers/shortUrlsList';
import './SearchBar.scss';
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
import './SearchBar.scss';
const propTypes = {
listShortUrls: PropTypes.func,
shortUrlsListParams: shortUrlsListParamsType,
};
export function SearchBarComponent({ listShortUrls, shortUrlsListParams }) {
const SearchBar = (Tag) => {
const SearchBar = ({ listShortUrls, shortUrlsListParams }) => {
const selectedTags = shortUrlsListParams.tags || [];
return (
@ -46,10 +44,11 @@ export function SearchBarComponent({ listShortUrls, shortUrlsListParams }) {
)}
</div>
);
}
};
SearchBarComponent.propTypes = propTypes;
SearchBar.propTypes = propTypes;
const SearchBar = connect(pick([ 'shortUrlsListParams' ]), { listShortUrls })(SearchBarComponent);
return SearchBar;
};
export default SearchBar;

View file

@ -1,27 +1,21 @@
import React from 'react';
import { connect } from 'react-redux';
import { assoc } from 'ramda';
import Paginator from './Paginator';
import SearchBar from './SearchBar';
import ShortUrlsList from './ShortUrlsList';
export function ShortUrlsComponent(props) {
const { match: { params } } = props;
const ShortUrls = (SearchBar, ShortUrlsList) => (props) => {
const { match: { params }, shortUrlsList } = props;
const { page, serverId } = params;
const { data = [], pagination } = shortUrlsList;
// Using a key on a component makes react to create a new instance every time the key changes
const urlsListKey = `${params.serverId}_${params.page}`;
const urlsListKey = `${serverId}_${page}`;
return (
<div className="shlink-container">
<div className="form-group"><SearchBar /></div>
<ShortUrlsList {...props} shortUrlsList={props.shortUrlsList.data || []} key={urlsListKey} />
<Paginator paginator={props.shortUrlsList.pagination} serverId={props.match.params.serverId} />
<ShortUrlsList {...props} shortUrlsList={data} key={urlsListKey} />
<Paginator paginator={pagination} serverId={serverId} />
</div>
);
}
const ShortUrls = connect(
(state) => assoc('shortUrlsList', state.shortUrlsList.shortUrls, state.shortUrlsList)
)(ShortUrlsComponent);
};
export default ShortUrls;

View file

@ -1,17 +1,15 @@
import caretDownIcon from '@fortawesome/fontawesome-free-solid/faCaretDown';
import caretUpIcon from '@fortawesome/fontawesome-free-solid/faCaretUp';
import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import { head, isEmpty, keys, pick, values } from 'ramda';
import { head, isEmpty, keys, values } from 'ramda';
import React from 'react';
import { connect } from 'react-redux';
import qs from 'qs';
import PropTypes from 'prop-types';
import { serverType } from '../servers/prop-types';
import SortingDropdown from '../utils/SortingDropdown';
import { determineOrderDir } from '../utils/utils';
import { ShortUrlsRow } from './helpers/ShortUrlsRow';
import { listShortUrls, shortUrlType } from './reducers/shortUrlsList';
import { resetShortUrlParams, shortUrlsListParamsType } from './reducers/shortUrlsListParams';
import { shortUrlType } from './reducers/shortUrlsList';
import { shortUrlsListParamsType } from './reducers/shortUrlsListParams';
import './ShortUrlsList.scss';
const SORTABLE_FIELDS = {
@ -21,7 +19,7 @@ const SORTABLE_FIELDS = {
visits: 'Visits',
};
export class ShortUrlsListComponent extends React.Component {
const ShortUrlsList = (ShortUrlsRow) => class ShortUrlsList extends React.Component {
static propTypes = {
listShortUrls: PropTypes.func,
resetShortUrlParams: PropTypes.func,
@ -167,11 +165,6 @@ export class ShortUrlsListComponent extends React.Component {
</React.Fragment>
);
}
}
const ShortUrlsList = connect(
pick([ 'selectedServer', 'shortUrlsListParams' ]),
{ listShortUrls, resetShortUrlParams }
)(ShortUrlsListComponent);
};
export default ShortUrlsList;

View file

@ -2,16 +2,14 @@ import { isEmpty } from 'ramda';
import React from 'react';
import Moment from 'react-moment';
import PropTypes from 'prop-types';
import Tag from '../../tags/helpers/Tag';
import { shortUrlsListParamsType } from '../reducers/shortUrlsListParams';
import { serverType } from '../../servers/prop-types';
import ExternalLink from '../../utils/ExternalLink';
import { shortUrlType } from '../reducers/shortUrlsList';
import { stateFlagTimeout } from '../../utils/utils';
import { ShortUrlsRowMenu } from './ShortUrlsRowMenu';
import './ShortUrlsRow.scss';
export class ShortUrlsRow extends React.Component {
const ShortUrlsRow = (Tag, ShortUrlsRowMenu) => class ShortUrlsRow extends React.Component {
static propTypes = {
refreshList: PropTypes.func,
shortUrlsListParams: shortUrlsListParamsType,
@ -72,4 +70,6 @@ export class ShortUrlsRow extends React.Component {
</tr>
);
}
}
};
export default ShortUrlsRow;

View file

@ -19,7 +19,7 @@ import EditTagsModal from './EditTagsModal';
import DeleteShortUrlModal from './DeleteShortUrlModal';
import './ShortUrlsRowMenu.scss';
export class ShortUrlsRowMenu extends React.Component {
export default class ShortUrlsRowMenu extends React.Component {
static propTypes = {
completeShortUrl: PropTypes.string,
onCopyToClipboard: PropTypes.func,

View file

@ -1,16 +1,14 @@
import React from 'react';
import { connect } from 'react-redux';
import { pick, splitEvery } from 'ramda';
import { splitEvery } from 'ramda';
import PropTypes from 'prop-types';
import MuttedMessage from '../utils/MuttedMessage';
import SearchField from '../utils/SearchField';
import { filterTags, forceListTags } from './reducers/tagsList';
import TagCard from './TagCard';
const { ceil } = Math;
const TAGS_GROUPS_AMOUNT = 4;
export class TagsListComponent extends React.Component {
export default class TagsList extends React.Component {
static propTypes = {
filterTags: PropTypes.func,
forceListTags: PropTypes.func,
@ -83,7 +81,3 @@ export class TagsListComponent extends React.Component {
);
}
}
const TagsList = connect(pick([ 'tagsList' ]), { forceListTags, filterTags })(TagsListComponent);
export default TagsList;

View file

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

View file

@ -2,7 +2,7 @@ import { identity, values } from 'ramda';
import React from 'react';
import { shallow } from 'enzyme';
import { DropdownItem, DropdownToggle } from 'reactstrap';
import { ServersDropdownComponent } from '../../src/servers/ServersDropdown';
import ServersDropdown from '../../src/servers/ServersDropdown';
describe('<ServersDropdown />', () => {
let wrapped;
@ -13,7 +13,7 @@ describe('<ServersDropdown />', () => {
};
beforeEach(() => {
wrapped = shallow(<ServersDropdownComponent servers={servers} listServers={identity} />);
wrapped = shallow(<ServersDropdown servers={servers} listServers={identity} />);
});
afterEach(() => wrapped.unmount());
@ -31,7 +31,7 @@ describe('<ServersDropdown />', () => {
});
it('contains a message when no servers exist yet', () => {
wrapped = shallow(<ServersDropdownComponent servers={{}} listServers={identity} />);
wrapped = shallow(<ServersDropdown servers={{}} listServers={identity} />);
const item = wrapped.find(DropdownItem);
expect(item).toHaveLength(1);