change(ui): funnel duck cleanup
This commit is contained in:
parent
452fe62ebe
commit
4f2d61d1cf
40 changed files with 8 additions and 1925 deletions
|
|
@ -1,159 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { Tabs, Loader } from 'UI'
|
||||
import FunnelHeader from 'Components/Funnels/FunnelHeader'
|
||||
import FunnelGraph from 'Components/Funnels/FunnelGraph'
|
||||
import FunnelSessionList from 'Components/Funnels/FunnelSessionList'
|
||||
import FunnelOverview from 'Components/Funnels/FunnelOverview'
|
||||
import FunnelIssues from 'Components/Funnels/FunnelIssues'
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
fetch, fetchInsights, fetchList, fetchFiltered, fetchIssuesFiltered, fetchSessionsFiltered, fetchIssueTypes, resetFunnel, refresh
|
||||
} from 'Duck/funnels';
|
||||
import { applyFilter, setFilterOptions, resetFunnelFilters, setInitialFilters } from 'Duck/funnelFilters';
|
||||
import { withRouter } from 'react-router';
|
||||
import { sessions as sessionsRoute, funnel as funnelRoute, withSiteId } from 'App/routes';
|
||||
import FunnelSearch from 'Shared/FunnelSearch';
|
||||
import cn from 'classnames';
|
||||
import IssuesEmptyMessage from 'Components/Funnels/IssuesEmptyMessage'
|
||||
|
||||
const TAB_ISSUES = 'ANALYSIS';
|
||||
const TAB_SESSIONS = 'SESSIONS';
|
||||
|
||||
const TABS = [ TAB_ISSUES, TAB_SESSIONS ].map(tab => ({
|
||||
text: tab,
|
||||
disabled: false,
|
||||
key: tab,
|
||||
}));
|
||||
|
||||
const FunnelDetails = (props) => {
|
||||
const { insights, funnels, funnel, funnelId, loading, liveFilters, issuesLoading, sessionsLoading, refresh } = props;
|
||||
const [activeTab, setActiveTab] = useState(TAB_ISSUES)
|
||||
const [showFilters, setShowFilters] = useState(false)
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const onTabClick = activeTab => setActiveTab(activeTab)
|
||||
|
||||
useEffect(() => {
|
||||
if (funnels.size === 0) {
|
||||
props.fetchList();
|
||||
}
|
||||
props.fetchIssueTypes()
|
||||
|
||||
props.fetch(funnelId).then(() => {
|
||||
setMounted(true);
|
||||
}).then(() => {
|
||||
props.refresh(funnelId);
|
||||
})
|
||||
|
||||
}, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (funnel && funnel.filter && liveFilters.events.size === 0) {
|
||||
// props.setInitialFilters();
|
||||
// }
|
||||
// }, [funnel])
|
||||
|
||||
const onBack = () => {
|
||||
props.history.push(sessionsRoute());
|
||||
}
|
||||
|
||||
const redirect = funnelId => {
|
||||
const { siteId, history } = props;
|
||||
props.resetFunnel();
|
||||
props.resetFunnelFilters();
|
||||
|
||||
history.push(withSiteId(funnelRoute(parseInt(funnelId)), siteId));
|
||||
}
|
||||
|
||||
const renderActiveTab = (tab, hasNoStages) => {
|
||||
switch(tab) {
|
||||
case TAB_ISSUES:
|
||||
return !hasNoStages && <FunnelIssues funnelId={funnelId} />
|
||||
case TAB_SESSIONS:
|
||||
return <FunnelSessionList funnelId={funnelId} />
|
||||
}
|
||||
}
|
||||
|
||||
const hasNoStages = !loading && insights.stages.length <= 1;
|
||||
const showEmptyMessage = hasNoStages && activeTab === TAB_ISSUES && !loading;
|
||||
|
||||
return (
|
||||
<div className="page-margin container-70">
|
||||
<FunnelHeader
|
||||
funnel={funnel}
|
||||
insights={insights}
|
||||
redirect={redirect}
|
||||
funnels={funnels}
|
||||
onBack={onBack}
|
||||
funnelId={parseInt(funnelId)}
|
||||
toggleFilters={() => setShowFilters(!showFilters)}
|
||||
showFilters={showFilters}
|
||||
/>
|
||||
<div className="my-3" />
|
||||
{showFilters && (
|
||||
<FunnelSearch />
|
||||
)
|
||||
}
|
||||
<div className="my-3" />
|
||||
<Tabs
|
||||
tabs={ TABS }
|
||||
active={ activeTab }
|
||||
onClick={ onTabClick }
|
||||
/>
|
||||
<div className="my-8" />
|
||||
<Loader loading={loading}>
|
||||
<IssuesEmptyMessage onAddEvent={() => setShowFilters(true)} show={showEmptyMessage}>
|
||||
<div>
|
||||
<div className={cn("flex items-start", { 'hidden' : activeTab === TAB_SESSIONS || hasNoStages })}>
|
||||
<div className="flex-1">
|
||||
<FunnelGraph data={insights.stages} funnelId={funnelId} />
|
||||
</div>
|
||||
<div style={{ width: '35%'}} className="px-14">
|
||||
<FunnelOverview funnel={insights} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-8" />
|
||||
<Loader loading={issuesLoading || sessionsLoading}>
|
||||
{ renderActiveTab(activeTab, hasNoStages) }
|
||||
</Loader>
|
||||
</div>
|
||||
</IssuesEmptyMessage>
|
||||
</Loader>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect((state, props) => {
|
||||
const insightsLoading = state.getIn(['funnels', 'fetchInsights', 'loading']);
|
||||
const issuesLoading = state.getIn(['funnels', 'fetchIssuesRequest', 'loading']);
|
||||
const funnelLoading = state.getIn(['funnels', 'fetchRequest', 'loading']);
|
||||
const sessionsLoading = state.getIn(['funnels', 'fetchSessionsRequest', 'loading']);
|
||||
return {
|
||||
funnels: state.getIn(['funnels', 'list']),
|
||||
funnel: state.getIn(['funnels', 'instance']),
|
||||
insights: state.getIn(['funnels', 'insights']),
|
||||
loading: funnelLoading || (insightsLoading && (issuesLoading || sessionsLoading)),
|
||||
issuesLoading,
|
||||
sessionsLoading,
|
||||
funnelId: props.match.params.funnelId,
|
||||
activeStages: state.getIn(['funnels', 'activeStages']),
|
||||
funnelFilters: state.getIn(['funnels', 'funnelFilters']),
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
liveFilters: state.getIn(['funnelFilters', 'appliedFilter']),
|
||||
}
|
||||
}, {
|
||||
fetch,
|
||||
fetchInsights,
|
||||
fetchFiltered,
|
||||
fetchIssuesFiltered,
|
||||
fetchList,
|
||||
applyFilter,
|
||||
setFilterOptions,
|
||||
fetchIssuesFiltered,
|
||||
fetchSessionsFiltered,
|
||||
fetchIssueTypes,
|
||||
resetFunnel,
|
||||
resetFunnelFilters,
|
||||
setInitialFilters,
|
||||
refresh,
|
||||
})(withRouter((FunnelDetails)))
|
||||
|
|
@ -1,303 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Icon, Tooltip as AppTooltip } from 'UI';
|
||||
import { numberCompact } from 'App/utils';
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
Cell,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
LabelList,
|
||||
|
||||
} from 'recharts';
|
||||
import { connect } from 'react-redux';
|
||||
import { setActiveStages } from 'Duck/funnels';
|
||||
import { Styles } from '../../Dashboard/Widgets/common';
|
||||
import { numberWithCommas } from 'App/utils';
|
||||
import { truncate } from 'App/utils';
|
||||
|
||||
const MIN_BAR_HEIGHT = 20;
|
||||
|
||||
function CustomTick(props) {
|
||||
const { x, y, payload } = props;
|
||||
return (
|
||||
<g transform={`translate(${x},${y})`}>
|
||||
<text x={0} y={0} dy={16} fontSize={12} textAnchor="middle" fill="#666">
|
||||
{payload.value}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
function FunnelGraph(props) {
|
||||
const { data, activeStages, funnelId, liveFilters } = props;
|
||||
const [activeIndex, setActiveIndex] = useState(activeStages);
|
||||
|
||||
const renderPercentage = (props) => {
|
||||
const { x, y, width, height, value } = props;
|
||||
const radius = 10;
|
||||
const _x = x + width / 2 + 45;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<svg width="46px" height="21px" version="1.1">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path
|
||||
d="M37.2387001,0.5 L45.3588127,10.5034561 L37.4215407,20.5 L0.5,20.5 L0.5,0.5 L37.2387001,0.5 Z"
|
||||
id="Rectangle"
|
||||
stroke="#AFACAC"
|
||||
fill="#FFFFFF"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
<text x={x} y={70} fill="#000" textAnchor="middle" dominantBaseline="middle">
|
||||
{numberCompact(value)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const renderCustomizedLabel = (props) => {
|
||||
const { x, y, width, height, value, textColor = '#fff' } = props;
|
||||
const radius = 10;
|
||||
|
||||
if (value === 0) return;
|
||||
|
||||
return (
|
||||
<g>
|
||||
<text
|
||||
x={x + width / 2}
|
||||
y={y - radius + 20}
|
||||
fill={textColor}
|
||||
font-size="12"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
{numberCompact(value)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
const handleClick = (data, index) => {
|
||||
if (activeStages.length === 1 && activeStages.includes(index)) {
|
||||
// selecting the same bar
|
||||
props.setActiveStages([], null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeStages.length === 2) {
|
||||
// already having two bars
|
||||
return;
|
||||
}
|
||||
|
||||
// new selection
|
||||
const arr = activeStages.concat([index]);
|
||||
props.setActiveStages(arr.sort(), arr.length === 2 && liveFilters, funnelId);
|
||||
};
|
||||
|
||||
const resetActiveSatges = () => {
|
||||
props.setActiveStages([], liveFilters, funnelId, true);
|
||||
};
|
||||
|
||||
const renderDropLabel = ({ x, y, width, value }) => {
|
||||
if (value === 0) return;
|
||||
return (
|
||||
<text fill="#cc0000" x={x + width / 2} y={y - 5} textAnchor="middle" fontSize="12">
|
||||
{value}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMainLabel = ({ x, y, width, value }) => {
|
||||
return (
|
||||
<text fill="#FFFFFF" x={x + width / 2} y={y + 14} textAnchor="middle" fontSize="12">
|
||||
{numberWithCommas(value)}
|
||||
</text>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomBar = (props) => {
|
||||
const { fill, x, y, width, height, sessionsCount, index, dropDueToIssues } = props;
|
||||
const yp = sessionsCount < MIN_BAR_HEIGHT ? MIN_BAR_HEIGHT - 1 : dropDueToIssues;
|
||||
const tmp = (height <= 20 ? 20 : height) - (TEMP[index].height > 20 ? 0 : TEMP[index].height);
|
||||
return (
|
||||
<svg>
|
||||
<rect x={x} y={y} width={width} height={tmp} fill={fill} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
const MainBar = (props) => {
|
||||
const {
|
||||
fill,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
sessionsCount,
|
||||
index,
|
||||
dropDueToIssues,
|
||||
hasSelection = false,
|
||||
} = props;
|
||||
const yp = sessionsCount < MIN_BAR_HEIGHT ? MIN_BAR_HEIGHT - 1 : dropDueToIssues;
|
||||
|
||||
TEMP[index] = { height, y };
|
||||
|
||||
return (
|
||||
<svg style={{ cursor: hasSelection ? '' : 'pointer' }}>
|
||||
<rect x={x} y={y} width={width} height={height} fill={fill} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDropPct = (props) => {
|
||||
// TODO
|
||||
const { fill, x, y, width, height, value, totalBars } = props;
|
||||
const barW = x + 730 / totalBars / 2;
|
||||
|
||||
return (
|
||||
<svg>
|
||||
<rect x={barW} y={80} width={width} height={20} fill="red" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const CustomTooltip = (props) => {
|
||||
const { payload } = props;
|
||||
if (payload.length === 0) return null;
|
||||
const { value, headerText } = payload[0].payload;
|
||||
|
||||
// const value = payload[0].payload.value;
|
||||
if (!value) return null;
|
||||
return (
|
||||
<div className="rounded border bg-white p-2">
|
||||
<div>{headerText}</div>
|
||||
{value.map((i) => (
|
||||
<div className="text-sm ml-2">{truncate(i, 30)}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// const CustomTooltip = ({ active, payload, msg = '' }) => {
|
||||
// return (
|
||||
// <div className="rounded border bg-white p-2">
|
||||
// <p className="text-sm">{msg}</p>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
const TEMP = {};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{activeStages.length === 2 && (
|
||||
<div
|
||||
className="absolute right-0 top-0 cursor-pointer z-10"
|
||||
style={{ marginRight: '60px', marginTop: '0' }}
|
||||
onClick={resetActiveSatges}
|
||||
>
|
||||
<AppTooltip title={`Reset Selection`}>
|
||||
<Icon name="sync-alt" size="15" color="teal" />
|
||||
</AppTooltip>
|
||||
</div>
|
||||
)}
|
||||
<BarChart
|
||||
width={800}
|
||||
height={190}
|
||||
data={data}
|
||||
margin={{ top: 20, right: 20, left: 0, bottom: 0 }}
|
||||
background={'transparent'}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="1 3" stroke="#BBB" vertical={false} />
|
||||
{/* {activeStages.length < 2 && <Tooltip cursor={{ fill: 'transparent' }} content={<CustomTooltip msg={activeStages.length > 0 ? 'Select one more event.' : 'Select any two events to analyze in depth.'} />} />} */}
|
||||
<Tooltip cursor={{ fill: 'transparent' }} content={CustomTooltip} />
|
||||
|
||||
<Bar
|
||||
dataKey="sessionsCount"
|
||||
onClick={handleClick}
|
||||
maxBarSize={80}
|
||||
stackId="a"
|
||||
shape={<MainBar hasSelection={activeStages.length === 2} />}
|
||||
cursor="pointer"
|
||||
minPointSize={MIN_BAR_HEIGHT}
|
||||
background={false}
|
||||
>
|
||||
<LabelList dataKey="sessionsCount" content={renderMainLabel} />
|
||||
{data.map((entry, index) => {
|
||||
const selected =
|
||||
activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]);
|
||||
const opacity = activeStages.length > 0 && !selected ? 0.4 : 1;
|
||||
return (
|
||||
<Cell
|
||||
cursor="pointer"
|
||||
fill={selected ? '#394EFF' : opacity === 1 ? '#3EAAAF' : '#CCC'}
|
||||
key={`cell-${index}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
|
||||
<Bar
|
||||
hide={activeStages.length !== 2}
|
||||
dataKey="dropDueToIssues"
|
||||
onClick={handleClick}
|
||||
maxBarSize={80}
|
||||
stackId="a"
|
||||
shape={<CustomBar />}
|
||||
minPointSize={MIN_BAR_HEIGHT}
|
||||
>
|
||||
<LabelList dataKey="dropDueToIssues" content={renderDropLabel} />
|
||||
{data.map((entry, index) => {
|
||||
const selected =
|
||||
activeStages.includes(index) || (index > activeStages[0] && index < activeStages[1]);
|
||||
const opacity = activeStages.length > 0 && !selected ? 0.4 : 1;
|
||||
return (
|
||||
<Cell
|
||||
opacity={opacity}
|
||||
cursor="pointer"
|
||||
fill={activeStages[1] === index ? '#cc000040' : 'transparent'}
|
||||
key={`cell-${index}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Bar>
|
||||
|
||||
<XAxis
|
||||
stroke={0}
|
||||
dataKey="label"
|
||||
strokeWidth={0}
|
||||
interval={0}
|
||||
// tick ={{ fill: '#666', fontSize: 12 }}
|
||||
tick={<CustomTick />}
|
||||
xAxisId={0}
|
||||
/>
|
||||
{/* <XAxis
|
||||
stroke={0}
|
||||
xAxisId={1}
|
||||
dataKey="value"
|
||||
strokeWidth={0}
|
||||
interval={0}
|
||||
dy={-15} dx={0}
|
||||
tick ={{ fill: '#666', fontSize: 12 }}
|
||||
tickFormatter={val => '"' + val + '"'}
|
||||
/> */}
|
||||
<YAxis
|
||||
interval={0}
|
||||
strokeWidth={0}
|
||||
tick={{ fill: '#999999', fontSize: 11 }}
|
||||
tickFormatter={(val) => Styles.tickFormatter(val)}
|
||||
/>
|
||||
</BarChart>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
activeStages: state.getIn(['funnels', 'activeStages']).toJS(),
|
||||
liveFilters: state.getIn(['funnelFilters', 'appliedFilter']),
|
||||
}),
|
||||
{ setActiveStages }
|
||||
)(FunnelGraph);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './FunnelGraph'
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router'
|
||||
import { Dropdown } from 'UI'
|
||||
import { funnel as funnelRoute, withSiteId } from 'App/routes';
|
||||
|
||||
function FunnelDropdown(props) {
|
||||
const { options, funnel } = props;
|
||||
|
||||
const writeOption = (e, { name, value }) => {
|
||||
const { siteId, history } = props;
|
||||
history.push(withSiteId(funnelRoute(parseInt(value)), siteId));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dropdown
|
||||
selection
|
||||
basic
|
||||
options={ options.toJS() }
|
||||
name="funnel"
|
||||
value={ funnel.funnelId || ''}
|
||||
defaultValue={ funnel.funnelId }
|
||||
icon={null}
|
||||
style={{ border: 'none' }}
|
||||
onChange={ writeOption }
|
||||
selectOnBlur={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect((state, props) => ({
|
||||
funnels: state.getIn(['funnels', 'list']),
|
||||
funnel: state.getIn(['funnels', 'instance']),
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
}), { })(withRouter(FunnelDropdown))
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Icon, BackLink, IconButton, Dropdown, Tooltip, TextEllipsis, Button } from 'UI';
|
||||
import {
|
||||
remove as deleteFunnel,
|
||||
fetch,
|
||||
fetchInsights,
|
||||
fetchIssuesFiltered,
|
||||
fetchSessionsFiltered,
|
||||
} from 'Duck/funnels';
|
||||
import { editFilter, editFunnelFilter, refresh } from 'Duck/funnels';
|
||||
import DateRange from 'Shared/DateRange';
|
||||
import { connect } from 'react-redux';
|
||||
import { confirm } from 'UI';
|
||||
import FunnelSaveModal from 'Components/Funnels/FunnelSaveModal';
|
||||
import stl from './funnelHeader.module.css';
|
||||
|
||||
const Info = ({ label = '', value = '', className = 'mx-4' }) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<span className="color-gray-medium">{label}</span>
|
||||
<span className="font-medium ml-2">{value}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FunnelHeader = (props) => {
|
||||
const {
|
||||
funnel,
|
||||
insights,
|
||||
funnels,
|
||||
onBack,
|
||||
funnelId,
|
||||
showFilters = false,
|
||||
funnelFilters,
|
||||
renameHandler,
|
||||
} = props;
|
||||
const [showSaveModal, setShowSaveModal] = useState(false);
|
||||
|
||||
const writeOption = (e, { name, value }) => {
|
||||
props.redirect(value);
|
||||
props.fetch(value).then(() => props.refresh(value));
|
||||
};
|
||||
|
||||
const deleteFunnel = async (e, funnel) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (
|
||||
await confirm({
|
||||
header: 'Delete Funnel',
|
||||
confirmButton: 'Delete',
|
||||
confirmation: `Are you sure you want to permanently delete "${funnel.name}"?`,
|
||||
})
|
||||
) {
|
||||
props.deleteFunnel(funnel.funnelId).then(props.onBack);
|
||||
} else {
|
||||
}
|
||||
};
|
||||
|
||||
const onDateChange = (e) => {
|
||||
props.editFunnelFilter(e, funnelId);
|
||||
};
|
||||
|
||||
const options = funnels.map(({ funnelId, name }) => ({ text: name, value: funnelId })).toJS();
|
||||
const selectedFunnel = funnels.filter((i) => i.funnelId === parseInt(funnelId)).first() || {};
|
||||
const eventsCount = funnel.filter.filters.filter((i) => i.isEvent).size;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="bg-white border rounded flex items-center w-full relative group pr-2">
|
||||
<BackLink
|
||||
onClick={onBack}
|
||||
vertical
|
||||
className="absolute"
|
||||
style={{ left: '-50px', top: '8px' }}
|
||||
/>
|
||||
<FunnelSaveModal show={showSaveModal} closeHandler={() => setShowSaveModal(false)} />
|
||||
<div className="flex items-center mr-auto relative">
|
||||
<Dropdown
|
||||
scrolling
|
||||
trigger={
|
||||
<div
|
||||
className="text-xl capitalize font-medium"
|
||||
style={{ maxWidth: '300px', overflow: 'hidden' }}
|
||||
>
|
||||
<TextEllipsis text={selectedFunnel.name} />
|
||||
</div>
|
||||
}
|
||||
options={options}
|
||||
className={stl.dropdown}
|
||||
name="funnel"
|
||||
value={parseInt(funnelId)}
|
||||
// icon={null}
|
||||
onChange={writeOption}
|
||||
selectOnBlur={false}
|
||||
icon={
|
||||
<Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} />
|
||||
}
|
||||
/>
|
||||
<Info label="Events" value={eventsCount} />
|
||||
<span>-</span>
|
||||
<Button variant="text-primary" onClick={props.toggleFilters}>
|
||||
{showFilters ? 'HIDE' : 'EDIT FUNNEL'}
|
||||
</Button>
|
||||
<Info label="Sessions" value={insights.sessionsCount} />
|
||||
<Info label="Conversion" value={`${insights.conversions}%`} />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center invisible group-hover:visible">
|
||||
<Tooltip title={`Edit Funnel`}>
|
||||
<IconButton icon="edit" onClick={() => setShowSaveModal(true)} />
|
||||
</Tooltip>
|
||||
<Tooltip title={`Remove Funnel`}>
|
||||
<IconButton
|
||||
icon="trash"
|
||||
onClick={(e) => deleteFunnel(e, funnel)}
|
||||
className="ml-2 mr-2"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<DateRange
|
||||
rangeValue={funnelFilters.rangeValue}
|
||||
startDate={funnelFilters.startDate}
|
||||
endDate={funnelFilters.endDate}
|
||||
onDateChange={onDateChange}
|
||||
customRangeRight
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default connect(
|
||||
(state) => ({
|
||||
funnelFilters: state.getIn(['funnels', 'funnelFilters']).toJS(),
|
||||
funnel: state.getIn(['funnels', 'instance']),
|
||||
}),
|
||||
{
|
||||
editFilter,
|
||||
editFunnelFilter,
|
||||
deleteFunnel,
|
||||
fetch,
|
||||
fetchInsights,
|
||||
fetchIssuesFiltered,
|
||||
fetchSessionsFiltered,
|
||||
refresh,
|
||||
}
|
||||
)(FunnelHeader);
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
.dropdown {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
border-radius: 0;
|
||||
border-radius: 0;
|
||||
color: $gray-darkest;
|
||||
font-weight: 500;
|
||||
height: 54px;
|
||||
padding-right: 20px;
|
||||
border-right: solid thin #eee;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-top-left-radius: 3px;
|
||||
&:hover {
|
||||
background-color: $gray-lightest;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownTrigger {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
&:hover {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownIcon {
|
||||
margin-top: 4px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './FunnelHeader';
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import IssueItem from 'Components/Funnels/IssueItem'
|
||||
import FunnelSessionList from 'Components/Funnels/FunnelSessionList'
|
||||
import { connect } from 'react-redux'
|
||||
import { withRouter } from 'react-router'
|
||||
import { fetchIssue, setNavRef, resetIssue } from 'Duck/funnels'
|
||||
import { funnel as funnelRoute, withSiteId } from 'App/routes'
|
||||
import { Loader } from 'UI'
|
||||
|
||||
function FunnelIssueDetails(props) {
|
||||
const { issue, issueId, funnelId, loading = false } = props;
|
||||
|
||||
useEffect(() => {
|
||||
props.fetchIssue(funnelId, issueId)
|
||||
|
||||
return () => {
|
||||
props.resetIssue();
|
||||
}
|
||||
}, [issueId])
|
||||
|
||||
const onBack = () => {
|
||||
const { siteId, history } = props;
|
||||
history.push(withSiteId(funnelRoute(parseInt(funnelId)), siteId));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-margin container-70" >
|
||||
<Loader loading={loading}>
|
||||
<IssueItem issue={issue} inDetails onBack={onBack} />
|
||||
<div className="my-6" />
|
||||
<FunnelSessionList funnelId={funnelId} issueId={issueId} inDetails />
|
||||
</Loader>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect((state, props) => ({
|
||||
loading: state.getIn(['funnels', 'fetchIssueRequest', 'loading']),
|
||||
issue: state.getIn(['funnels', 'issue']),
|
||||
issueId: props.match.params.issueId,
|
||||
funnelId: props.match.params.funnelId,
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
}), { fetchIssue, setNavRef, resetIssue })(withRouter(FunnelIssueDetails))
|
||||
|
|
@ -1 +1 @@
|
|||
export { default } from './FunnelIssueDetails'
|
||||
//export { default } from './FunnelIssueDetails'
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
import React, { useState } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { fetchIssues, fetchIssuesFiltered } from 'Duck/funnels'
|
||||
import { LoadMoreButton, NoContent } from 'UI'
|
||||
import FunnelIssuesHeader from '../FunnelIssuesHeader'
|
||||
import IssueItem from '../IssueItem';
|
||||
import { funnelIssue as funnelIssueRoute, withSiteId } from 'App/routes'
|
||||
import { withRouter } from 'react-router'
|
||||
import IssueFilter from '../IssueFilter';
|
||||
import SortDropdown from './SortDropdown';
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
function FunnelIssues(props) {
|
||||
const {
|
||||
funnel, list, loading = false,
|
||||
criticalIssuesCount, issueFilters, sort
|
||||
} = props;
|
||||
|
||||
const [showPages, setShowPages] = useState(1)
|
||||
|
||||
const addPage = () => setShowPages(showPages + 1);
|
||||
|
||||
const onClick = ({ issueId }) => {
|
||||
const { siteId, history } = props;
|
||||
history.push(withSiteId(funnelIssueRoute(funnel.funnelId, issueId), siteId));
|
||||
}
|
||||
|
||||
let filteredList = issueFilters.size > 0 ? list.filter(item => issueFilters.includes(item.type)) : list;
|
||||
filteredList = sort.sort ? filteredList.sortBy(i => i[sort.sort]) : filteredList;
|
||||
filteredList = sort.order === 'desc' ? filteredList.reverse() : filteredList;
|
||||
const displayedCount = Math.min(showPages * PER_PAGE, filteredList.size);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FunnelIssuesHeader criticalIssuesCount={criticalIssuesCount} />
|
||||
<div className="my-5 flex items-start justify-between">
|
||||
<IssueFilter />
|
||||
<div className="flex items-center ml-6 flex-shrink-0">
|
||||
<span className="mr-2 color-gray-medium">Sort By</span>
|
||||
<SortDropdown />
|
||||
</div>
|
||||
</div>
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_RESULTS} size="60" />
|
||||
<div className="mt-4">No Issues Found!</div>
|
||||
</div>
|
||||
}
|
||||
subtext="Please try changing your search parameters."
|
||||
// animatedIcon="no-results"
|
||||
show={ !loading && filteredList.size === 0}
|
||||
>
|
||||
{ filteredList.take(displayedCount).map(issue => (
|
||||
<div className="mb-4">
|
||||
<IssueItem
|
||||
key={ issue.issueId }
|
||||
issue={ issue }
|
||||
onClick={() => onClick(issue)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<LoadMoreButton
|
||||
className="mt-12 mb-12"
|
||||
displayedCount={displayedCount}
|
||||
totalCount={filteredList.size}
|
||||
loading={loading}
|
||||
onClick={addPage}
|
||||
/>
|
||||
</NoContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
list: state.getIn(['funnels', 'issues']),
|
||||
criticalIssuesCount: state.getIn(['funnels', 'criticalIssuesCount']),
|
||||
loading: state.getIn(['funnels', 'fetchIssuesRequest', 'loading']),
|
||||
siteId: state.getIn([ 'site', 'siteId' ]),
|
||||
funnel: state.getIn(['funnels', 'instance']),
|
||||
activeStages: state.getIn(['funnels', 'activeStages']),
|
||||
funnelFilters: state.getIn(['funnels', 'funnelFilters']),
|
||||
liveFilters: state.getIn(['funnelFilters', 'appliedFilter']),
|
||||
issueFilters: state.getIn(['funnels', 'issueFilters', 'filters']),
|
||||
sort: state.getIn(['funnels', 'issueFilters', 'sort']),
|
||||
}), { fetchIssues, fetchIssuesFiltered })(withRouter(FunnelIssues))
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Select from 'Shared/Select'
|
||||
import { sort } from 'Duck/sessions';
|
||||
import { applyIssueFilter } from 'Duck/funnels';
|
||||
|
||||
const sortOptionsMap = {
|
||||
'afectedUsers-desc': 'Affected Users (High)',
|
||||
'afectedUsers-asc': 'Affected Users (Low)',
|
||||
'conversionImpact-desc': 'Conversion Impact (High)',
|
||||
'conversionImpact-asc': 'Conversion Impact (Low)',
|
||||
'lostConversions-desc': 'Lost Conversions (High)',
|
||||
'lostConversions-asc': 'Lost Conversions (Low)',
|
||||
};
|
||||
|
||||
const sortOptions = Object.entries(sortOptionsMap)
|
||||
.map(([ value, label ]) => ({ value, label }));
|
||||
|
||||
@connect(state => ({
|
||||
sorts: state.getIn(['funnels', 'issueFilters', 'sort'])
|
||||
}), { sort, applyIssueFilter })
|
||||
export default class SortDropdown extends React.PureComponent {
|
||||
state = { value: null }
|
||||
sort = ({ value }) => {
|
||||
this.setState({ value: value })
|
||||
const [ sort, order ] = value.split('-');
|
||||
const sign = order === 'desc' ? -1 : 1;
|
||||
this.props.applyIssueFilter({ sort: { order, sort } });
|
||||
|
||||
this.props.sort(sort, sign)
|
||||
setTimeout(() => this.props.sort(sort, sign), 3000); //AAA
|
||||
}
|
||||
|
||||
render() {
|
||||
const { sorts } = this.props;
|
||||
|
||||
return (
|
||||
<Select
|
||||
plain
|
||||
right
|
||||
name="sortSessions"
|
||||
defaultValue={sorts.sort + '-' + sorts.order}
|
||||
options={sortOptions}
|
||||
onChange={ this.sort }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SortDropdown';
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
.dropdown {
|
||||
display: flex !important;
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
color: $gray-darkest;
|
||||
font-weight: 500;
|
||||
&:hover {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownTrigger {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
&:hover {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownIcon {
|
||||
margin-top: 2px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './FunnelIssues'
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { applyFilter, fetchList } from 'Duck/filters';
|
||||
import { fetchList as fetchFunnelsList } from 'Duck/funnels';
|
||||
import DateRangeDropdown from 'Shared/DateRangeDropdown';
|
||||
|
||||
@connect(state => ({
|
||||
rangeValue: state.getIn([ 'filters', 'appliedFilter', 'rangeValue' ]),
|
||||
startDate: state.getIn([ 'filters', 'appliedFilter', 'startDate' ]),
|
||||
endDate: state.getIn([ 'filters', 'appliedFilter', 'endDate' ]),
|
||||
}), {
|
||||
applyFilter, fetchList, fetchFunnelsList
|
||||
})
|
||||
export default class DateRange extends React.PureComponent {
|
||||
render() {
|
||||
const { startDate, endDate, rangeValue, className } = this.props;
|
||||
return (
|
||||
<DateRangeDropdown
|
||||
button
|
||||
rangeValue={ rangeValue }
|
||||
startDate={ startDate }
|
||||
endDate={ endDate }
|
||||
className={ className }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
function FunnelIssuesHeader({ criticalIssuesCount, filters }) {
|
||||
function FunnelIssuesHeader({ criticalIssuesCount, filters }) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center mr-auto text-xl">
|
||||
<div className="font-medium mr-2">
|
||||
<div className="font-medium mr-2">
|
||||
Significant issues
|
||||
</div>
|
||||
<div className="mr-2">in this funnel</div>
|
||||
</div>
|
||||
<div className="mr-2">in this funnel</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import React from 'react'
|
||||
import FunnelGraphSmall from '../FunnelGraphSmall'
|
||||
|
||||
function FunnelItem({ funnel, onClick = () => null }) {
|
||||
return (
|
||||
<div className="w-full flex items-center p-4 bg-white rounded border cursor-pointer" onClick={onClick}>
|
||||
<div className="mr-4">
|
||||
<FunnelGraphSmall data={funnel.stages} />
|
||||
</div>
|
||||
|
||||
<div className="mr-auto">
|
||||
<div className="text-xl mb-2">{funnel.name}</div>
|
||||
<div className="flex items-center text-sm">
|
||||
<div className="mr-3"><span className="font-medium">{funnel.stepsCount}</span> Steps</div>
|
||||
<div><span className="font-medium">{funnel.sessionsCount}</span> Sessions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm px-6">
|
||||
<div className="text-xl mb-2 color-red">{funnel.criticalIssuesCount}</div>
|
||||
<div>Critical Issues</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm px-6">
|
||||
<div className="text-xl mb-2">{funnel.missedConversions}%</div>
|
||||
<div>Missed Conversions</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FunnelItem
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button, Modal, Form, Icon, Checkbox, Input } from 'UI';
|
||||
import styles from './funnelSaveModal.module.css';
|
||||
import { edit, save, fetchList as fetchFunnelsList } from 'Duck/funnels';
|
||||
|
||||
@connect(
|
||||
(state) => ({
|
||||
filter: state.getIn(['search', 'instance']),
|
||||
funnel: state.getIn(['funnels', 'instance']),
|
||||
loading:
|
||||
state.getIn(['funnels', 'saveRequest', 'loading']) ||
|
||||
state.getIn(['funnels', 'updateRequest', 'loading']),
|
||||
}),
|
||||
{ edit, save, fetchFunnelsList }
|
||||
)
|
||||
export default class FunnelSaveModal extends React.PureComponent {
|
||||
state = { name: 'Untitled', isPublic: false };
|
||||
static getDerivedStateFromProps(props) {
|
||||
if (!props.show) {
|
||||
return {
|
||||
name: props.funnel.name || 'Untitled',
|
||||
isPublic: props.funnel.isPublic,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onNameChange = ({ target: { value } }) => {
|
||||
this.props.edit({ name: value });
|
||||
};
|
||||
|
||||
onChangeOption = (e, { checked, name }) => this.props.edit({ [name]: checked });
|
||||
|
||||
onSave = () => {
|
||||
const { funnel, filter } = this.props;
|
||||
if (funnel.name && funnel.name.trim() === '') return;
|
||||
this.props.save(funnel).then(
|
||||
function () {
|
||||
this.props.fetchFunnelsList();
|
||||
this.props.closeHandler();
|
||||
}.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { show, closeHandler, loading, funnel } = this.props;
|
||||
|
||||
return (
|
||||
<Modal size="small" open={show} onClose={this.props.closeHandler}>
|
||||
<Modal.Header className={styles.modalHeader}>
|
||||
<div>{'Save Funnel'}</div>
|
||||
<Icon
|
||||
role="button"
|
||||
tabIndex="-1"
|
||||
color="gray-dark"
|
||||
size="14"
|
||||
name="close"
|
||||
onClick={closeHandler}
|
||||
/>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Content>
|
||||
<Form onSubmit={this.onSave}>
|
||||
<Form.Field>
|
||||
<label>{'Title:'}</label>
|
||||
<Input
|
||||
autoFocus={true}
|
||||
className={styles.name}
|
||||
name="name"
|
||||
value={funnel.name}
|
||||
onChange={this.onNameChange}
|
||||
placeholder="Title"
|
||||
/>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field>
|
||||
<div className="flex items-center">
|
||||
<Checkbox
|
||||
name="isPublic"
|
||||
className="font-medium"
|
||||
type="checkbox"
|
||||
checked={funnel.isPublic}
|
||||
onClick={this.onChangeOption}
|
||||
className="mr-3"
|
||||
/>
|
||||
<div
|
||||
className="flex items-center cursor-pointer"
|
||||
onClick={() => this.props.edit({ isPublic: !funnel.isPublic })}
|
||||
>
|
||||
<Icon name="user-friends" size="16" />
|
||||
<span className="ml-2"> Team Visible</span>
|
||||
</div>
|
||||
</div>
|
||||
</Form.Field>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
<Modal.Footer className="">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={this.onSave}
|
||||
loading={loading}
|
||||
className="float-left mr-2"
|
||||
>
|
||||
{funnel.exists() ? 'Modify' : 'Save'}
|
||||
</Button>
|
||||
<Button onClick={closeHandler}>{'Cancel'}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
@import 'mixins.css';
|
||||
|
||||
.modalHeader {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
@mixin plainButton;
|
||||
}
|
||||
|
||||
.applyButton {
|
||||
@mixin basicButton;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './FunnelSaveModal'
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import SessionItem from 'Shared/SessionItem'
|
||||
import { fetchSessions, fetchSessionsFiltered } from 'Duck/funnels'
|
||||
import { setFunnelPage } from 'Duck/sessions'
|
||||
import { LoadMoreButton, NoContent } from 'UI'
|
||||
import FunnelSessionsHeader from '../FunnelSessionsHeader'
|
||||
import AnimatedSVG, { ICONS } from 'Shared/AnimatedSVG/AnimatedSVG';
|
||||
|
||||
const PER_PAGE = 10;
|
||||
|
||||
function FunnelSessionList(props) {
|
||||
const { funnelId, issueId, list, sessionsTotal, sessionsSort, inDetails = false } = props;
|
||||
|
||||
const [showPages, setShowPages] = useState(1)
|
||||
const displayedCount = Math.min(showPages * PER_PAGE, list.size);
|
||||
|
||||
const addPage = () => setShowPages(showPages + 1);
|
||||
|
||||
useEffect(() => {
|
||||
props.setFunnelPage({
|
||||
funnelId,
|
||||
issueId
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FunnelSessionsHeader sessionsCount={inDetails ? sessionsTotal : list.size} inDetails={inDetails} />
|
||||
<div className="mb-4" />
|
||||
<NoContent
|
||||
title={
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<AnimatedSVG name={ICONS.NO_RESULTS} size="60" />
|
||||
<div className="mt-4">No recordings found!</div>
|
||||
</div>
|
||||
}
|
||||
subtext="Please try changing your search parameters."
|
||||
// animatedIcon="no-results"
|
||||
show={ list.size === 0}
|
||||
>
|
||||
{ list.take(displayedCount).map(session => (
|
||||
<SessionItem
|
||||
key={ session.sessionId }
|
||||
session={ session }
|
||||
/>
|
||||
))}
|
||||
|
||||
<LoadMoreButton
|
||||
className="mt-12 mb-12"
|
||||
displayedCount={displayedCount}
|
||||
totalCount={list.size}
|
||||
onClick={addPage}
|
||||
/>
|
||||
</NoContent>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
list: state.getIn(['funnels', 'sessions']),
|
||||
sessionsTotal: state.getIn(['funnels', 'sessionsTotal']),
|
||||
funnel: state.getIn(['funnels', 'instance']),
|
||||
activeStages: state.getIn(['funnels', 'activeStages']).toJS(),
|
||||
liveFilters: state.getIn(['funnelFilters', 'appliedFilter']),
|
||||
funnelFilters: state.getIn(['funnels', 'funnelFilters']),
|
||||
sessionsSort: state.getIn(['funnels', 'sessionsSort']),
|
||||
}), { fetchSessions, fetchSessionsFiltered, setFunnelPage })(FunnelSessionList)
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './FunnelSessionList'
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { applyFilter, fetchList } from 'Duck/filters';
|
||||
import { fetchList as fetchFunnelsList } from 'Duck/funnels';
|
||||
import DateRangeDropdown from 'Shared/DateRangeDropdown';
|
||||
|
||||
@connect(state => ({
|
||||
rangeValue: state.getIn([ 'filters', 'appliedFilter', 'rangeValue' ]),
|
||||
startDate: state.getIn([ 'filters', 'appliedFilter', 'startDate' ]),
|
||||
endDate: state.getIn([ 'filters', 'appliedFilter', 'endDate' ]),
|
||||
}), {
|
||||
applyFilter, fetchList, fetchFunnelsList
|
||||
})
|
||||
export default class DateRange extends React.PureComponent {
|
||||
render() {
|
||||
const { startDate, endDate, rangeValue, className } = this.props;
|
||||
return (
|
||||
<DateRangeDropdown
|
||||
button
|
||||
// onChange={ this.onDateChange }
|
||||
rangeValue={ rangeValue }
|
||||
startDate={ startDate }
|
||||
endDate={ endDate }
|
||||
className={ className }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import Select from 'Shared/Select';
|
||||
import { setSessionsSort as sort } from 'Duck/funnels';
|
||||
import { setSessionsSort } from 'Duck/funnels';
|
||||
|
||||
@connect(state => ({
|
||||
sessionsSort: state.getIn(['funnels','sessionsSort'])
|
||||
}), { sort, setSessionsSort })
|
||||
export default class SortDropdown extends React.PureComponent {
|
||||
state = { value: null }
|
||||
sort = ({ value }) => {
|
||||
this.setState({ value: value })
|
||||
const [ sort, order ] = value.split('-');
|
||||
const sign = order === 'desc' ? -1 : 1;
|
||||
setTimeout(() => this.props.sort(sort, sign), 100);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { options, issuesSort } = this.props;
|
||||
return (
|
||||
<Select
|
||||
right
|
||||
plain
|
||||
name="sortSessions"
|
||||
options={options}
|
||||
defaultValue={ options[ 0 ].value }
|
||||
onChange={ this.sort }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SortDropdown';
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
.dropdown {
|
||||
display: flex !important;
|
||||
padding: 4px 6px;
|
||||
border-radius: 3px;
|
||||
color: $gray-darkest;
|
||||
font-weight: 500;
|
||||
&:hover {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownTrigger {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
&:hover {
|
||||
background-color: $gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownIcon {
|
||||
margin-top: 2px;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux';
|
||||
import { Icon, Dropdown, TagBadge } from 'UI'
|
||||
import { applyIssueFilter, removeIssueFilter } from 'Duck/funnels';
|
||||
import cn from 'classnames';
|
||||
import stl from './issueFilter.module.css';
|
||||
import { List } from 'immutable';
|
||||
|
||||
function IssueFilter(props) {
|
||||
const { filters, issueTypes, issueTypesMap } = props;
|
||||
|
||||
const onChangeFilter = (e, { name, value }) => {
|
||||
const errors = filters.toJS();
|
||||
errors.push(value);
|
||||
props.applyIssueFilter({ filters: List(errors) });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-start">
|
||||
<Dropdown
|
||||
trigger={
|
||||
<div className={cn("py-2 px-3 bg-white rounded-full flex items-center text-sm mb-2", stl.filterBtn)}>
|
||||
<Icon name="filter" size="12" color="teal" />
|
||||
<span className="ml-2 font-medium leading-none">Filter</span>
|
||||
</div>
|
||||
}
|
||||
options={ issueTypes.filter(i => !filters.includes(i.value)) }
|
||||
name="change"
|
||||
icon={null}
|
||||
onChange={onChangeFilter}
|
||||
basic
|
||||
scrolling
|
||||
selectOnBlur={false}
|
||||
/>
|
||||
<div className="flex items-center ml-3 flex-wrap">
|
||||
{filters.map(err => (
|
||||
<TagBadge
|
||||
className="mb-2"
|
||||
key={ err }
|
||||
hashed={false}
|
||||
text={ issueTypesMap[err] }
|
||||
onRemove={ () => props.removeIssueFilter(err) }
|
||||
outline
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
filters: state.getIn(['funnels', 'issueFilters', 'filters']),
|
||||
issueTypes: state.getIn(['funnels', 'issueTypes']).toJS(),
|
||||
issueTypesMap: state.getIn(['funnels', 'issueTypesMap']),
|
||||
}), { applyIssueFilter, removeIssueFilter })(IssueFilter)
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './IssueFilter'
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
.filterBtn {
|
||||
border: dashed 1px $teal !important;
|
||||
color: $teal;
|
||||
&:hover {
|
||||
background-color: $active-blue;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import React from 'react'
|
||||
import { Button } from 'UI'
|
||||
import { addEvent } from 'Duck/funnelFilters'
|
||||
import Event, { TYPES } from 'Types/filter/event';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
function IssuesEmptyMessage(props) {
|
||||
const { children, show } = props;
|
||||
const createHandler = () => {
|
||||
props.addEvent(Event({ type: TYPES.LOCATION, key: TYPES.LOCATION } ))
|
||||
props.onAddEvent();
|
||||
}
|
||||
return (show ? (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="flex flex-col items-center justify-center text-center my-6">
|
||||
<div className="text-3xl font-medium mb-4">See what's impacting conversions</div>
|
||||
<div className="mb-4 text-xl">Add events to your funnel to identify potential issues that are causing conversion loss.</div>
|
||||
<Button variant="primary" onClick={ createHandler }>+ ADD EVENTS</Button>
|
||||
</div>
|
||||
<img src="/assets/img/funnel_intro.png" />
|
||||
</div>
|
||||
) : children
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(null, { addEvent })(IssuesEmptyMessage)
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './IssuesEmptyMessage'
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import React from 'react';
|
||||
import FilterList from 'Shared/Filters/FilterList';
|
||||
import FilterSelection from 'Shared/Filters/FilterSelection';
|
||||
import { connect } from 'react-redux';
|
||||
import { IconButton } from 'UI';
|
||||
import { editFilter, addFilter } from 'Duck/funnels';
|
||||
import UpdateFunnelButton from 'Shared/UpdateFunnelButton';
|
||||
|
||||
interface Props {
|
||||
appliedFilter: any;
|
||||
editFilter: typeof editFilter;
|
||||
addFilter: typeof addFilter;
|
||||
}
|
||||
function FunnelSearch(props: Props) {
|
||||
const { appliedFilter } = props;
|
||||
const hasEvents = appliedFilter.filters.filter(i => i.isEvent).size > 0;
|
||||
const hasFilters = appliedFilter.filters.filter(i => !i.isEvent).size > 0;
|
||||
|
||||
const onAddFilter = (filter) => {
|
||||
props.addFilter(filter);
|
||||
}
|
||||
|
||||
const onUpdateFilter = (filterIndex, filter) => {
|
||||
const newFilters = appliedFilter.filters.map((_filter, i) => {
|
||||
if (i === filterIndex) {
|
||||
return filter;
|
||||
} else {
|
||||
return _filter;
|
||||
}
|
||||
});
|
||||
|
||||
props.editFilter({
|
||||
...appliedFilter,
|
||||
filters: newFilters,
|
||||
});
|
||||
}
|
||||
|
||||
const onRemoveFilter = (filterIndex) => {
|
||||
const newFilters = appliedFilter.filters.filter((_filter, i) => {
|
||||
return i !== filterIndex;
|
||||
});
|
||||
|
||||
props.editFilter({
|
||||
filters: newFilters,
|
||||
});
|
||||
}
|
||||
|
||||
const onChangeEventsOrder = (e, { name, value }) => {
|
||||
props.editFilter({
|
||||
eventsOrder: value,
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border bg-white rounded mt-4">
|
||||
<div className="p-5">
|
||||
<FilterList
|
||||
filter={appliedFilter}
|
||||
onUpdateFilter={onUpdateFilter}
|
||||
onRemoveFilter={onRemoveFilter}
|
||||
onChangeEventsOrder={onChangeEventsOrder}
|
||||
hideEventsOrder={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border-t px-5 py-1 flex items-center -mx-2">
|
||||
<div>
|
||||
<FilterSelection
|
||||
filter={undefined}
|
||||
onFilterClick={onAddFilter}
|
||||
>
|
||||
<IconButton primaryText label="ADD STEP" icon="plus" />
|
||||
</FilterSelection>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
<UpdateFunnelButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
appliedFilter: state.getIn([ 'funnels', 'instance', 'filter' ]),
|
||||
}), { editFilter, addFilter })(FunnelSearch);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './FunnelSearch';
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button } from 'UI';
|
||||
import FunnelSaveModal from 'App/components/Funnels/FunnelSaveModal';
|
||||
import { connect } from 'react-redux';
|
||||
import { init } from 'Duck/funnels';
|
||||
interface Props {
|
||||
filter: any
|
||||
init: (instance: any) => void
|
||||
}
|
||||
function SaveFunnelButton(props: Props) {
|
||||
const [showModal, setshowModal] = useState(false)
|
||||
|
||||
const handleClick = () => {
|
||||
props.init({ filter: props.filter })
|
||||
setshowModal(true)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
variant="text-primary"
|
||||
icon="funnel"
|
||||
onClick={handleClick}
|
||||
>SAVE FUNNEL</Button>
|
||||
|
||||
<FunnelSaveModal
|
||||
show={showModal}
|
||||
closeHandler={() => setshowModal(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
filter: state.getIn(['search', 'instance']),
|
||||
}), { init })(SaveFunnelButton);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './SaveFunnelButton';
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import React from 'react';
|
||||
import { IconButton } from 'UI';
|
||||
import { connect } from 'react-redux';
|
||||
import { save } from 'Duck/funnels';
|
||||
|
||||
interface Props {
|
||||
save: typeof save;
|
||||
loading: boolean;
|
||||
}
|
||||
function UpdateFunnelButton(props: Props) {
|
||||
const { loading } = props;
|
||||
return (
|
||||
<div>
|
||||
<IconButton
|
||||
className="mr-2"
|
||||
disabled={loading}
|
||||
onClick={() => props.save()} primaryText label="UPDATE FUNNEL" icon="funnel"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
loading: state.getIn(['funnels', 'saveRequest', 'loading']) ||
|
||||
state.getIn(['funnels', 'updateRequest', 'loading']),
|
||||
}), { save })(UpdateFunnelButton);
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './UpdateFunnelButton';
|
||||
|
|
@ -1,451 +0,0 @@
|
|||
import { List, Map } from 'immutable';
|
||||
import Funnel from 'Types/funnel';
|
||||
import FunnelIssue from 'Types/funnelIssue';
|
||||
import Session from 'Types/session';
|
||||
import { fetchListType, fetchType, saveType, editType, initType, removeType } from './funcTools/crud/types';
|
||||
import { createItemInListUpdater, mergeReducers, success, array } from './funcTools/tools';
|
||||
import { createRequestReducer } from './funcTools/request';
|
||||
import { getDateRangeFromValue } from 'App/dateRange';
|
||||
import { LAST_7_DAYS } from 'Types/app/period';
|
||||
import { filterMap, checkFilterValue, hasFilterApplied } from './search';
|
||||
|
||||
const name = 'funnel';
|
||||
const idKey = 'funnelId';
|
||||
const itemInListUpdater = createItemInListUpdater(idKey);
|
||||
|
||||
const FETCH_LIST = fetchListType('funnel/FETCH_LIST');
|
||||
const FETCH_ISSUES = fetchType('funnel/FETCH_ISSUES');
|
||||
const FETCH_ISSUE = fetchType('funnel/FETCH_ISSUE');
|
||||
const FETCH_ISSUE_TYPES = fetchType('funnel/FETCH_ISSUE_TYPES');
|
||||
const FETCH_SESSIONS = fetchType('funnel/FETCH_SESSIONS');
|
||||
const FETCH = fetchType('funnel/FETCH');
|
||||
const FETCH_INSIGHTS = fetchType('funnel/FETCH_INSIGHTS');
|
||||
const SAVE = saveType('funnel/SAVE');
|
||||
const UPDATE = saveType('funnel/UPDATE');
|
||||
const EDIT = editType('funnel/EDIT');
|
||||
const EDIT_FILTER = `${name}/EDIT_FILTER`;
|
||||
const EDIT_FUNNEL_FILTER = `${name}/EDIT_FUNNEL_FILTER`;
|
||||
const REMOVE = removeType('funnel/REMOVE');
|
||||
const INIT = initType('funnel/INIT');
|
||||
const SET_NAV_REF = 'funnels/SET_NAV_REF'
|
||||
|
||||
const RESET_FUNNEL = 'funnels/RESET_FUNNEL'
|
||||
const APPLY_FILTER = 'funnels/APPLY_FILTER'
|
||||
const APPLY_ISSUE_FILTER = 'funnels/APPLY_ISSUE_FILTER'
|
||||
const REMOVE_ISSUE_FILTER = 'funnels/REMOVE_ISSUE_FILTER'
|
||||
const SET_ACTIVE_STAGES = 'funnels/SET_ACTIVE_STAGES'
|
||||
const SET_SESSIONS_SORT = 'funnels/SET_SESSIONS_SORT'
|
||||
const BLINK = 'funnels/BLINK'
|
||||
|
||||
const RESET_ISSUE = 'funnles/RESET_ISSUE'
|
||||
|
||||
const FETCH_LIST_SUCCESS = success(FETCH_LIST);
|
||||
const FETCH_ISSUES_SUCCESS = success(FETCH_ISSUES);
|
||||
const FETCH_ISSUE_SUCCESS = success(FETCH_ISSUE);
|
||||
const FETCH_ISSUE_TYPES_SUCCESS = success(FETCH_ISSUE_TYPES);
|
||||
const FETCH_SESSIONS_SUCCESS = success(FETCH_SESSIONS);
|
||||
const FETCH_SUCCESS = success(FETCH);
|
||||
const FETCH_INSIGHTS_SUCCESS = success(FETCH_INSIGHTS);
|
||||
const SAVE_SUCCESS = success(SAVE);
|
||||
const UPDATE_SUCCESS = success(UPDATE);
|
||||
const REMOVE_SUCCESS = success(REMOVE);
|
||||
|
||||
const range = getDateRangeFromValue(LAST_7_DAYS);
|
||||
const defaultDateFilters = {
|
||||
rangeValue: LAST_7_DAYS,
|
||||
startDate: range.start.ts,
|
||||
endDate: range.end.ts
|
||||
}
|
||||
|
||||
const initialState = Map({
|
||||
list: List(),
|
||||
instance: Funnel(),
|
||||
insights: Funnel(),
|
||||
issues: List(),
|
||||
issue: FunnelIssue(),
|
||||
issuesTotal: 0,
|
||||
sessionsTotal: 0,
|
||||
sessions: List(),
|
||||
activeStages: List(),
|
||||
funnelFilters: Map(defaultDateFilters),
|
||||
sessionsSort: Map({ order: "desc", sort: "newest" }),
|
||||
issueFilters: Map({
|
||||
filters: List(),
|
||||
sort: { order: "desc", sort: "lostConversions" }
|
||||
}),
|
||||
sessionFilters: defaultDateFilters,
|
||||
navRef: null,
|
||||
issueTypes: List(),
|
||||
blink: true
|
||||
});
|
||||
|
||||
const reducer = (state = initialState, action = {}) => {
|
||||
switch(action.type) {
|
||||
case BLINK:
|
||||
return state.set('blink', action.state);
|
||||
case EDIT:
|
||||
return state.mergeIn([ 'instance' ], action.instance);
|
||||
case EDIT_FILTER:
|
||||
return state.mergeIn([ 'instance', 'filter' ], action.instance);
|
||||
case EDIT_FUNNEL_FILTER:
|
||||
return state.mergeIn([ 'funnelFilters' ], action.instance);
|
||||
case INIT:
|
||||
return state.set('instance', Funnel(action.instance))
|
||||
case FETCH_LIST_SUCCESS:
|
||||
return state.set('list', List(action.data).map(Funnel))
|
||||
case FETCH_ISSUES_SUCCESS:
|
||||
return state
|
||||
.set('issues', List(action.data.issues.significant).map(FunnelIssue))
|
||||
.set('criticalIssuesCount', action.data.issues.criticalIssuesCount)
|
||||
case FETCH_SESSIONS_SUCCESS:
|
||||
return state
|
||||
.set('sessions', List(action.data.sessions).map(s => new Session(s)))
|
||||
.set('total', action.data.total)
|
||||
case FETCH_ISSUE_SUCCESS:
|
||||
return state
|
||||
.set('issue', FunnelIssue(action.data.issue))
|
||||
.set('sessions', List(action.data.sessions.sessions).map(s => new Session(s)))
|
||||
.set('sessionsTotal', action.data.sessions.total)
|
||||
case RESET_ISSUE:
|
||||
return state.set('isses', FunnelIssue())
|
||||
.set('sections', List())
|
||||
.set('sessionsTotal', 0);
|
||||
case FETCH_SUCCESS:
|
||||
const funnel = Funnel(action.data);
|
||||
return state.set('instance', funnel)
|
||||
case FETCH_ISSUE_TYPES_SUCCESS:
|
||||
const tmpMap = {};
|
||||
action.data.forEach(element => {
|
||||
tmpMap[element.type] = element.title
|
||||
});
|
||||
return state
|
||||
.set('issueTypes', List(action.data.map(({ type, title }) => ({ text: title, value: type }))))
|
||||
.set('issueTypesMap', tmpMap);
|
||||
case FETCH_INSIGHTS_SUCCESS:
|
||||
let stages = [];
|
||||
if (action.isRefresh) {
|
||||
const activeStages = state.get('activeStages');
|
||||
const oldInsights = state.get('insights');
|
||||
const lastStage = action.data.stages[action.data.stages.length - 1]
|
||||
const lastStageIndex = activeStages.toJS()[1];
|
||||
stages = oldInsights.stages.map((stage, i) => {
|
||||
stage.dropDueToIssues = lastStageIndex === i ? lastStage.dropDueToIssues : 0;
|
||||
return stage;
|
||||
});
|
||||
return state.set('insights', Funnel({ totalDropDueToIssues: action.data.totalDropDueToIssues, stages, activeStages: activeStages.toJS() }));
|
||||
} else {
|
||||
stages = action.data.stages.map((stage, i) => {
|
||||
stage.dropDueToIssues = 0;
|
||||
return stage;
|
||||
});
|
||||
return state.set('insights', Funnel({ ...action.data, stages }))
|
||||
}
|
||||
case SAVE_SUCCESS:
|
||||
case UPDATE_SUCCESS:
|
||||
return state.update('list', itemInListUpdater(CustomField(action.data)))
|
||||
case REMOVE_SUCCESS:
|
||||
return state.update('list', list => list.filter(item => item.index !== action.index));
|
||||
case APPLY_FILTER:
|
||||
return state.mergeIn([ action.filterType ], Array.isArray(action.filter) ? action.filter : Map(action.filter));
|
||||
case APPLY_ISSUE_FILTER:
|
||||
return state.mergeIn(['issueFilters'], action.filter)
|
||||
case REMOVE_ISSUE_FILTER:
|
||||
return state.updateIn(['issueFilters', 'filters'], list => list.filter(item => item !== action.errorType))
|
||||
case SET_ACTIVE_STAGES:
|
||||
return state.set('activeStages', List(action.stages))
|
||||
case SET_NAV_REF:
|
||||
return state.set('navRef', action.navRef);
|
||||
case SET_SESSIONS_SORT:
|
||||
const comparator = (s1, s2) => {
|
||||
let diff = s1[ action.sortKey ] - s2[ action.sortKey ];
|
||||
diff = diff === 0 ? s1.startedAt - s2.startedAt : diff;
|
||||
return action.sign * diff;
|
||||
};
|
||||
return state
|
||||
.update('sessions', list => list.sort(comparator))
|
||||
.set('sessionsSort', { sort: action.sort, sign: action.sign });
|
||||
case RESET_FUNNEL:
|
||||
return state
|
||||
.set('instance', Funnel())
|
||||
.set('activeStages', List())
|
||||
.set('issuesSort', Map({}))
|
||||
// .set('funnelFilters', Map(defaultDateFilters))
|
||||
.set('insights', Funnel())
|
||||
.set('issues', List())
|
||||
.set('sessions', List());
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchList = (range) => {
|
||||
return {
|
||||
types: array(FETCH_LIST),
|
||||
call: client => client.get(`/funnels`),
|
||||
}
|
||||
}
|
||||
|
||||
export const fetch = (funnelId, params) => (dispatch, getState) => {
|
||||
return dispatch({
|
||||
types: array(FETCH),
|
||||
call: client => client.get(`/funnels/${funnelId}`, params)
|
||||
});
|
||||
}
|
||||
|
||||
// const eventMap = ({value, type, key, operator, source, custom}) => ({value, type, key, operator, source, custom});
|
||||
// const filterMap = ({value, type, key, operator, source, custom }) => ({value: Array.isArray(value) ? value: [value], custom, type, key, operator, source});
|
||||
|
||||
function getParams(params, state) {
|
||||
const filter = state.getIn([ 'funnels', 'instance', 'filter']).toData();
|
||||
filter.filters = filter.filters.map(filterMap);
|
||||
const funnelFilters = state.getIn([ 'funnels', 'funnelFilters']).toJS();
|
||||
|
||||
// const appliedFilter = state.getIn([ 'funnels', 'instance', 'filter' ]);
|
||||
// const filter = appliedFilter
|
||||
// .update('events', list => list.map(event => event.set('value', event.value || '*')).map(eventMap))
|
||||
// .toJS();
|
||||
|
||||
// filter.filters = state.getIn([ 'funnelFilters', 'appliedFilter', 'filters' ])
|
||||
// .map(filterMap).toJS();
|
||||
|
||||
return { ...filter, ...funnelFilters };
|
||||
}
|
||||
|
||||
export const fetchInsights = (funnelId, params = {}, isRefresh = false) => (dispatch, getState) => {
|
||||
return dispatch({
|
||||
types: array(FETCH_INSIGHTS),
|
||||
call: client => client.post(`/funnels/${funnelId}/insights`, getParams(params, getState())),
|
||||
isRefresh
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
export const fetchFiltered = (funnelId, params) => (dispatch, getState) => {
|
||||
return dispatch({
|
||||
types: array(FETCH),
|
||||
call: client => client.post(`/funnels/${funnelId}`, params),
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchIssuesFiltered = (funnelId, params) => (dispatch, getState) => {
|
||||
return dispatch({
|
||||
types: array(FETCH_ISSUES),
|
||||
call: client => client.post(`/funnels/${funnelId}/issues`, getParams(params, getState())),
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchSessionsFiltered = (funnelId, params) => (dispatch, getState) => {
|
||||
return dispatch({
|
||||
types: array(FETCH_SESSIONS),
|
||||
call: client => client.post(`/funnels/${funnelId}/sessions`, getParams(params, getState())),
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchIssue = (funnelId, issueId, params) => (dispatch, getState) => {
|
||||
const filters = getState().getIn([ 'funnelFilters', 'appliedFilter' ]);
|
||||
const _params = { ...filters.toData(), ...params };
|
||||
return dispatch({
|
||||
types: array(FETCH_ISSUE),
|
||||
call: client => client.post(`/funnels/${funnelId}/issues/${issueId}/sessions`, _params),
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchIssues = (funnelId, params) => {
|
||||
return {
|
||||
types: array(FETCH_ISSUES),
|
||||
call: client => client.get(`/funnels/${funnelId}/issues`, params),
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchSessions = (funnelId, params) => {
|
||||
return {
|
||||
types: array(FETCH_SESSIONS),
|
||||
call: client => client.get(`/funnels/${funnelId}/sessions`, params),
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchIssueTypes = () => {
|
||||
return {
|
||||
types: array(FETCH_ISSUE_TYPES),
|
||||
call: client => client.get(`/funnels/issue_types`),
|
||||
}
|
||||
}
|
||||
|
||||
export const save = () => (dispatch, getState) => {
|
||||
const instance = getState().getIn([ 'funnels', 'instance'])
|
||||
const filter = instance.get('filter').toData();
|
||||
filter.filters = filter.filters.map(filterMap);
|
||||
const isExist = instance.exists();
|
||||
|
||||
const _instance = instance instanceof Funnel ? instance : Funnel(instance);
|
||||
const url = isExist ? `/funnels/${ _instance[idKey] }` : `/funnels`;
|
||||
|
||||
return dispatch({
|
||||
types: array(isExist ? SAVE : UPDATE),
|
||||
call: client => client.post(url, { ..._instance.toData(), filter }),
|
||||
});
|
||||
}
|
||||
|
||||
export const updateFunnelFilters = (funnelId, filter) => {
|
||||
return {
|
||||
types: array(UPDATE),
|
||||
call: client => client.post(`/funnels/${funnelId}`, { filter }),
|
||||
}
|
||||
}
|
||||
|
||||
export const remove = (index) => {
|
||||
return {
|
||||
types: array(REMOVE),
|
||||
call: client => client.delete(`/funnels/${index}`),
|
||||
index,
|
||||
}
|
||||
}
|
||||
|
||||
export const applyFilter = (filterType='funnelFilters', filter) => {
|
||||
return {
|
||||
type: APPLY_FILTER,
|
||||
filter,
|
||||
filterType,
|
||||
}
|
||||
};
|
||||
|
||||
export const applyIssueFilter = (filter) => {
|
||||
return {
|
||||
type: APPLY_ISSUE_FILTER,
|
||||
filter
|
||||
}
|
||||
};
|
||||
|
||||
export const removeIssueFilter = errorType => {
|
||||
return {
|
||||
type: REMOVE_ISSUE_FILTER,
|
||||
errorType,
|
||||
}
|
||||
};
|
||||
|
||||
export const setActiveStages = (stages, filters, funnelId, forceRrefresh = false) => (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: SET_ACTIVE_STAGES,
|
||||
stages,
|
||||
})
|
||||
|
||||
if (stages.length === 2) {
|
||||
const filter = {...filters.toData(), firstStage: stages[0] + 1, lastStage: stages[1] + 1 };
|
||||
dispatch(fetchIssuesFiltered(funnelId, filter))
|
||||
dispatch(fetchInsights(funnelId, filter, true));
|
||||
dispatch(fetchSessionsFiltered(funnelId, filter));
|
||||
} else if (forceRrefresh) {
|
||||
const filter = {...filters.toData()};
|
||||
dispatch(fetchIssuesFiltered(funnelId, filter))
|
||||
dispatch(fetchInsights(funnelId, filter));
|
||||
dispatch(fetchSessionsFiltered(funnelId, filter));
|
||||
}
|
||||
};
|
||||
|
||||
export const edit = instance => {
|
||||
return {
|
||||
type: EDIT,
|
||||
instance,
|
||||
}
|
||||
};
|
||||
|
||||
export const init = instance => {
|
||||
return {
|
||||
type: INIT,
|
||||
instance,
|
||||
}
|
||||
};
|
||||
|
||||
export const setNavRef = ref => {
|
||||
return {
|
||||
type: SET_NAV_REF,
|
||||
navRef: ref
|
||||
}
|
||||
};
|
||||
|
||||
export const resetIssue = () => {
|
||||
return {
|
||||
type: RESET_ISSUE,
|
||||
}
|
||||
}
|
||||
|
||||
export const resetFunnel = () => {
|
||||
return {
|
||||
type: RESET_FUNNEL,
|
||||
}
|
||||
}
|
||||
|
||||
export const setSessionsSort = (sortKey, sign = 1) => {
|
||||
return {
|
||||
type: SET_SESSIONS_SORT,
|
||||
sortKey,
|
||||
sign
|
||||
}
|
||||
}
|
||||
|
||||
export const blink = (state = true) => {
|
||||
return {
|
||||
type: BLINK,
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
export const refresh = (funnelId) => (dispatch, getState) => {
|
||||
// dispatch(fetch(funnelId))
|
||||
dispatch(fetchInsights(funnelId))
|
||||
dispatch(fetchIssuesFiltered(funnelId, {}))
|
||||
dispatch(fetchSessionsFiltered(funnelId, {}))
|
||||
}
|
||||
|
||||
export default mergeReducers(
|
||||
reducer,
|
||||
createRequestReducer({
|
||||
fetchRequest: FETCH,
|
||||
fetchListRequest: FETCH_LIST,
|
||||
fetchInsights: FETCH_INSIGHTS,
|
||||
fetchIssueRequest: FETCH_ISSUE,
|
||||
saveRequest: SAVE,
|
||||
updateRequest: UPDATE,
|
||||
fetchIssuesRequest: FETCH_ISSUES,
|
||||
fetchSessionsRequest: FETCH_SESSIONS,
|
||||
}),
|
||||
)
|
||||
|
||||
const reduceThenFetchList = actionCreator => (...args) => (dispatch, getState) => {
|
||||
dispatch(actionCreator(...args));
|
||||
dispatch(refresh(getState().getIn([ 'funnels', 'instance', idKey ])));
|
||||
|
||||
// const filter = getState().getIn([ 'funnels', 'instance', 'filter']).toData();
|
||||
// filter.filters = filter.filters.map(filterMap);
|
||||
|
||||
// return dispatch(fetchSessionList(filter));
|
||||
};
|
||||
|
||||
|
||||
export const editFilter = reduceThenFetchList((instance) => ({
|
||||
type: EDIT_FILTER,
|
||||
instance,
|
||||
}));
|
||||
|
||||
export const editFunnelFilter = reduceThenFetchList((instance) => ({
|
||||
type: EDIT_FUNNEL_FILTER,
|
||||
instance,
|
||||
}));
|
||||
|
||||
export const addFilter = (filter) => (dispatch, getState) => {
|
||||
filter.value = checkFilterValue(filter.value);
|
||||
const instance = getState().getIn([ 'funnels', 'instance', 'filter']);
|
||||
|
||||
if (hasFilterApplied(instance.filters, filter)) {
|
||||
|
||||
} else {
|
||||
const filters = instance.filters.push(filter);
|
||||
return dispatch(editFilter(instance.set('filters', filters)));
|
||||
}
|
||||
}
|
||||
|
||||
export const addFilterByKeyAndValue = (key, value) => (dispatch, getState) => {
|
||||
let defaultFilter = filtersMap[key];
|
||||
defaultFilter.value = value;
|
||||
dispatch(addFilter(defaultFilter));
|
||||
}
|
||||
|
|
@ -4,12 +4,11 @@ import { combineReducers } from 'redux-immutable';
|
|||
import user from './user';
|
||||
import sessions from './sessions';
|
||||
import filters from './filters';
|
||||
import funnelFilters from './funnelFilters';
|
||||
import sources from './sources';
|
||||
import site from './site';
|
||||
import customFields from './customField';
|
||||
import integrations from './integrations';
|
||||
import funnels from './funnels';
|
||||
import errors from './errors';
|
||||
import customMetrics from './customMetrics';
|
||||
import search from './search';
|
||||
import liveSearch from './liveSearch';
|
||||
|
|
@ -18,10 +17,9 @@ const rootReducer = combineReducers({
|
|||
user,
|
||||
sessions,
|
||||
filters,
|
||||
funnelFilters,
|
||||
site,
|
||||
customFields,
|
||||
funnels,
|
||||
errors,
|
||||
customMetrics,
|
||||
search,
|
||||
liveSearch,
|
||||
|
|
@ -31,4 +29,4 @@ const rootReducer = combineReducers({
|
|||
|
||||
export type RootStore = ReturnType<typeof rootReducer>
|
||||
|
||||
export default rootReducer
|
||||
export default rootReducer
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue