at the end of the day, it was inevitable
This commit is contained in:
@@ -0,0 +1,548 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import TimeAgo from 'timeago-react';
|
||||
import ArticleComment from './ArticleComment';
|
||||
import {
|
||||
UncontrolledDropdown,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
CustomInput,
|
||||
Button
|
||||
} from 'reactstrap';
|
||||
import ShareMenu from './ShareMenu';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faFacebook,
|
||||
faInstagram,
|
||||
faPinterest,
|
||||
faReddit,
|
||||
faTumblr,
|
||||
faTwitter,
|
||||
faYoutube
|
||||
} from '@fortawesome/free-brands-svg-icons';
|
||||
import {
|
||||
faComments,
|
||||
faEye,
|
||||
faFrown,
|
||||
faMeh,
|
||||
faQuoteLeft,
|
||||
faShareAlt,
|
||||
faSmile,
|
||||
faThumbsDown,
|
||||
faThumbsUp
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
capOnlyFirstLetter,
|
||||
convertUTCtoLocal,
|
||||
abbreviateNumber,
|
||||
notNullAndUnd
|
||||
} from '../../../../../common/helper';
|
||||
import SourceIndexInfoPopup from '../SourceIndexSubTab/SourceIndexInfoPopup';
|
||||
|
||||
const icons = {
|
||||
twitter: faTwitter,
|
||||
facebook: faFacebook,
|
||||
instagram: faInstagram,
|
||||
tumblr: faTumblr,
|
||||
pinterest: faPinterest,
|
||||
reddit: faReddit,
|
||||
youtube: faYoutube,
|
||||
POSITIVE: faSmile,
|
||||
NEGATIVE: faFrown,
|
||||
NEUTRAL: faMeh
|
||||
};
|
||||
|
||||
const colors = {
|
||||
POSITIVE: '#3ac47d',
|
||||
NEGATIVE: '#FC3939',
|
||||
NEUTRAL: '#868e96',
|
||||
twitter: '#1DA1F2',
|
||||
facebook: '#4267B2',
|
||||
reddit: '#FF5700',
|
||||
instagram: '#8a3ab9',
|
||||
tumblr: '#34526F',
|
||||
pinterest: '#E60023',
|
||||
youtube: '#FF0000'
|
||||
};
|
||||
|
||||
export class Article extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
shareMenu: false,
|
||||
imgErr: false,
|
||||
sourceModal: false
|
||||
};
|
||||
|
||||
this.elemDesc = React.createRef();
|
||||
}
|
||||
|
||||
selectArticle = () => {
|
||||
this.props.selectArticle(this.props.article);
|
||||
};
|
||||
|
||||
showEmailPopup = () => {
|
||||
this.props.showEmailPopup([this.props.article]);
|
||||
};
|
||||
|
||||
showCommentPopup = () => {
|
||||
this.props.showCommentPopup(this.props.article);
|
||||
};
|
||||
|
||||
showDeletePopup = () => {
|
||||
this.props.showDeletePopup([this.props.article]);
|
||||
};
|
||||
|
||||
showClipPopup = () => {
|
||||
this.props.showClipPopup([this.props.article]);
|
||||
};
|
||||
|
||||
toggleShareMenu = () => {
|
||||
this.setState((prev) => ({ shareMenu: !prev.shareMenu }));
|
||||
};
|
||||
|
||||
loadMoreComments = () => {
|
||||
const {
|
||||
loadMoreComments,
|
||||
article: {
|
||||
id: articleId,
|
||||
comments: { count: offset }
|
||||
}
|
||||
} = this.props;
|
||||
loadMoreComments(articleId, offset);
|
||||
};
|
||||
|
||||
readLater = () => {
|
||||
this.props.readArticleLater(this.props.article);
|
||||
};
|
||||
|
||||
onImgError = () => {
|
||||
this.setState({ imgErr: true });
|
||||
};
|
||||
|
||||
toggleSourceModal = () => {
|
||||
this.setState((prev) => ({ sourceModal: !prev.sourceModal }));
|
||||
};
|
||||
|
||||
render() {
|
||||
const { article, t, i18n, showCommentPopup, deleteComment } = this.props;
|
||||
let {
|
||||
comments,
|
||||
id,
|
||||
source,
|
||||
sentiment,
|
||||
permalink,
|
||||
publisher,
|
||||
title,
|
||||
image,
|
||||
author,
|
||||
content,
|
||||
published,
|
||||
mentions,
|
||||
tags,
|
||||
likes,
|
||||
dislikes,
|
||||
views,
|
||||
shares,
|
||||
categories
|
||||
} = article;
|
||||
const { imgErr } = this.state;
|
||||
const {
|
||||
data: commentsData,
|
||||
count: commentsCount, // should get real post comment count
|
||||
totalCount: commentsTotalCount
|
||||
} = comments;
|
||||
|
||||
const isArticleChosen = !!this.props.selectedArticles.find(
|
||||
(item) => item.id === id
|
||||
);
|
||||
|
||||
const offsetWidth =
|
||||
this.elemDesc &&
|
||||
this.elemDesc.current &&
|
||||
this.elemDesc.current.offsetWidth;
|
||||
|
||||
const hasRightCounters =
|
||||
notNullAndUnd(likes) ||
|
||||
notNullAndUnd(dislikes) ||
|
||||
commentsCount || // add not null and undefined when counter shows
|
||||
notNullAndUnd(views) ||
|
||||
notNullAndUnd(shares) ||
|
||||
notNullAndUnd(mentions);
|
||||
|
||||
const isTwitter = source.siteType === 'twitter';
|
||||
const isInstagram = source.siteType === 'instagram';
|
||||
let username;
|
||||
if (isTwitter) {
|
||||
username =
|
||||
author.link &&
|
||||
author.link.match(
|
||||
/^https?:\/\/(www\.)?twitter\.com\/(#!\/)?([^\/]+)(\/\w+)*$/
|
||||
);
|
||||
username = username && username[3];
|
||||
}
|
||||
if (isInstagram) {
|
||||
username =
|
||||
author.link &&
|
||||
author.link.match(
|
||||
/(?:(?:http|https):\/\/)?(?:www\.)?(?:instagram\.com|instagr\.am)\/([A-Za-z0-9-_\.]+)/
|
||||
);
|
||||
username = username && username[1];
|
||||
}
|
||||
|
||||
const isRTL = document.documentElement.dir === 'rtl';
|
||||
return (
|
||||
<div className="post border b-radius-5 mb-4">
|
||||
<UncontrolledDropdown className="post__menu">
|
||||
<DropdownToggle
|
||||
outline
|
||||
color="primary"
|
||||
className="btn-icon btn-icon-only p-1 m-2"
|
||||
>
|
||||
<i className="lnr lnr-menu btn-icon-wrapper" />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu className={isRTL ? ' dropdown-menu-left' : ''}>
|
||||
<DropdownItem
|
||||
className="text-muted"
|
||||
onClick={this.showCommentPopup}
|
||||
>
|
||||
<i className="mr-2 fa fa-comments"> </i>
|
||||
<span>{t('searchTab.commentBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.showClipPopup}>
|
||||
<i className="mr-2 fa fa-cut"> </i>
|
||||
<span>{t('searchTab.clipBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.readLater}>
|
||||
<i className="mr-2 fa fa-bookmark"> </i>
|
||||
<span>{t('searchTab.readLaterBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.readLater}>
|
||||
<i className="mr-2 fa fa-archive"> </i>
|
||||
<span>{t('searchTab.archiveBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.showEmailPopup}>
|
||||
<i className="mr-2 fa fa-envelope"> </i>
|
||||
<span>{t('searchTab.emailBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.toggleShareMenu}>
|
||||
<i className="mr-2 fa fa-share-alt"> </i>
|
||||
<span>{t('searchTab.shareBtn')}</span>
|
||||
</DropdownItem>
|
||||
<DropdownItem className="text-muted" onClick={this.showDeletePopup}>
|
||||
<i className="mr-2 fa fa-trash"> </i>
|
||||
<span>{t('searchTab.deleteBtn')}</span>
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</UncontrolledDropdown>
|
||||
<div className="d-flex flex-row">
|
||||
<div className="post__icons">
|
||||
<CustomInput
|
||||
id={'article-check-' + id}
|
||||
type="checkbox"
|
||||
className="mb-3"
|
||||
onChange={this.selectArticle}
|
||||
checked={isArticleChosen}
|
||||
/>
|
||||
{source.siteType && (
|
||||
<FontAwesomeIcon
|
||||
title={capOnlyFirstLetter(source.siteType)}
|
||||
icon={icons[source.siteType]}
|
||||
size="lg"
|
||||
className="fa-w-16 mb-3"
|
||||
color={colors[source.siteType]}
|
||||
/>
|
||||
)}
|
||||
{sentiment && (
|
||||
<FontAwesomeIcon
|
||||
title={capOnlyFirstLetter(sentiment)}
|
||||
icon={icons[sentiment]}
|
||||
className="mb-3"
|
||||
size="lg"
|
||||
color={colors[sentiment]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="post_middlepart">
|
||||
<h2 className="post__title">
|
||||
{title && (
|
||||
<a href={permalink} target="_blank" rel="noopener noreferrer">
|
||||
{title}
|
||||
</a>
|
||||
)}
|
||||
</h2>
|
||||
<div
|
||||
ref={this.elemDesc}
|
||||
className={`post__content${
|
||||
offsetWidth && offsetWidth < 430 ? ' flex-column' : ''
|
||||
}`}
|
||||
>
|
||||
{image &&
|
||||
!imgErr &&
|
||||
(!title && permalink ? (
|
||||
<a href={permalink} target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
id={id}
|
||||
width="180px"
|
||||
className="post__img mb-2 mb-lg-0 mr-3"
|
||||
src={image}
|
||||
onError={this.onImgError}
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<img
|
||||
id={id}
|
||||
width="180px"
|
||||
className="post__img mb-2 mb-lg-0 mr-3"
|
||||
src={image}
|
||||
onError={this.onImgError}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div>
|
||||
{author.name ? (
|
||||
author.link ? (
|
||||
<a
|
||||
className="d-inline-block hover-link text-muted mb-2"
|
||||
href={author.link}
|
||||
target="_blank"
|
||||
>
|
||||
{username ? `@${username}` : author.name}
|
||||
</a>
|
||||
) : (
|
||||
<p className="text-muted mb-2">{author.name}</p>
|
||||
)
|
||||
) : null}
|
||||
{!title && permalink ? (
|
||||
<a
|
||||
href={permalink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="post__desc-link"
|
||||
>
|
||||
<p
|
||||
className="post__desc"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
></p>
|
||||
</a>
|
||||
) : (
|
||||
<p
|
||||
className="post__desc"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
></p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tags && tags.length && tags.length > 0 && (
|
||||
<div className="post__tags mt-2">
|
||||
<strong>{t('searchTab.tags')}</strong>: {tags.join(', ')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{categories && categories.length > 0 && (
|
||||
<p className="post__tags my-2">
|
||||
<strong>{t('searchTab.categories')}</strong>:{' '}
|
||||
{categories.join(', ')}
|
||||
</p>
|
||||
)}
|
||||
<div className="post__about-info text-muted mt-3">
|
||||
{published && (
|
||||
<Fragment>
|
||||
<span
|
||||
className="d-inline-block"
|
||||
title={convertUTCtoLocal(published, 'MM/DD/YYYY HH:mm:ss')}
|
||||
>
|
||||
<TimeAgo
|
||||
datetime={published}
|
||||
locale={i18n.language}
|
||||
opts={{ minInterval: 60 }}
|
||||
/>
|
||||
</span>
|
||||
<span className="mx-2">|</span>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{source.type && (
|
||||
<Fragment>
|
||||
<span>{capOnlyFirstLetter(source.type)}</span>
|
||||
<span className="mx-2">|</span>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{source.country && (
|
||||
<Fragment>
|
||||
<span>{source.country}</span>
|
||||
<span className="mx-2">|</span>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{publisher && (
|
||||
<Fragment>
|
||||
<Button
|
||||
color="link"
|
||||
className="btn-anchor"
|
||||
title="Click to see details"
|
||||
onClick={this.toggleSourceModal}
|
||||
>
|
||||
{publisher}
|
||||
</Button>
|
||||
<span className="mx-2">|</span>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
{source.title && (
|
||||
<Fragment>
|
||||
{publisher ? (
|
||||
<a
|
||||
href={source.link}
|
||||
style={{ overflowWrap: 'anywhere' }}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{source.title}
|
||||
</a>
|
||||
) : (
|
||||
<Button
|
||||
color="link"
|
||||
className="btn-anchor"
|
||||
title="Click to see details"
|
||||
onClick={this.toggleSourceModal}
|
||||
>
|
||||
{(isTwitter || isInstagram) && author.name
|
||||
? author.name
|
||||
: source.title}
|
||||
</Button>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{hasRightCounters && (
|
||||
<div className="post__extras p-3">
|
||||
<div className="post__icons-wrapper">
|
||||
{notNullAndUnd(likes) && (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon
|
||||
title="Likes"
|
||||
icon={faThumbsUp}
|
||||
className="text-success"
|
||||
/>
|
||||
<p className="ml-2" title={likes}>
|
||||
{abbreviateNumber(likes)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{notNullAndUnd(dislikes) && (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon title="Dislikes" icon={faThumbsDown} />
|
||||
<p className="ml-2" title={dislikes}>
|
||||
{abbreviateNumber(dislikes)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* {notNullAndUnd(commentsCount) && (
|
||||
Add above line when real comment counts are visible
|
||||
*/}
|
||||
{commentsCount ? (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon title="Comments" icon={faComments} />
|
||||
<p className="ml-2" title={commentsCount}>
|
||||
{abbreviateNumber(commentsCount)}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{notNullAndUnd(views) && (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon title="Viwes" icon={faEye} />
|
||||
<p className="ml-2 text-center" title={views}>
|
||||
{abbreviateNumber(views)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{notNullAndUnd(shares) && (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon title="Shares" icon={faShareAlt} />
|
||||
<p className="ml-2 text-center" title={shares}>
|
||||
{abbreviateNumber(shares)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{notNullAndUnd(mentions) && (
|
||||
<div className="post__icon-metrics mb-1">
|
||||
<FontAwesomeIcon title="Mentions" icon={faQuoteLeft} />
|
||||
<p className="ml-2 text-center" title={mentions}>
|
||||
{abbreviateNumber(mentions)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{commentsData && commentsData.length > 0 && (
|
||||
<div className="post__comments border-top px-3 pb-3">
|
||||
{commentsData.map((comment) => {
|
||||
return (
|
||||
<ArticleComment
|
||||
article={article}
|
||||
comment={comment}
|
||||
showCommentPopup={showCommentPopup}
|
||||
deleteComment={deleteComment}
|
||||
key={comment.id}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{commentsCount < commentsTotalCount && (
|
||||
<Button
|
||||
outline
|
||||
size="sm"
|
||||
color="light"
|
||||
className="mt-2 d-block ml-auto btn-icon"
|
||||
onClick={this.loadMoreComments}
|
||||
>
|
||||
<i className="lnr lnr-chevron-down btn-icon-wrapper" />{' '}
|
||||
{t('searchTab.moreComments')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.state.shareMenu && (
|
||||
<ShareMenu article={article} hideMenu={this.toggleShareMenu} />
|
||||
)}
|
||||
|
||||
{this.state.sourceModal && (
|
||||
<SourceIndexInfoPopup
|
||||
source={article.source}
|
||||
hideSourceInfoPopup={this.toggleSourceModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Article.propTypes = {
|
||||
article: PropTypes.object.isRequired,
|
||||
selectedArticles: PropTypes.array.isRequired,
|
||||
selectArticle: PropTypes.func.isRequired,
|
||||
showEmailPopup: PropTypes.func.isRequired,
|
||||
showDeletePopup: PropTypes.func.isRequired,
|
||||
showCommentPopup: PropTypes.func.isRequired,
|
||||
showClipPopup: PropTypes.func.isRequired,
|
||||
deleteComment: PropTypes.func.isRequired,
|
||||
readArticleLater: PropTypes.func.isRequired,
|
||||
loadMoreComments: PropTypes.func.isRequired,
|
||||
showShareMenu: PropTypes.func.isRequired,
|
||||
i18n: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(Article);
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate, Interpolate } from 'react-i18next'
|
||||
import TimeAgo from 'timeago-react'
|
||||
import { Button } from 'reactstrap'
|
||||
|
||||
export class ArticleComment extends React.Component {
|
||||
static propTypes = {
|
||||
article: PropTypes.object.isRequired,
|
||||
comment: PropTypes.func.isRequired,
|
||||
deleteComment: PropTypes.func.isRequired,
|
||||
showCommentPopup: PropTypes.func.isRequired,
|
||||
i18n: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
onEdit = () => {
|
||||
const { showCommentPopup, article, comment } = this.props
|
||||
showCommentPopup(article, comment)
|
||||
}
|
||||
|
||||
onDelete = () => {
|
||||
const { deleteComment, article, comment } = this.props
|
||||
deleteComment(comment.id, article.id)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { comment, i18n } = this.props
|
||||
|
||||
return (
|
||||
<div className="post__comment mt-2">
|
||||
<div className="d-flex justify-content-between">
|
||||
<div>
|
||||
<cite className="post__commentor mr-3">
|
||||
<Interpolate
|
||||
i18nKey="searchTab.commentMetadata"
|
||||
author={`${comment.author.firstName} ${comment.author.lastName}`}
|
||||
/>
|
||||
</cite>
|
||||
<span className="post__cmttime mr-3 text-muted">
|
||||
<TimeAgo
|
||||
datetime={comment.createdAt}
|
||||
locale={i18n.language}
|
||||
opts={{ minInterval: 30 }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<Button color="link" className="p-0" onClick={this.onEdit}>
|
||||
<i className="lnr lnr-pencil"></i>
|
||||
</Button>
|
||||
<Button color="link" className="ml-2 p-0" onClick={this.onDelete}>
|
||||
<i className="lnr lnr-trash"></i>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="post__cmt-content">
|
||||
<strong className="d-block mb-1">{comment.title}</strong>
|
||||
{comment.content}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(ArticleComment)
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import ClipDragSource from './ClipDragSource'
|
||||
import RecentFeed from './RecentFeed'
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap'
|
||||
|
||||
export class ClipArticlesPopup extends React.Component {
|
||||
static propTypes = {
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
clipArticles: PropTypes.func.isRequired,
|
||||
articles: PropTypes.array.isRequired,
|
||||
recentClipFeeds: PropTypes.array.isRequired,
|
||||
getRecentClipFeeds: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
hidePopupFromOutside = (e) => {
|
||||
if (e.target === e.currentTarget) this.hidePopup()
|
||||
}
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hidePopup()
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
this.hidePopup()
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
this.props.getRecentClipFeeds()
|
||||
}
|
||||
|
||||
onRecentFeedClick = (feed) => {
|
||||
this.props.clipArticles(feed.id)
|
||||
this.props.hidePopup()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, articles, recentClipFeeds } = this.props
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
toggle={this.hidePopup}
|
||||
backdrop={false}
|
||||
modalClassName="pointer-events-none"
|
||||
>
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('searchTab.clipPopup.header')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="text-center">
|
||||
<p>{t('searchTab.clipPopup.hint1')}</p>
|
||||
|
||||
<div className="draggable-container">
|
||||
<ClipDragSource articles={articles} />
|
||||
</div>
|
||||
|
||||
{recentClipFeeds && recentClipFeeds.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="mb-2">{t('searchTab.clipPopup.hint2')}</p>
|
||||
<div className="d-flex justify-content-center flex-wrap">
|
||||
{recentClipFeeds.map((feed) => {
|
||||
return (
|
||||
<RecentFeed
|
||||
onRecentFeedClick={this.onRecentFeedClick}
|
||||
key={feed.id}
|
||||
feed={feed}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('common:commonWords.Cancel')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
ClipArticlesPopup
|
||||
)
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { TYPES } from '../../../../../../redux/modules/appState/sidebar'
|
||||
import { Interpolate } from 'react-i18next'
|
||||
import { DragSource } from 'react-dnd'
|
||||
|
||||
const source = {
|
||||
beginDrag (props, monitor, component) {
|
||||
setTimeout(() => {
|
||||
component.setState({
|
||||
isDragging: true
|
||||
})
|
||||
}, 0)
|
||||
return {
|
||||
type: TYPES.CLIP_ARTICLE
|
||||
}
|
||||
},
|
||||
|
||||
endDrag (props, monitor, component) {
|
||||
component.setState({
|
||||
isDragging: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies which props to inject into component from Drag n Drop.
|
||||
*/
|
||||
function collect (connect) {
|
||||
return {
|
||||
// Call this function inside render()
|
||||
// to let React DnD handle the drag events:
|
||||
connectDragSource: connect.dragSource()
|
||||
}
|
||||
}
|
||||
|
||||
export class ClipDragSource extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
articles: PropTypes.array.isRequired,
|
||||
connectDragSource: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isDragging: false
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
const style = {
|
||||
visibility: this.state.isDragging ? 'hidden' : 'visible'
|
||||
}
|
||||
|
||||
return this.props.connectDragSource(
|
||||
<div className="draggable-item" style={style}>
|
||||
<span className="drag-handle" />
|
||||
<Interpolate
|
||||
i18nKey='searchTab.clipPopup.clippedArticles'
|
||||
count={this.props.articles.length}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DragSource(TYPES.CLIP_ARTICLE, source, collect)(ClipDragSource)
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Button } from 'reactstrap'
|
||||
|
||||
export default class RecentFeed extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
feed: PropTypes.object.isRequired,
|
||||
onRecentFeedClick: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
onClick = () => {
|
||||
this.props.onRecentFeedClick(this.props.feed)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { feed } = this.props
|
||||
|
||||
return (
|
||||
<Button color="light" className={'mr-2 mb-2 feed-icon ' + feed.class} onClick={this.onClick}>
|
||||
{feed.name}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate, Interpolate } from 'react-i18next';
|
||||
import TimeAgo from 'timeago-react';
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalHeader
|
||||
} from 'reactstrap';
|
||||
|
||||
const initCharactersCount = 5000;
|
||||
|
||||
export class CommentArticlePopup extends React.Component {
|
||||
static propTypes = {
|
||||
article: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object,
|
||||
commentArticle: PropTypes.func.isRequired,
|
||||
updateComment: PropTypes.func.isRequired,
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
i18n: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const content = props.comment ? props.comment.content : '';
|
||||
this.state = {
|
||||
charactersCount: initCharactersCount - content.length,
|
||||
title: props.comment ? props.comment.title : '',
|
||||
comment: content
|
||||
};
|
||||
}
|
||||
|
||||
handleTitleChange = (e) => {
|
||||
const { value } = e.target;
|
||||
this.setState({ title: value });
|
||||
};
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hidePopup();
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const newComment = {
|
||||
title: this.state.title,
|
||||
content: this.state.comment
|
||||
};
|
||||
if (this.props.comment) {
|
||||
//edit exisitng
|
||||
this.props.updateComment(newComment, this.props.article.id);
|
||||
} else {
|
||||
//create new comment
|
||||
this.props.commentArticle(newComment, this.props.article.id);
|
||||
}
|
||||
this.hidePopup();
|
||||
};
|
||||
|
||||
onChangeComment = (e) => {
|
||||
const charactersCount = initCharactersCount - e.target.value.length;
|
||||
|
||||
if (charactersCount >= 0) {
|
||||
this.setState({
|
||||
charactersCount: charactersCount,
|
||||
comment: e.target.value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, i18n, article, comment } = this.props;
|
||||
const popupTitle = comment
|
||||
? t('searchTab.commentPopup.editUserComment')
|
||||
: t('searchTab.commentPopup.addUserComment');
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static">
|
||||
<ModalHeader toggle={this.hidePopup}>{popupTitle}</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="mb-3">
|
||||
<a
|
||||
className="font-size-lg"
|
||||
href={article.permalink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{article.title}
|
||||
</a>
|
||||
<p>{article.author.name}</p>
|
||||
<p className="font-size-xs text-muted">
|
||||
<TimeAgo
|
||||
datetime={article.published}
|
||||
locale={i18n.language}
|
||||
opts={{ minInterval: 30 }}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
value={this.state.title}
|
||||
type="text"
|
||||
className="mb-2"
|
||||
onChange={this.handleTitleChange}
|
||||
placeholder={t('searchTab.commentPopup.inputTitlePlaceholder')}
|
||||
/>
|
||||
|
||||
<Input
|
||||
rows="3"
|
||||
type="textarea"
|
||||
value={this.state.comment}
|
||||
onChange={this.onChangeComment}
|
||||
placeholder={t('searchTab.commentPopup.commentPlanceholder')}
|
||||
/>
|
||||
|
||||
<p className="font-size-xs text-muted text-right mt-1">
|
||||
<Interpolate
|
||||
i18nKey="searchTab.commentPopup.charactersLeft"
|
||||
count={this.state.charactersCount}
|
||||
/>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('common:commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="primary" onClick={this.onSubmit}>
|
||||
{t('common:commonWords.submit')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
CommentArticlePopup
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Interpolate, translate } from 'react-i18next';
|
||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
|
||||
|
||||
export class DeleteArticlesPopup extends React.Component {
|
||||
static propTypes = {
|
||||
articles: PropTypes.array.isRequired,
|
||||
activeFeed: PropTypes.object,
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
deleteArticles: PropTypes.func.isRequired,
|
||||
deleteArticlesFromFeed: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const {
|
||||
articles,
|
||||
activeFeed,
|
||||
deleteArticles,
|
||||
deleteArticlesFromFeed,
|
||||
hidePopup
|
||||
} = this.props;
|
||||
const ids = articles.map((a) => a.id);
|
||||
if (activeFeed) {
|
||||
deleteArticlesFromFeed(ids, activeFeed.id);
|
||||
} else {
|
||||
deleteArticles(ids);
|
||||
}
|
||||
hidePopup();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, articles, hidePopup } = this.props;
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={hidePopup} backdrop="static">
|
||||
<ModalHeader toggle={hidePopup}>{t('commonWords.Confirm')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>
|
||||
{articles.length > 1 ? (
|
||||
<Interpolate
|
||||
t={t}
|
||||
i18nKey="tabsContent:searchTab.deleteArticlePopupText_plural"
|
||||
articlesLength={articles.length}
|
||||
/>
|
||||
) : (
|
||||
t('tabsContent:searchTab.deleteArticlePopupText')
|
||||
)}
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={hidePopup}>
|
||||
{t('commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="danger" onClick={this.onSubmit}>
|
||||
{t('commonWords.Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['common'], { wait: true })(DeleteArticlesPopup);
|
||||
@@ -0,0 +1,209 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import moment from 'moment'
|
||||
import Select from 'react-select'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
Label,
|
||||
Input,
|
||||
ModalFooter,
|
||||
FormGroup,
|
||||
Col,
|
||||
Container
|
||||
} from 'reactstrap'
|
||||
import QuillEditor from '../../../../common/QuillEditor'
|
||||
|
||||
const replyToEmail = 'support@socialhose.io'
|
||||
|
||||
export class EmailArticlesPopup extends React.Component {
|
||||
static propTypes = {
|
||||
articlesToEmail: PropTypes.array.isRequired,
|
||||
emailArticles: PropTypes.func.isRequired,
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
recipients: PropTypes.object.isRequired,
|
||||
loadRecipients: PropTypes.func.isRequired,
|
||||
children: PropTypes.any,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
selectedRecipients: ''
|
||||
}
|
||||
this.editorRef = React.createRef()
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
!this.props.recipients.all.length && this.props.loadRecipients()
|
||||
}
|
||||
|
||||
componentDidMount = () => {
|
||||
this.props.loadRecipients()
|
||||
}
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hidePopup()
|
||||
}
|
||||
|
||||
collectParams = () => { // need to change with states
|
||||
const recipients = this.state.selectedRecipients
|
||||
if (!recipients) return false
|
||||
return {
|
||||
emailTo: recipients.map((r) => r.value),
|
||||
emailReplyTo: document.getElementById('email-reply-to').value,
|
||||
subject: document.getElementById('email-subject').value,
|
||||
content: this.editorRef.current && this.editorRef.current.root.innerHTML
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
const params = this.collectParams()
|
||||
if (params) {
|
||||
this.props.emailArticles(params)
|
||||
}
|
||||
}
|
||||
|
||||
changeRecipient = (value) => {
|
||||
this.setState({
|
||||
selectedRecipients: value
|
||||
})
|
||||
}
|
||||
|
||||
validEmails = (str) => {
|
||||
const re = /\S+@\S+\.\S+/
|
||||
const arr = str.split(',')
|
||||
for (let s of arr) {
|
||||
if (!re.test(s)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
emailRe = /\S+@\S+\.\S+/
|
||||
|
||||
isValidNewOption = ({ label }) => {
|
||||
return this.emailRe.test(label)
|
||||
}
|
||||
|
||||
promptTextCreator = (label) => {
|
||||
return label
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, articlesToEmail, recipients } = this.props
|
||||
const { selectedRecipients } = this.state
|
||||
|
||||
const recipientsAll = recipients.all.map((recipient) => ({
|
||||
value: recipient,
|
||||
label: recipient
|
||||
}))
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen
|
||||
size="lg"
|
||||
toggle={this.hidePopup}
|
||||
backdrop="static"
|
||||
>
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('searchTab.emailPopup.header')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Container>
|
||||
<FormGroup row>
|
||||
<Label htmlFor="email-to" sm={2}>
|
||||
{t('searchTab.emailPopup.labelTo')}
|
||||
</Label>
|
||||
<Col sm={10}>
|
||||
{recipients.pending && <i className="fa fa-spinner fa-pulse m-2" />}
|
||||
{!recipients.pending && (
|
||||
<Select.Creatable
|
||||
multi
|
||||
value={selectedRecipients}
|
||||
options={recipientsAll}
|
||||
onChange={this.changeRecipient}
|
||||
isValidNewOption={this.isValidNewOption}
|
||||
promptTextCreator={this.promptTextCreator}
|
||||
noResultsText="Email not valid"
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup row>
|
||||
<Label htmlFor="email-reply-to" sm={2}>
|
||||
{t('searchTab.emailPopup.labelReplyTo')}
|
||||
</Label>
|
||||
<Col sm={10}>
|
||||
<Input
|
||||
type="email"
|
||||
id="email-reply-to"
|
||||
defaultValue={replyToEmail}
|
||||
/>
|
||||
</Col>
|
||||
</FormGroup>
|
||||
<FormGroup row>
|
||||
<Label htmlFor="email-subject" sm={2}>
|
||||
{t('searchTab.emailPopup.labelSubject')}
|
||||
</Label>
|
||||
<Col sm={10}>
|
||||
<Input type="text" id="email-subject" />
|
||||
</Col>
|
||||
</FormGroup>
|
||||
|
||||
<div className="email-popup">
|
||||
<QuillEditor
|
||||
className="email-popup__articles email-editor"
|
||||
reference={this.editorRef}
|
||||
id="email-editor"
|
||||
>
|
||||
{articlesToEmail.map((article) => {
|
||||
return (
|
||||
<div className="email-popup__article" key={article.id}>
|
||||
<h2 className="article__title">
|
||||
<a href={article.source.link}>{article.title}</a>
|
||||
</h2>
|
||||
|
||||
<div className="article__about-info">
|
||||
<a href={article.source.link} target="blank">
|
||||
{article.source.title}
|
||||
</a>{' '}
|
||||
<span> | </span>
|
||||
<a href={article.author.link} target="blank">
|
||||
{article.author.name}
|
||||
</a>{' '}
|
||||
<span> | </span>
|
||||
{moment(article.published).format('LLL')}
|
||||
</div>
|
||||
|
||||
<p className="article__desc">{article.content}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</QuillEditor>
|
||||
</div>
|
||||
</Container>
|
||||
{this.props.children}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('common:commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="primary" onClick={this.onSubmit}>
|
||||
{t('searchTab.emailPopup.submitBtn')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
EmailArticlesPopup
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'
|
||||
|
||||
class EmailConfirmPopup extends React.Component {
|
||||
static propTypes = {
|
||||
hidePopup: PropTypes.func.isRequired,
|
||||
hideEmailPopup: PropTypes.func.isRequired,
|
||||
sendDocumentsByEmail: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.hidePopup()
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
this.props.sendDocumentsByEmail()
|
||||
this.hidePopup()
|
||||
this.props.hideEmailPopup()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static">
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('common:commonWords.Confirm')}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<p>{t('searchTab.emailPopup.sendConfirmWithoutSubject')}</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('searchTab.emailPopup.dontSend')}
|
||||
</Button>
|
||||
<Button color="warning" onClick={this.onSubmit}>
|
||||
{t('searchTab.emailPopup.sendAnyway')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
EmailConfirmPopup
|
||||
)
|
||||
@@ -0,0 +1,175 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import { translate } from 'react-i18next';
|
||||
import SearchDatesPopup from './SearchDatesPopup';
|
||||
import { Modal, Button, ModalHeader, ModalBody } from 'reactstrap';
|
||||
import { IoIosCalendar } from 'react-icons/io';
|
||||
|
||||
// previous commented code
|
||||
// componentWillMount = () => {
|
||||
// const { actions, userSubscription } = this.props;
|
||||
// actions.setSearchLastDate(userSubscription);
|
||||
// };
|
||||
export function MediaTypes(props) {
|
||||
const [modal, setModal] = useState(false);
|
||||
|
||||
const {
|
||||
t,
|
||||
mediaTypes,
|
||||
actions,
|
||||
chosenMediaTypes,
|
||||
toggleMediaType,
|
||||
toggleAllMediaTypes,
|
||||
restrictions
|
||||
} = props;
|
||||
|
||||
const allSelected = mediaTypes.length === chosenMediaTypes.length;
|
||||
|
||||
function toggle() {
|
||||
setModal((modal) => !modal);
|
||||
}
|
||||
|
||||
// set only the allowed media types from restrictions initially
|
||||
function allowPermissions(mediaType) {
|
||||
if (!restrictions || !restrictions.plans) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// for selecting all
|
||||
if (!mediaType) {
|
||||
return mediaTypes.every((mt) => restrictions.plans[mt]);
|
||||
}
|
||||
|
||||
return restrictions.plans[mediaType];
|
||||
}
|
||||
|
||||
function toggleSingleType(mediaType, value) {
|
||||
/* const isFree = restrictions.plans.price === 0;
|
||||
// TODO: remove following restrictions when duplication fixes
|
||||
const restrictedTemporary =
|
||||
isFree && ['news', 'blogs'].includes(mediaType) && value;
|
||||
|
||||
if (!allowPermissions(mediaType) || restrictedTemporary) { */
|
||||
if (!allowPermissions(mediaType)) {
|
||||
return actions.toggleUpgradeModal();
|
||||
}
|
||||
toggleMediaType(mediaType, value); // restrict condition
|
||||
}
|
||||
|
||||
function toggleAllTypes() {
|
||||
// TODO: remove following restrictions when duplication fixes
|
||||
/* const isFree = restrictions.plans.price === 0;
|
||||
if (!allowPermissions() || isFree) { */
|
||||
if (!allowPermissions()) {
|
||||
return actions.toggleUpgradeModal();
|
||||
}
|
||||
toggleAllMediaTypes(!allSelected);
|
||||
}
|
||||
|
||||
/*
|
||||
const {
|
||||
chosenSearchDate,
|
||||
chosenSearchInterval
|
||||
chosenStartDate,
|
||||
chosenEndDate
|
||||
} = props.searchByFiltersState
|
||||
const isIntervalBetween = chosenSearchInterval === 'between';
|
||||
const searchDateBtnText = isIntervalBetween &&
|
||||
chosenStartDate !== '' ||
|
||||
isIntervalBetween &&
|
||||
chosenEndDate !== ''
|
||||
? chosenSearchDate : t('searchTab.userSubscription.' + chosenSearchDate);
|
||||
*/
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="d-flex justify-content-between align-items-start">
|
||||
<div data-tour="select-media-types">
|
||||
<Button
|
||||
outline
|
||||
size="sm"
|
||||
title={allSelected ? 'Click to deselect' : 'Click to select'}
|
||||
className="btn-pill mb-2 mr-2 px-3"
|
||||
color={cx('light', { active: allSelected })}
|
||||
onClick={toggleAllTypes}
|
||||
>
|
||||
{t('searchTab.sourceTypes.all')}
|
||||
</Button>
|
||||
{mediaTypes.map((mediaType, i) => {
|
||||
const isMediaTypeChosen =
|
||||
chosenMediaTypes.indexOf(mediaType) !== -1;
|
||||
return (
|
||||
<Button
|
||||
key={mediaType}
|
||||
outline
|
||||
size="sm"
|
||||
title={
|
||||
isMediaTypeChosen ? 'Click to deselect' : 'Click to select'
|
||||
}
|
||||
className="btn-pill mb-2 mr-2 px-3"
|
||||
color={cx('light', {
|
||||
active: isMediaTypeChosen
|
||||
})}
|
||||
onClick={() => toggleSingleType(mediaType, !isMediaTypeChosen)}
|
||||
>
|
||||
{t('searchTab.sourceTypes.' + mediaType)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
color="link"
|
||||
className="ml-2"
|
||||
onClick={toggle}
|
||||
data-tour="select-date-range"
|
||||
>
|
||||
<IoIosCalendar fontSize="24px" />
|
||||
{/* {t('searchTab.datesRange')} */}
|
||||
</Button>
|
||||
</div>
|
||||
<Modal isOpen={modal} toggle={toggle} data-tour="date-range-modal">
|
||||
<ModalHeader toggle={toggle}>Select dates</ModalHeader>
|
||||
<ModalBody>
|
||||
<SearchDatesPopup
|
||||
outsideClickIgnoreClass="react-datepicker"
|
||||
userSubscription={props.userSubscription}
|
||||
userSubscriptionDate={props.userSubscriptionDate}
|
||||
searchIntervals={props.searchByFiltersState.searchIntervals}
|
||||
searchLastDates={props.searchByFiltersState.searchLastDates}
|
||||
chosenSearchInterval={
|
||||
props.searchByFiltersState.chosenSearchInterval
|
||||
}
|
||||
chosenSearchLastDate={
|
||||
props.searchByFiltersState.chosenSearchLastDate
|
||||
}
|
||||
chosenStartDate={props.searchByFiltersState.chosenStartDate}
|
||||
chosenEndDate={props.searchByFiltersState.chosenEndDate}
|
||||
hideSearchDatesPopup={toggle}
|
||||
setSearchInterval={actions.setSearchInterval}
|
||||
setSearchLastDate={actions.setSearchLastDate}
|
||||
setSearchDate={actions.setSearchDate}
|
||||
setStartDate={actions.setStartDate}
|
||||
setEndDate={actions.setEndDate}
|
||||
/>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
MediaTypes.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
mediaTypes: PropTypes.array.isRequired,
|
||||
chosenMediaTypes: PropTypes.array.isRequired,
|
||||
toggleMediaType: PropTypes.func.isRequired,
|
||||
toggleAllMediaTypes: PropTypes.func.isRequired,
|
||||
restrictions: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
userSubscriptionDate: PropTypes.string.isRequired,
|
||||
userSubscription: PropTypes.string.isRequired,
|
||||
searchByFiltersState: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(MediaTypes);
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import FiltersTable from '../../../../common/FiltersTable/FiltersTable'
|
||||
import { Button } from 'reactstrap'
|
||||
|
||||
export class RefinePanel extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
advancedFilters: PropTypes.object.isRequired,
|
||||
selectedFilters: PropTypes.object.isRequired,
|
||||
clearPending: PropTypes.object.isRequired,
|
||||
filterPages: PropTypes.object.isRequired,
|
||||
onRefine: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
onHiderClick = (e) => {
|
||||
e.preventDefault()
|
||||
this.props.actions.toggleRefinePanel()
|
||||
};
|
||||
|
||||
onSelectFilter = (groupName, filterValue) => {
|
||||
this.props.actions.selectRefineFilter(groupName, filterValue)
|
||||
};
|
||||
|
||||
onClearFilters = (groupName) => {
|
||||
this.props.actions.clearRefineFilters(groupName)
|
||||
};
|
||||
|
||||
onClearAllFilters = () => {
|
||||
this.props.actions.clearAllRefineFilters()
|
||||
};
|
||||
|
||||
onMoreFilters = (groupName) => {
|
||||
this.props.actions.loadMoreRefineFilters(groupName)
|
||||
};
|
||||
|
||||
onLessFilters = (groupName) => {
|
||||
this.props.actions.loadLessRefineFilters(groupName)
|
||||
};
|
||||
|
||||
/* onPressEnter = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
const keyword = document.getElementById('refine-keyword').value
|
||||
this.props.actions.selectRefineFilter('keyword', keyword)
|
||||
setTimeout(() => {
|
||||
this.props.onRefine()
|
||||
})
|
||||
}
|
||||
}; */
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className="refine-panel px-4">
|
||||
<Button
|
||||
color="light"
|
||||
title="Hide refine panel"
|
||||
className="d-block ml-auto mb-3 btn-icon"
|
||||
onClick={this.onHiderClick}
|
||||
>
|
||||
{this.props.t('searchTab.hide')}
|
||||
</Button>
|
||||
{/* <Input
|
||||
type="text"
|
||||
className="mb-2"
|
||||
id="refine-keyword"
|
||||
placeholder={this.props.t('common:advancedFilters.keywordRefine')}
|
||||
onKeyUp={this.onPressEnter}
|
||||
/> */}
|
||||
<FiltersTable
|
||||
filters={this.props.advancedFilters}
|
||||
selectedFilters={this.props.selectedFilters}
|
||||
clearPending={this.props.clearPending}
|
||||
pages={this.props.filterPages}
|
||||
callbacks={{
|
||||
'selectFilter': this.onSelectFilter,
|
||||
'clearFilters': this.onClearFilters,
|
||||
'clearAllFilters': this.onClearAllFilters,
|
||||
'moreFilters': this.onMoreFilters,
|
||||
'lessFilters': this.onLessFilters,
|
||||
'refine': this.props.onRefine
|
||||
}}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(RefinePanel)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import {
|
||||
Button,
|
||||
Modal,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
Label,
|
||||
Input,
|
||||
ModalFooter,
|
||||
FormGroup
|
||||
} from 'reactstrap'
|
||||
|
||||
export class SaveFeedPopup extends React.Component {
|
||||
static propTypes = {
|
||||
feedCategories: PropTypes.array.isRequired,
|
||||
saveType: PropTypes.string.isRequired,
|
||||
toggleSaveFeedPopup: PropTypes.func.isRequired,
|
||||
addAlert: PropTypes.func.isRequired,
|
||||
onSaveAsFeed: PropTypes.func.isRequired,
|
||||
getSidebarCategories: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
isFeedNameError: false,
|
||||
feedCategoriesKeys: [],
|
||||
feedName: '',
|
||||
selectCategory: ''
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount = () => {
|
||||
let nestingCount = -1
|
||||
this.getCategoriesKeys(this.props.feedCategories, nestingCount)
|
||||
}
|
||||
|
||||
//function that generates new array of categories without nesting
|
||||
getCategoriesKeys = (categories, nestingCount) => {
|
||||
nestingCount += 1
|
||||
categories.forEach((category) => {
|
||||
if (category.subType === 'deleted_content') return false
|
||||
|
||||
const categoryName = '-'.repeat(nestingCount) + ' ' + category.name
|
||||
|
||||
const feedCategoriesKeys = this.state.feedCategoriesKeys
|
||||
feedCategoriesKeys.push({ id: category.id, name: categoryName })
|
||||
this.setState({
|
||||
feedCategoriesKeys: feedCategoriesKeys,
|
||||
selectCategory: feedCategoriesKeys[0].id.toString()
|
||||
})
|
||||
|
||||
if (category.childes.length) {
|
||||
this.getCategoriesKeys(category.childes, nestingCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
changeHandler = (e) => {
|
||||
const { name, value } = e.target
|
||||
this.setState({ [name]: value })
|
||||
}
|
||||
|
||||
hidePopupFromOutside = (e) => {
|
||||
if (e.target === e.currentTarget) this.hidePopup()
|
||||
}
|
||||
|
||||
hidePopup = () => {
|
||||
this.props.toggleSaveFeedPopup()
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
const { feedName: name, selectCategory: category } = this.state
|
||||
|
||||
if (!name || !name.trim()) {
|
||||
this.setState({ isFeedNameError: true })
|
||||
return false
|
||||
}
|
||||
|
||||
this.props.onSaveAsFeed(name, category)
|
||||
this.hidePopup()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props
|
||||
|
||||
const {
|
||||
feedCategoriesKeys,
|
||||
isFeedNameError,
|
||||
feedName,
|
||||
selectCategory
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
<Modal isOpen toggle={this.hidePopup} backdrop="static" data-tour="feed-save-modal">
|
||||
<ModalHeader toggle={this.hidePopup}>
|
||||
{t('searchTab.saveFeedPopup.' + this.props.saveType)}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t('searchTab.saveFeedPopup.nameLabel')}<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="feedName"
|
||||
type="text"
|
||||
value={feedName}
|
||||
onChange={this.changeHandler}
|
||||
/>
|
||||
{isFeedNameError && (
|
||||
<p className="text-danger">
|
||||
{t('searchTab.saveFeedPopup.feedNameErrorMsg')}
|
||||
</p>
|
||||
)}
|
||||
</FormGroup>
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t('searchTab.saveFeedPopup.folderLabel')}<span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
name="selectCategory"
|
||||
type="select"
|
||||
value={selectCategory}
|
||||
onChange={this.changeHandler}
|
||||
>
|
||||
{feedCategoriesKeys.map((category) => {
|
||||
return (
|
||||
<option key={category.id} value={category.id}>
|
||||
{category.name}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</Input>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="light" onClick={this.hidePopup}>
|
||||
{t('common:commonWords.Cancel')}
|
||||
</Button>
|
||||
<Button color="primary" onClick={this.onSubmit}>
|
||||
{t('searchTab.saveBtn')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
SaveFeedPopup
|
||||
)
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { DateRangePicker } from 'react-dates'
|
||||
import moment from 'moment'
|
||||
import { getMomentObject } from '../../../../../../common/helper'
|
||||
|
||||
export class BetweenDatepickers extends React.Component {
|
||||
state = {}
|
||||
|
||||
static propTypes = {
|
||||
chosenSearchInterval: PropTypes.string.isRequired,
|
||||
chosenStartDate: PropTypes.string.isRequired,
|
||||
chosenEndDate: PropTypes.string.isRequired,
|
||||
setSearchInterval: PropTypes.func.isRequired,
|
||||
setSearchDate: PropTypes.func.isRequired,
|
||||
setStartDate: PropTypes.func.isRequired,
|
||||
minDate: PropTypes.object,
|
||||
setEndDate: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
swapDate = (startDate, endDate) => {
|
||||
if (startDate.isAfter(endDate)) {
|
||||
const temp = startDate
|
||||
startDate = endDate
|
||||
endDate = temp
|
||||
}
|
||||
return { startDate, endDate }
|
||||
}
|
||||
/*
|
||||
setDates = (date, isStartDate) => {
|
||||
const {
|
||||
chosenStartDate,
|
||||
chosenEndDate,
|
||||
setStartDate,
|
||||
setEndDate,
|
||||
setSearchDate
|
||||
} = this.props
|
||||
|
||||
const hasStartDate = !!chosenStartDate
|
||||
const hasEndDate = !!chosenEndDate
|
||||
let startDate = hasStartDate ? moment(chosenStartDate) : moment()
|
||||
let endDate = hasEndDate ? moment(chosenEndDate) : moment()
|
||||
|
||||
startDate = isStartDate ? date : startDate
|
||||
endDate = !isStartDate ? date : endDate
|
||||
|
||||
const swappedDate = this.swapDate(startDate, endDate)
|
||||
startDate = swappedDate.startDate.format('YYYY-MM-DD')
|
||||
endDate = swappedDate.endDate.format('YYYY-MM-DD')
|
||||
|
||||
setStartDate(startDate.format('YYYY-MM-DD'))
|
||||
setEndDate(endDate.format('YYYY-MM-DD'))
|
||||
|
||||
const endDateLabel = hasEndDate ? endDate : 'now'
|
||||
const startDateLabel = hasStartDate ? startDate : 'until'
|
||||
let label = isStartDate
|
||||
? `${startDate} - ${endDateLabel}`
|
||||
: `${startDateLabel} - ${endDate}`
|
||||
setSearchDate(label)
|
||||
} */
|
||||
|
||||
setBetweenInterval = () => {
|
||||
const { chosenSearchInterval, setSearchInterval } = this.props
|
||||
if (chosenSearchInterval === 'between') return false
|
||||
|
||||
setSearchInterval('between')
|
||||
}
|
||||
|
||||
handleDateChange = ({ startDate, endDate }) => {
|
||||
const { setStartDate, setEndDate } = this.props
|
||||
|
||||
setStartDate(startDate ? startDate.format('YYYY-MM-DD') : null)
|
||||
setEndDate(endDate ? endDate.format('YYYY-MM-DD') : null)
|
||||
|
||||
if (startDate && endDate) {
|
||||
this.setBetweenInterval()
|
||||
}
|
||||
}
|
||||
|
||||
onFocusChange = (focus) => {
|
||||
this.setState({ focusedInput: focus })
|
||||
}
|
||||
|
||||
isOutsideRange = (date) => {
|
||||
const today = moment()
|
||||
return date.isAfter(today) || date.isBefore(this.props.minDate)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { chosenStartDate, chosenEndDate } = this.props
|
||||
const today = moment()
|
||||
const startDate = getMomentObject(chosenStartDate)
|
||||
const endDate = getMomentObject(chosenEndDate)
|
||||
|
||||
return (
|
||||
<div className="ml-3">
|
||||
<DateRangePicker
|
||||
startDateId="startDate"
|
||||
endDateId="endDate"
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onDatesChange={this.handleDateChange}
|
||||
focusedInput={this.state.focusedInput}
|
||||
onFocusChange={this.onFocusChange}
|
||||
displayFormat="MM/DD/YYYY"
|
||||
startDatePlaceholderText="Start Date"
|
||||
endDatePlaceholderText="End Date"
|
||||
numberOfMonths={1}
|
||||
maxDate={today}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
isOutsideRange={this.isOutsideRange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default BetweenDatepickers
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Col, CustomInput, FormGroup } from 'reactstrap';
|
||||
|
||||
export class DuplicatesTab extends React.Component {
|
||||
static propTypes = {
|
||||
includeDuplicates: PropTypes.bool.isRequired,
|
||||
toggleIncludeDuplicates: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Col sm={12}>
|
||||
<FormGroup>
|
||||
<CustomInput
|
||||
className="checkbox-input-hidden"
|
||||
type="checkbox"
|
||||
id="duplicates-check"
|
||||
checked={this.props.includeDuplicates}
|
||||
onChange={this.props.toggleIncludeDuplicates}
|
||||
label={t('searchTab.searchBySection.duplicates.includeDuplicates')}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(DuplicatesTab);
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Col, FormGroup, Input, Label } from 'reactstrap';
|
||||
|
||||
export class EmphasisTab extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
include: PropTypes.string.isRequired,
|
||||
exclude: PropTypes.string.isRequired,
|
||||
setHeadlineIncluded: PropTypes.func.isRequired,
|
||||
setHeadlineExcluded: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
setHeadInclude = (e) => {
|
||||
const headline = e.target.value;
|
||||
this.props.setHeadlineIncluded(headline);
|
||||
};
|
||||
|
||||
setHeadExclude = (e) => {
|
||||
const headline = e.target.value;
|
||||
this.props.setHeadlineExcluded(headline);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, include, exclude } = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Col sm="6">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t('searchTab.searchBySection.emphasis.headlineLabel')}{' '}
|
||||
{t('searchTab.searchBySection.emphasis.include')}
|
||||
</Label>
|
||||
<Input type="text" value={include} onChange={this.setHeadInclude} />
|
||||
</FormGroup>
|
||||
</Col>
|
||||
<Col sm="6">
|
||||
<FormGroup>
|
||||
<Label>
|
||||
{t('searchTab.searchBySection.emphasis.headlineLabel')}{' '}
|
||||
{t('searchTab.searchBySection.emphasis.exclude')}
|
||||
</Label>
|
||||
<Input type="text" value={exclude} onChange={this.setHeadExclude} />
|
||||
</FormGroup>
|
||||
</Col>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(EmphasisTab);
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Col, CustomInput, FormGroup } from 'reactstrap';
|
||||
|
||||
function ExtrasTab({ t, hasImages, toggleHasImages }) {
|
||||
return (
|
||||
<Col sm={12}>
|
||||
<FormGroup>
|
||||
<CustomInput
|
||||
id="has-images-check"
|
||||
type="checkbox"
|
||||
className="d-flex"
|
||||
checked={hasImages}
|
||||
label={t('searchTab.searchBySection.extras.hasImages')}
|
||||
onChange={toggleHasImages}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
ExtrasTab.propTypes = {
|
||||
hasImages: PropTypes.bool.isRequired,
|
||||
toggleHasImages: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(ExtrasTab);
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Col, CustomInput } from 'reactstrap';
|
||||
|
||||
export class LangsTab extends React.Component {
|
||||
static propTypes = {
|
||||
chosenLanguages: PropTypes.array.isRequired,
|
||||
searchLanguages: PropTypes.array.isRequired,
|
||||
toggleLang: PropTypes.func.isRequired,
|
||||
toggleAllLangs: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
toggleLangs = ({ target: { id, checked } }) => {
|
||||
this.props.toggleLang(id, checked);
|
||||
};
|
||||
|
||||
toggleAllLangs = (e) => {
|
||||
this.props.toggleAllLangs(e.target.checked);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { searchLanguages, chosenLanguages } = this.props;
|
||||
return (
|
||||
<Col sm={12} className="search-by-lang">
|
||||
<CustomInput
|
||||
id="article-check-all"
|
||||
type="checkbox"
|
||||
label={t('common:language.all')}
|
||||
checked={searchLanguages.length === chosenLanguages.length}
|
||||
onChange={this.toggleAllLangs}
|
||||
/>
|
||||
|
||||
{searchLanguages.map((lang) => (
|
||||
<CustomInput
|
||||
key={lang}
|
||||
id={lang}
|
||||
type="checkbox"
|
||||
checked={chosenLanguages.indexOf(lang) !== -1}
|
||||
label={t('common:language.' + lang)}
|
||||
onChange={this.toggleLangs}
|
||||
/>
|
||||
))}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(LangsTab);
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { DragSource } from 'react-dnd'
|
||||
|
||||
const Types = {
|
||||
LOC: 'location'
|
||||
}
|
||||
|
||||
const locationSource = {
|
||||
beginDrag (props) {
|
||||
// Return the data describing the dragged item
|
||||
return { oldDropTargetType: props.dropTargetType }
|
||||
},
|
||||
|
||||
endDrag (props, monitor, component) {
|
||||
// When dropped on a compatible target, do something
|
||||
if (monitor.getDropResult() !== null) {
|
||||
const locFrom = props.dropTargetType
|
||||
const locTo = monitor.getDropResult().newDropTargetType
|
||||
|
||||
const locationType = props.locationType
|
||||
const location = props.location
|
||||
|
||||
props.moveLocation(locFrom, locTo, locationType, location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies which props to inject into your component.
|
||||
*/
|
||||
function collectDragSource (connect) {
|
||||
return {
|
||||
// Call this function inside render()
|
||||
// to let React DnD handle the drag events:
|
||||
connectDragSource: connect.dragSource()
|
||||
}
|
||||
}
|
||||
|
||||
export class LocationsTabList extends React.Component {
|
||||
static propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
dropTargetType: PropTypes.string.isRequired,
|
||||
moveLocation: PropTypes.func.isRequired,
|
||||
connectDragSource: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render () {
|
||||
const { connectDragSource } = this.props
|
||||
const { location } = this.props
|
||||
|
||||
return connectDragSource(
|
||||
<li className="list-group-item cursor-move p-2">
|
||||
<span className="drag-handle" />
|
||||
{location.name}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DragSource(Types.LOC, locationSource, collectDragSource)(LocationsTabList)
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import LocationsTabList from './LocationsTabList';
|
||||
import { Button, Col, Row } from 'reactstrap';
|
||||
|
||||
export class LocationsTab extends React.Component {
|
||||
static propTypes = {
|
||||
locations: PropTypes.array.isRequired,
|
||||
locationsToInclude: PropTypes.array.isRequired,
|
||||
locationsToExclude: PropTypes.array.isRequired,
|
||||
chosenLocationsType: PropTypes.string.isRequired,
|
||||
changeLocationsType: PropTypes.func.isRequired,
|
||||
moveLocation: PropTypes.func.isRequired,
|
||||
clearLocations: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
dropdownOpen: false,
|
||||
dropDownValue: 'country'
|
||||
};
|
||||
}
|
||||
|
||||
onClearLocations = () => {
|
||||
this.props.clearLocations();
|
||||
this.props.changeLocationsType('country');
|
||||
this.setState({ dropDownValue: 'country' });
|
||||
};
|
||||
|
||||
selectLocation = (value) => {
|
||||
this.props.changeLocationsType(value);
|
||||
this.setState({ dropDownValue: value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
locations,
|
||||
chosenLocationsType,
|
||||
locationsToInclude,
|
||||
locationsToExclude
|
||||
} = this.props;
|
||||
const { t } = this.props;
|
||||
const locationsMainList = locations.filter((loc) => {
|
||||
return loc.type === chosenLocationsType;
|
||||
});
|
||||
const includeList = locationsToInclude.filter((loc) => {
|
||||
return loc.type === chosenLocationsType;
|
||||
});
|
||||
const excludeList = locationsToExclude.filter((loc) => {
|
||||
return loc.type === chosenLocationsType;
|
||||
});
|
||||
|
||||
const { dropDownValue } = this.state;
|
||||
return (
|
||||
<Col sm={12}>
|
||||
<Button
|
||||
outline
|
||||
active={dropDownValue === 'country'}
|
||||
color="secondary"
|
||||
className="mr-2 mb-3"
|
||||
onClick={() => this.selectLocation('country')}
|
||||
>
|
||||
{t('searchTab.searchBySection.locations.countriesSelect')}
|
||||
</Button>
|
||||
<Button
|
||||
outline
|
||||
active={dropDownValue === 'state'}
|
||||
color="secondary"
|
||||
className="mb-3"
|
||||
onClick={() => this.selectLocation('state')}
|
||||
>
|
||||
{t('searchTab.searchBySection.locations.statesSelect')}
|
||||
</Button>
|
||||
|
||||
<Row className="draggable">
|
||||
<Col md={4}>
|
||||
<LocationsTabList
|
||||
locations={locationsMainList}
|
||||
chosenLocationsType={chosenLocationsType}
|
||||
dropTargetType="locations"
|
||||
moveLocation={this.props.moveLocation}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<LocationsTabList
|
||||
locations={includeList}
|
||||
chosenLocationsType={chosenLocationsType}
|
||||
dropTargetType="locationsToInclude"
|
||||
moveLocation={this.props.moveLocation}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<LocationsTabList
|
||||
locations={excludeList}
|
||||
chosenLocationsType={chosenLocationsType}
|
||||
dropTargetType="locationsToExclude"
|
||||
moveLocation={this.props.moveLocation}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(LocationsTab);
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { DropTarget } from 'react-dnd';
|
||||
import flow from 'lodash/flow';
|
||||
import LocationItem from './LocationItem';
|
||||
import {
|
||||
ListGroup
|
||||
} from 'reactstrap';
|
||||
|
||||
const targetTypes = ['location'];
|
||||
const locationListTarget = {
|
||||
drop(props, monitor, component) {
|
||||
if (monitor.didDrop()) {
|
||||
//check whether some nested
|
||||
// target already handled drop
|
||||
return;
|
||||
}
|
||||
|
||||
return { newDropTargetType: props.dropTargetType };
|
||||
},
|
||||
|
||||
canDrop(props, monitor) {
|
||||
return props.dropTargetType !== monitor.getItem().oldDropTargetType;
|
||||
}
|
||||
};
|
||||
|
||||
function collectDropTarget(connect, monitor) {
|
||||
return {
|
||||
// Call this function inside render()
|
||||
// to let React DnD handle the drag events:
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
// You can ask the monitor about the current drag state:
|
||||
itemType: monitor.getItemType()
|
||||
};
|
||||
}
|
||||
|
||||
export class LocationsTabList extends React.Component {
|
||||
static propTypes = {
|
||||
locations: PropTypes.array.isRequired,
|
||||
chosenLocationsType: PropTypes.string.isRequired,
|
||||
dropTargetType: PropTypes.string.isRequired,
|
||||
moveLocation: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
connectDropTarget: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { locations, chosenLocationsType, dropTargetType } = this.props;
|
||||
const { t } = this.props;
|
||||
const { connectDropTarget } = this.props;
|
||||
|
||||
locations.forEach((location) => {
|
||||
location.name = t('common:' + location.type + '.' + location.code);
|
||||
});
|
||||
|
||||
const sortedLocations = locations.sort((a, b) => {
|
||||
const nameA = a.name.toLowerCase();
|
||||
const nameB = b.name.toLowerCase();
|
||||
if (nameA < nameB) {
|
||||
//sort string ascending
|
||||
return -1;
|
||||
}
|
||||
if (nameA > nameB) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return connectDropTarget(
|
||||
<div className="scroll-area-md border b-radius-5">
|
||||
<p className="text-muted border-bottom p-2">{t('searchTab.searchBySection.locations.' + dropTargetType)}</p>
|
||||
<ListGroup className="p-2">
|
||||
{sortedLocations.map((location, i) => {
|
||||
return (
|
||||
<LocationItem
|
||||
key={'location-' + i}
|
||||
location={location}
|
||||
dropTargetType={dropTargetType}
|
||||
locationType={chosenLocationsType}
|
||||
moveLocation={this.props.moveLocation}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ListGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default flow(
|
||||
DropTarget(targetTypes, locationListTarget, collectDropTarget),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
)(LocationsTabList);
|
||||
@@ -0,0 +1,163 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SearchByTabs from './SearchByTabs';
|
||||
import EmphasisTab from './EmphasisTab';
|
||||
import LangsTab from './LangsTab';
|
||||
import LocationsTab from './LocationsTab';
|
||||
import SourcesTab from './SourcesTab';
|
||||
import SourceListsTab from './SourceListsTab';
|
||||
import DuplicatesTab from './DuplicatesTab';
|
||||
import ExtrasTab from './ExtrasTab';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Button, Container, Row } from 'reactstrap';
|
||||
|
||||
export class SearchBy extends React.Component {
|
||||
static propTypes = {
|
||||
userSubscriptionDate: PropTypes.string.isRequired,
|
||||
userSubscription: PropTypes.string.isRequired,
|
||||
searchByFiltersState: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
animationDisabled: true,
|
||||
arrowPosition: true
|
||||
};
|
||||
}
|
||||
|
||||
onToggleSearchBy = () => {
|
||||
this.props.actions.toggleSearchBy();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
const { searchByFiltersState, actions } = this.props;
|
||||
const visibleClass = searchByFiltersState.isSearchByVisible
|
||||
? ' visible'
|
||||
: ' closed';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'search-by-container mb-3 mb-md-0' + visibleClass}
|
||||
data-tour="advanced-search"
|
||||
>
|
||||
<div className="search-by">
|
||||
<SearchByTabs
|
||||
searchByTabs={searchByFiltersState.searchByTabs}
|
||||
chooseSearchByTab={actions.chooseSearchByTab}
|
||||
chosenSearchByTab={searchByFiltersState.chosenSearchByTab}
|
||||
/>
|
||||
|
||||
<Container fluid>
|
||||
<Row className="mb-3" data-tour="advanced-search-content">
|
||||
{searchByFiltersState.chosenSearchByTab === 'emphasis' && (
|
||||
<EmphasisTab
|
||||
include={searchByFiltersState.headlineIncluded}
|
||||
exclude={searchByFiltersState.headlineExcluded}
|
||||
setHeadlineIncluded={actions.setHeadlineIncluded}
|
||||
setHeadlineExcluded={actions.setHeadlineExcluded}
|
||||
/>
|
||||
)}
|
||||
|
||||
{searchByFiltersState.chosenSearchByTab === 'languages' && (
|
||||
<LangsTab
|
||||
searchLanguages={searchByFiltersState.searchLanguages}
|
||||
chosenLanguages={searchByFiltersState.chosenLanguages}
|
||||
toggleLang={actions.toggleLang}
|
||||
toggleAllLangs={actions.toggleAllLangs}
|
||||
/>
|
||||
)}
|
||||
|
||||
{searchByFiltersState.chosenSearchByTab === 'locations' && (
|
||||
<LocationsTab
|
||||
locations={searchByFiltersState.locations}
|
||||
chosenLocationsType={searchByFiltersState.chosenLocationsType}
|
||||
locationsToInclude={searchByFiltersState.locationsToInclude}
|
||||
locationsToExclude={searchByFiltersState.locationsToExclude}
|
||||
changeLocationsType={actions.changeLocationsType}
|
||||
moveLocation={actions.moveLocation}
|
||||
clearLocations={actions.clearLocations}
|
||||
/>
|
||||
)}
|
||||
|
||||
{searchByFiltersState.chosenSearchByTab === 'sources' && (
|
||||
<SourcesTab
|
||||
chosenMediaTypes={searchByFiltersState.chosenMediaTypes}
|
||||
chosenLanguages={searchByFiltersState.chosenLanguages}
|
||||
searchBySources={searchByFiltersState.searchBySources}
|
||||
searchBySourcesType={searchByFiltersState.searchBySourcesType}
|
||||
selectedSearchBySources={
|
||||
searchByFiltersState.selectedSearchBySources
|
||||
}
|
||||
searchBySourcesQuery={
|
||||
searchByFiltersState.searchBySourcesQuery
|
||||
}
|
||||
setSearchBySourcesQuery={actions.setSearchBySourcesQuery}
|
||||
getSearchBySources={actions.getSearchBySources}
|
||||
addSelectedSearchBySource={actions.addSelectedSearchBySource}
|
||||
removeSelectedSearchBySource={
|
||||
actions.removeSelectedSearchBySource
|
||||
}
|
||||
clearSearchBySources={actions.clearSearchBySources}
|
||||
includeExcludeSearchBySources={
|
||||
actions.includeExcludeSearchBySources
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{searchByFiltersState.chosenSearchByTab === 'sourceLists' && (
|
||||
<SourceListsTab
|
||||
searchBySourceLists={
|
||||
searchByFiltersState.searchBySourceListsAvailable
|
||||
}
|
||||
searchBySourceListsToInclude={
|
||||
searchByFiltersState.searchBySourceListsToInclude
|
||||
}
|
||||
searchBySourceListsToExclude={
|
||||
searchByFiltersState.searchBySourceListsToExclude
|
||||
}
|
||||
getSourceLists={actions.getSearchBySourceLists}
|
||||
moveSourceList={actions.moveSourceList}
|
||||
/>
|
||||
)}
|
||||
|
||||
{searchByFiltersState.chosenSearchByTab === 'duplicates' && (
|
||||
<DuplicatesTab
|
||||
includeDuplicates={searchByFiltersState.includeDuplicates}
|
||||
toggleIncludeDuplicates={actions.toggleIncludeDuplicates}
|
||||
/>
|
||||
)}
|
||||
|
||||
{searchByFiltersState.chosenSearchByTab === 'extras' && (
|
||||
<ExtrasTab
|
||||
hasImages={searchByFiltersState.hasImages}
|
||||
toggleHasImages={actions.toggleHasImages}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</Container>
|
||||
</div>
|
||||
<hr className="mt-0 mb-2" />
|
||||
<Button
|
||||
outline
|
||||
size="sm"
|
||||
className="font-size-xs"
|
||||
color="secondary"
|
||||
onClick={this.onToggleSearchBy}
|
||||
>
|
||||
{t('searchTab.searchBySection.searchByBtn')}
|
||||
{searchByFiltersState.isSearchByVisible ? (
|
||||
<i className="lnr-chevron-up btn-icon-wrapper"></i>
|
||||
) : (
|
||||
<i className="lnr-chevron-down btn-icon-wrapper"></i>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SearchBy);
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Nav, NavLink, NavItem } from 'reactstrap';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
export class SearchByTabs extends React.Component {
|
||||
static propTypes = {
|
||||
searchByTabs: PropTypes.array.isRequired,
|
||||
chosenSearchByTab: PropTypes.string.isRequired,
|
||||
chooseSearchByTab: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
chooseSearchByTab = (newTab) => () => {
|
||||
this.props.chooseSearchByTab(newTab);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { searchByTabs } = this.props;
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Nav tabs className="font-size-xs">
|
||||
{searchByTabs.map((tab, i) => (
|
||||
<NavItem key={tab}>
|
||||
<NavLink
|
||||
className="d-block"
|
||||
active={tab === this.props.chosenSearchByTab}
|
||||
onClick={this.chooseSearchByTab(tab)}
|
||||
>
|
||||
{t('searchTab.searchBySection.' + tab + '.title')}
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
))}
|
||||
</Nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SearchByTabs);
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
export class SourceIcon extends React.Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
acceptedTypes = ['blogs', 'clippings', 'forums', 'mixed', 'news', 'prints', 'socials', 'user-added', 'user-comments', 'videos'];
|
||||
|
||||
render () {
|
||||
const { type } = this.props
|
||||
|
||||
if (!this.acceptedTypes.includes(type)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<img src={require('../../../../../../images/feed-type-' + type + '.png')} className="source-icon" />
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SourceIcon
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import SourceListsTabList from './SourceListsTabList';
|
||||
import { Col } from 'reactstrap';
|
||||
|
||||
export class SourceListsTab extends React.Component {
|
||||
static propTypes = {
|
||||
searchBySourceLists: PropTypes.array.isRequired,
|
||||
searchBySourceListsToInclude: PropTypes.array.isRequired,
|
||||
searchBySourceListsToExclude: PropTypes.array.isRequired,
|
||||
getSourceLists: PropTypes.func.isRequired,
|
||||
moveSourceList: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
componentWillMount = () => {
|
||||
this.props.getSourceLists({ page: 1, limit: 25 });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
searchBySourceLists,
|
||||
searchBySourceListsToInclude,
|
||||
searchBySourceListsToExclude
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Col md={4}>
|
||||
<SourceListsTabList
|
||||
sourceLists={searchBySourceLists}
|
||||
dropTargetType="searchBySourceListsAvailable"
|
||||
moveSourceList={this.props.moveSourceList}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<SourceListsTabList
|
||||
sourceLists={searchBySourceListsToInclude}
|
||||
dropTargetType="searchBySourceListsToInclude"
|
||||
moveSourceList={this.props.moveSourceList}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<SourceListsTabList
|
||||
sourceLists={searchBySourceListsToExclude}
|
||||
dropTargetType="searchBySourceListsToExclude"
|
||||
moveSourceList={this.props.moveSourceList}
|
||||
/>
|
||||
</Col>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SourceListsTab);
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { DragSource } from 'react-dnd';
|
||||
|
||||
const Types = {
|
||||
SOURCE_LIST: 'sourceList'
|
||||
};
|
||||
|
||||
const sourceListSource = {
|
||||
beginDrag(props) {
|
||||
// Return the data describing the dragged item
|
||||
return { oldDropTargetType: props.dropTargetType };
|
||||
},
|
||||
|
||||
endDrag(props, monitor, component) {
|
||||
// When dropped on a compatible target, do something
|
||||
if (monitor.getDropResult() !== null) {
|
||||
const from = props.dropTargetType;
|
||||
const to = monitor.getDropResult().newDropTargetType;
|
||||
|
||||
const sourceList = props.sourceList;
|
||||
|
||||
props.moveSourceList(from, to, sourceList);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Specifies which props to inject into your component.
|
||||
*/
|
||||
function collectDragSource(connect) {
|
||||
return {
|
||||
// Call this function inside render()
|
||||
// to let React DnD handle the drag events:
|
||||
connectDragSource: connect.dragSource()
|
||||
};
|
||||
}
|
||||
|
||||
export class SourceListsTabItem extends React.Component {
|
||||
static propTypes = {
|
||||
sourceList: PropTypes.func.isRequired,
|
||||
dropTargetType: PropTypes.string.isRequired,
|
||||
moveSourceList: PropTypes.func.isRequired,
|
||||
connectDragSource: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { connectDragSource } = this.props;
|
||||
const { sourceList } = this.props;
|
||||
|
||||
return connectDragSource(
|
||||
<li className="list-group-item cursor-move p-2">
|
||||
<span className="drag-handle" />
|
||||
{sourceList.name}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DragSource(
|
||||
Types.SOURCE_LIST,
|
||||
sourceListSource,
|
||||
collectDragSource
|
||||
)(SourceListsTabItem);
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { DropTarget } from 'react-dnd';
|
||||
import flow from 'lodash/flow';
|
||||
import SourceListsTabItem from './SourceListsTabItem';
|
||||
import { ListGroup } from 'reactstrap';
|
||||
|
||||
const targetTypes = ['sourceList'];
|
||||
const sourceListTarget = {
|
||||
drop(props, monitor, component) {
|
||||
if (monitor.didDrop()) {
|
||||
//check whether some nested
|
||||
// target already handled drop
|
||||
return;
|
||||
}
|
||||
|
||||
return { newDropTargetType: props.dropTargetType };
|
||||
},
|
||||
|
||||
canDrop(props, monitor) {
|
||||
return props.dropTargetType !== monitor.getItem().oldDropTargetType;
|
||||
}
|
||||
};
|
||||
|
||||
function collectDropTarget(connect, monitor) {
|
||||
return {
|
||||
// Call this function inside render()
|
||||
// to let React DnD handle the drag events:
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
// You can ask the monitor about the current drag state:
|
||||
itemType: monitor.getItemType()
|
||||
};
|
||||
}
|
||||
|
||||
export class SourceListsTabList extends React.Component {
|
||||
static propTypes = {
|
||||
sourceLists: PropTypes.array.isRequired,
|
||||
dropTargetType: PropTypes.string.isRequired,
|
||||
moveSourceList: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
connectDropTarget: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const { sourceLists, dropTargetType } = this.props;
|
||||
const { t } = this.props;
|
||||
const { connectDropTarget } = this.props;
|
||||
|
||||
return connectDropTarget(
|
||||
<div className="draggable scroll-area-md border b-radius-5">
|
||||
<p className="text-muted border-bottom p-2">
|
||||
{t('searchTab.searchBySection.sourceLists.' + dropTargetType)}
|
||||
</p>
|
||||
<ListGroup className="p-2">
|
||||
{sourceLists.map((sourceList, i) => {
|
||||
return (
|
||||
<SourceListsTabItem
|
||||
key={'sourceList-' + i}
|
||||
sourceList={sourceList}
|
||||
dropTargetType={dropTargetType}
|
||||
moveSourceList={this.props.moveSourceList}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ListGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default flow(
|
||||
DropTarget(targetTypes, sourceListTarget, collectDropTarget),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
)(SourceListsTabList);
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SourcesTabAvailSources from './SourcesTabAvailSources';
|
||||
import SourcesTabSelectedSources from './SourcesTabSelectedSources';
|
||||
import { Col } from 'reactstrap';
|
||||
|
||||
export class SourcesTab extends React.Component {
|
||||
static propTypes = {
|
||||
chosenMediaTypes: PropTypes.array.isRequired,
|
||||
chosenLanguages: PropTypes.array.isRequired,
|
||||
searchBySources: PropTypes.array.isRequired,
|
||||
selectedSearchBySources: PropTypes.array.isRequired,
|
||||
searchBySourcesType: PropTypes.string.isRequired,
|
||||
searchBySourcesQuery: PropTypes.string.isRequired,
|
||||
setSearchBySourcesQuery: PropTypes.func.isRequired,
|
||||
getSearchBySources: PropTypes.func.isRequired,
|
||||
addSelectedSearchBySource: PropTypes.func.isRequired,
|
||||
removeSelectedSearchBySource: PropTypes.func.isRequired,
|
||||
clearSearchBySources: PropTypes.func.isRequired,
|
||||
includeExcludeSearchBySources: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
searchBySourcesQuery,
|
||||
setSearchBySourcesQuery,
|
||||
chosenMediaTypes,
|
||||
chosenLanguages,
|
||||
searchBySources,
|
||||
getSearchBySources,
|
||||
addSelectedSearchBySource,
|
||||
searchBySourcesType,
|
||||
clearSearchBySources,
|
||||
selectedSearchBySources,
|
||||
removeSelectedSearchBySource,
|
||||
includeExcludeSearchBySources
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Col sm={8}>
|
||||
<SourcesTabAvailSources
|
||||
searchBySourcesQuery={searchBySourcesQuery}
|
||||
selectedSources={selectedSearchBySources}
|
||||
setSearchBySourcesQuery={setSearchBySourcesQuery}
|
||||
chosenMediaTypes={chosenMediaTypes}
|
||||
chosenLanguages={chosenLanguages}
|
||||
availSources={searchBySources}
|
||||
getSearchBySources={getSearchBySources}
|
||||
addSelectedSearchBySource={addSelectedSearchBySource}
|
||||
/>
|
||||
</Col>
|
||||
<Col sm={4}>
|
||||
<SourcesTabSelectedSources
|
||||
searchBySourcesType={searchBySourcesType}
|
||||
clearSearchBySources={clearSearchBySources}
|
||||
selectedSources={selectedSearchBySources}
|
||||
removeSelectedSearchBySource={removeSelectedSearchBySource}
|
||||
includeExcludeSearchBySources={includeExcludeSearchBySources}
|
||||
/>
|
||||
</Col>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SourcesTab;
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
// import SourceIcon from './SourceIcon';
|
||||
import { Button, Input, InputGroup, InputGroupAddon, Table } from 'reactstrap';
|
||||
import { capitalize } from 'lodash';
|
||||
import { getTitle } from '../../../../../../common/helper';
|
||||
import cx from 'classnames';
|
||||
import { domainNames } from '../SearchSubTab';
|
||||
|
||||
export class SourcesTabAvailSources extends React.Component {
|
||||
static propTypes = {
|
||||
chosenMediaTypes: PropTypes.array.isRequired,
|
||||
chosenLanguages: PropTypes.array.isRequired,
|
||||
availSources: PropTypes.array.isRequired,
|
||||
selectedSources: PropTypes.array.isRequired,
|
||||
searchBySourcesQuery: PropTypes.string.isRequired,
|
||||
setSearchBySourcesQuery: PropTypes.func.isRequired,
|
||||
getSearchBySources: PropTypes.func.isRequired,
|
||||
addSelectedSearchBySource: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
componentDidMount = () => {
|
||||
this.searchSources();
|
||||
};
|
||||
|
||||
searchSources = () => {
|
||||
const {
|
||||
chosenLanguages,
|
||||
chosenMediaTypes,
|
||||
getSearchBySources,
|
||||
searchBySourcesQuery
|
||||
} = this.props;
|
||||
const query = searchBySourcesQuery;
|
||||
const dataToSend = {};
|
||||
dataToSend.page = 1;
|
||||
dataToSend.limit = 100;
|
||||
dataToSend.query = query;
|
||||
dataToSend.filters = {};
|
||||
|
||||
const source = []
|
||||
const domain = []
|
||||
chosenMediaTypes.map((v) => {
|
||||
if (domainNames.includes(v)) {
|
||||
domain.push(`${v}.com`);
|
||||
} else {
|
||||
source.push(v);
|
||||
}
|
||||
})
|
||||
dataToSend.filters.publisher = { source, domain };
|
||||
|
||||
dataToSend.filters.language = chosenLanguages;
|
||||
getSearchBySources(dataToSend);
|
||||
};
|
||||
|
||||
chooseSource = (e) => {
|
||||
const dataset = e.currentTarget.dataset;
|
||||
const sourceTitle = dataset.sourceTitle;
|
||||
const sourceType = dataset.sourceType;
|
||||
const sourceId = dataset.sourceId;
|
||||
this.props.addSelectedSearchBySource({
|
||||
title: sourceTitle,
|
||||
type: sourceType,
|
||||
id: sourceId
|
||||
});
|
||||
};
|
||||
|
||||
onChangeSearchInput = (e) => {
|
||||
const val = e.target.value;
|
||||
this.props.setSearchBySourcesQuery(val);
|
||||
};
|
||||
|
||||
onEnterSearchInput = (e) => {
|
||||
if (e.keyCode === 13) this.searchSources();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { availSources, selectedSources } = this.props;
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<InputGroup className="mb-3">
|
||||
<Input
|
||||
type="text"
|
||||
id="search-by-sources-input"
|
||||
value={this.props.searchBySourcesQuery}
|
||||
onChange={this.onChangeSearchInput}
|
||||
onKeyUp={this.onEnterSearchInput}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
color="primary"
|
||||
className="btn-icon btn-icon-only"
|
||||
onClick={this.searchSources}
|
||||
>
|
||||
<i className="lnr-magnifier btn-icon-wrapper"></i>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<p className="text-muted">
|
||||
{t('searchTab.searchBySection.sources.availSources')}
|
||||
</p>
|
||||
<div className="source-table-wrap border">
|
||||
<Table striped bordered className="mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('searchTab.searchBySection.sources.source')}</th>
|
||||
<th>{t('searchTab.searchBySection.sources.siteType')}</th>
|
||||
<th>{t('searchTab.searchBySection.sources.mediatype')}</th>
|
||||
<th>{t('searchTab.searchBySection.sources.lang')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{availSources.length > 0 ? (
|
||||
availSources.map((source, i) => {
|
||||
return (
|
||||
<tr
|
||||
title="Click to select"
|
||||
className={cx('clickable', {
|
||||
active:
|
||||
selectedSources &&
|
||||
selectedSources.find((v) => v.id === source.id)
|
||||
})}
|
||||
data-source-title={source.title}
|
||||
data-source-type={source.type}
|
||||
data-source-id={source.id}
|
||||
onClick={this.chooseSource}
|
||||
key={i}
|
||||
>
|
||||
{/* <td>
|
||||
<SourceIcon type={source.type} />
|
||||
</td> */}
|
||||
<td>{getTitle(source.title)}</td>
|
||||
<td title={source.url}>
|
||||
{capitalize(source.siteType) || '-'}
|
||||
</td>
|
||||
<td>{capitalize(source.type) || '-'}</td>
|
||||
<td>{t(`common:language.${source.lang}`)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr className="p-4 text-center text-black-50">
|
||||
<td colSpan="4">{t('common:messages.noRows')}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(
|
||||
SourcesTabAvailSources
|
||||
);
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Button, CustomInput, Table } from 'reactstrap';
|
||||
import { IoIosCloseCircleOutline } from 'react-icons/io';
|
||||
import { capitalize } from 'lodash';
|
||||
import { getTitle } from '../../../../../../common/helper';
|
||||
|
||||
export class SourcesTabSelectedSources extends React.Component {
|
||||
static propTypes = {
|
||||
searchBySourcesType: PropTypes.string.isRequired,
|
||||
selectedSources: PropTypes.array.isRequired,
|
||||
removeSelectedSearchBySource: PropTypes.func.isRequired,
|
||||
clearSearchBySources: PropTypes.func.isRequired,
|
||||
includeExcludeSearchBySources: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
removeSource = (sourceId) => {
|
||||
this.props.removeSelectedSearchBySource(sourceId);
|
||||
};
|
||||
|
||||
includeExclide = (type) => {
|
||||
this.props.includeExcludeSearchBySources(type);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { selectedSources } = this.props;
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="d-flex flex-wrap my-3">
|
||||
<CustomInput
|
||||
type="radio"
|
||||
name="include-exclude-source"
|
||||
className="d-flex mr-2"
|
||||
checked={this.props.searchBySourcesType === 'include'}
|
||||
id="include-sources-radio"
|
||||
onChange={() => this.includeExclide('include')}
|
||||
label={t('searchTab.searchBySection.sources.includeText')}
|
||||
/>
|
||||
|
||||
<CustomInput
|
||||
type="radio"
|
||||
name="include-exclude-source"
|
||||
checked={this.props.searchBySourcesType === 'exclude'}
|
||||
className="d-flex mr-2"
|
||||
id="exclude-sources-radio"
|
||||
onChange={() => this.includeExclide('exclude')}
|
||||
label={t('searchTab.searchBySection.sources.excludeText')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-muted">
|
||||
{t('searchTab.searchBySection.sources.selectedSources')}
|
||||
</p>
|
||||
|
||||
<div className="source-table-wrap border">
|
||||
<Table striped className="mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{t('searchTab.searchBySection.sources.source')}</th>
|
||||
<th>{t('searchTab.searchBySection.sources.mediatype')}</th>
|
||||
<th style={{ width: '50px' }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{selectedSources.length > 0 ? (
|
||||
selectedSources.map((source, i) => {
|
||||
return (
|
||||
<tr key={i}>
|
||||
{/* <td>
|
||||
<SourceIcon type={source.type} />
|
||||
</td> */}
|
||||
<td>{getTitle(source.title)}</td>
|
||||
<td>{capitalize(source.type) || '-'}</td>
|
||||
<td>
|
||||
<button
|
||||
title="Remove"
|
||||
type="button"
|
||||
className="btn p-0"
|
||||
onClick={() => this.removeSource(source.id)}
|
||||
>
|
||||
<IoIosCloseCircleOutline
|
||||
size={22}
|
||||
className="text-danger ml-2"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr className="p-4 text-center text-black-50">
|
||||
<td colSpan="3">
|
||||
{t('common:messages.noRows')} <br />
|
||||
{t('searchTab.searchBySection.sources.selectSource')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
{selectedSources.length > 0 && (
|
||||
<Button
|
||||
size="sm"
|
||||
className="d-block ml-auto mt-2 mb-2"
|
||||
onClick={this.props.clearSearchBySources}
|
||||
>
|
||||
{t('searchTab.clearBtn')}
|
||||
</Button>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(
|
||||
SourcesTabSelectedSources
|
||||
);
|
||||
@@ -0,0 +1,178 @@
|
||||
import React from 'react'
|
||||
import moment from 'moment'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import BetweenDatepickers from './SearchBy/BetweenDatepickers'
|
||||
import { compose } from 'redux'
|
||||
import classnames from 'classnames'
|
||||
import { Button, CustomInput, FormGroup } from 'reactstrap'
|
||||
|
||||
export class SearchDatesPopup extends React.Component {
|
||||
static propTypes = {
|
||||
userSubscriptionDate: PropTypes.string.isRequired,
|
||||
userSubscription: PropTypes.string.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
searchIntervals: PropTypes.array.isRequired,
|
||||
searchLastDates: PropTypes.array.isRequired,
|
||||
chosenSearchInterval: PropTypes.string.isRequired,
|
||||
chosenSearchLastDate: PropTypes.string.isRequired,
|
||||
chosenStartDate: PropTypes.string.isRequired,
|
||||
chosenEndDate: PropTypes.string.isRequired,
|
||||
setSearchInterval: PropTypes.func.isRequired,
|
||||
setSearchLastDate: PropTypes.func.isRequired,
|
||||
setSearchDate: PropTypes.func.isRequired,
|
||||
setStartDate: PropTypes.func.isRequired,
|
||||
setEndDate: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
setSearchInterval = (e) => {
|
||||
const chosenInterval = e.target.dataset.interval
|
||||
const chosenStartDate = this.props.chosenStartDate
|
||||
const chosenEndDate = this.props.chosenEndDate
|
||||
const chosenLastDate = this.props.chosenSearchLastDate
|
||||
const isIntervalBetween = chosenInterval === 'between'
|
||||
|
||||
this.props.setSearchInterval(chosenInterval)
|
||||
|
||||
if (
|
||||
(isIntervalBetween && chosenStartDate !== '') ||
|
||||
(isIntervalBetween && chosenEndDate !== '')
|
||||
) {
|
||||
const endDate = chosenEndDate !== '' ? chosenEndDate : 'now'
|
||||
const startDate = chosenStartDate !== '' ? chosenStartDate : 'until'
|
||||
|
||||
this.props.setSearchDate(startDate + ' - ' + endDate)
|
||||
}
|
||||
|
||||
if (chosenInterval === 'all') {
|
||||
this.props.setSearchDate('all')
|
||||
}
|
||||
|
||||
if (chosenInterval === 'last') {
|
||||
this.props.setSearchDate(chosenLastDate)
|
||||
}
|
||||
}
|
||||
|
||||
setLastDate = (e) => {
|
||||
const chosenLastDate = e.target.dataset.lastDate
|
||||
const isDisabled = e.target.dataset.disabled === 'true'
|
||||
|
||||
if (isDisabled) return false
|
||||
|
||||
if (this.props.chosenSearchInterval !== 'last') {
|
||||
this.props.setSearchInterval('last')
|
||||
}
|
||||
|
||||
this.props.setSearchLastDate(chosenLastDate)
|
||||
this.props.setSearchDate(chosenLastDate)
|
||||
}
|
||||
|
||||
onReset = () => {
|
||||
this.props.setSearchInterval('all')
|
||||
this.props.setSearchDate('all')
|
||||
this.props.setStartDate('')
|
||||
this.props.setEndDate('')
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
chosenSearchInterval,
|
||||
chosenStartDate,
|
||||
chosenEndDate,
|
||||
setSearchInterval,
|
||||
setSearchDate,
|
||||
setStartDate,
|
||||
setEndDate,
|
||||
chosenSearchLastDate,
|
||||
searchIntervals,
|
||||
searchLastDates,
|
||||
userSubscription
|
||||
} = this.props
|
||||
const subscriptionLimitIndex = searchLastDates.indexOf(userSubscription)
|
||||
const minDate = moment().startOf('day').subtract(
|
||||
parseInt(userSubscription.slice(0, -1)),
|
||||
'days'
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex justify-content-between">
|
||||
<p className="mb-2">
|
||||
{t('searchTab.searchDates.subscriptionLabel')}:
|
||||
<strong>
|
||||
{t('searchTab.userSubscription.' + this.props.userSubscription)}
|
||||
</strong>
|
||||
</p>
|
||||
<div>
|
||||
<Button color="warning" className="mb-2" onClick={this.onReset}>
|
||||
{t('searchTab.searchDates.resetBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
{searchIntervals.map((interval, i) => {
|
||||
return (
|
||||
<div key={interval}>
|
||||
<CustomInput
|
||||
checked={this.props.chosenSearchInterval === interval}
|
||||
type="radio"
|
||||
id={'search-interval-' + interval}
|
||||
data-interval={interval}
|
||||
name="date-interval"
|
||||
label={t('searchTab.searchDates.' + interval)}
|
||||
onChange={this.setSearchInterval}
|
||||
/>
|
||||
|
||||
{interval === 'last' && (
|
||||
<ul className="search-last-dates mx-3">
|
||||
{searchLastDates.map((lastDate, i) => {
|
||||
const isDisabled = i > subscriptionLimitIndex
|
||||
const isActive =
|
||||
chosenSearchLastDate === lastDate &&
|
||||
chosenSearchInterval === 'last'
|
||||
const className = classnames('search-last-dates__item', {
|
||||
disabled: isDisabled,
|
||||
active: isActive
|
||||
})
|
||||
|
||||
return (
|
||||
<li
|
||||
key={'last-date-' + i}
|
||||
data-last-date={lastDate}
|
||||
data-disabled={isDisabled}
|
||||
className={className}
|
||||
onClick={this.setLastDate}
|
||||
>
|
||||
{t('searchTab.searchDates.' + lastDate)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{interval === 'between' && (
|
||||
<BetweenDatepickers
|
||||
chosenSearchInterval={chosenSearchInterval}
|
||||
chosenStartDate={chosenStartDate}
|
||||
chosenEndDate={chosenEndDate}
|
||||
minDate={minDate}
|
||||
setSearchInterval={setSearchInterval}
|
||||
setSearchDate={setSearchDate}
|
||||
setStartDate={setStartDate}
|
||||
setEndDate={setEndDate}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</FormGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const applyDecorators = compose(translate(['tabsContent'], { wait: true }))
|
||||
|
||||
export default applyDecorators(SearchDatesPopup)
|
||||
@@ -0,0 +1,439 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import SearchSubTabHead from './SearchSubTabHead';
|
||||
import MediaTypes from './MediaTypes';
|
||||
import SearchingBlock from './SearchingBlock';
|
||||
import SearchingResults from './SearchingResults';
|
||||
import SearchBy from './SearchBy/SearchBy';
|
||||
import RefinePanel from './RefinePanel';
|
||||
import Restrictions from '../../../../common/Restrictions/Restrictions';
|
||||
import { parseSearchDays } from '../../../../../common/Common';
|
||||
import reduxConnect from '../../../../../redux/utils/connect';
|
||||
import { Card, CardBody, CardTitle } from 'reactstrap';
|
||||
import { setDocumentData } from '../../../../../common/helper';
|
||||
import { translate } from 'react-i18next';
|
||||
import { compose } from 'redux';
|
||||
|
||||
export const domainNames = ['reddit', 'twitter', 'instagram'];
|
||||
|
||||
class SearchSubTab extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
store: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
get searchState() {
|
||||
return this.props.store.appState.search;
|
||||
}
|
||||
get searchByFiltersState() {
|
||||
return this.props.store.appState.searchByFilters;
|
||||
}
|
||||
get articlesState() {
|
||||
return this.props.store.appState.articles;
|
||||
}
|
||||
get authState() {
|
||||
return this.props.store.common.auth;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
setDocumentData('title', 'Search');
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
setDocumentData('title');
|
||||
}
|
||||
|
||||
_sendSearchQuery = (page, initialSearch = false) => {
|
||||
const { actions } = this.props;
|
||||
const dataToSend = this.gatherSearchQueryData();
|
||||
if (dataToSend) {
|
||||
dataToSend.page = page;
|
||||
dataToSend.advancedFilters = this.gatherAdvancedFilters();
|
||||
actions.getSearchResults(dataToSend, initialSearch);
|
||||
}
|
||||
};
|
||||
|
||||
_sendFeedQuery = (page, activeFeed) => {
|
||||
const { actions } = this.props;
|
||||
const params = {
|
||||
page: page,
|
||||
advancedFilters: this.gatherAdvancedFilters()
|
||||
};
|
||||
actions.getFeedResults(params, activeFeed.id);
|
||||
};
|
||||
|
||||
onSearchQuery = () => {
|
||||
this._sendSearchQuery(1, true);
|
||||
};
|
||||
|
||||
onRefine = () => {
|
||||
const { activeFeed } = this.searchState;
|
||||
if (activeFeed) {
|
||||
this._sendFeedQuery(1, activeFeed);
|
||||
} else {
|
||||
this._sendSearchQuery(1);
|
||||
}
|
||||
};
|
||||
|
||||
onPager = ({ currentPage: page }) => {
|
||||
const { activeFeed } = this.searchState;
|
||||
if (activeFeed) {
|
||||
this._sendFeedQuery(page, activeFeed);
|
||||
} else {
|
||||
this._sendSearchQuery(page);
|
||||
}
|
||||
};
|
||||
|
||||
onSaveAsFeed = (name, category) => {
|
||||
const dataToSend = this.getFeedData(name, category, 'query_feed');
|
||||
dataToSend && this.props.actions.saveAsFeed(dataToSend);
|
||||
};
|
||||
|
||||
onSaveFeed = () => {
|
||||
const { actions } = this.props;
|
||||
const { activeFeed } = this.searchState;
|
||||
const dataToSend = this.getFeedData(
|
||||
activeFeed.name,
|
||||
activeFeed.category,
|
||||
activeFeed.subType
|
||||
);
|
||||
dataToSend && actions.saveFeed(dataToSend, activeFeed.id);
|
||||
};
|
||||
|
||||
getFeedData = (name, category, feedSubType) => {
|
||||
let dataToSend = {};
|
||||
|
||||
const searchQueryData = this.gatherSearchQueryData();
|
||||
|
||||
if (!searchQueryData) return false;
|
||||
|
||||
dataToSend.search = searchQueryData;
|
||||
dataToSend.search.advancedFilters = this.gatherAdvancedFilters();
|
||||
|
||||
dataToSend.feed = {
|
||||
name: name,
|
||||
category: category,
|
||||
subType: feedSubType
|
||||
};
|
||||
|
||||
const excludedArticles = this.articlesState.excludedArticles;
|
||||
if (excludedArticles && excludedArticles.length) {
|
||||
dataToSend.feed.excludedDocuments = excludedArticles;
|
||||
}
|
||||
|
||||
return dataToSend;
|
||||
};
|
||||
|
||||
gatherSearchQueryData = () => {
|
||||
const searchState = this.searchState;
|
||||
const searchByFiltersState = this.searchByFiltersState;
|
||||
const { userSubscription } = this.authState;
|
||||
const { actions } = this.props;
|
||||
|
||||
let dataToSend = {};
|
||||
|
||||
const query = searchState.loadedFeedQuery;
|
||||
|
||||
if (!query) {
|
||||
actions.addAlert({ type: 'error', transKey: 'searchQueryEmpty' });
|
||||
return false;
|
||||
}
|
||||
|
||||
dataToSend.query = query;
|
||||
|
||||
dataToSend.filters = {}; //create filters prop
|
||||
|
||||
//setting media types filter
|
||||
if (searchByFiltersState.chosenMediaTypes.length) {
|
||||
const source = [];
|
||||
const domain = [];
|
||||
searchByFiltersState.chosenMediaTypes.map((v) => {
|
||||
if (domainNames.includes(v)) {
|
||||
domain.push(`${v}.com`);
|
||||
} else {
|
||||
source.push(v);
|
||||
}
|
||||
});
|
||||
dataToSend.filters.publisher = { source, domain };
|
||||
} else {
|
||||
actions.addAlert({ type: 'error', transKey: 'noMediaTypesSelected' });
|
||||
return false;
|
||||
}
|
||||
|
||||
// setting date filter
|
||||
const chosenInterval = searchByFiltersState.chosenSearchInterval;
|
||||
const chosenStartDate = searchByFiltersState.chosenStartDate;
|
||||
const chosenEndDate = searchByFiltersState.chosenEndDate;
|
||||
|
||||
if (chosenInterval === 'between') {
|
||||
if (chosenStartDate !== '' || chosenEndDate !== '') {
|
||||
dataToSend.filters.date = {
|
||||
type: 'between',
|
||||
start: chosenStartDate,
|
||||
end: chosenEndDate
|
||||
};
|
||||
} else {
|
||||
dataToSend.filters.date = {
|
||||
type: 'last',
|
||||
days:
|
||||
searchByFiltersState.chosenSearchDate === 'all'
|
||||
? parseSearchDays(userSubscription)
|
||||
: parseSearchDays(searchByFiltersState.chosenSearchDate)
|
||||
};
|
||||
}
|
||||
} else if (chosenInterval === 'all') {
|
||||
dataToSend.filters.date = {
|
||||
type: 'last',
|
||||
days: parseSearchDays(userSubscription)
|
||||
};
|
||||
} else {
|
||||
dataToSend.filters.date = {
|
||||
type: 'last',
|
||||
days: parseSearchDays(searchByFiltersState.chosenSearchLastDate)
|
||||
};
|
||||
}
|
||||
|
||||
//adding included or/and excluded headlines filter
|
||||
const headlineIncluded = searchByFiltersState.headlineIncluded;
|
||||
const headlineExcluded = searchByFiltersState.headlineExcluded;
|
||||
|
||||
if (headlineIncluded.length || headlineExcluded.length) {
|
||||
dataToSend.filters.headline = {};
|
||||
}
|
||||
|
||||
if (headlineIncluded.length) {
|
||||
dataToSend.filters.headline.include = headlineIncluded;
|
||||
}
|
||||
|
||||
if (headlineExcluded.length) {
|
||||
dataToSend.filters.headline.exclude = headlineExcluded;
|
||||
}
|
||||
|
||||
//setting languages filter
|
||||
const chosenLanguages = searchByFiltersState.chosenLanguages;
|
||||
|
||||
if (chosenLanguages.length) {
|
||||
dataToSend.filters.language = chosenLanguages;
|
||||
}
|
||||
|
||||
//setting locations filter
|
||||
const locationsToInclude = searchByFiltersState.locationsToInclude;
|
||||
const locationsToExclude = searchByFiltersState.locationsToExclude;
|
||||
|
||||
const countriesToInclude = locationsToInclude.filter((loc) => {
|
||||
return loc.type === 'country';
|
||||
});
|
||||
const statesToInclude = locationsToInclude.filter((loc) => {
|
||||
return loc.type === 'state';
|
||||
});
|
||||
const countriesToExclude = locationsToExclude.filter((loc) => {
|
||||
return loc.type === 'country';
|
||||
});
|
||||
const statesToExclude = locationsToExclude.filter((loc) => {
|
||||
return loc.type === 'state';
|
||||
});
|
||||
|
||||
if (countriesToInclude.length || countriesToExclude.length) {
|
||||
dataToSend.filters.country = {};
|
||||
}
|
||||
|
||||
if (statesToInclude.length || statesToExclude.length) {
|
||||
dataToSend.filters.state = {};
|
||||
}
|
||||
|
||||
if (countriesToInclude.length) {
|
||||
dataToSend.filters.country.include = countriesToInclude.map((loc) => {
|
||||
return loc.code;
|
||||
});
|
||||
}
|
||||
|
||||
if (countriesToExclude.length) {
|
||||
dataToSend.filters.country.exclude = countriesToExclude.map((loc) => {
|
||||
return loc.code;
|
||||
});
|
||||
}
|
||||
|
||||
if (statesToInclude.length) {
|
||||
dataToSend.filters.state.include = statesToInclude.map((loc) => {
|
||||
return loc.code;
|
||||
});
|
||||
}
|
||||
|
||||
if (statesToExclude.length) {
|
||||
dataToSend.filters.state.exclude = statesToExclude.map((loc) => {
|
||||
return loc.code;
|
||||
});
|
||||
}
|
||||
|
||||
//setting source filter
|
||||
const selectedSearchBySources =
|
||||
searchByFiltersState.selectedSearchBySources;
|
||||
if (selectedSearchBySources.length) {
|
||||
dataToSend.filters.source = {};
|
||||
dataToSend.filters.source.type = searchByFiltersState.searchBySourcesType;
|
||||
dataToSend.filters.source.ids = selectedSearchBySources.map((source) => {
|
||||
return source.id;
|
||||
});
|
||||
}
|
||||
|
||||
//setting source lists filter
|
||||
const sourceListsToInclude =
|
||||
searchByFiltersState.searchBySourceListsToInclude;
|
||||
const sourceListsToExclude =
|
||||
searchByFiltersState.searchBySourceListsToExclude;
|
||||
|
||||
if (sourceListsToInclude.length || sourceListsToExclude.length) {
|
||||
dataToSend.filters.sourceList = {};
|
||||
}
|
||||
|
||||
if (sourceListsToInclude.length) {
|
||||
dataToSend.filters.sourceList.include = sourceListsToInclude.map(
|
||||
(source) => {
|
||||
return source.id;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (sourceListsToExclude.length) {
|
||||
dataToSend.filters.sourceList.exclude = sourceListsToExclude.map(
|
||||
(source) => {
|
||||
return source.id;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
//setting duplicates filter
|
||||
//dataToSend.filters.duplicates = searchByFiltersState.includeDuplicates;
|
||||
|
||||
//setting 'has images' filter
|
||||
dataToSend.filters.hasImage = searchByFiltersState.hasImages;
|
||||
|
||||
return dataToSend;
|
||||
};
|
||||
|
||||
gatherAdvancedFilters = () => {
|
||||
return this.searchState.advancedFilters.selected;
|
||||
};
|
||||
|
||||
render() {
|
||||
const searchState = this.searchState;
|
||||
const searchByFiltersState = this.searchByFiltersState;
|
||||
const {
|
||||
userSubscription,
|
||||
userSubscriptionDate,
|
||||
user: { restrictions }
|
||||
} = this.authState;
|
||||
const { store, actions } = this.props;
|
||||
const feedCategories = store.appState.sidebar.categories;
|
||||
const articlesState = store.appState.articles;
|
||||
|
||||
const { advancedFilters } = searchState;
|
||||
const activeFeed = searchState.activeFeed;
|
||||
let isEditSearchVisible =
|
||||
!searchState.loadedFeedQuery || searchState.isEditingFeed;
|
||||
if (activeFeed && activeFeed.subType === 'clip_feed') {
|
||||
isEditSearchVisible = false;
|
||||
}
|
||||
const hasActiveFeed = !!activeFeed;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{!hasActiveFeed && (
|
||||
<Restrictions
|
||||
restrictions={restrictions && restrictions.limits}
|
||||
restrictionsIds={['searchesPerDay', 'savedFeeds']}
|
||||
/>
|
||||
)}
|
||||
<div className="search-tab">
|
||||
<Card className="main-card mb-3">
|
||||
<CardBody>
|
||||
<div className="search-block">
|
||||
{isEditSearchVisible && (
|
||||
<div className="search-edit-block">
|
||||
<SearchingBlock
|
||||
searchResultsErrors={searchState.searchResultsErrors}
|
||||
onSearchQuery={this.onSearchQuery}
|
||||
loadedFeedQuery={searchState.loadedFeedQuery}
|
||||
actions={actions}
|
||||
/>
|
||||
|
||||
<MediaTypes
|
||||
mediaTypes={searchByFiltersState.mediaTypes}
|
||||
chosenMediaTypes={searchByFiltersState.chosenMediaTypes}
|
||||
actions={actions}
|
||||
restrictions={restrictions}
|
||||
searchByFiltersState={searchByFiltersState}
|
||||
userSubscription={userSubscription}
|
||||
userSubscriptionDate={userSubscriptionDate}
|
||||
toggleMediaType={actions.toggleMediaType}
|
||||
toggleAllMediaTypes={actions.toggleAllMediaTypes}
|
||||
/>
|
||||
|
||||
<SearchBy
|
||||
userSubscription={userSubscription}
|
||||
userSubscriptionDate={userSubscriptionDate}
|
||||
searchByFiltersState={searchByFiltersState}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<SearchSubTabHead
|
||||
isSaveFeedPopupVisible={searchState.isSaveFeedPopupVisible}
|
||||
isSaving={searchState.isSavingFeed}
|
||||
feedCategories={feedCategories}
|
||||
onSaveAsFeed={this.onSaveAsFeed}
|
||||
toggleSaveFeedPopup={actions.toggleSaveFeedPopup}
|
||||
addAlert={actions.addAlert}
|
||||
getSidebarCategories={actions.getSidebarCategories}
|
||||
activeFeed={activeFeed}
|
||||
isEditingFeed={searchState.isEditingFeed}
|
||||
editFeed={actions.editFeed}
|
||||
setNewSearch={actions.setNewSearch}
|
||||
renewSearchBy={actions.renewSearchBy}
|
||||
changeActiveFeedName={actions.changeActiveFeedName}
|
||||
saveFeed={this.onSaveFeed}
|
||||
/>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<Card className="main-card mb-3">
|
||||
<CardBody>
|
||||
<CardTitle>{this.props.t('searchTab.results')}</CardTitle>
|
||||
<div className="search-content">
|
||||
<SearchingResults
|
||||
searchState={searchState}
|
||||
articlesState={articlesState}
|
||||
actions={actions}
|
||||
isRefinePanelVisible={advancedFilters.isVisible}
|
||||
toggleRefinePanel={actions.toggleRefinePanel}
|
||||
onPager={this.onPager}
|
||||
/>
|
||||
|
||||
{searchState.isLoaded && advancedFilters.isVisible && (
|
||||
<RefinePanel
|
||||
advancedFilters={advancedFilters.all}
|
||||
selectedFilters={advancedFilters.selected}
|
||||
clearPending={advancedFilters.pending}
|
||||
filterPages={advancedFilters.pages}
|
||||
onRefine={this.onRefine}
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const applyDecorators = compose(
|
||||
reduxConnect(),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
);
|
||||
|
||||
export default applyDecorators(SearchSubTab);
|
||||
@@ -0,0 +1,135 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import SaveFeedPopup from './SaveFeedPopup'
|
||||
import { Button } from 'reactstrap'
|
||||
export class SearchSubTabHead extends React.Component {
|
||||
static propTypes = {
|
||||
feedCategories: PropTypes.array.isRequired,
|
||||
isSaveFeedPopupVisible: PropTypes.bool.isRequired,
|
||||
activeFeed: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isEditingFeed: PropTypes.bool.isRequired,
|
||||
addAlert: PropTypes.func.isRequired,
|
||||
toggleSaveFeedPopup: PropTypes.func.isRequired,
|
||||
onSaveAsFeed: PropTypes.func.isRequired,
|
||||
getSidebarCategories: PropTypes.func.isRequired,
|
||||
editFeed: PropTypes.func.isRequired,
|
||||
setNewSearch: PropTypes.func.isRequired,
|
||||
renewSearchBy: PropTypes.func.isRequired,
|
||||
changeActiveFeedName: PropTypes.func.isRequired,
|
||||
saveFeed: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
openSaveFeedPopup = () => {
|
||||
this.props.toggleSaveFeedPopup()
|
||||
}
|
||||
|
||||
saveFeed = () => {
|
||||
this.props.saveFeed()
|
||||
}
|
||||
|
||||
onEditFeed = () => {
|
||||
this.props.editFeed()
|
||||
}
|
||||
|
||||
onNewSearch = () => {
|
||||
this.props.setNewSearch()
|
||||
this.props.renewSearchBy()
|
||||
}
|
||||
|
||||
onChangeFeedName = (event) => {
|
||||
this.props.changeActiveFeedName(event.target.value)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
isEditingFeed,
|
||||
isSaveFeedPopupVisible,
|
||||
isSaving,
|
||||
activeFeed
|
||||
} = this.props
|
||||
const feedIsLoaded = !!activeFeed
|
||||
const showEditButton =
|
||||
!!activeFeed && !isEditingFeed && activeFeed.subType === 'query_feed'
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="d-flex flex-wrap justify-content-between">
|
||||
<div>
|
||||
{!isEditingFeed && activeFeed && <h4 className="text-primary mb-2 mb-md-0">{activeFeed.name}</h4>}
|
||||
</div>
|
||||
<div className="text-right" data-tour="search-buttons">
|
||||
<Button
|
||||
className="btn-icon mb-2 mb-lg-0 ml-2"
|
||||
color="primary"
|
||||
onClick={this.onNewSearch}
|
||||
>
|
||||
<i className="lnr-plus-circle btn-icon-wrapper"></i>
|
||||
{t('searchTab.newSearchBtn')}
|
||||
</Button>
|
||||
|
||||
{!feedIsLoaded && (
|
||||
<Button
|
||||
className="btn-icon mb-2 mb-lg-0 ml-2"
|
||||
color="success"
|
||||
onClick={this.openSaveFeedPopup}
|
||||
>
|
||||
<i className="lnr-checkmark-circle btn-icon-wrapper"></i>
|
||||
{isSaving ? t('searchTab.savingBtn') : t('searchTab.saveBtn')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{feedIsLoaded && isEditingFeed && (
|
||||
<Button
|
||||
className="btn-icon mb-2 mb-lg-0 ml-2"
|
||||
color="success"
|
||||
onClick={this.saveFeed}
|
||||
>
|
||||
<i className="lnr-checkmark-circle btn-icon-wrapper"></i>
|
||||
{isSaving ? t('searchTab.savingBtn') : t('searchTab.saveBtn')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{feedIsLoaded && isEditingFeed && (
|
||||
<Button
|
||||
className="btn-icon mb-2 mb-lg-0 ml-2"
|
||||
color="success"
|
||||
onClick={this.openSaveFeedPopup}
|
||||
>
|
||||
<i className="lnr-checkmark-circle btn-icon-wrapper"></i>
|
||||
{t('searchTab.saveAsBtn')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{showEditButton && (
|
||||
<Button
|
||||
className="btn-icon mb-2 mb-lg-0 ml-2"
|
||||
color="warning"
|
||||
onClick={this.onEditFeed}
|
||||
>
|
||||
<i className="lnr-pencil btn-icon-wrapper"></i>
|
||||
{t('searchTab.editFeedBtn')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSaveFeedPopupVisible && (
|
||||
<SaveFeedPopup
|
||||
saveType="typeSaveAs"
|
||||
feedCategories={this.props.feedCategories}
|
||||
toggleSaveFeedPopup={this.props.toggleSaveFeedPopup}
|
||||
addAlert={this.props.addAlert}
|
||||
onSaveAsFeed={this.props.onSaveAsFeed}
|
||||
getSidebarCategories={this.props.getSidebarCategories}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SearchSubTabHead)
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import { Button, Input, InputGroup, InputGroupAddon } from 'reactstrap'
|
||||
export class SearchingBlock extends React.Component {
|
||||
static propTypes = {
|
||||
searchResultsErrors: PropTypes.array.isRequired,
|
||||
loadedFeedQuery: PropTypes.string,
|
||||
onSearchQuery: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
onPressEnter = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
this.props.onSearchQuery()
|
||||
}
|
||||
}
|
||||
|
||||
onChangeQuery = (e) => {
|
||||
const { actions } = this.props
|
||||
const value = e.target.value;
|
||||
// replace smart quotation marks with normal
|
||||
let filterQuotes = value.replace(/[\u2018\u2019]/g, '\'').replace(/[\u201C\u201D]/g, '"')
|
||||
// add space before operator if not
|
||||
filterQuotes = filterQuotes.replace(/\s*\+/g, ' +').replace(/\s*\-/g, ' -').trimStart()
|
||||
actions.changeFeedQuery(filterQuotes)
|
||||
}
|
||||
|
||||
render() {
|
||||
let { t, loadedFeedQuery } = this.props
|
||||
loadedFeedQuery = loadedFeedQuery || ''
|
||||
|
||||
return (
|
||||
<div className="search-input-field mb-2">
|
||||
<InputGroup>
|
||||
<Input
|
||||
type="text"
|
||||
value={loadedFeedQuery}
|
||||
data-tour="input-field-search"
|
||||
onChange={this.onChangeQuery}
|
||||
placeholder={t('searchTab.searchInputPlaceholder')}
|
||||
onKeyUp={this.onPressEnter}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
color="primary"
|
||||
className="btn-icon btn-icon-only px-3"
|
||||
data-tour="search-button"
|
||||
onClick={this.props.onSearchQuery}
|
||||
>
|
||||
<i className="lnr-magnifier btn-icon-wrapper font-weight-bold"></i>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SearchingBlock)
|
||||
@@ -0,0 +1,178 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SearchingResultsTopPanel from './SearchingResultsTopPanel'
|
||||
import Article from './Article'
|
||||
import DeleteArticlesPopup from './DeleteArticlesPopup'
|
||||
import EmailArticlesPopup from './EmailArticlesPopup'
|
||||
import CommentArticlePopup from './CommentArticlePopup'
|
||||
import ClipArticlesPopup from './ClipArticles/ClipArticlesPopup'
|
||||
import Pager from '../../../../common/Pager/Pager'
|
||||
import EmailConfirmPopup from './EmailConfirmPopup'
|
||||
import NoRecords from '../../../../common/NoRecords'
|
||||
import Loading from '../../../../common/Loading'
|
||||
import { Interpolate, translate } from 'react-i18next'
|
||||
|
||||
export class SearchingResults extends React.Component {
|
||||
static propTypes = {
|
||||
searchState: PropTypes.object.isRequired,
|
||||
articlesState: PropTypes.object.isRequired,
|
||||
isRefinePanelVisible: PropTypes.bool.isRequired,
|
||||
toggleRefinePanel: PropTypes.func.isRequired,
|
||||
onPager: PropTypes.func.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
forEachArticle = (cb) => {
|
||||
const { searchState, articlesState } = this.props
|
||||
return searchState.searchResults
|
||||
.filter((article) => !articlesState.excludedArticles.includes(article.id))
|
||||
.map(cb)
|
||||
};
|
||||
|
||||
render () {
|
||||
const { searchState, articlesState, actions, t } = this.props
|
||||
const isSearchResultsLoaded = searchState.searchResults.length > 0
|
||||
const numPages = Math.ceil(
|
||||
searchState.searchResultTotalCount / searchState.searchResultLimit
|
||||
)
|
||||
|
||||
const noRecords = searchState.searchResultsPending || !isSearchResultsLoaded || !searchState.isSynced
|
||||
|
||||
if (searchState.searchResultsPending) {
|
||||
return (
|
||||
<div className="search-results">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!searchState.isSynced) {
|
||||
return (
|
||||
<div className="search-results">
|
||||
<NoRecords message={t('searchTab.notSynchronized')} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (searchState.isSynced && !isSearchResultsLoaded) {
|
||||
return (
|
||||
<div className="search-results">
|
||||
<NoRecords message={t('searchTab.noResults')} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="search-results">
|
||||
<SearchingResultsTopPanel
|
||||
noRecords={noRecords}
|
||||
searchResultsCount={searchState.searchResults.length}
|
||||
selectedArticles={searchState.selectedArticles}
|
||||
selectAllArticles={actions.selectAllArticles}
|
||||
showDeleteArticlesPopup={actions.showDeleteArticlesPopup}
|
||||
showEmailArticlesPopup={actions.showEmailArticlesPopup}
|
||||
showClipArticlesPopup={actions.showClipArticlesPopup}
|
||||
isRefinePanelVisible={noRecords ? false : this.props.isRefinePanelVisible}
|
||||
toggleRefinePanel={this.props.toggleRefinePanel}
|
||||
/>
|
||||
|
||||
{isSearchResultsLoaded &&
|
||||
<p className="text-muted font-size-xs">
|
||||
<Interpolate
|
||||
t={t}
|
||||
i18nKey="searchTab.articlesCountDivider"
|
||||
resultsCount={searchState.searchResultCount}
|
||||
totalCount={searchState.searchResultTotalCount}
|
||||
/>
|
||||
</p>
|
||||
}
|
||||
<div className="search-results-block mt-1">
|
||||
{isSearchResultsLoaded &&
|
||||
this.forEachArticle((article, i) => {
|
||||
return (
|
||||
<Article
|
||||
key={'article-' + i}
|
||||
article={article}
|
||||
selectedArticles={searchState.selectedArticles}
|
||||
selectArticle={actions.selectArticle}
|
||||
showDeletePopup={actions.showDeleteArticlesPopup}
|
||||
showEmailPopup={actions.showEmailArticlesPopup}
|
||||
showCommentPopup={actions.showCommentArticlePopup}
|
||||
showClipPopup={actions.showClipArticlesPopup}
|
||||
deleteComment={actions.deleteComment}
|
||||
readArticleLater={actions.readArticleLater}
|
||||
loadMoreComments={actions.loadMoreComments}
|
||||
showShareMenu={actions.showShareMenu}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
{isSearchResultsLoaded && (
|
||||
<Pager
|
||||
pagerAction={this.props.onPager}
|
||||
currentPage={searchState.searchResultPage}
|
||||
numPages={numPages}
|
||||
limitByPage={searchState.searchResultLimit}
|
||||
hideLimitSelector
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{articlesState.deletePopup.visible && (
|
||||
<DeleteArticlesPopup
|
||||
articles={articlesState.deletePopup.articles}
|
||||
hidePopup={actions.hideDeleteArticlesPopup}
|
||||
activeFeed={searchState.activeFeed}
|
||||
deleteArticles={actions.deleteArticles}
|
||||
deleteArticlesFromFeed={actions.deleteArticlesFromFeed}
|
||||
addAlert={actions.addAlert}
|
||||
/>
|
||||
)}
|
||||
|
||||
{articlesState.emailPopup.visible && (
|
||||
<EmailArticlesPopup
|
||||
articlesToEmail={articlesState.emailPopup.articles}
|
||||
emailArticles={actions.emailArticles}
|
||||
hidePopup={actions.hideEmailArticlesPopup}
|
||||
addAlert={actions.addAlert}
|
||||
loadRecipients={actions.loadRecipients}
|
||||
recipients={articlesState.emailPopup.recipients}
|
||||
>
|
||||
{articlesState.emailConfirmPopup.visible && (
|
||||
<EmailConfirmPopup
|
||||
hidePopup={actions.hideEmailConfirmPopup}
|
||||
hideEmailPopup={actions.hideEmailArticlesPopup}
|
||||
sendDocumentsByEmail={actions.sendDocumentsByEmail}
|
||||
/>
|
||||
)}
|
||||
</EmailArticlesPopup>
|
||||
)}
|
||||
|
||||
{articlesState.commentPopup.visible && (
|
||||
<CommentArticlePopup
|
||||
article={articlesState.commentPopup.article}
|
||||
comment={articlesState.commentPopup.comment}
|
||||
commentArticle={actions.commentArticle}
|
||||
updateComment={actions.updateComment}
|
||||
hidePopup={actions.hideCommentArticlePopup}
|
||||
addAlert={actions.addAlert}
|
||||
/>
|
||||
)}
|
||||
|
||||
{articlesState.clipPopup.visible && (
|
||||
<ClipArticlesPopup
|
||||
articles={articlesState.clipPopup.articles}
|
||||
recentClipFeeds={articlesState.recentClipFeeds}
|
||||
getRecentClipFeeds={actions.getRecentClipFeeds}
|
||||
hidePopup={actions.hideClipArticlesPopup}
|
||||
clipArticles={actions.clipArticles}
|
||||
addAlert={actions.addAlert}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SearchingResults)
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ButtonGroup, Button, CustomInput } from 'reactstrap';
|
||||
import { translate } from 'react-i18next';
|
||||
|
||||
export class SearchingResultsTopPanel extends React.Component {
|
||||
static propTypes = {
|
||||
noRecords: PropTypes.bool,
|
||||
selectedArticles: PropTypes.array.isRequired,
|
||||
searchResultsCount: PropTypes.number.isRequired,
|
||||
selectAllArticles: PropTypes.func.isRequired,
|
||||
showDeleteArticlesPopup: PropTypes.func.isRequired,
|
||||
showEmailArticlesPopup: PropTypes.func.isRequired,
|
||||
showClipArticlesPopup: PropTypes.func.isRequired,
|
||||
isRefinePanelVisible: PropTypes.bool.isRequired,
|
||||
toggleRefinePanel: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
onShowClick = (e) => {
|
||||
e.preventDefault();
|
||||
this.props.toggleRefinePanel();
|
||||
};
|
||||
|
||||
selectAllArticles = (e) => {
|
||||
const isChecked = e.target.checked;
|
||||
if (this.props.searchResultsCount > 0) {
|
||||
this.props.selectAllArticles(isChecked);
|
||||
}
|
||||
};
|
||||
|
||||
showDeleteArticlesPopup = () => {
|
||||
if (this.props.selectedArticles.length > 0) {
|
||||
this.props.showDeleteArticlesPopup(this.props.selectedArticles);
|
||||
}
|
||||
};
|
||||
|
||||
showEmailArticlesPopup = () => {
|
||||
if (this.props.selectedArticles.length > 0) {
|
||||
this.props.showEmailArticlesPopup(this.props.selectedArticles);
|
||||
}
|
||||
};
|
||||
|
||||
showClipArticlesPopup = () => {
|
||||
if (this.props.selectedArticles.length > 0) {
|
||||
this.props.showClipArticlesPopup(this.props.selectedArticles);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, searchResultsCount, noRecords } = this.props;
|
||||
const chosenArticlesCount = this.props.selectedArticles.length;
|
||||
const isAllArticlesChosen =
|
||||
this.props.searchResultsCount > 0
|
||||
? searchResultsCount === chosenArticlesCount
|
||||
: false;
|
||||
|
||||
if (noRecords) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="d-flex justify-content-end mb-3 mb-md-0">
|
||||
<ButtonGroup>
|
||||
<Button color="light">
|
||||
<CustomInput
|
||||
id="toggle-all-results"
|
||||
type="checkbox"
|
||||
checked={isAllArticlesChosen}
|
||||
onChange={this.selectAllArticles}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{/* <Button color="secondary">
|
||||
<i className="fa fa-tag mr-2"> </i>
|
||||
{t('searchTab.tagBtn')}
|
||||
</Button> */}
|
||||
|
||||
<Button color="secondary" onClick={this.showClipArticlesPopup}>
|
||||
<i className="fa fa-scissors mr-2"> </i>
|
||||
{t('searchTab.clipBtn')}
|
||||
</Button>
|
||||
<Button color="secondary" onClick={this.showEmailArticlesPopup}>
|
||||
<i className="fa fa-envelope-o mr-2"> </i>
|
||||
{t('searchTab.emailBtn')}
|
||||
</Button>
|
||||
<Button color="secondary" onClick={this.showDeleteArticlesPopup}>
|
||||
<i className="fa fa-trash mr-2"> </i>
|
||||
{t('searchTab.deleteBtn')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
{!this.props.isRefinePanelVisible && (
|
||||
<Button
|
||||
color="light"
|
||||
title="Show refine panel"
|
||||
className="btn-icon ml-3"
|
||||
onClick={this.onShowClick}
|
||||
>
|
||||
<i className="pe-7s-filter btn-icon-wrapper"></i>
|
||||
{t('searchTab.filter')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(
|
||||
SearchingResultsTopPanel
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import {translate} from 'react-i18next'
|
||||
import onClickOutside from 'react-onclickoutside'
|
||||
import {compose} from 'redux'
|
||||
|
||||
class ShareMenu extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
article: PropTypes.object.isRequired,
|
||||
hideMenu: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
handleClickOutside = () => {
|
||||
this.props.hideMenu()
|
||||
};
|
||||
|
||||
_winOpen = (url) => {
|
||||
window.open(url, 'share', 'width=600, height=450, top=0, left=0, toolbar=no')
|
||||
};
|
||||
|
||||
onTweet = () => {
|
||||
this._winOpen('https://twitter.com/intent/tweet?url=' + this.props.article.source.link)
|
||||
this.props.hideMenu()
|
||||
};
|
||||
|
||||
onYammer = () => {
|
||||
this._winOpen('https://www.yammer.com/')
|
||||
this.props.hideMenu()
|
||||
};
|
||||
|
||||
render () {
|
||||
const { t } = this.props
|
||||
|
||||
return (
|
||||
<div className="article-share-menu">
|
||||
<a onClick={this.onTweet}>{t('searchTab.tweet')}</a>
|
||||
<a onClick={this.onYammer}>{t('searchTab.yammer')}</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const applyDecorators = compose(
|
||||
translate(['tabsContent'], {wait: true}),
|
||||
onClickOutside
|
||||
)
|
||||
|
||||
export default applyDecorators(ShareMenu)
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
|
||||
import SubTabWrapper from '../../AppHeader/SubTabWrapper';
|
||||
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
|
||||
import SearchSubTab from './SearchSubTab/SearchSubTab';
|
||||
import SourceIndexSubTab from './SourceIndexSubTab/SourceIndexSubTab';
|
||||
import SourceListsSubTab from './SourceListsSubTab/SourceListsSubTab';
|
||||
|
||||
class SearchTab extends React.Component {
|
||||
static propTypes = {
|
||||
activeTabName: PropTypes.string,
|
||||
match: PropTypes.object,
|
||||
subTabs: PropTypes.array
|
||||
};
|
||||
|
||||
render() {
|
||||
const { activeTabName, subTabs, match } = this.props;
|
||||
return (
|
||||
<CSSTransitionGroup
|
||||
component="div"
|
||||
transitionName="TabsAnimation"
|
||||
transitionAppear
|
||||
transitionAppearTimeout={0}
|
||||
transitionEnter={false}
|
||||
transitionLeave={false}
|
||||
>
|
||||
<SubTabWrapper activeTabName={activeTabName} subTabs={subTabs}>
|
||||
<Switch>
|
||||
<Route path={`${match.url}/search`} component={SearchSubTab} />
|
||||
<Route
|
||||
path={`${match.url}/source-index`}
|
||||
component={SourceIndexSubTab}
|
||||
/>
|
||||
<Route
|
||||
path={`${match.url}/source-lists`}
|
||||
component={SourceListsSubTab}
|
||||
/>
|
||||
<Redirect to={`${match.url}/search`} />
|
||||
</Switch>
|
||||
</SubTabWrapper>
|
||||
</CSSTransitionGroup>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(SearchTab);
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import { Col } from 'reactstrap';
|
||||
|
||||
export class InfoField extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
label: PropTypes.string,
|
||||
labelValue: PropTypes.string,
|
||||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.element])
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, label, children, labelValue } = this.props;
|
||||
|
||||
return (
|
||||
<li className="row">
|
||||
<Col sm="4">
|
||||
<p className="mb-1">{labelValue || t(label)}</p>
|
||||
</Col>
|
||||
<Col sm="8">
|
||||
<p className="mb-1">{children}</p>
|
||||
</Col>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(InfoField);
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate } from 'react-i18next';
|
||||
import PopupLayout from '../../../../common/Popups/PopupLayout';
|
||||
import InfoField from './InfoField';
|
||||
import {
|
||||
capOnlyFirstLetter,
|
||||
getTitle,
|
||||
notNullAndUnd
|
||||
} from '../../../../../common/helper';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
class SourceIndexInfoPopup extends React.Component {
|
||||
static propTypes = {
|
||||
source: PropTypes.object.isRequired,
|
||||
hideSourceInfoPopup: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
render() {
|
||||
let { t, source, hideSourceInfoPopup } = this.props;
|
||||
/*
|
||||
const loc = cl(
|
||||
source.city,
|
||||
source.state,
|
||||
source.country && t(`common:country.${source.country}`)
|
||||
)
|
||||
.split(' ')
|
||||
.join(', '); */
|
||||
/*
|
||||
source = {
|
||||
...source,
|
||||
tags: ['Lorem', 'ipsum', 'dolor', 'ipsum', 'dolor', 'ipsum', 'dolor'],
|
||||
verified: true,
|
||||
followers: 3333,
|
||||
following: 33,
|
||||
favorites: 333,
|
||||
title: 'Title',
|
||||
url: 'URL',
|
||||
type: 'Type',
|
||||
subType: 'Sub Type',
|
||||
lang: 'en',
|
||||
location: 'Washington, DC',
|
||||
country: 'US',
|
||||
spam_probability: '20%',
|
||||
likes: 3
|
||||
}; */
|
||||
|
||||
return (
|
||||
<PopupLayout
|
||||
className="source-info-popup"
|
||||
title="sourceIndexTab.sourceInfoPopupTitle"
|
||||
showFooter={false}
|
||||
onHide={hideSourceInfoPopup}
|
||||
>
|
||||
<ul className="container">
|
||||
<InfoField label="sourceIndexTab.titleLabel">
|
||||
<a href={source.url} target="_blank" rel="noopener noreferrer">
|
||||
{getTitle(source.title)}
|
||||
</a>
|
||||
</InfoField>
|
||||
|
||||
{source.url && (
|
||||
<InfoField label="sourceIndexTab.homeUrl">{source.url}</InfoField>
|
||||
)}
|
||||
|
||||
{source.type && (
|
||||
<InfoField label="sourceIndexTab.mediaType">
|
||||
{capOnlyFirstLetter(source.type)}
|
||||
</InfoField>
|
||||
)}
|
||||
|
||||
{source.subType && (
|
||||
<InfoField labelValue="Sub Type">
|
||||
{capOnlyFirstLetter(source.subType)}
|
||||
</InfoField>
|
||||
)}
|
||||
|
||||
{source.verified && (
|
||||
<InfoField labelValue="Verified">
|
||||
<FontAwesomeIcon
|
||||
title="Source Verified"
|
||||
className="text-primary"
|
||||
icon={faCheckCircle}
|
||||
/>
|
||||
</InfoField>
|
||||
)}
|
||||
|
||||
{source.lang && (
|
||||
<InfoField label="sourceIndexTab.lang">
|
||||
{t(`common:language.${source.lang}`, '-')}
|
||||
</InfoField>
|
||||
)}
|
||||
|
||||
{source.location && (
|
||||
<InfoField labelValue="Location">{source.location}</InfoField>
|
||||
)}
|
||||
|
||||
{source.country && (
|
||||
<InfoField label="sourceIndexTab.country">
|
||||
{t(`common:country.${source.country}`)}
|
||||
</InfoField>
|
||||
)}
|
||||
|
||||
{notNullAndUnd(source.followers) && (
|
||||
<InfoField labelValue="Followers">{source.followers}</InfoField>
|
||||
)}
|
||||
|
||||
{notNullAndUnd(source.following) && (
|
||||
<InfoField labelValue="Following">{source.following}</InfoField>
|
||||
)}
|
||||
|
||||
{notNullAndUnd(source.favorites) && (
|
||||
<InfoField labelValue="Favorites">{source.favorites}</InfoField>
|
||||
)}
|
||||
|
||||
{notNullAndUnd(source.likes) && (
|
||||
<InfoField labelValue="Likes">{source.likes}</InfoField>
|
||||
)}
|
||||
|
||||
{source.tags && source.tags.length > 0 && (
|
||||
<InfoField labelValue="Tags">{source.tags.join(', ')}</InfoField>
|
||||
)}
|
||||
|
||||
{source.spam_probability && (
|
||||
<InfoField labelValue="Spam Probability">
|
||||
{source.spam_probability}
|
||||
</InfoField>
|
||||
)}
|
||||
|
||||
{source.source_profiles && (
|
||||
<InfoField labelValue="Source profiles">
|
||||
{source.source_profiles.join(', ')}
|
||||
</InfoField>
|
||||
)}
|
||||
</ul>
|
||||
</PopupLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SourceIndexInfoPopup);
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import SourceIndexTable from './SourceIndexTable'
|
||||
import SourceIndexUpdatePopup from './SourceIndexUpdatePopup'
|
||||
import FiltersTable from '../../../../common/FiltersTable/FiltersTable'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import reduxConnect from '../../../../../redux/utils/connect'
|
||||
import { compose } from 'redux'
|
||||
import { Button, ButtonGroup, Input, InputGroup, InputGroupAddon } from 'reactstrap'
|
||||
import { setDocumentData } from '../../../../../common/helper'
|
||||
|
||||
class SourceIndexSubTab extends React.Component {
|
||||
static propTypes = {
|
||||
sourcesState: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
setDocumentData('title', 'Source Index | Search')
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
setDocumentData('title')
|
||||
}
|
||||
|
||||
_sourceIndexesState = () => this.props.sourcesState.sourceIndexesState;
|
||||
_sourceLists = () => this.props.sourcesState.sourceListsState.data;
|
||||
|
||||
loadSourceIndexes = (params) => {
|
||||
this.props.actions.getSourceIndexes(params || null)
|
||||
};
|
||||
|
||||
onSearchSources = () => {
|
||||
this.loadSourceIndexes()
|
||||
};
|
||||
|
||||
onEnterSearchInput = (e) => {
|
||||
if (e.keyCode === 13) this.loadSourceIndexes()
|
||||
};
|
||||
|
||||
onChangeSearchInput = (e) => {
|
||||
this.props.actions.setSourceIndexSearchQuery(e.target.value)
|
||||
};
|
||||
|
||||
onFetchData = (params) => {
|
||||
this.loadSourceIndexes(params)
|
||||
};
|
||||
|
||||
showAddToListPopup = () => {
|
||||
const { actions } = this.props
|
||||
const sourceIndexesState = this._sourceIndexesState()
|
||||
if (sourceIndexesState.selectedIds.length === 0) {
|
||||
actions.addAlert({
|
||||
type: 'notice',
|
||||
transKey: 'noListsSelected',
|
||||
id: 'noListsSelected'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
actions.toggleAddSourceToListPopup()
|
||||
};
|
||||
|
||||
onSelectFilter = (groupName, filterValue) => {
|
||||
this.props.actions.selectSourcesFilter(groupName, filterValue)
|
||||
};
|
||||
|
||||
onClearFilters = (groupName) => {
|
||||
this.props.actions.clearSourcesFilters(groupName)
|
||||
};
|
||||
|
||||
onClearAllFilters = () => {
|
||||
this.props.actions.clearAllSourcesFilters()
|
||||
};
|
||||
|
||||
onMoreFilters = (groupName) => {
|
||||
this.props.actions.loadMoreSourcesFilters(groupName)
|
||||
};
|
||||
|
||||
onLessFilters = (groupName) => {
|
||||
this.props.actions.loadLessSourcesFilters(groupName)
|
||||
};
|
||||
|
||||
render () {
|
||||
const { t, actions } = this.props
|
||||
const sourceIndexesState = this._sourceIndexesState()
|
||||
const sourceLists = this._sourceLists()
|
||||
const {
|
||||
searchQuery,
|
||||
selectedIds,
|
||||
chosenListsToAddSources,
|
||||
chosenSourceToUpdate,
|
||||
advancedFilters
|
||||
} = sourceIndexesState
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<InputGroup className="mb-3">
|
||||
<Input
|
||||
type="text"
|
||||
id="source-index-search"
|
||||
placeholder={t('sourceIndexTab.mainInputPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={this.onChangeSearchInput}
|
||||
onKeyUp={this.onEnterSearchInput}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
color="primary"
|
||||
className="btn-icon btn-icon-only"
|
||||
onClick={this.onSearchSources}
|
||||
>
|
||||
<i className="lnr-magnifier btn-icon-wrapper"></i>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
<ButtonGroup className="mb-3">
|
||||
<Button
|
||||
onClick={this.showAddToListPopup}
|
||||
color="secondary"
|
||||
>
|
||||
<i className="fa fa-plus fa-1px for-small mr-1"> </i>{" "}
|
||||
{t('sourceIndexTab.addToSourceListsBtn')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<div className="search-content">
|
||||
<SourceIndexTable
|
||||
tableState={sourceIndexesState}
|
||||
type="sourceIndexesState"
|
||||
onFetch={this.onFetchData}
|
||||
actions={actions}
|
||||
/>
|
||||
|
||||
<FiltersTable
|
||||
filters={advancedFilters.all}
|
||||
pages={advancedFilters.pages}
|
||||
selectedFilters={advancedFilters.selected}
|
||||
clearPending={advancedFilters.pending}
|
||||
callbacks={{
|
||||
selectFilter: this.onSelectFilter,
|
||||
clearFilters: this.onClearFilters,
|
||||
clearAllFilters: this.onClearAllFilters,
|
||||
moreFilters: this.onMoreFilters,
|
||||
lessFilters: this.onLessFilters,
|
||||
refine: this.onSearchSources
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{sourceIndexesState.isAddPopupVisible && (
|
||||
<SourceIndexUpdatePopup
|
||||
type="add"
|
||||
sourceLists={sourceLists}
|
||||
chosenLists={chosenListsToAddSources}
|
||||
chosenSourceIndexes={selectedIds}
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sourceIndexesState.isUpdatePopupVisible && (
|
||||
<SourceIndexUpdatePopup
|
||||
type="update"
|
||||
sourceLists={sourceLists}
|
||||
chosenLists={chosenSourceToUpdate.listIds}
|
||||
chosenSourceIndexes={[chosenSourceToUpdate.id]}
|
||||
updateItemTitle={chosenSourceToUpdate.title}
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const applyDecorators = compose(
|
||||
withRouter,
|
||||
reduxConnect('sourcesState', ['appState', 'sourcesState']),
|
||||
translate(['tabsContent'], { wait: true })
|
||||
)
|
||||
|
||||
export default applyDecorators(SourceIndexSubTab)
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate, Interpolate } from 'react-i18next'
|
||||
|
||||
import Table from '../../../../common/Table/Table'
|
||||
import CheckboxCell from '../../../../common/Table/CheckboxCell'
|
||||
import SortableTh from '../../../../common/Table/SortableTh'
|
||||
import SourceIndexInfoPopup from './SourceIndexInfoPopup'
|
||||
import { Button } from 'reactstrap'
|
||||
import { getTitle } from '../../../../../common/helper'
|
||||
|
||||
export class SourceIndexTable extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
tableState: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
onFetch: PropTypes.func.isRequired,
|
||||
onDeleteIndex: PropTypes.func,
|
||||
actions: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
onFetch = (page, pageSize, sorted) => {
|
||||
const { tableState, onFetch } = this.props
|
||||
const params = {
|
||||
page: page + 1,
|
||||
limit: pageSize,
|
||||
query: tableState.searchQuery
|
||||
}
|
||||
if (sorted.length) {
|
||||
const sortedField = sorted[0]
|
||||
const sort = {
|
||||
field: sortedField.id,
|
||||
direction: sortedField.desc ? 'desc' : 'asc'
|
||||
}
|
||||
params['sort'] = sort
|
||||
}
|
||||
onFetch(params)
|
||||
};
|
||||
|
||||
selectAllAction = (event) => {
|
||||
const { actions } = this.props
|
||||
actions.toggleAllSourceIndexes()
|
||||
};
|
||||
|
||||
selectRowAction = (itemId) => {
|
||||
const { actions } = this.props
|
||||
actions.toggleSourceIndex(itemId) // TODO
|
||||
};
|
||||
|
||||
showUpdateSourcePopup = (source) => (e) => {
|
||||
e.preventDefault()
|
||||
this.props.actions.showUpdateSourcePopup(source)
|
||||
};
|
||||
|
||||
deleteSourceIndex = (source) => (e) => {
|
||||
e.preventDefault()
|
||||
this.props.onDeleteIndex(source)
|
||||
};
|
||||
|
||||
toggleInfoPopup = (source) => () => {
|
||||
const { type, actions } = this.props
|
||||
actions.toggleInfoSourcePopup(type, source)
|
||||
};
|
||||
|
||||
getColumns = () => {
|
||||
const {t, type, tableState} = this.props
|
||||
|
||||
let columns = [
|
||||
{
|
||||
id: 'selectCheckbox',
|
||||
accessor: '',
|
||||
sortable: false,
|
||||
width: 45,
|
||||
className: 'cw-center-cell',
|
||||
headerClassName: 'cw-center-cell',
|
||||
Header: () => {
|
||||
return (
|
||||
<CheckboxCell
|
||||
checked={tableState.isAllSelected}
|
||||
onChange={this.selectAllAction}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Cell: ({original}) => {
|
||||
const isSelected = tableState.selectedIds.includes(original.id)
|
||||
return (
|
||||
<CheckboxCell
|
||||
id={original.id}
|
||||
checked={isSelected}
|
||||
onChange={this.selectRowAction}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}, {
|
||||
Header: <SortableTh title='sourceIndexTab.name' />,
|
||||
accessor: 'name',
|
||||
Cell: ({original}) => {
|
||||
return (
|
||||
<Button
|
||||
color="link"
|
||||
className="btn-anchor"
|
||||
title="Click to see details"
|
||||
onClick={this.toggleInfoPopup(original)}
|
||||
>
|
||||
{getTitle(original.title)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}, {
|
||||
id: 'mediaType',
|
||||
Header: <SortableTh title='sourceIndexTab.mediaType' />,
|
||||
accessor: item => t(`searchTab.sourceTypes.${item.type}`)
|
||||
}, {
|
||||
id: 'country',
|
||||
Header: <SortableTh title='sourceIndexTab.country' />,
|
||||
accessor: item => {
|
||||
return item.country ? t(`common:country.${item.country}`) : ''
|
||||
}
|
||||
}, {
|
||||
id: 'action',
|
||||
Header: t('sourceIndexTab.action'),
|
||||
sortable: false,
|
||||
Cell: ({original}) => {
|
||||
return (
|
||||
<Button
|
||||
outline
|
||||
color="info"
|
||||
className="border-0"
|
||||
size="sm"
|
||||
onClick={this.showUpdateSourcePopup(original)}
|
||||
>
|
||||
<Interpolate
|
||||
i18nKey='sourceIndexTab.actionBtn'
|
||||
listsCount={original.listIds.length}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}, {
|
||||
id: 'deleteAction',
|
||||
Header: t('sourceIndexTab.action'),
|
||||
sortable: false,
|
||||
Cell: ({original}) => {
|
||||
return (
|
||||
<Button
|
||||
outline
|
||||
size="sm"
|
||||
color="secondary"
|
||||
className="border-0"
|
||||
onClick={this.deleteSourceIndex(original)}
|
||||
>
|
||||
{t('sourceListsTab.delete')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const sourceIndexCols = ['selectCheckbox', 'name', 'mediaType', 'country', 'action']
|
||||
const sourceOfListCols = ['name', 'mediaType', 'country', 'deleteAction']
|
||||
let cols = type === 'sourceIndexesState' ? sourceIndexCols : sourceOfListCols
|
||||
return columns.filter(col => cols.includes(col.id) || cols.includes(col.accessor))
|
||||
};
|
||||
|
||||
render () {
|
||||
const {tableState} = this.props
|
||||
const columns = this.getColumns()
|
||||
const infoPopup = tableState.infoPopup
|
||||
|
||||
return (
|
||||
<div className="sources-table">
|
||||
<Table
|
||||
columns={columns}
|
||||
data={tableState.data}
|
||||
totalCount={tableState.totalCount}
|
||||
showTotalCount
|
||||
limit={tableState.limit}
|
||||
page={tableState.page}
|
||||
isLoading={tableState.isLoading}
|
||||
onFetchData={this.onFetch}
|
||||
/>
|
||||
|
||||
{infoPopup.visible && infoPopup.item &&
|
||||
<SourceIndexInfoPopup
|
||||
source={infoPopup.item}
|
||||
hideSourceInfoPopup={this.toggleInfoPopup(null)}
|
||||
/>
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SourceIndexTable)
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate, Interpolate } from 'react-i18next'
|
||||
import PopupLayout from '../../../../common/Popups/PopupLayout'
|
||||
import { CustomInput } from 'reactstrap'
|
||||
import { getTitle } from '../../../../../common/helper'
|
||||
|
||||
export class SourceIndexUpdatePopup extends React.Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
sourceLists: PropTypes.array.isRequired,
|
||||
chosenLists: PropTypes.array.isRequired,
|
||||
chosenSourceIndexes: PropTypes.array.isRequired,
|
||||
updateItemTitle: PropTypes.string,
|
||||
actions: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
componentWillMount = () => {
|
||||
const { sourceLists, actions } = this.props
|
||||
if (sourceLists.length === 0) {
|
||||
actions.getMainSourceLists({page: 1, limit: 50})
|
||||
}
|
||||
};
|
||||
|
||||
onChoseList = (e) => {
|
||||
const { type, chosenLists, actions } = this.props
|
||||
const isChecked = e.target.checked
|
||||
const listId = parseInt(e.target.dataset.listId)
|
||||
const lists = isChecked ? chosenLists.concat(listId) : chosenLists.filter((id) => listId !== id)
|
||||
const action = type === 'add' ? actions.setChosenListsToAddSources : actions.setChosenListsToUpdateSources
|
||||
|
||||
action(lists)
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const { actions, chosenSourceIndexes, chosenLists, type } = this.props
|
||||
|
||||
actions.addSourcesToList({
|
||||
sources: chosenSourceIndexes,
|
||||
sourceLists: chosenLists
|
||||
}, type === 'add')
|
||||
};
|
||||
|
||||
getBodyTitle () {
|
||||
const { t, type, updateItemTitle } = this.props
|
||||
if (type === 'add') {
|
||||
return <p className="mb-3">{t('sourceListsTab.popup.addToListDesc')}</p>
|
||||
}
|
||||
else {
|
||||
return (
|
||||
<p className="mb-3">
|
||||
<Interpolate
|
||||
i18nKey='sourceListsTab.popup.updateListDesc'
|
||||
name={getTitle(updateItemTitle)}
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { type, sourceLists, chosenLists, actions } = this.props
|
||||
const isAdd = type === 'add'
|
||||
const title = isAdd ? 'addToListTitle' : 'updateListTitle'
|
||||
const submitText = isAdd ? 'addBtn' : 'saveBtn'
|
||||
const hideAction = isAdd ? actions.toggleAddSourceToListPopup : actions.hideUpdateSourcePopup
|
||||
|
||||
return (
|
||||
<PopupLayout
|
||||
title={`sourceListsTab.popup.${title}`}
|
||||
submitText={`sourceListsTab.popup.${submitText}`}
|
||||
onHide={hideAction}
|
||||
onSubmit={this.onSubmit}
|
||||
>
|
||||
<div>
|
||||
{this.getBodyTitle()}
|
||||
|
||||
{sourceLists.length > 0 &&
|
||||
<ul className="row">
|
||||
{sourceLists.map((list, i) => {
|
||||
const isListChosen = chosenLists.includes(list.id)
|
||||
return (
|
||||
<li key={i} className="col-md-4 col-sm-6 mb-2">
|
||||
<CustomInput
|
||||
type="checkbox"
|
||||
id={'sourceListCheck-' + i}
|
||||
className="d-flex"
|
||||
data-list-id={list.id}
|
||||
checked={isListChosen}
|
||||
onChange={this.onChoseList}
|
||||
label={list.name}
|
||||
/>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
</div>
|
||||
</PopupLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(SourceIndexUpdatePopup)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import SourceListsAddPopup from './SourceListsAddPopup'
|
||||
import SourceListsDeletePopup from './SourceListsDeletePopup'
|
||||
import SourceListsRenamePopup from './SourceListsRenamePopup'
|
||||
import SourceListsClonePopup from './SourceListsClonePopup'
|
||||
import SourceListsTable from './SourceListsTable'
|
||||
import { Button, CustomInput } from 'reactstrap'
|
||||
|
||||
export class SourceLists extends React.Component {
|
||||
static propTypes = {
|
||||
sourceListsState: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
onGlobalOnlyClick = () => {
|
||||
const { actions, sourceListsState } = this.props
|
||||
actions.toggleOnlyGlobal()
|
||||
const params = {
|
||||
page: sourceListsState.page,
|
||||
limit: sourceListsState.limit,
|
||||
onlyShared: !sourceListsState.onlyGlobal,
|
||||
sort: {
|
||||
field: sourceListsState.sortByField,
|
||||
direction: sourceListsState.sortDirection
|
||||
}
|
||||
}
|
||||
actions.getMainSourceLists(params)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t, sourceListsState, actions } = this.props
|
||||
const {
|
||||
isAddListPopupVisible,
|
||||
isDeletePopupVisible,
|
||||
isRenameListPopupVisible,
|
||||
isCloneListPopupVisible,
|
||||
listToEdit
|
||||
} = sourceListsState
|
||||
|
||||
return (
|
||||
<div className="source-lists-tab">
|
||||
<div className="d-flex justify-content-between align-items-end flex-wrap-reverse flex-sm-nowrap">
|
||||
<CustomInput
|
||||
id="show-global"
|
||||
type="checkbox"
|
||||
className="d-flex mb-3"
|
||||
checked={sourceListsState.onlyGlobal}
|
||||
onChange={this.onGlobalOnlyClick}
|
||||
label={t('sourceListsTab.showGlobalCheck')}
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
className="btn-icon mb-3"
|
||||
onClick={actions.toggleAddListPopup}
|
||||
>
|
||||
<i className="lnr lnr-plus-circle btn-icon-wrapper" />
|
||||
{t('sourceListsTab.addListBtn')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SourceListsTable tableState={sourceListsState} actions={actions} />
|
||||
|
||||
{isAddListPopupVisible && (
|
||||
<SourceListsAddPopup
|
||||
toggleAddListPopup={actions.toggleAddListPopup}
|
||||
addSourceList={actions.addSourceList}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDeletePopupVisible && (
|
||||
<SourceListsDeletePopup
|
||||
listToEdit={listToEdit}
|
||||
toggleDeleteListPopup={actions.toggleDeleteListPopup}
|
||||
deleteSourceList={actions.deleteSourceList}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isRenameListPopupVisible && (
|
||||
<SourceListsRenamePopup
|
||||
listToEdit={listToEdit}
|
||||
toggleRenameListPopup={actions.toggleRenameListPopup}
|
||||
renameSourceList={actions.renameSourceList}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isCloneListPopupVisible && (
|
||||
<SourceListsClonePopup
|
||||
listToEdit={listToEdit}
|
||||
toggleCloneListPopup={actions.toggleCloneListPopup}
|
||||
cloneSourceList={actions.cloneSourceList}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SourceLists)
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import PopupLayout from '../../../../common/Popups/PopupLayout'
|
||||
import { FormGroup, Input, Label } from 'reactstrap'
|
||||
|
||||
export class SourceListsAddPopup extends React.Component {
|
||||
static propTypes = {
|
||||
toggleAddListPopup: PropTypes.func.isRequired,
|
||||
addSourceList: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
state = {
|
||||
name: ''
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
const { addSourceList } = this.props
|
||||
addSourceList(this.state.name)
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
const { value } = e.target
|
||||
this.setState({ name: value })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { toggleAddListPopup, t } = this.props
|
||||
|
||||
return (
|
||||
<PopupLayout
|
||||
title="Add a List"
|
||||
submitText="Submit"
|
||||
onHide={toggleAddListPopup}
|
||||
onSubmit={this.onSubmit}
|
||||
>
|
||||
<div>
|
||||
<FormGroup>
|
||||
<Label>{t('sourceListsTab.popup.enterListName')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={this.state.name}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
</PopupLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
SourceListsAddPopup
|
||||
)
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import PopupLayout from '../../../../common/Popups/PopupLayout'
|
||||
import { FormGroup, Input, Label } from 'reactstrap'
|
||||
|
||||
export class SourceListsClonePopup extends React.Component {
|
||||
static propTypes = {
|
||||
listToEdit: PropTypes.func.isRequired,
|
||||
toggleCloneListPopup: PropTypes.func.isRequired,
|
||||
cloneSourceList: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
name:
|
||||
props.listToEdit && props.listToEdit.name
|
||||
? `${props.listToEdit.name} (copy)`
|
||||
: ''
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
const { value } = e.target
|
||||
this.setState({
|
||||
name: value
|
||||
})
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
const { listToEdit, cloneSourceList } = this.props
|
||||
cloneSourceList({
|
||||
id: listToEdit.id,
|
||||
name: this.state.name
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { toggleCloneListPopup, t } = this.props
|
||||
|
||||
return (
|
||||
<PopupLayout
|
||||
title="Clone"
|
||||
submitText="sourceListsTab.popup.cloneListSubmitBtn"
|
||||
onHide={toggleCloneListPopup}
|
||||
onSubmit={this.onSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<Label>{t('sourceListsTab.popup.renameListTitle')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={this.state.name}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</PopupLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
SourceListsClonePopup
|
||||
)
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate, Interpolate } from 'react-i18next'
|
||||
import PopupLayout from '../../../../common/Popups/PopupLayout'
|
||||
import { getTitle } from '../../../../../common/helper';
|
||||
|
||||
export class SourceListsDeletePopup extends React.Component {
|
||||
static propTypes = {
|
||||
listToEdit: PropTypes.func.isRequired,
|
||||
toggleDeleteListPopup: PropTypes.func.isRequired,
|
||||
deleteSourceList: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
const { listToEdit, deleteSourceList } = this.props
|
||||
deleteSourceList(listToEdit)
|
||||
};
|
||||
|
||||
render () {
|
||||
const { listToEdit, toggleDeleteListPopup } = this.props
|
||||
const value = listToEdit.name || listToEdit.title || ''
|
||||
|
||||
return (
|
||||
<PopupLayout
|
||||
title='sourceListsTab.popup.deleteListTitle'
|
||||
submitText='Delete'
|
||||
onHide={toggleDeleteListPopup}
|
||||
onSubmit={this.onSubmit}
|
||||
submitColor="danger"
|
||||
>
|
||||
<Interpolate
|
||||
i18nKey='sourceListsTab.popup.deleteListDesc'
|
||||
name={getTitle(value)}
|
||||
/>
|
||||
</PopupLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(SourceListsDeletePopup)
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import PopupLayout from '../../../../common/Popups/PopupLayout'
|
||||
import { FormGroup, Input, Label } from 'reactstrap'
|
||||
|
||||
export class SourceListsRenamePopup extends React.Component {
|
||||
static propTypes = {
|
||||
listToEdit: PropTypes.func.isRequired,
|
||||
toggleRenameListPopup: PropTypes.func.isRequired,
|
||||
renameSourceList: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
name: (props.listToEdit && props.listToEdit.name) || ''
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
const { value } = e.target
|
||||
this.setState({
|
||||
name: value
|
||||
})
|
||||
}
|
||||
|
||||
onSubmit = () => {
|
||||
const { listToEdit, renameSourceList } = this.props
|
||||
const data = {
|
||||
id: listToEdit.id,
|
||||
name: this.state.name
|
||||
}
|
||||
|
||||
renameSourceList(data, listToEdit.name)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { toggleRenameListPopup, t } = this.props
|
||||
|
||||
return (
|
||||
<PopupLayout
|
||||
title="Rename"
|
||||
submitText="sourceListsTab.popup.renameListSubmitBtn"
|
||||
onHide={toggleRenameListPopup}
|
||||
onSubmit={this.onSubmit}
|
||||
>
|
||||
<FormGroup>
|
||||
<Label>{t('sourceListsTab.popup.renameListTitle')}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={this.state.name}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</PopupLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent', 'common'], { wait: true })(
|
||||
SourceListsRenamePopup
|
||||
)
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import React, { Fragment } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import SourceLists from './SourceLists'
|
||||
import SourcesOfList from './SourcesOfList'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import reduxConnect from '../../../../../redux/utils/connect'
|
||||
import { compose } from 'redux'
|
||||
import { setDocumentData } from '../../../../../common/helper'
|
||||
|
||||
class SourceListsSubTab extends React.Component {
|
||||
static propTypes = {
|
||||
sourcesState: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
setDocumentData('title', 'Source Lists | Search')
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
setDocumentData('title')
|
||||
}
|
||||
|
||||
render() {
|
||||
const { sourcesState, actions } = this.props
|
||||
const { sourcesOfListState, sourceListsState } = sourcesState
|
||||
const sourcesOfListVisible = sourcesOfListState.isSourcesOfListVisible
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{!sourcesOfListVisible && (
|
||||
<SourceLists sourceListsState={sourceListsState} actions={actions} />
|
||||
)}
|
||||
|
||||
{sourcesOfListVisible && (
|
||||
<SourcesOfList
|
||||
sourcesOfListState={sourcesOfListState}
|
||||
actions={actions}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const applyDecorators = compose(
|
||||
withRouter,
|
||||
reduxConnect('sourcesState', ['appState', 'sourcesState'])
|
||||
)
|
||||
|
||||
export default applyDecorators(SourceListsSubTab)
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import moment from 'moment'
|
||||
|
||||
import Table from '../../../../common/Table/Table'
|
||||
import SortableTh from '../../../../common/Table/SortableTh'
|
||||
import { Button } from 'reactstrap'
|
||||
|
||||
export class SourceListsTable extends React.Component {
|
||||
static propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
tableState: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired
|
||||
}
|
||||
|
||||
onFetch = (page, pageSize, sorted) => {
|
||||
const { actions, tableState } = this.props
|
||||
const params = {
|
||||
page: page + 1,
|
||||
limit: pageSize,
|
||||
onlyShared: tableState.onlyGlobal
|
||||
}
|
||||
if (sorted.length) {
|
||||
const sortedField = sorted[0]
|
||||
const sort = {
|
||||
field: sortedField.id,
|
||||
direction: sortedField.desc ? 'desc' : 'asc'
|
||||
}
|
||||
params['sort'] = sort
|
||||
}
|
||||
actions.getMainSourceLists(params)
|
||||
}
|
||||
|
||||
showDeleteListPopup = (item) => () => {
|
||||
this.props.actions.toggleDeleteListPopup(item)
|
||||
}
|
||||
|
||||
showRenameListPopup = (item) => () => {
|
||||
this.props.actions.toggleRenameListPopup(item)
|
||||
}
|
||||
|
||||
showCloneListPopup = (item) => () => {
|
||||
this.props.actions.toggleCloneListPopup(item)
|
||||
}
|
||||
|
||||
showSourcesOfList = (item) => () => {
|
||||
this.props.actions.showSourcesOfList(item)
|
||||
}
|
||||
|
||||
onShareList = (id) => () => {
|
||||
this.props.actions.shareSourceList(id)
|
||||
}
|
||||
|
||||
onUnshareList = (id) => () => {
|
||||
this.props.actions.unshareSourceList(id)
|
||||
}
|
||||
|
||||
getColumns() {
|
||||
const { t } = this.props
|
||||
|
||||
let columns = [
|
||||
{
|
||||
Header: <SortableTh title="sourceListsTab.tableLabels.name" />,
|
||||
accessor: 'name',
|
||||
Cell: ({ original }) => {
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
onClick={this.showSourcesOfList(original)}
|
||||
>
|
||||
{original.name}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'sources',
|
||||
Header: <SortableTh title="sourceListsTab.tableLabels.sources" />,
|
||||
accessor: (item) => item.sourceNumber
|
||||
},
|
||||
{
|
||||
id: 'createdBy',
|
||||
Header: <SortableTh title="sourceListsTab.tableLabels.createdBy" />,
|
||||
accessor: (item) => `${item.user.firstName} ${item.user.lastName}`
|
||||
},
|
||||
{
|
||||
id: 'lastUpdated',
|
||||
Header: <SortableTh title="sourceListsTab.tableLabels.lastUpdated" />,
|
||||
accessor: (item) =>
|
||||
item.updatedAt && moment(item.updatedAt).format('Do MMM YYYY')
|
||||
},
|
||||
{
|
||||
id: 'lastUpdatedBy',
|
||||
Header: <SortableTh title="sourceListsTab.tableLabels.lastUpdatedBy" />,
|
||||
accessor: (item) =>
|
||||
item.updatedBy &&
|
||||
`${item.updatedBy.firstName} ${item.updatedBy.lastName}`
|
||||
},
|
||||
{
|
||||
id: 'action',
|
||||
Header: t('sourceIndexTab.action'),
|
||||
// sortable: false,
|
||||
minWidth: 220,
|
||||
Cell: ({ original }) => {
|
||||
return (
|
||||
// <UncontrolledButtonDropdown>
|
||||
// <DropdownToggle
|
||||
// // caret
|
||||
// // className="btn-icon btn-icon-only btn btn-link"
|
||||
// color="link"
|
||||
// >
|
||||
// <i className="lnr-menu-circle btn-icon-wrapper" />
|
||||
// </DropdownToggle>
|
||||
// <DropdownMenu>
|
||||
// <h2>Hello</h2>
|
||||
// {/* <DropdownItem onClick={this.onUnshareList(original.id)}>
|
||||
// <i className="dropdown-icon lnr-inbox"> </i>
|
||||
// <span>{t("sourceListsTab.unshare")}</span>
|
||||
// </DropdownItem>
|
||||
// <DropdownItem onClick={this.onShareList(original.id)}>
|
||||
// <i className="dropdown-icon lnr-file-empty"> </i>
|
||||
// <span>{t("sourceListsTab.share")}</span>
|
||||
// </DropdownItem>
|
||||
// <DropdownItem onClick={this.showRenameListPopup(original)}>
|
||||
// <i className="dropdown-icon lnr-book"> </i>
|
||||
// <span>{t("sourceListsTab.rename")}</span>
|
||||
// </DropdownItem>
|
||||
// <DropdownItem onClick={this.showCloneListPopup(original)}>
|
||||
// <i className="dropdown-icon lnr-picture"> </i>
|
||||
// <span>{t("sourceListsTab.clone")}</span>
|
||||
// </DropdownItem>
|
||||
// <DropdownItem onClick={this.showDeleteListPopup(original)}>
|
||||
// <i className="dropdown-icon lnr-picture"> </i>
|
||||
// <span>{t("sourceListsTab.delete")}</span>
|
||||
// </DropdownItem> */}
|
||||
// </DropdownMenu>
|
||||
// </UncontrolledButtonDropdown>
|
||||
|
||||
// <div className="d-block w-100 text-center">
|
||||
// <UncontrolledButtonDropdown>
|
||||
// <DropdownToggle
|
||||
// caret
|
||||
// className="btn-icon btn-icon-only btn btn-link"
|
||||
// color="link"
|
||||
// >
|
||||
// <i className="lnr-menu-circle btn-icon-wrapper" />
|
||||
// </DropdownToggle>
|
||||
// <DropdownMenu className="rm-pointers dropdown-menu-hover-link">
|
||||
// <DropdownItem onClick={this.onUnshareList(original.id)}>
|
||||
// <i className="dropdown-icon lnr-inbox"> </i>
|
||||
// <span>{t("sourceListsTab.unshare")}</span>
|
||||
// </DropdownItem>
|
||||
// <DropdownItem onClick={this.onShareList(original.id)}>
|
||||
// <i className="dropdown-icon lnr-file-empty"> </i>
|
||||
// <span>{t("sourceListsTab.share")}</span>
|
||||
// </DropdownItem>
|
||||
// <DropdownItem onClick={this.showRenameListPopup(original)}>
|
||||
// <i className="dropdown-icon lnr-book"> </i>
|
||||
// <span>{t("sourceListsTab.rename")}</span>
|
||||
// </DropdownItem>
|
||||
// <DropdownItem onClick={this.showCloneListPopup(original)}>
|
||||
// <i className="dropdown-icon lnr-picture"> </i>
|
||||
// <span>{t("sourceListsTab.clone")}</span>
|
||||
// </DropdownItem>
|
||||
// <DropdownItem onClick={this.showDeleteListPopup(original)}>
|
||||
// <i className="dropdown-icon lnr-picture"> </i>
|
||||
// <span>{t("sourceListsTab.delete")}</span>
|
||||
// </DropdownItem>
|
||||
// </DropdownMenu>
|
||||
// </UncontrolledButtonDropdown>
|
||||
// </div>
|
||||
<div>
|
||||
<Button
|
||||
outline
|
||||
size="sm"
|
||||
color="info"
|
||||
className="border-0"
|
||||
onClick={
|
||||
original.shared
|
||||
? this.onUnshareList(original.id)
|
||||
: this.onShareList(original.id)
|
||||
}
|
||||
>
|
||||
{original.shared
|
||||
? t('sourceListsTab.unshare')
|
||||
: t('sourceListsTab.share')}
|
||||
</Button>
|
||||
<Button
|
||||
outline
|
||||
size="sm"
|
||||
color="info"
|
||||
className="border-0"
|
||||
onClick={this.showRenameListPopup(original)}
|
||||
>
|
||||
{t('sourceListsTab.rename')}
|
||||
</Button>
|
||||
<Button
|
||||
outline
|
||||
size="sm"
|
||||
color="info"
|
||||
className="border-0"
|
||||
onClick={this.showCloneListPopup(original)}
|
||||
>
|
||||
{t('sourceListsTab.clone')}
|
||||
</Button>
|
||||
<Button
|
||||
outline
|
||||
size="sm"
|
||||
color="secondary"
|
||||
className="border-0"
|
||||
onClick={this.showDeleteListPopup(original)}
|
||||
>
|
||||
{t('sourceListsTab.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const cols = [
|
||||
'name',
|
||||
'sources',
|
||||
'createdBy',
|
||||
'lastUpdated',
|
||||
'lastUpdatedBy',
|
||||
'action'
|
||||
]
|
||||
return columns.filter(
|
||||
(col) => cols.includes(col.id) || cols.includes(col.accessor)
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tableState } = this.props
|
||||
const columns = this.getColumns()
|
||||
|
||||
return (
|
||||
<div className="sources-table">
|
||||
<Table
|
||||
columns={columns}
|
||||
data={tableState.data}
|
||||
totalCount={tableState.totalCount}
|
||||
showTotalCount
|
||||
limit={tableState.limit}
|
||||
page={tableState.page}
|
||||
isLoading={tableState.isLoading}
|
||||
onFetchData={this.onFetch}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SourceListsTable)
|
||||
@@ -0,0 +1,111 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { translate } from 'react-i18next'
|
||||
import SourceIndexTable from '../SourceIndexSubTab/SourceIndexTable'
|
||||
import SourceListsDeletePopup from './SourceListsDeletePopup'
|
||||
import { Button, Input, InputGroup, InputGroupAddon } from 'reactstrap'
|
||||
|
||||
export class SourcesOfList extends React.Component {
|
||||
static propTypes = {
|
||||
sourcesOfListState: PropTypes.object.isRequired,
|
||||
actions: PropTypes.object.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
componentWillMount = () => {
|
||||
this.searchSources('')
|
||||
};
|
||||
|
||||
searchSources = (query) => {
|
||||
const { actions, sourcesOfListState } = this.props
|
||||
|
||||
actions.getSourcesOfList(sourcesOfListState.visibleList.id, {
|
||||
query: query,
|
||||
page: sourcesOfListState.page,
|
||||
limit: sourcesOfListState.limit
|
||||
})
|
||||
};
|
||||
|
||||
onSearchSources = () => {
|
||||
const query = this.props.sourcesOfListState.searchQuery
|
||||
this.searchSources(query)
|
||||
};
|
||||
|
||||
onEnterSearchInput = (e) => {
|
||||
if (e.keyCode === 13) this.onSearchSources()
|
||||
};
|
||||
|
||||
onChangeSearchInput = (e) => {
|
||||
const val = e.target.value
|
||||
this.props.actions.setSourcesOfListSearchQuery(val)
|
||||
};
|
||||
|
||||
onFetchData = (params) => {
|
||||
const { sourcesOfListState, actions } = this.props
|
||||
actions.getSourcesOfList(sourcesOfListState.visibleList.id, params)
|
||||
};
|
||||
|
||||
onDeleteIndex = (source) => {
|
||||
const { sourcesOfListState, actions } = this.props
|
||||
const listId = sourcesOfListState.visibleList.id
|
||||
|
||||
actions.updateListSources({
|
||||
id: source.id,
|
||||
sourceLists: source.listIds.filter(id => id !== listId)
|
||||
})
|
||||
};
|
||||
|
||||
render () {
|
||||
const { t, sourcesOfListState, actions } = this.props
|
||||
const { searchQuery, visibleList, isDeletePopupVisible, listToEdit } = sourcesOfListState
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button className="btn-wide mb-3" size="sm" color="info" onClick={actions.hideSourcesOfList}>
|
||||
<i className="lnr lnr-chevron-left"> </i>
|
||||
</Button>
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-primary text-uppercase font-weight-bold mb-2">{visibleList.name} ({visibleList.sourceNumber})</p>
|
||||
<InputGroup>
|
||||
<Input
|
||||
id="source-index-search"
|
||||
placeholder={t('sourceIndexTab.mainInputPlaceholder')}
|
||||
value={searchQuery}
|
||||
onChange={this.onChangeSearchInput}
|
||||
onKeyUp={this.onEnterSearchInput}
|
||||
/>
|
||||
<InputGroupAddon addonType="append">
|
||||
<Button
|
||||
color="primary"
|
||||
className="btn-icon btn-icon-only"
|
||||
onClick={this.onSearchSources}
|
||||
>
|
||||
<i className="lnr-magnifier btn-icon-wrapper"></i>
|
||||
</Button>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
<SourceIndexTable
|
||||
tableState={sourcesOfListState}
|
||||
type='sourcesOfListState'
|
||||
onFetch={this.onFetchData}
|
||||
onDeleteIndex={actions.toggleDeleteListIndexPopup}
|
||||
actions={actions}
|
||||
/>
|
||||
|
||||
{isDeletePopupVisible &&
|
||||
<SourceListsDeletePopup
|
||||
listToEdit={listToEdit}
|
||||
toggleDeleteListPopup={actions.toggleDeleteListIndexPopup}
|
||||
deleteSourceList={this.onDeleteIndex}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(['tabsContent'], { wait: true })(SourcesOfList)
|
||||
|
||||
Reference in New Issue
Block a user