commit
8871f9c681
12 changed files with 237 additions and 66 deletions
|
|
@ -772,7 +772,7 @@ def get_funnel_issue_sessions(projectId: int, funnelId: int, issueId: str,
|
|||
|
||||
@app.get('/{projectId}/funnels/{funnelId}', tags=["funnels"])
|
||||
def get_funnel(projectId: int, funnelId: int, context: schemas.CurrentContext = Depends(OR_context)):
|
||||
data = funnels.get(funnel_id=funnelId, project_id=projectId, user_id=context.user_id, flatten=False)
|
||||
data = funnels.get(funnel_id=funnelId, project_id=projectId, user_id=context.user_id)
|
||||
if data is None:
|
||||
return {"errors": ["funnel not found"]}
|
||||
return {"data": data}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,13 @@ 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
|
||||
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 EventFilter from 'Shared/EventFilter';
|
||||
import FunnelSearch from 'Shared/FunnelSearch';
|
||||
import cn from 'classnames';
|
||||
import IssuesEmptyMessage from 'Components/Funnels/IssuesEmptyMessage'
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ const TABS = [ TAB_ISSUES, TAB_SESSIONS ].map(tab => ({
|
|||
}));
|
||||
|
||||
const FunnelDetails = (props) => {
|
||||
const { insights, funnels, funnel, funnelId, loading, liveFilters, issuesLoading, sessionsLoading } = 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);
|
||||
|
|
@ -40,16 +41,17 @@ const FunnelDetails = (props) => {
|
|||
|
||||
props.fetch(funnelId).then(() => {
|
||||
setMounted(true);
|
||||
}).then(() => {
|
||||
props.refresh(funnelId);
|
||||
})
|
||||
|
||||
props.fetchInsights(funnelId, {})
|
||||
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (funnel && funnel.filter && liveFilters.events.size === 0) {
|
||||
props.setInitialFilters();
|
||||
}
|
||||
}, [funnel])
|
||||
// useEffect(() => {
|
||||
// if (funnel && funnel.filter && liveFilters.events.size === 0) {
|
||||
// props.setInitialFilters();
|
||||
// }
|
||||
// }, [funnel])
|
||||
|
||||
const onBack = () => {
|
||||
props.history.push(sessionsRoute());
|
||||
|
|
@ -83,16 +85,19 @@ const FunnelDetails = (props) => {
|
|||
redirect={redirect}
|
||||
funnels={funnels}
|
||||
onBack={onBack}
|
||||
funnelId={funnelId}
|
||||
funnelId={parseInt(funnelId)}
|
||||
toggleFilters={() => setShowFilters(!showFilters)}
|
||||
showFilters={showFilters}
|
||||
/>
|
||||
<div className="my-3" />
|
||||
{showFilters &&
|
||||
<EventFilter
|
||||
funnel={funnel}
|
||||
onHide={() => setShowFilters(!showFilters)}
|
||||
/>}
|
||||
{showFilters && (
|
||||
<FunnelSearch />
|
||||
// <EventFilter
|
||||
// funnel={funnel}
|
||||
// onHide={() => setShowFilters(!showFilters)}
|
||||
// />
|
||||
)
|
||||
}
|
||||
<div className="my-3" />
|
||||
<Tabs
|
||||
tabs={ TABS }
|
||||
|
|
@ -154,5 +159,6 @@ export default connect((state, props) => {
|
|||
fetchIssueTypes,
|
||||
resetFunnel,
|
||||
resetFunnelFilters,
|
||||
setInitialFilters
|
||||
setInitialFilters,
|
||||
refresh,
|
||||
})(withRouter((FunnelDetails)))
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ const FunnelHeader = (props) => {
|
|||
selectOnBlur={false}
|
||||
icon={ <Icon name="chevron-down" color="gray-dark" size="14" className={stl.dropdownIcon} /> }
|
||||
/>
|
||||
<Info label="Events" value={funnel.filter.events.size} />
|
||||
<Info label="Events" value={funnel.filter.filters.size} />
|
||||
<span>-</span>
|
||||
<Button plain onClick={props.toggleFilters}>{ showFilters ? 'HIDE' : 'EDIT FUNNEL' }</Button>
|
||||
<Info label="Sessions" value={insights.sessionsCount} />
|
||||
|
|
@ -117,5 +117,5 @@ const FunnelHeader = (props) => {
|
|||
}
|
||||
|
||||
export default connect(state => ({
|
||||
funnelFilters: state.getIn([ 'funnelFilters', 'appliedFilter']),
|
||||
funnelFilters: state.getIn([ 'funnels', 'instance', 'filter' ]),
|
||||
}), { applyFilter, deleteFunnel, fetch, fetchInsights, fetchIssuesFiltered, fetchSessionsFiltered })(FunnelHeader)
|
||||
|
|
|
|||
|
|
@ -26,10 +26,12 @@ function FilterAutoCompleteLocal(props: Props) {
|
|||
icon = null,
|
||||
} = props;
|
||||
const [showModal, setShowModal] = useState(true)
|
||||
const [query, setQuery] = useState(value);
|
||||
const [query, setQuery] = useState(value);
|
||||
const debounceOnSelect = React.useCallback(debounce(props.onSelect, 500), []);
|
||||
|
||||
const onInputChange = ({ target: { value } }) => {
|
||||
setQuery(value);
|
||||
debounceOnSelect(null, { value });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -53,7 +55,7 @@ function FilterAutoCompleteLocal(props: Props) {
|
|||
<input
|
||||
name="query"
|
||||
onChange={ onInputChange }
|
||||
onBlur={ onBlur }
|
||||
// onBlur={ onBlur }
|
||||
onFocus={ () => setShowModal(true)}
|
||||
value={ query }
|
||||
autoFocus={ true }
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ interface Props {
|
|||
onUpdateFilter: (filterIndex, filter) => void;
|
||||
onRemoveFilter: (filterIndex) => void;
|
||||
onChangeEventsOrder: (e, { name, value }) => void;
|
||||
hideEventsOrder?: boolean;
|
||||
}
|
||||
function FilterList(props: Props) {
|
||||
const { filter } = props;
|
||||
const { filter, hideEventsOrder = false } = props;
|
||||
const filters = filter.filters;
|
||||
const hasEvents = filter.filters.filter(i => i.isEvent).size > 0;
|
||||
const hasFilters = filter.filters.filter(i => !i.isEvent).size > 0;
|
||||
|
|
@ -30,29 +31,32 @@ function FilterList(props: Props) {
|
|||
<>
|
||||
<div className="flex items-center mb-2">
|
||||
<div className="text-sm color-gray-medium mr-auto">EVENTS</div>
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2 color-gray-medium text-sm" style={{ textDecoration: 'underline dotted'}}>
|
||||
<Popup
|
||||
trigger={<div>Events Order</div>}
|
||||
content={ `Events Order` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
{ !hideEventsOrder && (
|
||||
<div className="flex items-center">
|
||||
<div className="mr-2 color-gray-medium text-sm" style={{ textDecoration: 'underline dotted'}}>
|
||||
<Popup
|
||||
trigger={<div>Events Order</div>}
|
||||
content={ `Events Order` }
|
||||
size="tiny"
|
||||
inverted
|
||||
position="top center"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="eventsOrder"
|
||||
extraSmall={true}
|
||||
onSelect={props.onChangeEventsOrder}
|
||||
value={{ value: filter.eventsOrder }}
|
||||
list={ [
|
||||
{ name: 'THEN', value: 'then' },
|
||||
{ name: 'AND', value: 'and' },
|
||||
{ name: 'OR', value: 'or' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<SegmentSelection
|
||||
primary
|
||||
name="eventsOrder"
|
||||
extraSmall={true}
|
||||
onSelect={props.onChangeEventsOrder}
|
||||
value={{ value: filter.eventsOrder }}
|
||||
list={ [
|
||||
{ name: 'THEN', value: 'then' },
|
||||
{ name: 'AND', value: 'and' },
|
||||
{ name: 'OR', value: 'or' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{filters.map((filter, filterIndex) => filter.isEvent ? (
|
||||
<FilterItem
|
||||
|
|
|
|||
91
frontend/app/components/shared/FunnelSearch/FunnelSearch.tsx
Normal file
91
frontend/app/components/shared/FunnelSearch/FunnelSearch.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
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);
|
||||
// filter.value = [""]
|
||||
// const newFilters = appliedFilter.filters.concat(filter);
|
||||
// props.edit({
|
||||
// ...appliedFilter.filter,
|
||||
// filters: newFilters,
|
||||
// });
|
||||
}
|
||||
|
||||
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
frontend/app/components/shared/FunnelSearch/index.ts
Normal file
1
frontend/app/components/shared/FunnelSearch/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './FunnelSearch';
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import React, { useState } from 'react';
|
||||
import { IconButton } from 'UI';
|
||||
import FunnelSaveModal from 'App/components/Funnels/FunnelSaveModal';
|
||||
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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from './UpdateFunnelButton';
|
||||
|
|
@ -7,7 +7,7 @@ import { createItemInListUpdater, mergeReducers, success, array } from './funcTo
|
|||
import { createRequestReducer } from './funcTools/request';
|
||||
import { getDateRangeFromValue } from 'App/dateRange';
|
||||
import { LAST_7_DAYS } from 'Types/app/period';
|
||||
import { filterMap as searchFilterMap } from './search';
|
||||
import { filterMap, checkFilterValue, hasFilterApplied } from './search';
|
||||
|
||||
const name = 'funnel';
|
||||
const idKey = 'funnelId';
|
||||
|
|
@ -23,6 +23,7 @@ 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 REMOVE = removeType('funnel/REMOVE');
|
||||
const INIT = initType('funnel/INIT');
|
||||
const SET_NAV_REF = 'funnels/SET_NAV_REF'
|
||||
|
|
@ -84,6 +85,8 @@ const reducer = (state = initialState, action = {}) => {
|
|||
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 INIT:
|
||||
return state.set('instance', Funnel(action.instance))
|
||||
case FETCH_LIST_SUCCESS:
|
||||
|
|
@ -191,19 +194,22 @@ export const fetch = (funnelId, params) => (dispatch, getState) => {
|
|||
});
|
||||
}
|
||||
|
||||
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});
|
||||
// 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 appliedFilters = state.getIn([ 'funnelFilters', 'appliedFilter' ]);
|
||||
const filter = appliedFilters
|
||||
.update('events', list => list.map(event => event.set('value', event.value || '*')).map(eventMap))
|
||||
.toJS();
|
||||
|
||||
filter.filters = state.getIn([ 'funnelFilters', 'appliedFilter', 'filters' ])
|
||||
.map(filterMap).toJS();
|
||||
const filter = state.getIn([ 'funnels', 'instance', 'filter']).toData();
|
||||
filter.filters = filter.filters.map(filterMap);
|
||||
|
||||
return {...filter, ...params };
|
||||
// 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;
|
||||
}
|
||||
|
||||
export const fetchInsights = (funnelId, params = {}, isRefresh = false) => (dispatch, getState) => {
|
||||
|
|
@ -266,18 +272,17 @@ export const fetchIssueTypes = () => {
|
|||
}
|
||||
}
|
||||
|
||||
export const save = (instance) => (dispatch, getState) => {
|
||||
// export const save = (instance) => {
|
||||
const filter = getState().getIn([ 'search', 'instance']).toData();
|
||||
filter.filters = filter.filters.map(searchFilterMap);
|
||||
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 = _instance.exists()
|
||||
? `/funnels/${ _instance[idKey] }`
|
||||
: `/funnels`;
|
||||
const url = isExist ? `/funnels/${ _instance[idKey] }` : `/funnels`;
|
||||
|
||||
return dispatch({
|
||||
types: array(_instance.exists() ? SAVE : UPDATE),
|
||||
types: array(isExist ? SAVE : UPDATE),
|
||||
call: client => client.post(url, { ..._instance.toData(), filter }),
|
||||
});
|
||||
}
|
||||
|
|
@ -387,7 +392,7 @@ export const blink = (state = true) => {
|
|||
}
|
||||
|
||||
export const refresh = (funnelId) => (dispatch, getState) => {
|
||||
dispatch(fetch(funnelId))
|
||||
// dispatch(fetch(funnelId))
|
||||
dispatch(fetchInsights(funnelId))
|
||||
dispatch(fetchIssuesFiltered(funnelId, {}))
|
||||
dispatch(fetchSessionsFiltered(funnelId, {}))
|
||||
|
|
@ -405,4 +410,38 @@ export default mergeReducers(
|
|||
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 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));
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ export default Record({
|
|||
startDate,
|
||||
endDate,
|
||||
events: List(events).map(Event),
|
||||
filters: List(filters).map(i => NewFilter(i).toData()),
|
||||
filters: List(filters).map(i => NewFilter(i).toData()).concat(List(events).map(i => NewFilter(i).toData())),
|
||||
custom: Map(custom),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export default Record({
|
|||
conversionImpact,
|
||||
firstStage: firstStage && firstStage.label + ' ' + truncate(firstStage.value || '', 10) || '',
|
||||
lastStage: lastStage && lastStage.label + ' ' + truncate(lastStage.value || '', 10) || '',
|
||||
filter: Filter(filter),
|
||||
filter: Filter(filter),
|
||||
sessionsCount: lastStage && lastStage.sessionsCount,
|
||||
stepsCount: stages ? stages.length : 0,
|
||||
conversions: 100 - conversionImpact
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue