change(ui): funnel duck cleanup

This commit is contained in:
Shekar Siri 2024-09-13 16:10:00 +05:30
parent 452fe62ebe
commit 4f2d61d1cf
40 changed files with 8 additions and 1925 deletions

View file

@ -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)))

View file

@ -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);

View file

@ -1 +0,0 @@
export { default } from './FunnelGraph'

View file

@ -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))

View file

@ -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);

View file

@ -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;
}

View file

@ -1 +0,0 @@
export { default } from './FunnelHeader';

View file

@ -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))

View file

@ -1 +1 @@
export { default } from './FunnelIssueDetails'
//export { default } from './FunnelIssueDetails'

View file

@ -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))

View file

@ -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 }
/>
);
}
}

View file

@ -1 +0,0 @@
export { default } from './SortDropdown';

View file

@ -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;
}

View file

@ -1 +0,0 @@
export { default } from './FunnelIssues'

View file

@ -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 }
/>
);
}
}

View file

@ -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>
)
}

View file

@ -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

View file

@ -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>
);
}
}

View file

@ -1,15 +0,0 @@
@import 'mixins.css';
.modalHeader {
display: flex !important;
align-items: center;
justify-content: space-between;
}
.cancelButton {
@mixin plainButton;
}
.applyButton {
@mixin basicButton;
}

View file

@ -1 +0,0 @@
export { default } from './FunnelSaveModal'

View file

@ -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)

View file

@ -1 +0,0 @@
export { default } from './FunnelSessionList'

View file

@ -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 }
/>
);
}
}

View file

@ -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 }
/>
);
}
}

View file

@ -1 +0,0 @@
export { default } from './SortDropdown';

View file

@ -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;
}

View file

@ -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)

View file

@ -1 +0,0 @@
export { default } from './IssueFilter'

View file

@ -1,7 +0,0 @@
.filterBtn {
border: dashed 1px $teal !important;
color: $teal;
&:hover {
background-color: $active-blue;
}
}

View file

@ -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)

View file

@ -1 +0,0 @@
export { default } from './IssuesEmptyMessage'

View file

@ -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);

View file

@ -1 +0,0 @@
export { default } from './FunnelSearch';

View file

@ -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);

View file

@ -1 +0,0 @@
export { default } from './SaveFunnelButton';

View file

@ -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);

View file

@ -1 +0,0 @@
export { default } from './UpdateFunnelButton';

View file

@ -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));
}

View file

@ -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