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