Merge pull request #323 from openreplay/dev

v1.5.0 UI fixes
This commit is contained in:
Mehdi Osman 2022-02-14 17:54:55 +01:00 committed by GitHub
commit 8871f9c681
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 237 additions and 66 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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