Created common component to handle tags and modal to edit tags

This commit is contained in:
Alejandro Celaya 2018-08-15 11:14:44 +02:00
parent 707c097ed9
commit 854851fefc
9 changed files with 115 additions and 30 deletions

View file

@ -3,7 +3,7 @@
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: .25rem; border-radius: .25rem;
overflow: hidden; overflow: hidden;
min-height: calc(2.6rem + 2px); min-height: 2.6rem;
padding: 6px 0 0 6px; padding: 6px 0 0 6px;
} }
@ -21,16 +21,13 @@
.react-tagsinput-tag { .react-tagsinput-tag {
font-size: 1rem; font-size: 1rem;
background-color: #f1f1f1; background-color: #f1f1f1;
border-radius: 2px; border-radius: 4px;
border: 1px solid #d1d1d1;
display: inline-block; display: inline-block;
font-weight: 400; font-weight: 400;
margin: 0 5px 6px 0; margin: 0 5px 6px 0;
padding: 6px 8px; padding: 6px 8px;
line-height: 1; line-height: 1;
} color: #fff;
.react-tagsinput-tag:hover {
border-color: #b1b1b1;
} }
.react-tagsinput-remove { .react-tagsinput-remove {
@ -41,7 +38,7 @@
.react-tagsinput-tag a::before { .react-tagsinput-tag a::before {
content: "\2715"; content: "\2715";
color: #aaa; color: #fff;
} }
.react-tagsinput-input { .react-tagsinput-input {

View file

@ -4,11 +4,11 @@ import FontAwesomeIcon from '@fortawesome/react-fontawesome';
import { assoc, dissoc, isNil, pick, pipe, replace, trim } from 'ramda'; import { assoc, dissoc, isNil, pick, pipe, replace, trim } from 'ramda';
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import TagsInput from 'react-tagsinput'
import { Collapse } from 'reactstrap'; import { Collapse } from 'reactstrap';
import DateInput from '../common/DateInput'; import DateInput from '../common/DateInput';
import CreateShortUrlResult from './helpers/CreateShortUrlResult'; import CreateShortUrlResult from './helpers/CreateShortUrlResult';
import { createShortUrl, resetCreateShortUrl } from './reducers/shortUrlCreationResult'; import { createShortUrl, resetCreateShortUrl } from './reducers/shortUrlCreationResult';
import TagsSelector from '../utils/TagsSelector';
export class CreateShortUrl extends React.Component { export class CreateShortUrl extends React.Component {
state = { state = {
@ -68,13 +68,7 @@ export class CreateShortUrl extends React.Component {
<Collapse isOpen={this.state.moreOptionsVisible}> <Collapse isOpen={this.state.moreOptionsVisible}>
<div className="form-group"> <div className="form-group">
<TagsInput <TagsSelector tags={this.state.tags} onChange={changeTags} />
value={this.state.tags}
onChange={changeTags}
inputProps={{ placeholder: 'Add tags to the URL' }}
onlyUnique
addOnBlur // FIXME Workaround to be able to add tags on Android
/>
</div> </div>
<div className="row"> <div className="row">

View file

@ -1,7 +1,7 @@
import caretDownIcon from '@fortawesome/fontawesome-free-solid/faCaretDown' import caretDownIcon from '@fortawesome/fontawesome-free-solid/faCaretDown'
import caretUpIcon from '@fortawesome/fontawesome-free-solid/faCaretUp' import caretUpIcon from '@fortawesome/fontawesome-free-solid/faCaretUp'
import FontAwesomeIcon from '@fortawesome/react-fontawesome' import FontAwesomeIcon from '@fortawesome/react-fontawesome'
import { head, isEmpty, pick, toPairs } from 'ramda' import { head, isEmpty, pick, toPairs, keys, values } from 'ramda'
import React from 'react' import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap' import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'
@ -34,7 +34,7 @@ export class ShortUrlsList extends React.Component {
'DESC': undefined, 'DESC': undefined,
}; };
return this.state.orderDir ? newOrderMap[this.state.orderDir] : 'ASC'; return this.state.orderDir ? newOrderMap[this.state.orderDir] : 'ASC';
} };
orderBy = field => { orderBy = field => {
const newOrderDir = this.determineOrderDir(field); const newOrderDir = this.determineOrderDir(field);
this.setState({ orderField: newOrderDir !== undefined ? field : undefined, orderDir: newOrderDir }); this.setState({ orderField: newOrderDir !== undefined ? field : undefined, orderDir: newOrderDir });
@ -58,8 +58,8 @@ export class ShortUrlsList extends React.Component {
const { orderBy } = props.shortUrlsListParams; const { orderBy } = props.shortUrlsListParams;
this.state = { this.state = {
orderField: orderBy ? head(Object.keys(orderBy)) : undefined, orderField: orderBy ? head(keys(orderBy)) : undefined,
orderDir: orderBy ? head(Object.values(orderBy)) : undefined, orderDir: orderBy ? head(values(orderBy)) : undefined,
} }
} }
@ -71,7 +71,11 @@ export class ShortUrlsList extends React.Component {
renderShortUrls() { renderShortUrls() {
const { shortUrlsList, selectedServer, loading, error, shortUrlsListParams } = this.props; const { shortUrlsList, selectedServer, loading, error, shortUrlsListParams } = this.props;
if (error) { if (error) {
return <tr><td colSpan="6" className="text-center table-danger">Something went wrong while loading short URLs :(</td></tr>; return (
<tr>
<td colSpan="6" className="text-center table-danger">Something went wrong while loading short URLs :(</td>
</tr>
);
} }
if (loading) { if (loading) {
@ -102,7 +106,7 @@ export class ShortUrlsList extends React.Component {
</DropdownToggle> </DropdownToggle>
<DropdownMenu className="short-urls-list__order-dropdown"> <DropdownMenu className="short-urls-list__order-dropdown">
{toPairs(SORTABLE_FIELDS).map(([key, value]) => {toPairs(SORTABLE_FIELDS).map(([key, value]) =>
<DropdownItem active={this.state.orderField === key} onClick={() => this.orderBy(key)}> <DropdownItem key={key} active={this.state.orderField === key} onClick={() => this.orderBy(key)}>
{value} {value}
{this.renderOrderIcon(key, 'short-urls-list__header-icon--mobile')} {this.renderOrderIcon(key, 'short-urls-list__header-icon--mobile')}
</DropdownItem>)} </DropdownItem>)}

View file

@ -0,0 +1,27 @@
import React from 'react';
import { Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import TagsSelector from '../../utils/TagsSelector';
export default class EditTagsModal extends React.Component {
constructor(props) {
super(props);
this.state = { tags: props.shortUrl.tags };
}
render() {
const { isOpen, toggle, url } = this.props;
const changeTags = tags => this.setState({ tags });
return (
<Modal isOpen={isOpen} toggle={toggle} centered>
<ModalHeader toggle={toggle}>Edit tags for <a target="_blank" href={url}>{url}</a></ModalHeader>
<ModalBody>
<TagsSelector tags={this.state.tags} onChange={changeTags} />
</ModalBody>
<ModalFooter>
<button className="btn btn-outline-primary" type="button">Save</button>
</ModalFooter>
</Modal>
);
}
}

View file

@ -4,7 +4,7 @@ import './QrCodeModal.scss';
export default function QrCodeModal ({ url, toggle, isOpen }) { export default function QrCodeModal ({ url, toggle, isOpen }) {
return ( return (
<Modal isOpen={isOpen} toggle={toggle} centered={true}> <Modal isOpen={isOpen} toggle={toggle} centered>
<ModalHeader toggle={toggle}>QR code for <a target="_blank" href={url}>{url}</a></ModalHeader> <ModalHeader toggle={toggle}>QR code for <a target="_blank" href={url}>{url}</a></ModalHeader>
<ModalBody> <ModalBody>
<div className="text-center"> <div className="text-center">

View file

@ -45,9 +45,9 @@ export class ShortUrlsRow extends React.Component {
Copied short URL! Copied short URL!
</small> </small>
<ShortUrlsRowMenu <ShortUrlsRowMenu
shortUrl={completeShortUrl} completeShortUrl={completeShortUrl}
selectedServer={selectedServer} selectedServer={selectedServer}
shortCode={shortUrl.shortCode} shortUrl={shortUrl}
onCopyToClipboard={() => { onCopyToClipboard={() => {
this.setState({ copiedToClipboard: true }); this.setState({ copiedToClipboard: true });
setTimeout(() => this.setState({ copiedToClipboard: false }), 2000); setTimeout(() => this.setState({ copiedToClipboard: false }), 2000);

View file

@ -1,5 +1,6 @@
import copyIcon from '@fortawesome/fontawesome-free-regular/faCopy'; import copyIcon from '@fortawesome/fontawesome-free-regular/faCopy';
import pictureIcon from '@fortawesome/fontawesome-free-regular/faImage'; import pictureIcon from '@fortawesome/fontawesome-free-regular/faImage';
import tagsIcon from '@fortawesome/fontawesome-free-solid/faTags';
import pieChartIcon from '@fortawesome/fontawesome-free-solid/faChartPie'; import pieChartIcon from '@fortawesome/fontawesome-free-solid/faChartPie';
import menuIcon from '@fortawesome/fontawesome-free-solid/faEllipsisV'; import menuIcon from '@fortawesome/fontawesome-free-solid/faEllipsisV';
import qrIcon from '@fortawesome/fontawesome-free-solid/faQrcode'; import qrIcon from '@fortawesome/fontawesome-free-solid/faQrcode';
@ -11,16 +12,23 @@ import { ButtonDropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reac
import PreviewModal from './PreviewModal'; import PreviewModal from './PreviewModal';
import QrCodeModal from './QrCodeModal'; import QrCodeModal from './QrCodeModal';
import './ShortUrlsRowMenu.scss'; import './ShortUrlsRowMenu.scss';
import EditTagsModal from './EditTagsModal';
export class ShortUrlsRowMenu extends React.Component { export class ShortUrlsRowMenu extends React.Component {
state = { isOpen: false, isQrModalOpen: false, isPreviewOpen: false }; state = {
isOpen: false,
isQrModalOpen: false,
isPreviewOpen: false,
isTagsModalOpen: false,
};
toggle = () => this.setState({ isOpen: !this.state.isOpen }); toggle = () => this.setState({ isOpen: !this.state.isOpen });
render() { render() {
const { shortUrl, onCopyToClipboard, selectedServer, shortCode } = this.props; const { completeShortUrl, onCopyToClipboard, selectedServer, shortUrl } = this.props;
const serverId = selectedServer ? selectedServer.id : ''; const serverId = selectedServer ? selectedServer.id : '';
const toggleQrCode = () => this.setState({isQrModalOpen: !this.state.isQrModalOpen}); const toggleQrCode = () => this.setState({isQrModalOpen: !this.state.isQrModalOpen});
const togglePreview = () => this.setState({isPreviewOpen: !this.state.isPreviewOpen}); const togglePreview = () => this.setState({isPreviewOpen: !this.state.isPreviewOpen});
const toggleTags = () => this.setState({isTagsModalOpen: !this.state.isTagsModalOpen});
return ( return (
<ButtonDropdown toggle={this.toggle} isOpen={this.state.isOpen} direction="left"> <ButtonDropdown toggle={this.toggle} isOpen={this.state.isOpen} direction="left">
@ -28,9 +36,18 @@ export class ShortUrlsRowMenu extends React.Component {
&nbsp;<FontAwesomeIcon icon={menuIcon}/>&nbsp; &nbsp;<FontAwesomeIcon icon={menuIcon}/>&nbsp;
</DropdownToggle> </DropdownToggle>
<DropdownMenu> <DropdownMenu>
<DropdownItem tag={Link} to={`/server/${serverId}/short-code/${shortCode}/visits`}> <DropdownItem tag={Link} to={`/server/${serverId}/short-code/${shortUrl.shortCode}/visits`}>
<FontAwesomeIcon icon={pieChartIcon}/> &nbsp;Visit Stats <FontAwesomeIcon icon={pieChartIcon}/> &nbsp;Visit Stats
</DropdownItem> </DropdownItem>
<DropdownItem onClick={toggleTags}>
<FontAwesomeIcon icon={tagsIcon}/> &nbsp;Edit tags
</DropdownItem>
<EditTagsModal
url={completeShortUrl}
shortUrl={shortUrl}
isOpen={this.state.isTagsModalOpen}
toggle={toggleTags}
/>
<DropdownItem divider/> <DropdownItem divider/>
@ -38,7 +55,7 @@ export class ShortUrlsRowMenu extends React.Component {
<FontAwesomeIcon icon={pictureIcon}/> &nbsp;Preview <FontAwesomeIcon icon={pictureIcon}/> &nbsp;Preview
</DropdownItem> </DropdownItem>
<PreviewModal <PreviewModal
url={shortUrl} url={completeShortUrl}
isOpen={this.state.isPreviewOpen} isOpen={this.state.isPreviewOpen}
toggle={togglePreview} toggle={togglePreview}
/> />
@ -47,14 +64,14 @@ export class ShortUrlsRowMenu extends React.Component {
<FontAwesomeIcon icon={qrIcon}/> &nbsp;QR code <FontAwesomeIcon icon={qrIcon}/> &nbsp;QR code
</DropdownItem> </DropdownItem>
<QrCodeModal <QrCodeModal
url={shortUrl} url={completeShortUrl}
isOpen={this.state.isQrModalOpen} isOpen={this.state.isQrModalOpen}
toggle={toggleQrCode} toggle={toggleQrCode}
/> />
<DropdownItem divider/> <DropdownItem divider/>
<CopyToClipboard text={shortUrl} onCopy={onCopyToClipboard}> <CopyToClipboard text={completeShortUrl} onCopy={onCopyToClipboard}>
<DropdownItem> <DropdownItem>
<FontAwesomeIcon icon={copyIcon}/> &nbsp;Copy to clipboard <FontAwesomeIcon icon={copyIcon}/> &nbsp;Copy to clipboard
</DropdownItem> </DropdownItem>

View file

@ -1,4 +1,5 @@
import Storage from './Storage'; import Storage from './Storage';
import PropTypes from 'prop-types';
const buildRandomColor = () => { const buildRandomColor = () => {
const letters = '0123456789ABCDEF'; const letters = '0123456789ABCDEF';
@ -29,4 +30,8 @@ export class ColorGenerator {
}; };
} }
export const colorGeneratorType = PropTypes.shape({
getColorForKey: PropTypes.func,
});
export default new ColorGenerator(Storage); export default new ColorGenerator(Storage);

41
src/utils/TagsSelector.js Normal file
View file

@ -0,0 +1,41 @@
import React from 'react';
import TagsInput from 'react-tagsinput';
import ColorGenerator, { colorGeneratorType } from './ColorGenerator';
import PropTypes from 'prop-types';
const defaultProps = {
colorGenerator: ColorGenerator,
placeholder: 'Add tags to the URL',
};
const propTypes = {
tags: PropTypes.arrayOf(PropTypes.string).isRequired,
onChange: PropTypes.func.isRequired,
placeholder: PropTypes.string,
colorGenerator: colorGeneratorType
};
export default function TagsSelector({ tags, onChange, placeholder, colorGenerator }) {
const renderTag = (props) => {
const { tag, key, disabled, onRemove, classNameRemove, getTagDisplayValue, ...other } = props;
return (
<span key={key} style={{ backgroundColor: colorGenerator.getColorForKey(tag) }} {...other}>
{getTagDisplayValue(tag)}
{!disabled && <a className={classNameRemove} onClick={() => onRemove(key)} />}
</span>
)
};
return (
<TagsInput
value={tags}
onChange={onChange}
inputProps={{ placeholder }}
onlyUnique
addOnBlur // FIXME Workaround to be able to add tags on Android
renderTag={renderTag}
/>
);
}
TagsSelector.defaultProps = defaultProps;
TagsSelector.propTypes = propTypes;