at the end of the day, it was inevitable

This commit is contained in:
Mo Elzubeir
2022-12-09 08:36:26 -06:00
commit 1218570914
1768 changed files with 887087 additions and 0 deletions
@@ -0,0 +1,56 @@
import React from 'react';
import PropTypes from 'prop-types';
import CSSTransitionGroup from 'react-transition-group/CSSTransitionGroup';
import SubTabWrapper from '../../AppHeader/SubTabWrapper';
import { Redirect, Route, Switch, withRouter } from 'react-router-dom';
import ShowCharts from './CreateAnalysisSubTab/ShowCharts';
import SavedAnalysisSubTab from './SavedAnalysisSubTab/SavedAnalysisSubTab';
import CreateAnalysisSubTab from './CreateAnalysisSubTab/CreateAnalysisSubTab';
function AnalyzeTab(props) {
const { subTabs, allowAnalytics, history, activeTabName, match } = props;
if (!allowAnalytics) {
history.push('/app/search/search');
return null;
}
return (
<CSSTransitionGroup
component="div"
transitionName="TabsAnimation"
transitionAppear
transitionAppearTimeout={0}
transitionEnter={false}
transitionLeave={false}
>
<SubTabWrapper activeTabName={activeTabName} subTabs={subTabs}>
<Switch>
{/* <Route path={`${match.url}/welcome`} component={WelcomeSubTab} /> */}
<Route path={`${match.url}/saved`} component={SavedAnalysisSubTab} />
<Route
path={`${match.url}/create`}
component={CreateAnalysisSubTab}
/>
<Route
path={`${match.url}/edit/:id`}
component={CreateAnalysisSubTab}
/>
<Route path={`${match.url}/:id`} component={ShowCharts} />
<Redirect to={`${match.url}/saved`} />
</Switch>
</SubTabWrapper>
</CSSTransitionGroup>
);
}
AnalyzeTab.propTypes = {
activeTabName: PropTypes.string,
children: PropTypes.any,
history: PropTypes.object,
match: PropTypes.object,
allowAnalytics: PropTypes.bool,
subTabs: PropTypes.array
};
export default withRouter(AnalyzeTab);
@@ -0,0 +1,293 @@
import React, { Fragment, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
Button,
Form,
FormGroup,
Label,
Modal,
ModalBody,
ModalFooter,
ModalHeader
} from 'reactstrap';
import Select from 'react-select';
import { Input, Checkbox, RadioButton } from '../../../../common/FormControls';
import useForm from '../../../../common/hooks/useForm.js';
import { EXTRAS } from '../../../../../redux/modules/appState/share/forms/alertForm';
import { createAlertAPI } from '../../../../../api/analytics/createAnalytics';
import { getCurrentTimezone, timezones } from '../../../../../common/Timezones';
import { compose } from 'redux';
import reduxConnect from '../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { THEME_TYPES } from '../../../../../redux/modules/appState/share/forms/notificationForm';
const initialForm = {
name: '',
recipients: [],
subject: '',
automatedSubject: false,
unsubscribeNotification: false,
published: false,
allowUnsubscribe: false,
articleExtracts: EXTRAS.CONTEXTUAL,
highlight: false,
showSourceCountry: false,
showUserComments: false,
themeType: THEME_TYPES.PLAIN,
sendWhenEmpty: false,
timezone: getCurrentTimezone(),
notificationType: 'alert',
// automatic: [], // auto schedule
// sentUntil: '',
errors: {
name: null
}
};
function AlertDialog(props) {
const { toggle, isOpen, alertCharts, actions, resetAlertChart, user } = props;
const [loading, setLoading] = useState(false);
const {
form,
handleChange,
handleValidation,
errors,
validateSubmit,
resetForm
} = useForm(initialForm);
function handleSubmit() {
const obj = validateSubmit();
if (!obj) {
return actions.addAlert({ type: 'error', transKey: 'requiredInfo' });
}
setLoading(true);
if (obj.automatedSubject) {
delete obj.subject;
}
obj.sources = alertCharts.map((chart) => ({
id: chart.id,
type: 'chart'
}));
createAlertAPI(obj).then((res) => {
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
actions.addAlert({ type: 'notice', transKey: 'alertSaved' });
setLoading(false);
toggle();
resetForm();
resetAlertChart();
});
}
useEffect(() => {
if (form.recipients && user.recipient && user.recipient.id) {
handleChange('recipients', [user.recipient.id]);
}
return () => resetForm();
}, []);
return (
<Modal isOpen={isOpen} toggle={toggle} backdrop="static" size="lg">
<ModalHeader toggle={toggle}>Create Alert</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<Label>Selected Charts</Label>
<div className="b-radius-5 bg-light p-2">
{alertCharts.map((chart, i, arr) => (
<Fragment key={chart.name}>
<span className="d-inline-block mr-1">
{chart.name}
{arr.length - 1 !== i ? ', ' : ''}
</span>
</Fragment>
))}
</div>
</FormGroup>
<Input
name="name"
title="Name"
required
value={form.name}
error={errors.name}
handleChange={handleChange}
handleValidation={handleValidation}
/>
<Checkbox
name="automatedSubject"
title="Automated Subject"
description="Use automated email subject based on the feeds"
value={form.automatedSubject}
error={errors.automatedSubject}
handleChange={handleChange}
/>
{!form.automatedSubject && (
<Input
name="subject"
title="Email Subject"
value={form.subject}
error={errors.subject}
handleChange={handleChange}
handleValidation={handleValidation}
/>
)}
<Checkbox
name="published"
title="Publish"
description="Alerts and Newsletters that are Published are available for other users to subscribe"
value={form.published}
error={errors.publish}
handleChange={handleChange}
/>
<Checkbox
name="allowUnsubscribe"
title="Unsubscribe Link"
description="Allow recipients to unsubscribe from Alert"
value={form.allowUnsubscribe}
error={errors.allowUnsubscribe}
handleChange={handleChange}
/>
<Checkbox
name="unsubscribeNotification"
title="Notifications"
description="Notify creator when recipients unsubscribe"
value={form.unsubscribeNotification}
error={errors.unsubscribeNotification}
handleChange={handleChange}
/>
<FormGroup className="radio-options">
<Label>Options</Label>
<RadioButton
name="articleExtracts"
title="Article Extracts"
formClass="mb-0"
options={[
{ label: 'Contextual extract', value: EXTRAS.CONTEXTUAL },
{ label: 'Start of text extract', value: EXTRAS.START },
{ label: 'No article extract', value: EXTRAS.NO }
]}
inline
value={form.articleExtracts}
error={errors.articleExtracts}
handleChange={handleChange}
/>
<RadioButton
name="highlight"
title="Highlight Keywords"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.highlight}
error={errors.highlight}
handleChange={handleChange}
/>
<RadioButton
name="showSourceCountry"
title="Show Source Country"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.showSourceCountry}
error={errors.showSourceCountry}
handleChange={handleChange}
/>
<RadioButton
name="showUserComments"
title="Show User Comments"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.showUserComments}
error={errors.showUserComments}
handleChange={handleChange}
/>
<RadioButton
name="themeType"
title="Layout"
formClass="mb-0"
options={[
{ label: 'Enhanced HTML', value: THEME_TYPES.ENHANCED },
{ label: 'Plain HTML', value: THEME_TYPES.PLAIN }
]}
inline
value={form.themeType}
error={errors.themeType}
handleChange={handleChange}
/>
<RadioButton
name="sendWhenEmpty"
title="Send When Empty"
formClass="mb-0"
options={[
{ label: 'Yes', value: true },
{ label: 'No', value: false }
]}
inline
value={form.sendWhenEmpty}
error={errors.sendWhenEmpty}
handleChange={handleChange}
/>
</FormGroup>
<FormGroup>
<Label>Timezone</Label>
<Select
className="timezone-select"
value={form.timezone}
options={timezones}
clearable={false}
onChange={function (v) {
handleChange('timezone', v.value);
}}
/>
</FormGroup>
{/* <FormGroup>
<Label>Automatic</Label>
<Scheduling state={state.scheduling} actions={actions} />
</FormGroup> */}
</Form>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={toggle}>
Cancel
</Button>
<Button color="primary" disabled={loading} onClick={handleSubmit}>
{loading ? 'Loading...' : 'Submit'}
</Button>
</ModalFooter>
</Modal>
);
}
AlertDialog.propTypes = {
toggle: PropTypes.func,
resetAlertChart: PropTypes.func,
isOpen: PropTypes.bool,
alertCharts: PropTypes.array,
user: PropTypes.object,
actions: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('user', ['common', 'auth', 'user']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(AlertDialog);
@@ -0,0 +1,83 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Card,
CardBody,
CardHeader,
DropdownItem,
DropdownMenu,
DropdownToggle,
UncontrolledButtonDropdown
} from 'reactstrap';
import cx from 'classnames';
import { IoIosMenu } from 'react-icons/io';
function ChartWrapper(props) {
let { title, children, menus } = props;
const hasShowMore = menus.find((menu) => !menu.hide && menu.showInMore);
// TODO: hide alert until API is ready
menus = menus.filter((menu) => menu.title);
const isRTL = document.documentElement.dir === 'rtl';
return (
<Card className="mb-3">
<CardHeader>
{title && <div>{title}</div>}
<div className="btn-actions-pane-right actions-icon-btn">
<div className="align-content-center d-flex d-inline-flex">
{menus &&
menus.map((menu) =>
!menu.hide && !menu.showInMore && menu.icon ? (
<button
key={menu.title}
title={menu.title}
className="btn btn-icon-only mr-2 p-0"
onClick={menu.fn}
disabled={!menu.fn}
>
<menu.icon size={menu.size || 16} />
</button>
) : null
)}
</div>
{menus && hasShowMore && (
<UncontrolledButtonDropdown>
<DropdownToggle className="btn-icon btn-icon-only" color="link">
<div className="btn-icon-wrapper">
<IoIosMenu size={24} />
</div>
</DropdownToggle>
<DropdownMenu
className={`dropdown-menu-shadow dropdown-menu-hover-link${
isRTL ? ' dropdown-menu-left' : ''
}`}
>
{menus.map((menu) =>
!menu.hide && menu.showInMore ? (
<DropdownItem onClick={menu.fn} key={menu.title}>
{menu.icon && (
<i className={cx('dropdown-icon', menu.icon)}></i>
)}
<span>{menu.title}</span>
</DropdownItem>
) : null
)}
</DropdownMenu>
</UncontrolledButtonDropdown>
)}
</div>
</CardHeader>
<CardBody>{children}</CardBody>
</Card>
);
}
ChartWrapper.propTypes = {
title: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
menus: PropTypes.array
};
export default ChartWrapper;
@@ -0,0 +1,272 @@
/* eslint-disable react/jsx-no-bind */
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { DateRangePicker } from 'react-dates';
import { translate } from 'react-i18next';
import { compose } from 'redux';
import { useHistory, useParams } from 'react-router-dom';
import {
Button,
Card,
CardBody,
CardTitle,
Col,
FormGroup,
InputGroup,
Label,
Row
} from 'reactstrap';
import Loader from 'react-loader-advanced';
import { Loader as LoaderAnim } from 'react-loaders';
import { useDrop } from 'react-dnd';
import { IoIosCloseCircleOutline } from 'react-icons/io';
import {
addEditAnalyticsAPI,
getAnalyticDetailsAPI
} from '../../../../../api/analytics/createAnalytics';
import { TYPES } from '../../../../../redux/modules/appState/sidebar';
import reduxConnect from '../../../../../redux/utils/connect';
import useIsMounted from '../../../../common/hooks/useIsMounted';
import { subChartCategories } from './ShowCharts';
import { getMomentObject, setDocumentData } from '../../../../../common/helper';
const initialState = {
feeds: [],
startDate: null,
endDate: null
};
const spinner = <LoaderAnim color="#ffffff" type="ball-pulse" />;
function CreateAnalysisSubTab({ t, actions }) {
const isMounted = useIsMounted();
const history = useHistory();
const { id } = useParams();
const [form, setForm] = useState(initialState);
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
const [fetching, setFetching] = useState(!!id);
const [focusedInput, setFocusedInput] = useState();
const [{ canDrop, isOver }, drop] = useDrop({
accept: [TYPES.FEED, TYPES.CLIP_ARTICLE],
drop: droppedFeeds,
canDrop: canDroppable,
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
})
});
function getAnalyticData() {
setFetching(true);
getAnalyticDetailsAPI(id).then((res) => {
if (!isMounted.current) {
return;
}
if (res.error || !res.data || !res.data.context) {
setFetching(false);
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
history.push('/app/analyze/saved');
return;
}
const { context } = res.data;
const date = context && context.rawFilters && context.rawFilters.date;
setForm({
feeds: context.feeds.map((item) => ({
feed: { name: item.name },
id: item.id
})),
startDate: getMomentObject(date && date.start),
endDate: getMomentObject(date && date.end)
});
setFetching(false);
});
}
useEffect(() => {
setDocumentData('title', `${id ? 'Update' : 'Create'} Analysis | Analyze`);
return () => {
setDocumentData('title');
};
}, []);
useEffect(() => {
if (id) {
getAnalyticData();
}
}, [id]);
function canDroppable(item) {
if (form.feeds.find((val) => val.id === item.id)) {
return false;
}
return true;
}
function droppedFeeds(item) {
if (form.feeds.find((val) => val.id === item.id)) {
return;
}
setForm((prev) => ({ ...prev, feeds: [...prev.feeds, item] }));
}
function removeFeeds(id) {
setForm((prev) => {
const modifiedFeeds = form.feeds.filter((val) => val.id !== id);
return { ...prev, feeds: modifiedFeeds };
});
}
const isActive = canDrop && isOver;
function handleSubmit() {
const isValid = Object.values(form).every((value) =>
value ? (Array.isArray(value) ? value.length > 0 : true) : false
);
if (!isValid) {
return setError(t('common:alerts.error.requiredInfo'));
}
setError(false);
setLoading(true);
addEditAnalyticsAPI(form, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.id) {
// on error
setLoading(false);
setError(res.errorMessage);
return;
}
actions.resetAlertChart();
setLoading(false);
history.push(`/app/analyze/${res.data.id}/${subChartCategories[0].path}`);
});
}
function handleDateChange({ startDate, endDate }) {
setForm((prev) => ({ ...prev, startDate, endDate }));
}
function onFocusChange(focus) {
setFocusedInput(focus);
}
function isOutsideRange() {
return false;
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<Card className="mb-3">
<Loader message={spinner} show={fetching}>
<CardBody>
<CardTitle>
{id ? t('analyzeTab.updateDetails') : t('analyzeTab.enterDetails')}
</CardTitle>
<Row>
<Col sm="12">
<FormGroup data-tour="drop-feeds-box">
<div>
{form.feeds.length > 0 && (
<div className="mb-3">
<Label>{t('analyzeTab.selectedFeeds')}</Label>
<div>
{form.feeds.map((item) => (
<div
key={item.id}
className="bg-light d-inline d-inline-flex align-items-center mr-2 p-2 text-dark"
>
<p>{item.feed.name}</p>
<button
className="btn p-0"
onClick={function () {
removeFeeds(item.id);
}}
>
<IoIosCloseCircleOutline
size={22}
className="text-danger ml-2"
/>
</button>
</div>
))}
</div>
</div>
)}
<Label>{t('analyzeTab.selectFeeds')}</Label>
<div ref={drop} className="dropzone-wrapper">
<div>
<div className="dropzone-content">
<p>
{isActive
? t('analyzeTab.releaseDesc')
: t('analyzeTab.dropDesc')}
</p>
</div>
</div>
</div>
</div>
</FormGroup>
<FormGroup data-tour="analytics-data-range">
<Label className="mr-sm-2">{t('analyzeTab.dateRange')}</Label>
<InputGroup>
<DateRangePicker
startDateId="startDate"
endDateId="endDate"
startDate={form.startDate}
endDate={form.endDate}
onDatesChange={handleDateChange}
focusedInput={focusedInput}
onFocusChange={onFocusChange}
displayFormat="MM/DD/YYYY"
startDatePlaceholderText={t('analyzeTab.startDatePlaceholder')}
endDatePlaceholderText={t('analyzeTab.endDatePlaceholder')}
numberOfMonths={1}
isOutsideRange={isOutsideRange}
isRTL={isRTL}
/>
</InputGroup>
</FormGroup>
{error && <div className="text-danger mb-2">{error}</div>}
<Button
className="mb-2 mr-2 btn-icon"
color="primary"
disabled={loading}
data-tour="create-analytics-button"
onClick={handleSubmit}
>
{loading
? 'Loading...'
: id
? t('analyzeTab.updateBtn')
: t('analyzeTab.createBtn')}
</Button>
</Col>
</Row>
</CardBody>
</Loader>
</Card>
);
}
CreateAnalysisSubTab.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
const applyDecorators = compose(
reduxConnect(),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(CreateAnalysisSubTab);
@@ -0,0 +1,276 @@
import React, {
useState,
useCallback,
Fragment,
useEffect,
useMemo
} from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import {
NavLink,
Redirect,
Route,
Switch,
useHistory,
useParams
} from 'react-router-dom';
import {
Button,
DropdownItem,
DropdownMenu,
DropdownToggle,
UncontrolledDropdown
} from 'reactstrap';
import { IoIosTrash } from 'react-icons/io';
import {
Results,
Performance,
Influencers,
Sentiment,
Themes,
Demographics
// WorldMap
} from './Tabs';
import AlertDialog from './AlertDialog';
import reduxConnect from '../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import { getAnalyticDetailsAPI } from '../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../common/hooks/useIsMounted';
import { setDocumentData } from '../../../../../common/helper';
import { Interpolate } from 'react-i18next';
// exported for routing
export const subChartCategories = [
{
title: 'Overview',
transKey: 'overview',
path: 'overview',
component: Results
},
{
title: 'Performance',
transKey: 'performance',
path: 'performance',
component: Performance
},
{
title: 'Influencers',
transKey: 'influencers',
path: 'influencers',
component: Influencers
},
{
title: 'Sentiment',
transKey: 'sentiment',
path: 'sentiment',
component: Sentiment
},
{ title: 'Themes', transKey: 'themes', path: 'themes', component: Themes },
{
title: 'Demographics',
transKey: 'demographics',
path: 'demographics',
component: Demographics
}
// { title: 'World Map', transKey: 'worldMap', path: 'worldmap', component: WorldMap }
];
function ShowCharts({ analyze, actions, t }) {
const isMounted = useIsMounted();
const history = useHistory();
const params = useParams();
const [chartData, setChartData] = useState({});
const [alertModal, setAlertModal] = useState(false);
const [fetching, setFetching] = useState(true);
const [feedData, setFeedData] = useState(null);
const { removeAlertChart, resetAlertChart } = actions;
const { alertCharts } = analyze;
useEffect(() => {
setDocumentData('title', 'View Analysis | Analyze');
return () => {
setDocumentData('title');
};
}, []);
useEffect(() => {
if (!params.id || isNaN(params.id)) {
history.push('/app/analyze/saved');
} else {
getAnalyticData();
}
return () => resetAlertChart(); // reset store
}, [params.id]);
const updateResult = useCallback((data, chartName) => {
setChartData((prev) => ({ ...prev, [chartName]: data }));
}, []);
const subChartRoutes = useMemo(() => {
return subChartCategories.map(({ path, component: SubChart }) => (
<Route exact key={path} path={`/app/analyze/${params.id}/${path}`}>
<SubChart
id={params.id}
feedData={feedData}
chartData={chartData}
updateResult={updateResult}
/>
</Route>
));
}, [updateResult, chartData, feedData, params.id]);
function getAnalyticData() {
setFetching(true);
getAnalyticDetailsAPI(params.id).then((res) => {
if (!isMounted.current) {
return;
}
if (res.error || !res.data || !res.data.context) {
setFetching(false);
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
history.push('/app/analyze/saved');
return;
}
const { context } = res.data;
const date = context && context.rawFilters && context.rawFilters.date;
setFeedData({
feeds: context.feeds.map((item) => ({
feed: item.name,
id: item.id
})),
startDate: date && date.start,
endDate: date && date.end
});
setFetching(false);
});
}
function toggleModal() {
setAlertModal((prev) => !prev);
}
if (fetching) {
return 'Loading...';
}
const isRTL = document.documentElement.dir === 'rtl';
return (
<Fragment>
<div
className="d-flex"
style={{ position: 'absolute', top: 0, right: 0 }}
>
{alertCharts && alertCharts.length > 0 && (
<UncontrolledDropdown className="d-inline-block">
<DropdownToggle color="info" className="btn-shadow" caret>
<Interpolate
t={t}
i18nKey="analyzeTab.createAlert"
alertsLength={alertCharts.length}
/>
</DropdownToggle>
<DropdownMenu
className={`dropdown-menu-right rm-pointers dropdown-menu-shadow dropdown-menu-hover-link${
isRTL ? ' dropdown-menu-left' : ''
}`}
>
<DropdownItem header>
{t('analyzeTab.selectedCharts')}
</DropdownItem>
{alertCharts.map((chart, i) => (
<div className="dropdown-item" key={`${chart.name}_${i}}`}>
<span>
{chart.name}
{isNaN(chart.id) ? '' : ` (#${chart.id})`}
</span>
<Button
className="btn-icon btn-icon-only ml-auto mr-2 p-1"
color="danger"
onClick={function () {
removeAlertChart({ name: chart.name, id: chart.id });
}}
>
<IoIosTrash fontSize="1rem" className="ml-auto" />
</Button>
</div>
))}
<DropdownItem divider />
<div className="p-2 pr-3 text-right">
<Button
className="btn-shadow btn-sm"
color="primary"
onClick={toggleModal}
>
{t('analyzeTab.createAlertBtn')}
</Button>
</div>
</DropdownMenu>
</UncontrolledDropdown>
)}
{/*
<Button
className="btn-icon ml-2"
color="info"
// change style for mobile view
>
<IoIosSave className="btn-icon-wrapper" />
Save
</Button> */}
</div>
<div className="btn-actions-pane-right mask-line overflow-auto mb-3 pl-3">
{subChartCategories.map((cat, i, arr) => (
<Button
key={cat.title}
title={cat.title}
tag={NavLink}
to={`/app/analyze/${params.id}/${cat.path}`}
size="sm"
outline
color="primary"
className={cx('btn-pill btn-wide', {
'mr-1 ml-1': i !== 0 && i !== arr.length - 1
})}
activeClassName="active"
>
{t(`analyzeTab.overviewCharts.${cat.transKey}`)}
</Button>
))}
</div>
<AlertDialog
isOpen={alertModal}
toggle={toggleModal}
alertCharts={alertCharts}
resetAlertChart={resetAlertChart}
/>
<Switch>
{subChartRoutes}
<Redirect
to={`/app/analyze/${params.id}/${subChartCategories[0].path}`}
/>
</Switch>
</Fragment>
);
}
ShowCharts.propTypes = {
t: PropTypes.func.isRequired,
analyze: PropTypes.object,
actions: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(ShowCharts);
@@ -0,0 +1,357 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import { getOverviewPieAPI } from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function Demographics(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barCountriesData, setBarCountriesData] = useState(initialBar);
const [barLanguagesData, setBarLanguagesData] = useState(initialBar);
const [genderData, setGenderData] = useState(initialPie);
useEffect(() => {
if (!id) {
return;
}
// getCountriesData()
getLanguagesData();
getGenderData();
}, []);
useEffect(() => {
if (barCountriesData.data) {
setBarCountriesData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barCountriesData.vertical]);
useEffect(() => {
if (barLanguagesData.data) {
setBarLanguagesData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barLanguagesData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
// getCountriesData()
return;
case cn.second:
getLanguagesData();
return;
case cn.third:
getGenderData();
return;
default:
return;
}
}
/* Uncomment when country chart shows up
function getCountriesData() {
setBarCountriesData((prev) => ({ ...prev, loading: true }))
getOverviewPieAPI('country', id).then((res) => {
if (!isMounted.current) {
return false
}
if (res.error || !res.data.data) {
// on error
setBarCountriesData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}))
return
}
const { data } = res.data
const barOptions = {}
const errors = {}
Object.entries(data).forEach((feed) => {
const [name, value] = feed
const labels = ['Results']
const datasets = Object.keys(value).map((item) => ({
name: item,
type: 'bar',
data: [value[item]]
}))
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
barOptions[name] = getBarOptions(datasets, labels)
})
setBarCountriesData({
data: barOptions,
error: errors,
loading: false,
vertical: false
})
})
} */
function getLanguagesData() {
setBarLanguagesData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('language', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarLanguagesData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
const errors = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
const labels = ['Results'];
const datasets = Object.keys(value).map((item) => ({
name: item,
type: 'bar',
data: [value[item]]
}));
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
barOptions[name] = getBarOptions(datasets, labels);
});
setBarLanguagesData({
data: barOptions,
error: errors,
loading: false,
vertical: false
});
});
}
function getGenderData() {
setGenderData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('gender', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setGenderData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = {};
const errors = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
if (!value || (Array.isArray(value) && value.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
pieOptions[name] = getPieOptions(
Object.entries(value).map((v) => ({
name: v[0],
value: v[1]
}))
);
});
setGenderData({
data: pieOptions,
error: errors,
loading: false
});
});
}
function changeVertical(name, id) {
name === cn.first
? setBarCountriesData((prev) => ({ ...prev, vertical: !prev.vertical }))
: setBarLanguagesData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChartAlert = (name, id) =>
analyze.alertCharts.find((v) => v.name === name && v.id === id);
const hideChartPieAlert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.third && v.id === id);
const barchartMenus = (name, id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: name, id }),
showInMore: false,
hide: hideChartAlert(name, id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChartAlert(name, id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, name),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(name, id),
showInMore: true
}
];
const piechartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.third, id }),
showInMore: false,
hide: hideChartPieAlert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChartPieAlert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.third),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return (
<Row>
{/* {feedData.feeds.map((feed) => (
<Col key={feed.id} md="6">
<ChartWrapper
title={`${t('analyzeTab.charts.topLanguages')} (${feed.feed})`}
menus={barchartMenus(cn.first, feed.id)}
>
<ECharts
xLabel={barCountriesData.labels}
loading={barCountriesData.loading}
options={barCountriesData.data[feed.feed]}
message={
barCountriesData.error && barCountriesData.error[feed.feed]
}
/>
</ChartWrapper>
</Col>
))} */}
{feedData.feeds.map((feed) => (
<Col key={feed.id} md="6">
<ChartWrapper
title={`${t('analyzeTab.charts.topLanguages')} (${feed.feed})`}
menus={barchartMenus(cn.second, feed.id)}
>
<ECharts
xLabel={barLanguagesData.labels}
loading={barLanguagesData.loading}
options={barLanguagesData.data[feed.feed]}
message={
barLanguagesData.error && barLanguagesData.error[feed.feed]
}
/>
</ChartWrapper>
</Col>
))}
{feedData.feeds.map((feed) => (
<Col key={feed.id} md="6">
<ChartWrapper
title={`${t('analyzeTab.charts.gender')} (${feed.feed})`}
menus={piechartMenus(feed.id)}
>
<ECharts
loading={genderData.loading}
options={genderData.data[feed.feed]}
message={genderData.error && genderData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
))}
</Row>
);
}
const cn = {
first: 'Top Countries',
second: 'Top Languages',
third: 'Gender'
};
Demographics.propTypes = {
t: PropTypes.func.isRequired,
chartData: PropTypes.object,
actions: PropTypes.object,
id: PropTypes.string,
feedData: PropTypes.object,
analyze: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Demographics));
@@ -0,0 +1,290 @@
/* eslint-disable react/prop-types */
import React, {
useState,
useCallback,
Fragment,
useEffect,
useMemo
} from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { compose } from 'redux';
import { Table } from '../../../../../common/Table/Table';
import { getInfluencersAPI } from '../../../../../../api/analytics/createAnalytics';
import { reduxActions } from '../../../../../../redux/utils/connect';
import {
getQueryParams,
removeHttpsUrl,
capOnlyFirstLetter,
getValidHttpUrl
} from '../../../../../../common/helper';
import i18n from '../../../../../../i18n';
function Influencers(props) {
const [dataSource, setDataSource] = useState(null);
const [loading, setLoading] = useState(true);
const [filter] = useState(filtersNames[1].id);
const { t, actions, id, feedData } = props;
useEffect(() => {
if (!id || !dataSource) {
return;
}
getInfluencers(); //called from table
}, [filter]);
const getDetailsColumns = (id) => {
return id === filtersNames[0].id ? sourceDetails : authorDetails;
};
const authorDetails = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.rank'),
accessor: 'source_hashcode',
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.index + 1}</div>
),
minWidth: 52
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.influencers'),
accessor: 'influence',
Cell: (row) =>
getValidHttpUrl(row.value) ? (
<a
target="_blank"
rel="nofollow noopener"
href={getValidHttpUrl(row.value)}
>
{row.original && row.original.author_name}
</a>
) : (
removeHttpsUrl(row.value)
),
minWidth: 130
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sourceType'),
accessor: 'source_type',
Cell: (row) => capOnlyFirstLetter(row.value),
minWidth: 102
}
],
[i18n.language]
);
const sourceDetails = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.rank'),
accessor: 'source_hashcode',
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.index + 1}</div>
),
minWidth: 52
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.influencers'),
accessor: 'influence',
Cell: (row) =>
getValidHttpUrl(row.value) ? (
<a
target="_blank"
rel="nofollow noopener"
href={getValidHttpUrl(row.value)}
>
{removeHttpsUrl(row.value)}
</a>
) : (
removeHttpsUrl(row.value)
),
minWidth: 130
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sourceType'),
accessor: 'source_type',
Cell: (row) => capOnlyFirstLetter(row.value),
minWidth: 102
}
],
[i18n.language]
);
const sentimentColumns = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.total'),
// accessor: d => d.nop.total
accessor: 'totalSentiment',
minWidth: 52,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.positive'),
accessor: 'POSITIVE',
minWidth: 78,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.neutral'),
accessor: 'NEUTRAL',
minWidth: 78,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.negative'),
accessor: 'NEGATIVE',
minWidth: 78,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
}
],
[i18n.language]
);
const reachColumns = useMemo(
() => [
/* {
Header: i18n.t('tabsContent:analyzeTab.influencerCols.reach'),
accessor: 'reach',
minWidth: 65,
Cell: (row) => <div style={{ textAlign: 'center' }}>{row.value || 0}</div>
}, */
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.engagement'),
accessor: 'engagement',
minWidth: 105,
Cell: (row) => (
<div style={{ textAlign: 'center' }}>{row.value || 0}</div>
)
}
/* {
Header: i18n.t('tabsContent:analyzeTab.influencerCols.engagementPerMention'),
accessor: 'engagement_per_mention',
Cell: (row) => <div style={{ textAlign: 'center' }}>{row.value || 0}</div>
} */
],
[i18n.language]
);
const columnsList = useMemo(
() => [
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.details'),
headerClassName: 'text-center',
columns: getDetailsColumns(filter)
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.sentiments'),
headerClassName: 'text-center',
columns: sentimentColumns
},
{
Header: i18n.t('tabsContent:analyzeTab.influencerCols.reach'),
headerClassName: 'text-center',
columns: reachColumns
}
],
[filter, i18n.language]
);
const getInfluencers = useCallback(
(page = 0, pageSize = 10) => {
setLoading(true);
const filterParams = getQueryParams({ page, pageSize });
getInfluencersAPI(id, filter, filterParams).then((res) => {
// if (false) {
if (res.error || res.data === null || !res.data.data) {
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'somethingWrong'
});
}
const tableData = {};
res.data.data.forEach((v) => {
tableData[v.name] = v.data;
});
setDataSource(tableData);
setLoading(false);
});
},
[id, filter]
);
return (
<Fragment>
{/* <ButtonGroup size="sm" className="mb-3 d-block text-right">
{filtersNames.map((item) => (
<Button
outline
key={item.id}
title={item.name}
color="secondary"
onClick={function () {
setFilter(item.id)
}}
active={filter === item.id}
>
{item.name}
</Button>
))}
</ButtonGroup> */}
{feedData.feeds.map((feed) => {
let tableData = dataSource;
if (!tableData || !tableData[feed.feed]) {
tableData = { [feed.feed]: [] };
// uncomment for pagination
// tableData[feed.feed] = { data: [], totalCount: 0, limit: 0, page: 0 }
}
const { totalCount = 0, limit = 0, page = 0 } = tableData[feed.feed];
return (
<Table
key={feed.id}
t={t}
cardTitle={`${t('analyzeTab.charts.topInfluencers')} (${
feed.feed
})`}
columns={columnsList}
data={tableData[feed.feed]}
totalCount={totalCount}
showTotalCount
limit={limit}
page={page}
isLoading={loading}
onFetchData={getInfluencers}
/>
);
})}
</Fragment>
);
}
const filtersNames = [
{ name: 'Source', id: 0 },
{ name: 'Author', id: 1 }
];
Influencers.propTypes = {
t: PropTypes.func.isRequired,
feedData: PropTypes.object,
id: PropTypes.string,
actions: PropTypes.object
};
const applyDecorators = compose(
translate(['tabsContent'], { wait: true }),
reduxActions()
);
export default applyDecorators(Influencers);
@@ -0,0 +1,722 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getEngagementsAPI,
getEngagementsTimeAPI,
getOverviewBarAPI,
getOverviewPieAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
function Performance(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState({
data: [],
error: undefined,
loading: true,
vertical: false
});
const [engBarData, setEngBarData] = useState({
data: [],
error: undefined,
loading: true,
vertical: false
});
const [potentialBarData, setPotentialBarData] = useState({
data: [],
error: undefined,
loading: true,
vertical: false
});
const [sentimentBar, setSentimentBar] = useState({
data: [],
error: undefined,
loading: true
});
const [pieMentions, setpieMentions] = useState({
data: [],
error: undefined,
loading: true
});
const [pieEng, setpieEng] = useState({
data: [],
error: undefined,
loading: true
});
/* const [pieReach, setpieReach] = useState({
data: [],
error: undefined,
loading: true
}); */
useEffect(() => {
// pass filter
if (!id) {
return;
}
getBarChart();
getEngBarChart();
// getPotentialChart()
getSentimentChart();
getpieMentions();
getpieEngg();
// getpieReach()
}, []);
function updateResult(foo, id) {
switch (id) {
case cn.first:
getBarChart();
return;
case cn.second:
getEngBarChart();
return;
case cn.third:
// getPotentialChart() // Uncomment when API has data
return;
case cn.fourth:
getSentimentChart();
return;
case cn.fifth:
getpieMentions();
return;
case cn.sixth:
getpieEngg();
return;
case cn.seventh:
// getpieReach() // Uncomment when API has data
return;
default:
return;
}
}
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
useEffect(() => {
if (engBarData.data) {
setEngBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [engBarData.vertical]);
useEffect(() => {
if (potentialBarData.data) {
setPotentialBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [potentialBarData.vertical]);
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = Object.keys(data[0].data);
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
const barOptions = getBarOptions(datasets, labels);
setBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getEngBarChart() {
setEngBarData((prev) => ({ ...prev, loading: true }));
getEngagementsTimeAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setEngBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = Object.keys(data[0].data);
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
const barOptions = getBarOptions(datasets, labels);
setEngBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
/*
function getPotentialChart() {
setPotentialBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setPotentialBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = Object.keys(data);
const datasets = {
name: 'Potential reach over time',
type: potentialBarData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(data)
};
const barOptions = getBarOptions(datasets, labels);
setPotentialBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
} */
function getSentimentChart() {
setSentimentBar((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('sentiment', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setSentimentBar((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
Object.keys(data).forEach((feed) => {
const labels = ['Results'];
const datasets = ['POSITIVE', 'NEGATIVE', 'NEUTRAL'].map((item) => ({
name: item,
type: 'bar',
data: [data[feed][item]]
}));
barOptions[feed] = getBarOptions(datasets, labels);
});
setSentimentBar({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getpieMentions() {
setpieMentions((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setpieMentions((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setpieMentions({
data: pieOptions,
error: false,
loading: false
});
});
}
function getpieEngg() {
setpieEng((prev) => ({ ...prev, loading: true }));
getEngagementsAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setpieEng((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
// condition for other filter than 0
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setpieEng({
data: pieOptions,
error: false,
loading: false
});
});
}
/*
function getpieReach() {
setpieReach((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('none', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setpieReach((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setpieReach({
data: pieOptions,
error: false,
loading: false
});
});
} */
function changeVertical(chart) {
switch (chart) {
case cn.first:
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
return;
case cn.second:
setEngBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
return;
case cn.third:
setPotentialBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
return;
default:
return;
}
}
const hideChart1Alert = analyze.alertCharts.find((v) => v.name === cn.first);
const hideChart2Alert = analyze.alertCharts.find((v) => v.name === cn.second);
// const hideChart3Alert = analyze.alertCharts.find((v) => v.name === cn.third);
const hideChart4Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.fourth && v.id === id);
const hideChart5Alert = analyze.alertCharts.find((v) => v.name === cn.fifth);
const hideChart6Alert = analyze.alertCharts.find((v) => v.name === cn.sixth);
/* const hideChart7Alert = analyze.alertCharts.find(
(v) => v.name === cn.seventh
); */
const barchart1Menus = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id: 'none' }),
showInMore: false,
hide: hideChart1Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(cn.first),
showInMore: true
}
];
const barchart2Menus = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id: 'none' }),
showInMore: false,
hide: hideChart2Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(cn.second),
showInMore: true
}
];
/*
const barchart3Menus = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.third, id: 'none' }),
showInMore: false,
hide: hideChart3Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart3Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.third),
showInMore: false
},
{
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
},
{
title: t('analyzeTab.chartMenus.toggleHV'),
fn: () => changeVertical(cn.third),
showInMore: true
}
];
*/
function barchart4Menus(id) {
return [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.fourth, id }),
showInMore: false,
hide: hideChart4Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart4Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.fourth, id),
showInMore: false
}
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
} */
];
}
const pieChart1 = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.fifth, id: 'none' }),
showInMore: false,
hide: hideChart5Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart5Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.fifth),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
const pieChart2 = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.sixth, id: 'none' }),
showInMore: false,
hide: hideChart6Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart6Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.sixth),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
/*
const pieChart3 = [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.seventh, id: 'none' }),
showInMore: false,
hide: hideChart7Alert
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart7Alert
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.seventh),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
*/
return (
<Row>
<Col md="8">
<ChartWrapper
title={t('analyzeTab.charts.mentionsOverTime')}
menus={barchart1Menus}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper title={t('analyzeTab.charts.mentions')} menus={pieChart1}>
<ECharts
xLabel={pieMentions.labels}
loading={pieMentions.loading}
options={pieMentions.data}
/>
</ChartWrapper>
</Col>
<Col md="8">
<ChartWrapper
title={t('analyzeTab.charts.engagementOverTime')}
menus={barchart2Menus}
>
<ECharts
xLabel={engBarData.labels}
loading={engBarData.loading}
options={engBarData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={t('analyzeTab.charts.engagement')}
menus={pieChart2}
>
<ECharts
xLabel={pieEng.labels}
loading={pieEng.loading}
options={pieEng.data}
/>
</ChartWrapper>
</Col>
{/* <Col md="8">
<ChartWrapper title={t('analyzeTab.charts.potentialReachOverTime')} menus={barchart3Menus}>
<ECharts
xLabel={potentialBarData.labels}
loading={potentialBarData.loading}
options={potentialBarData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper title={t('analyzeTab.charts.potentialReach')} menus={pieChart3}>
<ECharts
xLabel={pieReach.labels}
loading={pieReach.loading}
options={pieReach.data}
/>
</ChartWrapper>
</Col> */}
{feedData.feeds.map((feed) => (
<Col md="12" key={feed.id}>
<ChartWrapper
title={`${t('analyzeTab.charts.proportionofSentiment')} (${
feed.feed
})`}
menus={barchart4Menus(feed.id)}
>
<ECharts
xLabel={sentimentBar.labels}
loading={sentimentBar.loading}
options={sentimentBar.data[feed.feed]}
/>
</ChartWrapper>
</Col>
))}
</Row>
);
}
const cn = {
first: 'Mentions over time',
second: 'Engagement over time',
third: 'Potential reach over time',
fourth: 'Proportion of sentiment',
fifth: 'Mentions',
sixth: 'Engagement',
seventh: 'Potential Reach'
};
Performance.propTypes = {
chartData: PropTypes.object,
actions: PropTypes.object,
feedData: PropTypes.object,
id: PropTypes.string,
analyze: PropTypes.object,
t: PropTypes.func
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Performance));
@@ -0,0 +1,403 @@
import React, { Fragment, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Button, ButtonGroup, Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getOverviewBarAPI,
getOverviewPieAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function ResultsTab(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState(initialBar);
const [barTimeData, setBarTimeData] = useState(initialBar);
const [pieData, setPieData] = useState(initialPie);
const [pieTimeData, setPieTimeData] = useState(initialPie);
const [filter, setFilter] = useState(filtersNames[0].id);
useEffect(() => {
if (!id) {
return;
}
if (filter === filtersNames[0].id) {
getBarChart();
getPieChart();
} else {
getBarChartFeeds();
getPieChartFeeds();
}
}, [filter]);
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
filter === filtersNames[0].id ? getBarChart() : getBarChartFeeds();
return;
case cn.second:
filter === filtersNames[0].id ? getPieChart() : getPieChartFeeds();
return;
}
}
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const labels = data[0] ? Object.keys(data[0].data) : [];
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
const barOptions = getBarOptions(datasets, labels);
setBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getBarChartFeeds() {
setBarTimeData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarTimeData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
const errors = {};
data.map((feed) => {
const { name, data } = feed;
if (!data || (Array.isArray(data) && data.length < 1)) {
errors[name] = t('analyzeTab.noData');
return;
}
const labels = Object.keys(data[0].data).sort();
const datasets = data.map((item) => ({
name: item.name,
type: barTimeData.vertical ? 'bar' : 'line',
smooth: true,
data: labels.map((v) => item.data[v])
}));
barOptions[name] = getBarOptions(datasets, labels);
});
setBarTimeData({
data: barOptions,
error: errors,
loading: false,
vertical: false
});
});
}
function getPieChart() {
setPieData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = getPieOptions(
Object.entries(data).map((v) => ({ name: v[0], value: v[1] }))
);
setPieData({
data: pieOptions,
error: false,
loading: false
});
});
}
function getPieChartFeeds() {
setPieTimeData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI(filter, id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieTimeData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = {};
const errors = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
if (!value || (Array.isArray(value) && value.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
pieOptions[name] = getPieOptions(
Object.entries(value).map((v) => ({
name: v[0],
value: v[1]
}))
);
});
setPieTimeData({
data: pieOptions,
error: errors,
loading: false
});
});
}
function changeVertical() {
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChart1Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
const hideChart2Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
const barchartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id }),
showInMore: false,
hide: hideChart1Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: 'Toggle Horizontal/Vertical',
fn: changeVertical,
showInMore: true
}
];
const piechartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id }),
showInMore: false,
hide: hideChart2Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return (
<Fragment>
<div className="mask-line overflow-auto white-space-nowrap pl-3 mb-3">
<ButtonGroup size="sm">
{filtersNames.map((item) => (
<Button
outline
key={item.id}
title={item.name}
color="secondary"
onClick={function () {
setFilter(item.id);
}}
active={filter === item.id}
>
{t(`analyzeTab.overviewCharts.${item.transKey}`)}
</Button>
))}
</ButtonGroup>
</div>
{filter === filtersNames[0].id ? ( // feeds in single graph
<Row>
<Col md="8">
<ChartWrapper
title={t('analyzeTab.charts.mentionsOverTime')}
menus={barchartMenus('none')}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={t('analyzeTab.charts.mentions')}
menus={piechartMenus('none')}
>
<ECharts loading={pieData.loading} options={pieData.data} />
</ChartWrapper>
</Col>
</Row>
) : (
feedData.feeds.map((feed) => (
<Row key={feed.id}>
<Col md="8">
<ChartWrapper
title={`${t('analyzeTab.charts.mentionsOverTime')} (${
feed.feed
})`}
menus={barchartMenus(feed.id)}
>
<ECharts
xLabel={barTimeData.labels}
loading={barTimeData.loading}
options={barTimeData.data && barTimeData.data[feed.feed]}
message={barTimeData.error && barTimeData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={`${t('analyzeTab.charts.mentions')} (${feed.feed})`}
menus={piechartMenus(feed.id)}
>
<ECharts
loading={pieTimeData.loading}
options={pieTimeData.data[feed.feed]}
message={pieTimeData.error && pieTimeData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
</Row>
))
)}
</Fragment>
);
}
const cn = {
first: 'Mentions Over Time',
second: 'Share of Mentions'
};
const filtersNames = [
{ name: 'None', transKey: 'none', id: 'none' },
{ name: 'Media Types', transKey: 'mediaTypes', id: 'media' },
{ name: 'Sentiments', transKey: 'sentiments', id: 'sentiment' },
// { name: 'Countries', transKey:'countries', id: 'country' },
{ name: 'Languages', transKey: 'languages', id: 'language' }
];
ResultsTab.propTypes = {
actions: PropTypes.object,
id: PropTypes.string,
t: PropTypes.func,
feedData: PropTypes.object,
analyze: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(ResultsTab));
@@ -0,0 +1,255 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
getPieOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getOverviewBarAPI,
getOverviewPieAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function Sentiment(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState(initialBar);
const [pieData, setPieData] = useState(initialPie);
useEffect(() => {
if (!id) {
return;
}
getBarChart();
getPieChart();
}, []);
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
getBarChart();
return;
case cn.second:
getPieChart();
return;
}
}
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getOverviewBarAPI('sentiment', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const barOptions = {};
data.forEach((feed) => {
const { name, data } = feed;
const labels = Object.keys(data[0].data).sort();
const datasets = data.map((item) => ({
name: item.name,
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: labels.map((v) => item.data[v])
}));
barOptions[name] = getBarOptions(datasets, labels);
});
setBarData({
data: barOptions,
error: false,
loading: false,
vertical: false
});
});
}
function getPieChart() {
setPieData((prev) => ({ ...prev, loading: true }));
getOverviewPieAPI('sentiment', id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const pieOptions = {};
Object.entries(data).forEach((feed) => {
const [name, value] = feed;
pieOptions[name] = getPieOptions(
Object.entries(value).map((v) => ({
name: v[0],
value: v[1]
}))
);
});
setPieData({
data: pieOptions,
error: false,
loading: false
});
});
}
function changeVertical() {
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChart1Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
const hideChart2Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
const barchartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id }),
showInMore: false,
hide: hideChart1Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: 'Toggle Horizontal/Vertical',
fn: changeVertical,
showInMore: true
}
];
const piechartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id }),
showInMore: false,
hide: hideChart2Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return feedData.feeds.map((feed) => (
<Row key={feed.id}>
<Col md="8">
<ChartWrapper
title={`${t('analyzeTab.charts.sentimentOverTime')} (${feed.feed})`}
menus={barchartMenus(feed.id)}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data[feed.feed]}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={`${t('analyzeTab.charts.shareofSentiment')} (${feed.feed})`}
menus={piechartMenus(feed.id)}
>
<ECharts
loading={pieData.loading}
options={pieData.data[feed.feed]}
/>
</ChartWrapper>
</Col>
</Row>
));
}
const cn = {
first: 'Sentiment Over Time',
second: 'Share of Sentiment'
};
Sentiment.propTypes = {
actions: PropTypes.object,
feedData: PropTypes.object,
analyze: PropTypes.object,
t: PropTypes.func
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Sentiment));
@@ -0,0 +1,284 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { Col, Row } from 'reactstrap';
import ECharts from '../../../../../common/charts/ECharts';
import 'echarts-wordcloud';
import { capitalize } from 'lodash';
import ChartWrapper from '../ChartWrapper';
import {
getBarOptions,
PieToolbox,
WordCloudOptions
} from '../../../../../common/charts/ChartsOptions';
import { IoIosAdd, IoIosRefresh, IoIosCheckmark } from 'react-icons/io';
import reduxConnect from '../../../../../../redux/utils/connect';
import translate from 'react-i18next/dist/commonjs/translate';
import { compose } from 'redux';
import {
getThemesCloudAPI,
getThemesTimeAPI
} from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
import { capFirstLetter } from '../../../../../../common/helper';
const initialBar = {
data: [],
error: undefined,
loading: true,
vertical: false
};
const initialPie = { data: [], error: undefined, loading: true };
function Themes(props) {
const { actions, analyze, feedData, id, t } = props;
const isMounted = useIsMounted();
const [barData, setBarData] = useState(initialBar);
const [wordData, setWordData] = useState(initialPie);
useEffect(() => {
if (!id) {
return;
}
getBarChart();
getWordCloud();
}, []);
useEffect(() => {
if (barData.data) {
setBarData((prev) => ({
...prev,
data: {
...prev.data,
xAxis: prev.data.yAxis,
yAxis: prev.data.xAxis
}
}));
}
}, [barData.vertical]);
function updateResult(foo, id) {
switch (id) {
case cn.first:
getBarChart();
return;
case cn.second:
getWordCloud();
return;
}
}
function getBarChart() {
setBarData((prev) => ({ ...prev, loading: true }));
getThemesTimeAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// on error
setBarData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
let labels = null;
const barOptions = {};
const errors = {};
data.forEach((feedData) => {
const { name, data } = feedData;
const datasets = data.map((item) => ({
name: capitalize(item.name),
type: barData.vertical ? 'bar' : 'line',
smooth: true,
data: Object.values(item.data)
}));
if (!labels && data && data[0] && data[0].data) {
labels = Object.keys(data[0].data);
}
barOptions[name] = getBarOptions(datasets, labels);
if (!datasets || (Array.isArray(datasets) && datasets.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
});
setBarData({
data: barOptions,
error: errors,
loading: false,
vertical: false
});
});
}
function getWordCloud() {
setWordData((prev) => ({ ...prev, loading: true }));
getThemesCloudAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setWordData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const cloudOptions = {};
const errors = {};
data.forEach((feed) => {
const { name, data } = feed;
if (!data || (Array.isArray(data) && data.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
cloudOptions[name] = {
tooltip: {
show: true
},
toolbox: PieToolbox,
series: [
{
...WordCloudOptions,
data: Object.entries(data).map((v) => ({
name: capFirstLetter(v[0]),
value: v[1]
}))
}
]
};
});
setWordData({
data: cloudOptions,
error: false,
loading: false
});
});
}
function changeVertical() {
setBarData((prev) => ({ ...prev, vertical: !prev.vertical }));
}
const hideChart1Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.first && v.id === id);
const hideChart2Alert = (id) =>
analyze.alertCharts.find((v) => v.name === cn.second && v.id === id);
const barchartMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.first, id }),
showInMore: false,
hide: hideChart1Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart1Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.first),
showInMore: false
},
/* {
title: t('analyzeTab.chartMenus.addToDashboard'),
fn: () => {},
showInMore: true
}, */
{
title: 'Toggle Horizontal/Vertical',
fn: changeVertical,
showInMore: true
}
];
const wordCloudMenus = (id) => [
{
title: '', // t('analyzeTab.chartMenus.addToAlert'),
icon: IoIosAdd,
size: 24,
fn: () => actions.addAlertChart({ name: cn.second, id }),
showInMore: false,
hide: hideChart2Alert(id)
},
{
title: '', // t('analyzeTab.chartMenus.addedToAlerts'),
icon: IoIosCheckmark,
size: 24,
showInMore: false,
hide: !hideChart2Alert(id)
},
{
title: t('analyzeTab.chartMenus.refresh'),
icon: IoIosRefresh,
fn: () => updateResult(null, cn.second),
showInMore: false
}
// { title: t('analyzeTab.chartMenus.addToDashboard'), fn: () => {}, showInMore: true }
];
return feedData.feeds.map((feed) => (
<Row key={feed.id}>
<Col md="8">
<ChartWrapper
title={`${t('analyzeTab.charts.themesOverTime')} (${feed.feed})`}
menus={barchartMenus(feed.id)}
>
<ECharts
xLabel={barData.labels}
loading={barData.loading}
options={barData.data[feed.feed]}
message={barData.error && barData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
<Col md="4">
<ChartWrapper
title={`${t('analyzeTab.charts.topThemes')} (${feed.feed})`}
menus={wordCloudMenus(feed.id)}
>
<ECharts
loading={wordData.loading}
options={wordData.data[feed.feed]}
message={barData.error && barData.error[feed.feed]}
/>
</ChartWrapper>
</Col>
</Row>
));
}
const cn = {
first: 'Themes over time',
second: 'Top Themes'
};
Themes.propTypes = {
chartData: PropTypes.object,
actions: PropTypes.object,
feedData: PropTypes.object,
t: PropTypes.func,
analyze: PropTypes.object
};
const applyDecorators = compose(
reduxConnect('analyze', ['appState', 'analyze']),
translate(['tabsContent'], { wait: true })
);
export default applyDecorators(React.memo(Themes));
@@ -0,0 +1,208 @@
import React, { useEffect, useRef, useState, Fragment } from 'react';
import PropTypes from 'prop-types';
import { Row, Col, ButtonGroup, Button } from 'reactstrap';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
import 'leaflet-dvf/dist/leaflet-dvf';
// keep above 3 in sequence
import ChartWrapper from '../ChartWrapper';
import { getWorldMapAPI } from '../../../../../../api/analytics/createAnalytics';
import useIsMounted from '../../../../../common/hooks/useIsMounted';
import { translate } from 'react-i18next';
const initialPie = {
data: [],
error: undefined,
loading: true,
selected: undefined
};
function WorldMap(props) {
const { id, t } = props;
const mapRef = useRef();
const isMounted = useIsMounted();
const [pieData, setPieData] = useState(initialPie);
const [markers, setMarkers] = useState([]);
const feedNames = (pieData.data && Object.keys(pieData.data)) || [];
useEffect(() => {
mapRef.current = L.map('leaflet-map', {
center: [0, 0],
zoom: 2,
layers: [
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
noWrap: true,
attribution:
'&copy; <a target="_blank" noreferrer noopener href="http://osm.org/copyright">OpenStreetMap</a> contributors'
})
]
});
mapRef.current.whenReady(getMapSentiments);
}, []);
useEffect(() => {
const { data, selected, error } = pieData;
const selectedData = data[feedNames[selected]];
const hasErr = error && error[feedNames[selected]];
clearMap();
if (selectedData && !hasErr) {
// loop to add marker
const markersList = [];
selectedData.forEach((data) => {
const [lat, lng] = getLatLong(data.LatLng);
if (!lat || !lng) {
return;
}
let pieChartMarker = new L.PieChartMarker(new L.LatLng(lat, lng), {
...options,
data: {
positive: data.POSITIVE,
negative: data.NEGATIVE,
neutral: data.NEUTRAL
}
});
pieChartMarker.addTo(mapRef.current);
markersList.push(pieChartMarker);
});
// eslint-disable-next-line new-cap
const group = new L.featureGroup(markersList);
mapRef.current.fitBounds(group.getBounds());
setMarkers(markersList);
}
}, [pieData.data, pieData.selected]);
function getLatLong(str) {
const [lat, lng] = str.split(', ');
return [lat && parseFloat(lat), lng && parseFloat(lng)];
}
function clearMap() {
if (mapRef.current) {
markers.forEach((v) => {
mapRef.current.removeLayer(v);
});
}
}
function getMapSentiments() {
setPieData((prev) => ({ ...prev, loading: true }));
getWorldMapAPI(id).then((res) => {
if (!isMounted.current) {
return false;
}
if (res.error || !res.data.data) {
// alert on error
setPieData((prev) => ({
...prev,
loading: false,
error: res.errorMessage
}));
return;
}
const { data } = res.data;
const dataValues = {};
const errors = {};
data.map((feed) => {
const { name, data } = feed;
if (!data || (Array.isArray(data) && data.length < 1)) {
errors[name] = t('analyzeTab.noData');
}
dataValues[name] = data;
});
setPieData({
data: dataValues,
error: errors,
loading: false,
selected: 0
});
});
}
const style = {
height: 'max(300px, calc(100vh - 200px))'
};
return (
<Row>
<Col md="12">
<ChartWrapper title="Distribution by Sentiments">
<Fragment>
<ButtonGroup size="sm" className="d-block mb-2 text-right">
{feedNames.map((name, i) => (
<Button
outline
key={name}
title={name}
color="secondary"
onClick={function () {
setPieData((prev) => ({
...prev,
selected: i
}));
}}
active={pieData.selected === i}
>
{name}
</Button>
))}
</ButtonGroup>
<div className="position-relative">
<div id="leaflet-map" style={style} />
{pieData.error && pieData.error[feedNames[pieData.selected]] ? (
<div className="no-data" style={{ zIndex: 1000 }}>
{pieData.error[feedNames[pieData.selected]]}
</div>
) : null}
</div>
</Fragment>
</ChartWrapper>
</Col>
</Row>
);
}
const options = {
stroke: false,
fillOpacity: 0.7,
radius: 20,
gradient: false,
chartOptions: {
positive: {
fillColor: '#00FF00',
displayText: function (value) {
return value.toFixed(0);
}
},
negative: {
fillColor: '#FF0000',
displayText: function (value) {
return value.toFixed(0);
}
},
neutral: {
fillColor: '#000000',
displayText: function (value) {
return value.toFixed(0);
}
}
}
// Other L.Path style options
};
WorldMap.propTypes = {
actions: PropTypes.object,
feedData: PropTypes.object,
id: PropTypes.string,
t: PropTypes.func.isRequired,
analyze: PropTypes.object
};
export default translate(['tabsContent'], { wait: true })(WorldMap);
@@ -0,0 +1,17 @@
import Results from './Results'
import Performance from './Performance'
import Influencers from './Influencers'
import Sentiment from './Sentiment'
import Themes from './Themes'
import Demographics from './Demographics'
import WorldMap from './WorldMap'
export {
Results,
Performance,
Influencers,
Sentiment,
Themes,
Demographics,
WorldMap
}
@@ -0,0 +1,58 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from 'reactstrap';
import { deleteAnalytics } from '../../../../../api/analytics/savedAnalytics';
import { translate } from 'react-i18next';
function DeleteDialog(props) {
const [loading, setLoading] = useState(false);
const { actions, data, toggle, fetchData, t } = props;
function handleSubmit() {
setLoading(true);
deleteAnalytics(data.value).then((res) => {
if (res.error) {
res.data
? actions.addAlert(res.data)
: actions.addAlert({ type: 'error', transKey: 'somethingWrong' });
setLoading(false);
return;
}
actions.addAlert({ type: 'notice', transKey: 'analyticsDeleted' });
setLoading(false);
toggle();
fetchData();
});
}
return (
<Modal isOpen={!!data} toggle={toggle} backdrop="static">
<ModalHeader toggle={toggle}>
{t('tabsContent:analyzeTab.deleteAnalysis')}
</ModalHeader>
<ModalBody>
<div>
<p>{t('messages.deleteMessage')}</p>
</div>
</ModalBody>
<ModalFooter>
<Button color="link" onClick={toggle}>
{t('commonWords.Cancel')}
</Button>
<Button color="danger" disabled={loading} onClick={handleSubmit}>
{loading ? t('commonWords.loading') : t('commonWords.Delete')}
</Button>
</ModalFooter>
</Modal>
);
}
DeleteDialog.propTypes = {
toggle: PropTypes.func,
t: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
fetchData: PropTypes.func,
actions: PropTypes.object
};
export default React.memo(translate(['common'], { wait: true })(DeleteDialog));
@@ -0,0 +1,170 @@
/* eslint-disable react/prop-types */
import React, {
useState,
useCallback,
useMemo,
Fragment,
useEffect
} from 'react';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import { compose } from 'redux';
import { Table } from '../../../../common/Table/Table';
import { savedAnalytics } from '../../../../../api/analytics/savedAnalytics';
import reduxConnect from '../../../../../redux/utils/connect';
import {
getDate,
getQueryParams,
setDocumentData
} from '../../../../../common/helper';
import { Button } from 'reactstrap';
import DeleteDialog from './DeleteDialog';
import i18n from '../../../../../i18n';
function SavedAnalysisSubTab(props) {
const [dataSource, setDataSource] = useState({ data: [] });
const [loading, setLoading] = useState(true);
const [deleteValues, setDeleteValues] = useState(false);
const { t, actions } = props;
useEffect(() => {
setDocumentData('title', 'Saved Analysis | Analyze');
return () => {
setDocumentData('title');
};
}, []);
const columns = useMemo(() => {
const columnsList = [
{
id: 'feeds',
Header: t('analyzeTab.savedAnalytics.feeds'),
accessor: (d) => d.context.feeds,
Cell: (props) =>
props.value ? props.value.map((v) => v.name).join(', ') : ''
},
{
id: 'date',
Header: t('analyzeTab.savedAnalytics.dateRange'),
accessor: (d) => d.context.rawFilters.date,
Cell: (props) =>
props.value
? `${getDate(props.value.start, 'MM/DD/YYYY')} to ${getDate(
props.value.end,
'MM/DD/YYYY'
)}`
: '-'
},
{
Header: t('analyzeTab.savedAnalytics.createdAt'),
accessor: 'createdAt',
Cell: (props) => getDate(props.value, 'MM/DD/YYYY')
},
{
Header: t('analyzeTab.savedAnalytics.actions'),
accessor: 'id',
Cell: (props) => getActions(props)
}
];
return columnsList;
}, [getActions, i18n.language]);
const getActions = useCallback((props) => {
return (
<div>
<Button
outline
className="border-0 btn-transition"
color="primary"
size="sm"
tag={Link}
to={`/app/analyze/${props.value}/overview`}
>
{t('analyzeTab.savedAnalytics.view')}
</Button>
<Button
outline
className="border-0 btn-transition"
color="secondary"
tag={Link}
to={`/app/analyze/edit/${props.value}`}
>
{t('analyzeTab.savedAnalytics.edit')}
</Button>
<Button
outline
className="border-0 btn-transition"
color="secondary"
onClick={function () {
setDeleteValues(props);
}}
>
{t('analyzeTab.savedAnalytics.delete')}
</Button>
</div>
);
}, []);
const getSavedList = useCallback(
(page, pageSize) => {
setLoading(true);
const params = getQueryParams({ page, pageSize });
savedAnalytics(params).then((res) => {
if (res.error || res.data === null || !res.data) {
setLoading(false);
return actions.addAlert({
type: 'error',
transKey: 'somethingWrong'
});
}
res.data.length > 0 && setDataSource(res.data[0]);
setLoading(false);
});
},
[savedAnalytics]
);
const { data = [], totalCount = 0, limit = 10, page = 1 } = dataSource;
return (
<Fragment>
<Table
t={t}
cardTitle={t('analyzeTab.savedAnalysis')}
columns={columns}
data={data}
totalCount={totalCount}
showTotalCount
limit={limit}
page={page}
isLoading={loading}
onFetchData={getSavedList}
/>
{deleteValues && (
<DeleteDialog
data={deleteValues}
actions={actions}
toggle={function () {
setDeleteValues(false);
}}
fetchData={function () {
getSavedList(dataSource.page - 1, dataSource.limit);
}}
/>
)}
</Fragment>
);
}
SavedAnalysisSubTab.propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object
};
const applyDecorators = compose(
translate(['tabsContent'], { wait: true }),
reduxConnect()
);
export default applyDecorators(SavedAnalysisSubTab);
@@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { Link } from 'react-router-dom'
import { compose } from 'redux'
import { Card, Col, Row } from 'reactstrap'
class WelcomeSubTab extends React.Component {
render () {
const { t } = this.props
return (
<Card className="py-md-5 mb-3">
<Row className="justify-content-center no-gutters">
<Col sm="6" md="4" xl="4" className="m-4">
<div className="border b-radius-5 text-center p-4">
<div className="icon-wrapper mb-4 rounded-circle">
<div className="icon-wrapper-bg bg-primary" />
<i className="lnr-plus-circle text-primary" />
</div>
<div>
<h5 className="mb-5">{t('analyzeTab.createNewAnalysis')}</h5>
<Link
to="/app/analyze/create"
className="btn btn-primary btn-block fsize-1 btn-lg mr-1"
>
{t('analyzeTab.go')}
</Link>
</div>
</div>
</Col>
<Col sm="6" md="4" xl="4" className="m-4">
<div className="border b-radius-5 text-center p-4">
<div className="icon-wrapper mb-4 rounded-circle">
<div className="icon-wrapper-bg bg-primary" />
<i className="lnr-list text-primary" />
</div>
<div>
<h5 className="mb-5">{t('analyzeTab.viewSavedAnalysis')}</h5>
<Link
to="/app/analyze/saved"
className="btn btn-primary btn-block fsize-1 btn-lg mr-1"
>
{t('analyzeTab.view')}
</Link>
</div>
</div>
</Col>
</Row>
</Card>
)
}
}
WelcomeSubTab.propTypes = {
t: PropTypes.func.isRequired
}
const applyDecorators = compose(translate(['tabsContent'], { wait: true }))
export default applyDecorators(WelcomeSubTab)
@@ -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)
@@ -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
)
@@ -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)
@@ -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>
)
}
}
@@ -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
)
@@ -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
@@ -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);
@@ -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);
@@ -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)
@@ -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);
@@ -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);
@@ -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
@@ -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);
@@ -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);
@@ -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;
@@ -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
);
@@ -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)
@@ -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);
@@ -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);
@@ -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)
@@ -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)
@@ -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)
@@ -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
)
@@ -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
)
@@ -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)
@@ -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
)
@@ -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)
@@ -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)
@@ -0,0 +1,60 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import ExportFeedsTableRow from './ExportFeedsTableRow'
import LoadersAdvanced from '../../../../common/Loader/Loader'
import { Table, Card, CardBody } from 'reactstrap'
class ExportFeedsTable extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
tableData: PropTypes.array.isRequired,
showPopup: PropTypes.func.isRequired,
unexportFeed: PropTypes.func.isRequired,
goToFeed: PropTypes.func.isRequired
}
render() {
const {
tableData,
isLoading,
showPopup,
unexportFeed,
goToFeed,
t
} = this.props
return (
<Card className="main-card mb-3">
{isLoading && <LoadersAdvanced />}
<CardBody>
<Table striped bordered className="mb-0">
<thead>
<tr>
<th>{t('exportTab.feedName')}</th>
<th>{t('exportTab.exportWith')}</th>
<th>{t('exportTab.actions')}</th>
</tr>
</thead>
<tbody>
{tableData.map((feed) => {
return (
<ExportFeedsTableRow
key={feed.id}
feed={feed}
showPopup={showPopup}
unexportFeed={unexportFeed}
goToFeed={goToFeed}
/>
)
})}
</tbody>
</Table>
</CardBody>
</Card>
)
}
}
export default translate(['tabsContent'], { wait: true })(ExportFeedsTable)
@@ -0,0 +1,132 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Interpolate, translate } from 'react-i18next';
import Select from 'react-select';
import { Modal, ModalBody, ModalFooter, Button, ModalHeader } from 'reactstrap';
class ExportFeedsTableRow extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
feed: PropTypes.object.isRequired,
showPopup: PropTypes.func.isRequired,
unexportFeed: PropTypes.func.isRequired,
goToFeed: PropTypes.func.isRequired
};
constructor(props) {
super(props);
this.state = {
format: 'rss',
modal: false
};
}
showExportPopup = () => {
this.props.showPopup(this.props.feed, this.state.format);
};
toggle = () => {
this.setState((prev) => ({ modal: !prev.modal }));
};
exportOptions = [
{ label: 'RSS 2.0', value: 'rss' },
{ label: 'Atom 1.0', value: 'atom' },
{ label: 'TSV', value: 'tsv' },
{ label: 'HTML', value: 'html' }
];
onChangeFormat = (format) => {
this.setState({
format: format
});
};
onDeleteClick = () => {
this.setState({ modal: false });
this.props.unexportFeed(this.props.feed.id);
};
goToFeed = (e) => {
e.preventDefault();
this.props.goToFeed(this.props.feed.id);
};
render() {
const { feed, t } = this.props;
return (
<tr>
<td>
<Button
color="link"
className={`feed-icon font-size-lg p-0 feed-type-mixed ${feed.class}`}
onClick={this.goToFeed}
>
{feed.name}
</Button>
</td>
<td>
<Select
options={this.exportOptions}
value={this.state.format}
simpleValue
onChange={this.onChangeFormat}
clearable={false}
/>
</td>
<td>
<Button
size="sm"
color="primary"
className="border-0 mr-2"
onClick={this.showExportPopup}
>
{t('exportTab.export')}
</Button>
<Button
outline
size="sm"
color="secondary"
className="border-0"
onClick={this.toggle}
>
{t('exportTab.delete')}
</Button>
<Modal
isOpen={this.state.modal}
toggle={this.toggle}
backdrop="static"
>
<ModalHeader toggle={this.toggle}>
{t('exportTab.confirm')}
</ModalHeader>
<ModalBody>
<p>
<Interpolate
t={t}
i18nKey="exportTab.exportDeleteMessage"
feedName={feed.name}
/>
</p>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.toggle}>
{t('common:commonWords.Cancel')}
</Button>
<Button color="danger" onClick={this.onDeleteClick}>
{t('common:commonWords.Delete')}
</Button>
</ModalFooter>
</Modal>
</td>
</tr>
);
}
}
export default translate(['tabsContent'], { wait: true })(ExportFeedsTableRow);
@@ -0,0 +1,98 @@
import React from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import config from '../../../../../appConfig';
import {
Button,
Modal,
ModalHeader,
ModalBody,
ModalFooter,
Table
} from 'reactstrap';
class ExportPopup extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
feed: PropTypes.object.isRequired,
hidePopup: PropTypes.func.isRequired,
exportFormat: PropTypes.string.isRequired
};
hidePopup = () => {
this.props.hidePopup();
};
hidePopupFromOutside = (e) => {
if (e.target === e.currentTarget) this.hidePopup();
};
exportOptions = {
rss: 'RSS 2.0',
atom: 'Atom 1.0',
tsv: 'TSV',
html: 'HTML'
};
render() {
const { t, feed, exportFormat } = this.props;
const href = `${config.apiUrl}/feed/${feed.id}.${exportFormat}`;
return (
<Modal isOpen toggle={this.hidePopup} backdrop="static" size="lg">
<ModalHeader toggle={this.hidePopup}>
{this.exportOptions[exportFormat] + ' ' + t('exportTab.export')}
</ModalHeader>
<ModalBody>
<div className="mb-4">
<p>{t('exportTab.exportPopup.line1')}</p>
<p className="text-muted font-size-xs mb-2">
({t('exportTab.exportPopup.line2')})
</p>
<a
href={href}
target="_blank"
className="font-weight-bold"
rel="noopener noreferrer"
>
{href}
</a>
</div>
<p className="mb-2">{t('exportTab.exportPopup.line3')}</p>
<Table striped>
<tbody>
<tr>
<th scope="row">n</th>
<td>{t('exportTab.exportPopup.param1')}</td>
</tr>
<tr>
<th scope="row">ext</th>
<td>{t('exportTab.exportPopup.param2')}</td>
</tr>
{exportFormat !== 'tsv' && (
<tr>
<th scope="row">img</th>
<td>{t('exportTab.exportPopup.param3')}</td>
</tr>
)}
{exportFormat !== 'tsv' && exportFormat !== 'html' && (
<tr>
<th scope="row">text_format</th>
<td>{t('exportTab.exportPopup.param4')}</td>
</tr>
)}
</tbody>
</Table>
</ModalBody>
<ModalFooter>
<Button color="light" onClick={this.hidePopup}>
{t('exportTab.close')}
</Button>
</ModalFooter>
</Modal>
);
}
}
export default translate(['tabsContent'], { wait: true })(ExportPopup);
@@ -0,0 +1,71 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import ExportFeedsTable from './ExportFeedsTable'
import ExportPopup from './ExportPopup'
import { withRouter } from 'react-router-dom'
import reduxConnect from '../../../../../redux/utils/connect'
import { compose } from 'redux'
import { setDocumentData } from '../../../../../common/helper'
class ExportSubTab extends React.Component {
static propTypes = {
exportFeedsState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
componentDidMount() {
setDocumentData('title', 'Export | Share')
}
componentWillUnmount() {
setDocumentData('title')
}
componentWillMount = () => {
this.props.actions.loadExportedFeeds()
};
goToFeed = (feedId) => {
const {
history,
actions: { getFeedResults }
} = this.props
history.push('/app/search/search')
getFeedResults({ page: 1 }, feedId)
};
render () {
const { t, exportFeedsState, actions } = this.props
return (
<div>
<p className="text-muted mb-3">{t('exportTab.topMessage')}</p>
<ExportFeedsTable
isLoading={exportFeedsState.isLoading}
tableData={exportFeedsState.tableData}
showPopup={actions.showExportPopup}
unexportFeed={actions.unexportFeed}
goToFeed={this.goToFeed}
/>
{exportFeedsState.popupVisible && (
<ExportPopup
feed={exportFeedsState.selectedFeed}
hidePopup={actions.hideExportPopup}
exportFormat={exportFeedsState.exportFormat}
/>
)}
</div>
)
}
}
const applyDecorators = compose(
withRouter,
reduxConnect('exportFeedsState', ['appState', 'share', 'exportFeeds']),
translate(['tabsContent'], { wait: true })
)
export default applyDecorators(ExportSubTab)
@@ -0,0 +1,16 @@
import PropTypes from 'prop-types'
import {AlertForm as BaseAlertForm} from '../NotificatoinsSubTab/forms/AlertForm'
import {translate} from 'react-i18next'
export class AlertForm extends BaseAlertForm {
static propTypes = {
t: PropTypes.func.isRequired,
state: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
switchShareSubScreen: PropTypes.func.isRequired
}
}
export default translate(['tabsContent'], { wait: true })(AlertForm)
@@ -0,0 +1,91 @@
import React from 'react'
import { translate } from 'react-i18next'
import PropTypes from 'prop-types'
import SortableTh from '../../../../common/Table/SortableTh'
import { MyEmailsTable } from '../NotificatoinsSubTab/MyEmailsTable' // default export doesn't work
import { ButtonGroup, Button } from 'reactstrap'
class EmailsTable extends MyEmailsTable {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
tableActions: PropTypes.object.isRequired,
deleteSingleText: PropTypes.string.isRequired,
deleteMultipleText: PropTypes.string.isRequired
};
nameClickAction = (item) => {
const { actions } = this.props
actions.startEditNotification(item, 'emails', 'emails')
};
defineColumns () {
return {
...super.defineColumns(),
owner: {
Header: <SortableTh title="manageEmailsTab.owner" />,
accessor: (item) => item.owner.email,
width: 170
}
}
}
onRefreshButtonClick = () => {
this.props.tableActions.loadTable({})
};
getColumns () {
return [
'selectCheckbox',
'name',
'type',
'owner',
'published',
'ScheduledTimes',
'sourcesCount',
'Recipients',
'active',
'delete'
]
}
getActionsPanel = () => {
const { t } = this.props
return (
<ButtonGroup className="mb-3">
<Button
onClick={this.onActivateButtonClick}
color="secondary"
>
<i className="fa fa-play fa-1px for-small mr-1"> </i>{" "}
{t('notificationsTab.activate')}
</Button>
<Button
color="secondary"
onClick={this.onPauseButtonClick}
>
<i className="fa fa-pause fa-1px for-small mr-1"> </i>{" "}
{t('notificationsTab.pause')}
</Button>
<Button
color="secondary"
onClick={this.onDeleteButtonClick}
>
<i className="fa fa-trash for-small mr-1"> </i>{" "}
{t('notificationsTab.delete')}
</Button>
<Button
color="secondary"
onClick={this.onRefreshButtonClick}
>
<i className="fa fa-refresh fa-1px for-small mr-1"> </i>{" "}
{t('manageEmailsTab.refresh')}
</Button>
</ButtonGroup>
)
};
}
export default translate(['tabsContent'], { wait: true })(EmailsTable)
@@ -0,0 +1,49 @@
import React from 'react'
import PropTypes from 'prop-types'
import {translate} from 'react-i18next'
import {GenericTable} from '../common/GenericTable'
import SortableTh from '../../../../common/Table/SortableTh'
import {EMAILS_SUBSCREENS} from '../../../../../redux/modules/appState/share/tabs'
export class FiltersTable extends GenericTable {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
tableActions: PropTypes.object.isRequired
};
defineColumns () {
return {
'name': {
Header: <SortableTh title='manageEmailsTab.filter' />,
accessor: 'name'
},
'notifications': {
Header: <SortableTh title='manageEmailsTab.notifications' />,
width: 270,
accessor: 'notifications'
}
}
}
getColumns () {
return ['name', 'notifications']
}
onRowClick = (e, state, rowInfo) => {
const { actions } = this.props
const filter = {
type: rowInfo.original.type,
id: rowInfo.original.id,
name: rowInfo.original.name
}
actions.shareTables.emails.setFilter(filter)
actions.switchShareSubScreen('emails', EMAILS_SUBSCREENS.EMAILS_TABLE)
actions.shareTables.emails.loadTable({})
};
}
export default translate(['tabsContent'], { wait: true })(FiltersTable)
@@ -0,0 +1,74 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import Select from 'react-select'
import { EMAILS_SUBSCREENS } from '../../../../../redux/modules/appState/share/tabs'
import { Button } from 'reactstrap'
export class FiltersTopBar extends React.Component {
static propTypes = {
actions: PropTypes.object.isRequired,
filterType: PropTypes.string.isRequired,
t: PropTypes.func.isRequired
};
onSelectFilterType = (filterType) => {
const { actions } = this.props
actions.shareTables.emailFilters.loadTable({ filterType })
};
clearFilters = () => {
const { actions } = this.props
actions.shareTables.emails.clearFilter()
actions.switchShareSubScreen('emails', EMAILS_SUBSCREENS.EMAILS_TABLE)
};
backToTable = () => {
const { actions } = this.props
actions.switchShareSubScreen('emails', EMAILS_SUBSCREENS.EMAILS_TABLE)
}
filterTypes = [
{ label: 'Owner', value: 'owner' },
{ label: 'Recipient', value: 'recipient' },
{ label: 'Feed', value: 'feed' }
];
render () {
const { t, filterType } = this.props
return (
<Fragment>
<Button className="btn-wide mb-2" size="sm" color="info" onClick={this.backToTable}>
<i className="lnr lnr-chevron-left"> </i>
</Button>
<div className="notifications-topbar align-items-center">
<div className="text-muted">{t('manageEmailsTab.emailFilter')}</div>
<div className="d-flex align-items-center">
<label className="mr-1">{t('manageEmailsTab.filterBy')}</label>
<div style={{ minWidth: '150px' }}>
<Select
value={filterType}
onChange={this.onSelectFilterType}
options={this.filterTypes}
simpleValue
searchable={false}
clearable={false}
/>
</div>
<Button
color="secondary"
className="ml-2"
onClick={this.clearFilters}
>
{t('manageEmailsTab.allEmails')}
</Button>
</div>
</div>
</Fragment>
)
}
}
export default translate(['tabsContent'], { wait: true })(FiltersTopBar)
@@ -0,0 +1,87 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import TopBar from './TopBar';
import EmailsTable from './EmailsTable';
import reduxConnect from '../../../../../redux/utils/connect';
import AlertForm from './AlertForm';
import Navigation from './Navigation';
import FiltersTable from './FiltersTable';
import FiltersTopBar from './FiltersTopBar';
import { EMAILS_SUBSCREENS } from '../../../../../redux/modules/appState/share/tabs';
import { setDocumentData } from '../../../../../common/helper';
class ManageEmailsSubTab extends React.Component {
static propTypes = {
shareState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
componentDidMount() {
setDocumentData('title', 'Manage Recipients | Share')
}
componentWillUnmount() {
setDocumentData('title')
}
render() {
const { shareState, actions } = this.props;
const { subScreenVisible } = shareState.tabs.emails;
return (
<div className="notifications-tab">
{subScreenVisible === EMAILS_SUBSCREENS.EMAILS_TABLE && (
<div>
<TopBar tableState={shareState.tables.emails} actions={actions} />
<EmailsTable
tableState={shareState.tables.emails}
actions={actions}
tableActions={actions.shareTables.emails}
deleteSingleText="email"
deleteMultipleText="emails"
/>
</div>
)}
{(subScreenVisible === EMAILS_SUBSCREENS.ALERT_FORM ||
subScreenVisible === EMAILS_SUBSCREENS.NEWSLETTER_FORM) && (
<Navigation actions={actions} />
)}
{subScreenVisible === EMAILS_SUBSCREENS.ALERT_FORM && (
<AlertForm
state={shareState.forms.alert}
switchShareSubScreen={actions.switchShareSubScreen}
actions={actions.shareForms.alert}
/>
)}
{/* {subScreenVisible === EMAILS_SUBSCREENS.NEWSLETTER_FORM &&
<NewsletterForm
state={shareState.forms.newsletter}
switchShareSubScreen={actions.switchShareSubScreen}
actions={actions.shareForms.newsletter}
/>
} */}
{subScreenVisible === EMAILS_SUBSCREENS.FILTERS_TABLE && (
<Fragment>
<FiltersTopBar
actions={actions}
filterType={shareState.tables.emailFilters.filterType}
/>
<FiltersTable
actions={actions}
tableState={shareState.tables.emailFilters}
tableActions={actions.shareTables.emailFilters}
/>
</Fragment>
)}
</div>
);
}
}
export default reduxConnect('shareState', ['appState', 'share'])(
ManageEmailsSubTab
);
@@ -0,0 +1,30 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { EMAILS_SUBSCREENS } from '../../../../../redux/modules/appState/share/tabs'
import { Button } from 'reactstrap'
class Navigation extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired
};
backToTable = () => {
this.props.actions.switchShareSubScreen(
'emails',
EMAILS_SUBSCREENS.EMAILS_TABLE
)
};
render () {
return (
<Button className="btn-wide mb-2" size="sm" color="info" onClick={this.backToTable}>
<i className="lnr lnr-chevron-left"> </i>
</Button>
)
}
}
export default translate(['tabsContent'], { wait: true })(Navigation)
@@ -0,0 +1,14 @@
import PropTypes from 'prop-types'
import {NewsletterForm as BaseNewsletterForm} from '../NotificatoinsSubTab/forms/NewsletterForm'
import {translate} from 'react-i18next'
export class NewsletterForm extends BaseNewsletterForm {
static propTypes = {
t: PropTypes.func.isRequired,
state: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
}
export default translate(['tabsContent'], { wait: true })(NewsletterForm)
@@ -0,0 +1,72 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { EMAILS_SUBSCREENS } from '../../../../../redux/modules/appState/share/tabs'
import { Button } from 'reactstrap'
export class TopBar extends React.Component {
static propTypes = {
actions: PropTypes.object.isRequired,
tableState: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
};
onCreate = (type) => () => {
const { actions } = this.props
actions.startCreateNotification(type, 'emails', 'emails')
};
goToFiltersTable = () => {
const { actions } = this.props
actions.switchShareSubScreen('emails', EMAILS_SUBSCREENS.FILTERS_TABLE)
};
render () {
const {
t,
tableState: { filter }
} = this.props
const filterName = filter
? `${filter.name} (${t('manageEmailsTab.' + filter.type)})`
: t('manageEmailsTab.allEmails')
return (
<div className="notifications-topbar">
<p className="text-muted align-self-center">
<strong>{t('manageEmailsTab.currentFilter') + ': '}</strong>{" "}
{filterName}
</p>
<div>
<Button
className="btn-icon mr-2"
onClick={this.goToFiltersTable}
>
<i className="lnr lnr-funnel btn-icon-wrapper" />
{t('manageEmailsTab.selectFilter')}
</Button>
<div className="notifications-buttons">
<Button
color="primary"
className="btn-icon"
onClick={this.onCreate(EMAILS_SUBSCREENS.ALERT_FORM)}
>
<i className="lnr lnr-alarm btn-icon-wrapper" />
{t('notificationsTab.newAlert')}
</Button>
{/* <Button
color="primary"
className="btn-icon"
onClick={this.onCreate(EMAILS_SUBSCREENS.NEWSLETTER_FORM)}
>
<i className="lnr lnr-file-add btn-icon-wrapper" />
{t('notificationsTab.newNewsletter')}
</Button> */}
</div>
</div>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(TopBar)
@@ -0,0 +1,52 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import ReceiversTable from './ReceiversTable'
import SortableTh from '../../../../common/Table/SortableTh'
import LinkCell from '../../../../common/Table/LinkCell'
class GroupsTable extends ReceiversTable {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
tableActions: PropTypes.object.isRequired,
deleteSingleText: PropTypes.string.isRequired,
deleteMultipleText: PropTypes.string.isRequired
};
nameClickAction = (item) => {
this.props.actions.startEditGroup(item)
};
defineColumns () {
//const {t} = this.props;
const colDefs = super.defineColumns()
return {
...colDefs,
'recipientsNumber': {
Header: <SortableTh title='manageRecipientsTab.recipientsNumber' />,
accessor: item => item.recipients.length || '',
width: 140
},
'name': {
Header: <SortableTh title='manageRecipientsTab.groupName' />,
accessor: 'name',
Cell: (row) => {
return (
<LinkCell item={row.original} onClick={this.nameClickAction}>
{row.value}
</LinkCell>
)
}
}
}
}
getColumns () {
return ['selectCheckbox', 'name', 'recipientsNumber', 'subscriptions', 'creationDate', 'active']
}
}
export default translate(['tabsContent'], { wait: true })(GroupsTable)
@@ -0,0 +1,83 @@
import React from 'react'
import PropTypes from 'prop-types'
import TopBar from './TopBar'
import RecipientsTable from './RecipientsTable'
import { RECEIVER_TABLES, RECEIVER_SUBSCREENS } from '../../../../../redux/modules/appState/share/tabs'
import {RecipientForm} from './forms/ReceiverForm'
import GroupsTable from './GroupsTable'
import {withRouter} from 'react-router-dom'
import reduxConnect from '../../../../../redux/utils/connect'
import {compose} from 'redux'
import { setDocumentData } from '../../../../../common/helper'
class ManageRecipientsSubTab extends React.Component {
static propTypes = {
shareState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
componentDidMount() {
setDocumentData('title', 'Manage Emails | Share')
}
componentWillUnmount() {
setDocumentData('title')
}
render () {
const { shareState, actions } = this.props
const { subScreenVisible, tableVisible } = shareState.tabs.recipients
const tableState = shareState.tables[tableVisible]
return (
<div className="notifications-tab">
{subScreenVisible === RECEIVER_SUBSCREENS.TABLES &&
<div>
<TopBar
tableVisible={tableVisible}
tables={[RECEIVER_TABLES.RECIPIENTS, RECEIVER_TABLES.GROUPS]}
actions={actions}
/>
{tableVisible === RECEIVER_TABLES.RECIPIENTS &&
<RecipientsTable
tableState={tableState}
actions={actions}
tableActions={actions.shareTables[tableVisible]}
deleteSingleText='recipient'
deleteMultipleText='recipients'
/>
}
{tableVisible === RECEIVER_TABLES.GROUPS &&
<GroupsTable
tableState={tableState}
actions={actions}
tableActions={actions.shareTables[tableVisible]}
deleteSingleText='group'
deleteMultipleText='groups'
/>
}
</div>
}
{(subScreenVisible === RECEIVER_SUBSCREENS.RECIPIENT_FORM || subScreenVisible === RECEIVER_SUBSCREENS.GROUP_FORM) &&
<RecipientForm
formType={subScreenVisible}
shareState={shareState}
actions={actions}
/>
}
</div>
)
}
}
const applyDecorators = compose(
withRouter,
reduxConnect('shareState', ['appState', 'share'])
)
export default applyDecorators(ManageRecipientsSubTab)
@@ -0,0 +1,108 @@
import React from 'react'
import GenericTable from '../common/GenericTable'
import SortableTh from '../../../../common/Table/SortableTh'
import PropTypes from 'prop-types'
import { ButtonGroup, Button } from 'reactstrap'
import { convertUTCtoLocal } from '../../../../../common/helper'
class ReceiversTable extends GenericTable {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
tableActions: PropTypes.object.isRequired,
deleteSingleText: PropTypes.string.isRequired,
deleteMultipleText: PropTypes.string.isRequired
};
onActivateButtonClick = () => {
const { tableState, tableActions } = this.props
tableActions.toggleActive(tableState.selectedIds, true)
};
onPauseButtonClick = () => {
const { tableState, tableActions } = this.props
tableActions.toggleActive(tableState.selectedIds, false)
};
togglerOnAction = (itemId) => {
const { tableActions } = this.props
tableActions.toggleActive([itemId], true)
};
togglerOffAction = (itemId) => {
const { tableActions } = this.props
tableActions.toggleActive([itemId], false)
};
getActionsPanel = () => {
const { t } = this.props
return (
<ButtonGroup className="mb-3">
<Button
color="secondary"
onClick={this.onActivateButtonClick}
>
<i className="fa fa-play mr-1 for-small" />
{t('notificationsTab.activate')}
</Button>
<Button
color="secondary"
onClick={this.onPauseButtonClick}
>
<i className="fa fa-pause for-small mr-1" />
{t('notificationsTab.pause')}
</Button>
<Button
color="secondary"
onClick={this.onDeleteButtonClick}
>
<i className="fa fa-trash for-small mr-1" />
{t('notificationsTab.delete')}
</Button>
</ButtonGroup>
)
};
_formatSubscriptions (subscriptions) {
const { t } = this.props
const result = []
if (subscriptions.alert > 0) {
result.push(`${subscriptions.alert} ${t('notificationsTab.alerts')}`)
}
if (subscriptions.newsletter > 0) {
result.push(`${subscriptions.newsletter} ${t('notificationsTab.newsletters')}`)
}
return result.join(', ')
}
defineColumns () {
const { t } = this.props
const colDefinitions = super.defineColumns()
return {
...colDefinitions,
subscriptions: {
sortable: false,
Header: t('manageRecipientsTab.subscriptions'),
accessor: (item) => this._formatSubscriptions(item.subscriptions),
width: 170
},
creationDate: {
Header: <SortableTh title="manageRecipientsTab.creationDate" />,
accessor: (item) => convertUTCtoLocal(item.creationDate, 'DD MMM YYYY HH:mm'),
width: 100
},
active: this.createTogglerColumn(
'manageRecipientsTab.status',
'active',
'active',
'paused',
this.togglerOnAction,
this.togglerOffAction
)
}
}
}
export default ReceiversTable
@@ -0,0 +1,60 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import ReceiversTable from './ReceiversTable'
import SortableTh from '../../../../common/Table/SortableTh'
import LinkCell from '../../../../common/Table/LinkCell'
class RecipientsTable extends ReceiversTable {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
tableActions: PropTypes.object.isRequired,
deleteSingleText: PropTypes.string.isRequired,
deleteMultipleText: PropTypes.string.isRequired
};
nameClickAction = (item) => {
this.props.actions.startEditRecipient(item)
};
defineColumns () {
const {t} = this.props
const colDefs = super.defineColumns()
return {
...colDefs,
'email': {
Header: <SortableTh title='manageRecipientsTab.email' />,
accessor: 'email',
width: 170
},
'groups': {
sortable: false,
Header: t('manageRecipientsTab.groups'),
accessor: item => item.groups.map(group => group.name).join(', '),
width: 170
},
'name': {
Header: <SortableTh title='manageRecipientsTab.name' />,
accessor: 'name',
Cell: (row) => {
const {original} = row
const name = `${original.firstName} ${original.lastName}`
return (
<LinkCell item={original} onClick={this.nameClickAction}>
{name}
</LinkCell>
)
}
}
}
}
getColumns () {
return ['selectCheckbox', 'name', 'email', 'groups', 'subscriptions', 'creationDate', 'active']
}
}
export default translate(['tabsContent'], { wait: true })(RecipientsTable)
@@ -0,0 +1,77 @@
import React from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import { Button, Input, InputGroup, InputGroupAddon } from 'reactstrap'
const INPUT_THROTTLE_TIME = 300
export class TableFilter extends React.Component {
static propTypes = {
type: PropTypes.string.isRequired,
onFilterRequest: PropTypes.func.isRequired
};
constructor () {
super()
this.state = {
value: ''
}
}
onFilter = (event) => {
this._onFilterImpl(event.target.value)
};
_onFilterImpl (filterValue) {
const { onFilterRequest } = this.props
this.setState({ value: filterValue })
if (this.inputDelay) {
clearTimeout(this.inputDelay)
}
this.inputDelay = setTimeout(() => {
onFilterRequest(filterValue)
}, INPUT_THROTTLE_TIME)
}
onClear = () => {
const value = this.state.value
value && this._onFilterImpl('')
};
render () {
const { type } = this.props
const value = this.state.value
const hasValue = !!value
const iconClasses = classnames('fa', {
'fa-search': !hasValue,
'fa-times': hasValue
})
const placeholder = `Find ${type}`
return (
<InputGroup className="mb-3">
<Input
type="text"
placeholder={placeholder}
value={value}
onChange={this.onFilter}
/>
<InputGroupAddon addonType="append">
<Button color="primary" onClick={this.onClear}>
<i className={iconClasses}></i>
</Button>
</InputGroupAddon>
{/* <button
className="cw-grid__filter-button"
type="button"
onClick={this.onClear}
>
<i className={iconClasses} />
</button> */}
</InputGroup>
)
}
}
export default TableFilter
@@ -0,0 +1,77 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import TableFilter from './TableFilter'
import TableSwitcher from '../common/TableSwitcher/TableSwitcher'
import { Button } from 'reactstrap'
export class TopBar extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
tables: PropTypes.array.isRequired,
tableVisible: PropTypes.string.isRequired,
actions: PropTypes.object.isRequired
};
onNewRecipient = () => {
const { actions } = this.props
actions.startCreateRecipient()
};
onNewGroup = () => {
const { actions } = this.props
actions.startCreateGroup()
};
onFilterRequest = (filter) => {
this.loadTable({ filter })
};
loadTable = (params) => {
const { tableVisible: type } = this.props
this.props.actions.shareTables[type].loadTable(params || null)
};
render () {
const { t, tables, tableVisible, actions } = this.props
return (
<Fragment>
<div className="notifications-topbar align-items-center">
<TableSwitcher
tables={tables}
tableVisible={tableVisible}
subTab="recipients"
switchTable={actions.switchShareTable}
loadTable={this.loadTable}
/>
<div className="notifications-buttons">
<Button
color="primary"
className="btn-icon mr-2"
onClick={this.onNewRecipient}
>
<i className="lnr lnr-location for-small btn-icon-wrapper" />
{t('manageRecipientsTab.newRecipient')}
</Button>
<Button
color="primary"
className="btn-icon"
onClick={this.onNewGroup}
>
<i className="lnr lnr-users for-small btn-icon-wrapper" />
{t('manageRecipientsTab.newGroup')}
</Button>
</div>
</div>
<TableFilter
type={tableVisible}
onFilterRequest={this.onFilterRequest}
/>
</Fragment>
)
}
}
export default translate(['tabsContent'], { wait: true })(TopBar)
@@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import InputField from './InputField'
import { Card, CardBody, CardTitle, Form, FormGroup, Input, Label } from 'reactstrap'
export class BasicGroupInfo extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
item: PropTypes.object.isRequired,
formActions: PropTypes.object.isRequired
};
onChangeFor = (field) => (event) => {
const { formActions } = this.props
formActions.changeField(field, event.target.value)
};
render () {
const { t, item } = this.props
return (
<Card>
<CardBody>
<CardTitle>{t('manageRecipientsTab.form.group.basicInfo')}</CardTitle>
<Form>
<InputField
formType="group"
field="name"
value={item.name}
onChangeFor={this.onChangeFor}
/>
<FormGroup>
<Label>
{t('manageRecipientsTab.form.group.description')}
</Label>
<Input
type="textarea"
rows="5"
onChange={this.onChangeFor('description')}
value={item.description}
/>
</FormGroup>
</Form>
<hr />
{!!item.recipients && (
<p>
{t('manageRecipientsTab.form.group.recipientsNumber')}:{' '}
{item.recipients.length}
</p>
)}
</CardBody>
</Card>
)
}
}
export default translate(['tabsContent'], { wait: true })(BasicGroupInfo)
@@ -0,0 +1,54 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import InputField from './InputField'
import { Card, CardBody, CardTitle, Form } from 'reactstrap'
export class BasicRecipientInfo extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
item: PropTypes.object.isRequired,
formActions: PropTypes.object.isRequired
}
onChangeFor = (field) => (event) => {
const { formActions } = this.props
formActions.changeField(field, event.target.value)
}
render() {
const { item, t } = this.props
return (
<Card>
<CardBody>
<CardTitle>{t('manageRecipientsTab.form.recipient.basicInfo')}</CardTitle>
<Form>
<InputField
formType="recipient"
field="firstName"
value={item.firstName}
onChangeFor={this.onChangeFor}
/>
<InputField
formType="recipient"
field="lastName"
value={item.lastName}
onChangeFor={this.onChangeFor}
/>
<InputField
formType="recipient"
field="email"
value={item.email}
onChangeFor={this.onChangeFor}
/>
</Form>
</CardBody>
</Card>
)
}
}
export default translate(['tabsContent'], { wait: true })(BasicRecipientInfo)
@@ -0,0 +1,27 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
export class BreadCrumbs extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
title: PropTypes.string.isRequired,
onBack: PropTypes.func.isRequired
};
render () {
const { t, title, onBack } = this.props
return (
<div>
<a href="#" onClick={onBack}>
{t('tableSwitcher.recipients')}
</a>
<span> &gt; {title}</span>
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(BreadCrumbs)
@@ -0,0 +1,119 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import Toggler from '../../../../../common/Table/Toggler'
import { Button, Card, CardBody, CardTitle, Label } from 'reactstrap'
import { convertUTCtoLocal } from '../../../../../../common/helper'
export class FormTopBar extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
formType: PropTypes.string.isRequired,
receiver: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
}
togglerAction = () => {
const { actions, formType } = this.props
actions.shareForms[formType].toggleActive()
}
onBack = () => {
this.props.actions.switchShareSubScreen('recipients', 'tables')
}
onDelete = () => {
const { formType, actions } = this.props
actions.shareForms[formType].confirmDelete()
}
onSave = () => {
const { actions, formType } = this.props
actions.shareForms[formType].saveReceiver()
}
render() {
const { t, formType, receiver } = this.props
const hasItem = !!receiver.id
const trPath = 'manageRecipientsTab.form'
let title = t(`${trPath}.${formType}.unsaved`)
if (hasItem) {
title =
formType === 'group'
? receiver.name
: `${receiver.firstName} ${receiver.lastName}`
}
return (
<Fragment>
<Button
className="btn-wide mb-3"
size="sm"
color="info"
onClick={this.onBack}
>
<i className="lnr lnr-chevron-left"> </i>
</Button>
<Card className="main-card mb-3">
<CardBody>
<CardTitle>{title}</CardTitle>
<div className="d-flex justify-content-between flex-wrap align-items-center">
<div>
<Label className="mr-2">
{t(`${trPath}.${formType}.nameStatus`)}
</Label>
<Toggler
id={receiver.id}
turnOnAction={this.togglerAction}
turnOffAction={this.togglerAction}
state={receiver.active}
enabledText="active"
disabledText="paused"
/>
</div>
<div>
{hasItem && (
<Button
color="danger"
className="btn-icon mr-2"
onClick={this.onDelete}
>
<i className="lnr lnr-trash btn-icon-wrapper"></i>
{t(`${trPath}.${formType}.deleteButton`)}
</Button>
)}
<Button
color="secondary"
className="btn-icon mr-2"
onClick={this.onBack}
>
<i className="lnr lnr-cross btn-icon-wrapper"></i>
{t(`${trPath}.cancel`)}
</Button>
<Button
color="success"
className="btn-icon mr-2"
onClick={this.onSave}
>
<i className="lnr lnr-checkmark-circle btn-icon-wrapper" />
{t(`${trPath}.save`)}
</Button>
</div>
</div>
{hasItem && receiver.creationDate && (
<p className="mt-1">
{t(`${trPath}.${formType}.creationDate`)}:
{convertUTCtoLocal(receiver.creationDate, 'DD MMM YYYY HH:mm')}
</p>
)}
</CardBody>
</Card>
</Fragment>
)
}
}
export default translate(['tabsContent'], { wait: true })(FormTopBar)
@@ -0,0 +1,29 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { FormGroup, Input, Label } from 'reactstrap'
export class InputField extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
formType: PropTypes.string.isRequired,
field: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
onChangeFor: PropTypes.func.isRequired
};
render () {
const { t, formType, field, value, onChangeFor } = this.props
const trPath = `manageRecipientsTab.form.${formType}`
return (
<FormGroup>
<Label>{t(`${trPath}.${field}`)}</Label>
<Input type="text" onChange={onChangeFor(field)} value={value} />
</FormGroup>
)
}
}
export default translate(['tabsContent'], { wait: true })(InputField)
@@ -0,0 +1,138 @@
import React, { Fragment } from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import FormTopBar from './FormTopBar'
import BasicRecipientInfo from './BasicRecipientInfo'
import BasicGroupInfo from './BasicGroupInfo'
import TablesTabs from './TablesTabs'
import EmailHistoryTable from './tables/EmailHistoryTable'
import DeletePopup from '../../common/DeletePopup'
import {
RECIPIENT_FORM_TABLES,
GROUP_FORM_TABLES,
RECEIVER_SUBSCREENS
} from '../../../../../../redux/modules/appState/share/tabs'
import ReceiverSubscriptionsTable from './tables/ReceiverSubscriptionsTable'
import ReceiverGroupsTable from './tables/ReceiverGroupsTable'
import ReceiverRecipientsTable from './tables/ReceiverRecipientsTable'
import { Card, CardBody, CardHeader, Col, Nav, Row } from 'reactstrap'
export class RecipientForm extends React.Component {
static propTypes = {
formType: PropTypes.string.isRequired,
shareState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
}
chooseTableTab = (tab) => {
const { actions, formType } = this.props
actions.shareForms[formType].chooseTableTab(tab)
}
render() {
const { formType, shareState, actions } = this.props
const formState = shareState.forms[formType] // receiver
const formActions = actions.shareForms[formType]
let allTabs = formState.tabs.all
if (!formState.id) {
allTabs = allTabs.filter(
(tab) => tab !== RECIPIENT_FORM_TABLES.EMAIL_HISTORY
)
}
const activeTab = formState.tabs.active
const tableState = shareState.tables.receiverForm[activeTab]
const tableActions = actions.shareTables.receiverForm[activeTab]
const deleteText =
formType === RECEIVER_SUBSCREENS.GROUP_FORM ? 'group' : 'recipient'
return (
<Fragment>
<FormTopBar
formType={formType}
receiver={formState}
actions={actions}
/>
<Row>
<Col lg="4">
{formType === RECEIVER_SUBSCREENS.RECIPIENT_FORM && (
<BasicRecipientInfo item={formState} formActions={formActions} />
)}
{formType === RECEIVER_SUBSCREENS.GROUP_FORM && (
<BasicGroupInfo item={formState} formActions={formActions} />
)}
</Col>
<Col lg="8">
<Card className="mb-3">
<CardHeader>
<Nav justified>
<TablesTabs
tabs={allTabs}
activeTab={activeTab}
chooseTableTab={this.chooseTableTab}
/>
</Nav>
</CardHeader>
<CardBody>
{activeTab === RECIPIENT_FORM_TABLES.SUBSCRIPTIONS && (
<ReceiverSubscriptionsTable
tableState={tableState}
actions={actions}
tableActions={tableActions}
receiver={formState}
formActions={formActions}
/>
)}
{activeTab === RECIPIENT_FORM_TABLES.GROUPS && (
<ReceiverGroupsTable
tableState={tableState}
actions={actions}
tableActions={tableActions}
receiver={formState}
formActions={formActions}
/>
)}
{activeTab === RECIPIENT_FORM_TABLES.EMAIL_HISTORY && (
<EmailHistoryTable
type={activeTab}
tableState={tableState}
actions={actions}
tableActions={tableActions}
receiver={formState}
/>
)}
{activeTab === GROUP_FORM_TABLES.RECIPIENTS && (
<ReceiverRecipientsTable
tableState={tableState}
actions={actions}
tableActions={tableActions}
receiver={formState}
formActions={formActions}
/>
)}
</CardBody>
</Card>
</Col>
</Row>
{formState.isDeletePopupVisible && (
<DeletePopup
actions={formActions}
idsToDelete={[formState.id]}
deleteSingleText={deleteText}
/>
)}
</Fragment>
)
}
}
export default translate(['tabsContent'], { wait: true })(RecipientForm)
@@ -0,0 +1,35 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { NavItem, NavLink } from 'reactstrap'
export class TablesTabs extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
tabs: PropTypes.array.isRequired,
activeTab: PropTypes.string.isRequired,
chooseTableTab: PropTypes.func.isRequired
}
chooseTableTab = (tab) => () => {
this.props.chooseTableTab(tab)
}
render() {
const { t, tabs, activeTab } = this.props
return tabs.map((tab, i) => (
<NavItem key={tab}>
<NavLink
key={`table-tab-${i}`}
active={tab === activeTab}
onClick={this.chooseTableTab(tab)}
>
{t(`manageRecipientsTab.tables.${tab}`)}
</NavLink>
</NavItem>
))
}
}
export default translate(['tabsContent'], { wait: true })(TablesTabs)
@@ -0,0 +1,45 @@
import React from 'react'
import PropTypes from 'prop-types'
import {translate} from 'react-i18next'
import ReceiverFormTable from './ReceiverFormTable'
import SortableTh from '../../../../../../common/Table/SortableTh'
import { convertUTCtoLocal } from '../../../../../../../common/helper'
export class EmailHistoryTable extends ReceiverFormTable {
static propTypes = {
t: PropTypes.func.isRequired,
receiver: PropTypes.object.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
tableActions: PropTypes.object.isRequired
};
defineColumns () {
const {t} = this.props
return {
...super.defineColumns(),
'ScheduledTimes': {
sortable: false,
Header: t('notificationsTab.ScheduledTimes'),
accessor: item => this.scheduleFormat(item.schedule),
width: 170
},
'sentTime': {
Header: <SortableTh title='notificationsTab.sentTime' />,
accessor: item => convertUTCtoLocal(item.sentTime, 'DD MMM YYYY HH:mm'),
width: 170
}
}
}
getColumns () {
return ['name', 'type', 'ScheduledTimes', 'sentTime']
}
noCard () {
return true
}
}
export default translate(['tabsContent'], { wait: true })(EmailHistoryTable)
@@ -0,0 +1,82 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import TableFilter from '../../TableFilter'
import { Nav, NavItem, NavLink } from 'reactstrap'
class FormTableTopBar extends React.Component {
static propTypes = {
tableActions: PropTypes.object.isRequired,
statusFilter: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
yesText: PropTypes.string.isRequired,
noText: PropTypes.string.isRequired,
allText: PropTypes.string.isRequired,
receiver: PropTypes.object.isRequired,
t: PropTypes.func.isRequired
}
onFilterRequest = (filter) => {
const { tableActions, receiver } = this.props
tableActions.loadTable({ filter }, receiver)
}
onStatusFilter = (statusFilter) => {
return () => {
const { tableActions, receiver } = this.props
tableActions.loadTable({ statusFilter }, receiver)
}
}
render() {
const {
type,
t,
yesText,
noText,
allText,
statusFilter,
receiver
} = this.props
return (
<div>
{receiver.id && (
<Nav pills justified>
<NavItem>
<NavLink
className="d-block"
active={statusFilter === 'all'}
onClick={this.onStatusFilter('all')}
>
{t('manageRecipientsTab.' + allText)}
</NavLink>
</NavItem>
<NavItem>
<NavLink
className="d-block"
active={statusFilter === 'yes'}
onClick={this.onStatusFilter('yes')}
>
{t('manageRecipientsTab.' + yesText)}
</NavLink>
</NavItem>
<NavItem>
<NavLink
className="d-block"
active={statusFilter === 'no'}
onClick={this.onStatusFilter('no')}
>
{t('manageRecipientsTab.' + noText)}
</NavLink>
</NavItem>
</Nav>
)}
<TableFilter type={type} onFilterRequest={this.onFilterRequest} />
</div>
)
}
}
export default translate(['tabsContent'], { wait: true })(FormTableTopBar)
@@ -0,0 +1,45 @@
import React from 'react'
import PropTypes from 'prop-types'
import GenericTable from '../../../common/GenericTable'
import SortableTh from '../../../../../../common/Table/SortableTh'
class ReceiverFormTable extends GenericTable {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
tableActions: PropTypes.object.isRequired,
receiver: PropTypes.object.isRequired
};
fetchData = (page, pageSize, sorted) => {
const { tableActions, receiver } = this.props
const params = {
page: page + 1,
limit: pageSize
}
if (sorted.length) {
const sortedField = sorted[0]
params['sortField'] = sortedField.id
params['sortDirection'] = sortedField.desc ? 'desc' : 'asc'
}
tableActions.loadTable(params, receiver)
};
defineColumns () {
const {t} = this.props
const colDefs = super.defineColumns()
return {
...colDefs,
'active': {
Header: <SortableTh title='notificationsTab.status' />,
accessor: item => item.active ? t('notificationsTab.active') : t('notificationsTab.paused'),
width: 100
}
}
}
}
export default ReceiverFormTable
@@ -0,0 +1,108 @@
import React from 'react'
import PropTypes from 'prop-types'
import {translate} from 'react-i18next'
import ReceiverFormTable from './ReceiverFormTable'
import SortableTh from '../../../../../../common/Table/SortableTh'
import LinkCell from '../../../../../../common/Table/LinkCell'
import FormTableTopBar from './FormTableTopBar'
export class ReceiverGroupsTable extends ReceiverFormTable {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
tableActions: PropTypes.object.isRequired,
receiver: PropTypes.object.isRequired,
formActions: PropTypes.object.isRequired
};
togglerOnAction = (itemId) => {
this.props.formActions.toggleGroup(itemId, true)
this.props.tableActions.toggleEnrolled(itemId, true)
};
togglerOffAction = (itemId) => {
this.props.formActions.toggleGroup(itemId, false)
this.props.tableActions.toggleEnrolled(itemId, false)
};
_formatSubscriptions (subscriptions) {
console.log('format subsc', subscriptions)
const result = []
if (subscriptions.alert > 0) {
result.push(`${subscriptions.alert} Alerts`)
}
if (subscriptions.newsletter > 0) {
result.push(`${subscriptions.newsletter} Newsletters`)
}
return result.join(', ')
};
_formatRecipients (number) {
if (number) {
if (number === 1) {
return '1 Recipient'
} else {
return number + ' Recipients'
}
}
return ''
}
defineColumns () {
const {t} = this.props
return {
...super.defineColumns(),
'groupName': {
Header: <SortableTh title='manageRecipientsTab.groupName' />,
accessor: 'name',
Cell: (row) => {
return (
<LinkCell item={row.original} onClick={this.nameClickAction}>
{row.value}
</LinkCell>
)
}
},
'enrolled': this.createTogglerColumn('manageRecipientsTab.form.recipient.enroll', 'enrolled', 'yes', 'no', this.togglerOnAction, this.togglerOffAction),
'subscriptions': {
sortable: false,
Header: t('manageRecipientsTab.subscriptions'),
accessor: item => this._formatSubscriptions(item.subscriptions),
width: 170
},
'recipients': {
sortable: false,
Header: t('manageRecipientsTab.recipients'),
accessor: item => this._formatRecipients(item.recipients.length),
width: 170
}
}
}
getColumns () {
return ['groupName', 'subscriptions', 'recipients', 'active', 'enrolled']
}
noCard () {
return true
}
getActionsPanel () {
const {tableState, tableActions, receiver} = this.props
return (
<FormTableTopBar
tableActions={tableActions}
statusFilter={tableState.statusFilter}
receiver={receiver}
type="groups"
yesText="Enrolled"
noText="NotEnrolled"
allText="All"
/>
)
}
}
export default translate(['tabsContent'], { wait: true })(ReceiverGroupsTable)
@@ -0,0 +1,75 @@
import React from 'react'
import PropTypes from 'prop-types'
import {translate} from 'react-i18next'
import ReceiverFormTable from './ReceiverFormTable'
import SortableTh from '../../../../../../common/Table/SortableTh'
import FormTableTopBar from './FormTableTopBar'
import { convertUTCtoLocal } from '../../../../../../../common/helper'
export class ReceiverRecipientsTable extends ReceiverFormTable {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
tableActions: PropTypes.object.isRequired,
receiver: PropTypes.object.isRequired,
formActions: PropTypes.object.isRequired
};
togglerOnAction = (itemId) => {
this.props.formActions.toggleRecipient(itemId, true)
this.props.tableActions.toggleEnrolled(itemId, true)
};
togglerOffAction = (itemId) => {
this.props.formActions.toggleRecipient(itemId, false)
this.props.tableActions.toggleEnrolled(itemId, false)
};
defineColumns () {
const {t} = this.props
return {
...super.defineColumns(),
'enrolled': this.createTogglerColumn('manageRecipientsTab.form.recipient.enroll', 'enrolled', 'yes', 'no', this.togglerOnAction, this.togglerOffAction),
'name': {
Header: <SortableTh title='manageRecipientsTab.name' />,
accessor: item => `${item.firstName} ${item.lastName}`
},
'email': {
Header: <SortableTh title='manageRecipientsTab.email' />,
accessor: 'email',
width: 170
},
'addedDate': {
Header: t('manageRecipientsTab.form.group.addedDate'),
accessor: item => item.creationDate ? convertUTCtoLocal(item.creationDate, 'DD MMM YYYY HH:mm') : '',
width: 170
}
}
}
getColumns () {
return ['name', 'email', 'addedDate', 'active', 'enrolled']
}
noCard () {
return true
}
getActionsPanel () {
const {tableState, tableActions, receiver} = this.props
return (
<FormTableTopBar
tableActions={tableActions}
statusFilter={tableState.statusFilter}
receiver={receiver}
type="recipients"
yesText="Enrolled"
noText="NotEnrolled"
allText="All"
/>
)
}
}
export default translate(['tabsContent'], { wait: true })(ReceiverRecipientsTable)
@@ -0,0 +1,58 @@
import React from 'react'
import PropTypes from 'prop-types'
import {translate} from 'react-i18next'
import ReceiverFormTable from './ReceiverFormTable'
import FormTableTopBar from './FormTableTopBar'
export class ReceiverSubscriptionsTable extends ReceiverFormTable {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
tableActions: PropTypes.object.isRequired,
receiver: PropTypes.object.isRequired,
formActions: PropTypes.object.isRequired
};
togglerOnAction = (itemId) => {
this.props.formActions.toggleSubscription(itemId, true)
this.props.tableActions.toggleSubscribed(itemId, true)
};
togglerOffAction = (itemId) => {
this.props.formActions.toggleSubscription(itemId, false)
this.props.tableActions.toggleSubscribed(itemId, false)
};
defineColumns () {
return {
...super.defineColumns(),
'subscribed': this.createTogglerColumn('notificationsTab.action', 'subscribed', 'subscribed', 'unsubscribed', this.togglerOnAction, this.togglerOffAction)
}
}
getColumns () {
return ['name', 'type', 'ScheduledTimes', 'active', 'subscribed']
}
noCard () {
return true
}
getActionsPanel () {
const {tableState, tableActions, receiver} = this.props
return (
<FormTableTopBar
tableActions={tableActions}
statusFilter={tableState.statusFilter}
receiver={receiver}
type="subscriptions"
yesText="Subscribed"
noText="Unsubscribed"
allText="All"
/>
)
}
}
export default translate(['tabsContent'], { wait: true })(ReceiverSubscriptionsTable)
@@ -0,0 +1,146 @@
import React, { Fragment } from 'react'
import GenericTable from '../common/GenericTable'
import { translate } from 'react-i18next'
import PropTypes from 'prop-types'
import { NOTIFICATION_TABLES } from '../../../../../redux/modules/appState/share/tabs'
import SortableTh from '../../../../common/Table/SortableTh'
import { ButtonGroup, Button } from 'reactstrap'
export class MyEmailsTable extends GenericTable {
static propTypes = {
t: PropTypes.func.isRequired,
tableState: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired,
tableActions: PropTypes.object.isRequired,
restrictions: PropTypes.object,
deleteSingleText: PropTypes.string.isRequired,
deleteMultipleText: PropTypes.string.isRequired
};
togglerOnAction = (itemId) => {
this.props.tableActions.toggleActive([itemId], true)
};
togglerOffAction = (itemId) => {
this.props.tableActions.toggleActive([itemId], false)
};
nameClickAction = (item) => {
const { actions } = this.props
actions.startEditNotification(item, NOTIFICATION_TABLES.MY_EMAILS)
};
onPublishButtonClick = () => {
const { tableState, tableActions } = this.props
tableActions.togglePublish(tableState.selectedIds, true)
};
onUnPublishButtonClick = () => {
const { tableState, tableActions } = this.props
tableActions.togglePublish(tableState.selectedIds, false)
};
_recipientsFormat (recipients) {
if (recipients.length === 1) {
return recipients[0].email
}
return `${recipients.length} ${this.props.t(
'notificationsTab.recipients'
)}`
}
defineColumns () {
const { t } = this.props
const colDefinitions = super.defineColumns()
return {
...colDefinitions,
active: this.createTogglerColumn(
'notificationsTab.action',
'active',
'active',
'paused',
this.togglerOnAction,
this.togglerOffAction
),
Recipients: {
sortable: false,
Header: t('notificationsTab.Recipients'),
accessor: (item) => this._recipientsFormat(item.recipients),
width: 110
},
published: {
Header: <SortableTh title="notificationsTab.published" />,
accessor: (item) =>
item.published
? t('common:commonWords.Yes')
: t('common:commonWords.No'),
width: 100
}
}
}
getColumns () {
return [
'selectCheckbox',
'name',
'type',
'published',
'ScheduledTimes',
'sourcesCount',
'Recipients',
'active',
'delete'
]
}
getActionsPanel () {
const { t, restrictions } = this.props
return (
<Fragment>
{this.getRestrictions(restrictions)}
<ButtonGroup className="mb-3">
<Button
color="secondary"
onClick={this.onActivateButtonClick}
>
<i className="fa fa-play for-small mr-1"> </i>{" "}
{t('notificationsTab.activate')}
</Button>
<Button
color="secondary"
onClick={this.onPauseButtonClick}
>
<i className="fa fa-pause for-small mr-1"> </i>{" "}
{t('notificationsTab.pause')}
</Button>
<Button
color="secondary"
onClick={this.onDeleteButtonClick}
>
<i className="fa fa-trash for-small mr-1"> </i>{" "}
{t('notificationsTab.delete')}
</Button>
<Button
color="secondary"
onClick={this.onPublishButtonClick}
>
<i className="fa fa-upload for-small mr-1"> </i>{" "}
{t('notificationsTab.publish')}
</Button>
<Button
color="secondary"
onClick={this.onUnPublishButtonClick}
>
<i className="fa fa-ban for-small mr-1"> </i>{" "}
{t('notificationsTab.unpublish')}
</Button>
</ButtonGroup>
</Fragment>
)
}
}
export default translate(['tabsContent'], { wait: true })(MyEmailsTable)
@@ -0,0 +1,26 @@
import React from 'react'
import PropTypes from 'prop-types'
import { translate } from 'react-i18next'
import { Button } from 'reactstrap'
class Navigation extends React.Component {
static propTypes = {
t: PropTypes.func.isRequired,
actions: PropTypes.object.isRequired
};
backToTables = () => {
this.props.actions.switchShareSubScreen('notifications', 'tables')
};
render () {
return (
<Button className="btn-wide mb-2" size="sm" color="info" onClick={this.backToTables}>
<i className="lnr lnr-chevron-left"> </i>
</Button>
)
}
}
export default translate(['tabsContent'], { wait: true })(Navigation)
@@ -0,0 +1,106 @@
import React from 'react'
import PropTypes from 'prop-types'
import TopBar from './TopBar'
import Navigation from './Navigation'
import AlertForm from './forms/AlertForm'
import {NOTIFICATION_TABLES, NOTIFICATION_SUBSCREENS} from '../../../../../redux/modules/appState/share/tabs'
import MyEmailsTable from './MyEmailsTable'
import PublishedEmailsTable from './PublishedEmailsTable'
import {withRouter} from 'react-router-dom'
import reduxConnect from '../../../../../redux/utils/connect'
import {compose} from 'redux'
import { setDocumentData } from '../../../../../common/helper'
class NotificationsSubTab extends React.Component {
static propTypes = {
store: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
_shareState = () => this.props.store.appState.share;
_authState = () => this.props.store.common.auth;
componentDidMount() {
setDocumentData('title', 'Alerts | Share')
}
componentWillUnmount() {
setDocumentData('title')
}
render () {
const { actions } = this.props
const shareState = this._shareState()
const {user: {restrictions}} = this._authState()
const { subScreenVisible, tableVisible } = shareState.tabs.notifications
return (
<div className="notifications-tab">
{subScreenVisible === NOTIFICATION_SUBSCREENS.TABLES &&
<div>
<TopBar
tables={[NOTIFICATION_TABLES.MY_EMAILS, NOTIFICATION_TABLES.PUBLISHED]}
tableVisible={tableVisible}
actions={actions}
/>
{tableVisible === NOTIFICATION_TABLES.MY_EMAILS &&
<MyEmailsTable
tableState={shareState.tables[tableVisible]}
restrictions={restrictions && restrictions.limits}
actions={actions}
tableActions={actions.shareTables[tableVisible]}
deleteSingleText='alert'
deleteMultipleText='alerts'
/>
}
{tableVisible === NOTIFICATION_TABLES.PUBLISHED &&
<PublishedEmailsTable
tableState={shareState.tables[tableVisible]}
restrictions={restrictions && restrictions.limits}
actions={actions}
tableActions={actions.shareTables[tableVisible]}
deleteSingleText='alert'
deleteMultipleText='alerts'
/>
}
</div>
}
{(subScreenVisible === NOTIFICATION_SUBSCREENS.ALERT_FORM || subScreenVisible === NOTIFICATION_SUBSCREENS.NEWSLETTER_FORM) &&
<Navigation actions={actions} />
}
{subScreenVisible === NOTIFICATION_SUBSCREENS.ALERT_FORM &&
<AlertForm
state={shareState.forms.alert}
switchShareSubScreen={actions.switchShareSubScreen}
actions={actions.shareForms.alert}
/>
}
{/* {subScreenVisible === NOTIFICATION_SUBSCREENS.NEWSLETTER_FORM &&
<NewsletterForm
state={shareState.forms.newsletter}
switchShareSubScreen={actions.switchShareSubScreen}
actions={actions.shareForms.newsletter}
/>
} */}
</div>
)
}
}
const applyDecorators = compose(
withRouter,
reduxConnect()
)
export default applyDecorators(NotificationsSubTab)

Some files were not shown because too many files have changed in this diff Show More