diff --git a/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js b/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js index 067be7c52..53fec2048 100644 --- a/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js +++ b/frontend/app/components/Funnels/FunnelSaveModal/FunnelSaveModal.js @@ -28,9 +28,9 @@ export default class FunnelSaveModal extends React.PureComponent { onChangeOption = (e, { checked, name }) => this.props.edit({ [ name ]: checked }) onSave = () => { - const { funnel, closeHandler } = this.props; + const { funnel, filter } = this.props; if (funnel.name.trim() === '') return; - this.props.save({ ...funnel, filter: filter }).then(function() { + this.props.save(funnel).then(function() { this.props.fetchFunnelsList(); this.props.closeHandler(); }.bind(this)); diff --git a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx index 3f7d432b0..de15e33a9 100644 --- a/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx +++ b/frontend/app/components/shared/Filters/FilterAutoComplete/FilterAutoComplete.tsx @@ -45,8 +45,7 @@ function FilterAutoComplete(props: Props) { const [query, setQuery] = useState(value); - const requestValues = (q) => { - // const { params, method } = props; + const requestValues = (q) => { setLoading(true); return new APIClient()[method?.toLowerCase()](endpoint, { ...params, q }) @@ -55,13 +54,7 @@ function FilterAutoComplete(props: Props) { if (errors) { // this.setError(); } else { - setOptions(data); - // this.setState({ - // ddOpen: true, - // values: data, - // loading: false, - // noResultsMessage: NO_RESULTS_MSG, - // }); + setOptions(data); } }).finally(() => setLoading(false)); // .catch(this.setError); @@ -81,6 +74,17 @@ function FilterAutoComplete(props: Props) { debouncedRequestValues(query) }, [query]) + useEffect(() => { + if(value === '') { + setQuery(value); + } + }, [value]) + + const onBlur = (e) => { + setTimeout(() => { setShowModal(false) }, 200) + props.onSelect(e, { value: query }) + } + const onItemClick = (e, item) => { e.stopPropagation(); e.preventDefault(); @@ -103,7 +107,7 @@ function FilterAutoComplete(props: Props) { setTimeout(() => { setShowModal(false) }, 200) } + onBlur={ onBlur } onFocus={ () => setShowModal(true)} value={ query } autoFocus={ true } diff --git a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx index a5300dc44..a8760428b 100644 --- a/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx +++ b/frontend/app/components/shared/Filters/FilterItem/FilterItem.tsx @@ -14,6 +14,7 @@ interface Props { } function FilterItem(props: Props) { const { isFilter = false, filterIndex, filter } = props; + const canShowValues = !(filter.operator === "isAny" || filter.operator === "onAny"); const replaceFilter = (filter) => { props.onUpdate({ ...filter, value: [""]}); @@ -53,9 +54,7 @@ function FilterItem(props: Props) { className="mx-2 flex-shrink-0" value={filter.operator} /> - { !(filter.operator === "isAny" || filter.operator === "onAny") && ( - - )} + { canShowValues && () }
diff --git a/frontend/app/components/shared/Filters/FilterSource/FilterSource.tsx b/frontend/app/components/shared/Filters/FilterSource/FilterSource.tsx index 79e4d4aa1..180f0f2a4 100644 --- a/frontend/app/components/shared/Filters/FilterSource/FilterSource.tsx +++ b/frontend/app/components/shared/Filters/FilterSource/FilterSource.tsx @@ -1,5 +1,5 @@ import { FilterType } from 'App/types/filter/filterType'; -import React from 'react'; +import React, { useState, useEffect } from 'react'; import stl from './FilterSource.css'; interface Props { @@ -8,11 +8,22 @@ interface Props { } function FilterSource(props: Props) { const { filter } = props; + const [value, setValue] = useState(filter.source[0] || ''); const onChange = ({ target: { value, name } }) => { props.onUpdate({ ...filter, [name]: [value] }) } + useEffect(() => { + setValue(filter.source[0] || ''); + }, [filter]) + + useEffect(() => { + props.onUpdate({ ...filter, source: [value] }) + }, [value]) + + const write = ({ target: { value, name } }) => setValue(value) + const renderFiled = () => { switch(filter.sourceType) { case FilterType.NUMBER: @@ -20,9 +31,9 @@ function FilterSource(props: Props) { ) diff --git a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx index e5357d01a..4292a834b 100644 --- a/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx +++ b/frontend/app/components/shared/Filters/FilterValue/FilterValue.tsx @@ -11,6 +11,8 @@ interface Props { function FilterValue(props: Props) { const { filter } = props; const [durationValues, setDurationValues] = useState({ minDuration: filter.value[0], maxDuration: filter.value[1] }); + const showCloseButton = filter.value.length > 1; + const lastIndex = filter.value.length - 1; const onAddValue = () => { const newValues = filter.value.concat("") @@ -65,8 +67,7 @@ function FilterValue(props: Props) { } const renderValueFiled = (value, valueIndex) => { - const showCloseButton = filter.value.length > 1; - const showOrButton = valueIndex === filter.value.length - 1; + const showOrButton = valueIndex === lastIndex; switch(filter.type) { case FilterType.DROPDOWN: return ( diff --git a/frontend/app/constants/filterOptions.js b/frontend/app/constants/filterOptions.js index b88e9b808..b59f4744d 100644 --- a/frontend/app/constants/filterOptions.js +++ b/frontend/app/constants/filterOptions.js @@ -122,6 +122,10 @@ const targetFilterKeys = ['on', 'notOn', 'onAny']; const signUpStatusFilterKeys = ['isSignedUp', 'notSignedUp']; const rangeFilterKeys = ['before', 'after', 'on', 'inRange', 'notInRange', 'withInLast', 'notWithInLast']; +const getOperatorsByKeys = (keys) => { + return options.filter(option => keys.includes(option.key)); +}; + export const baseOperators = options.filter(({key}) => filterKeys.includes(key)); export const stringOperators = options.filter(({key}) => stringFilterKeys.includes(key)); export const targetOperators = options.filter(({key}) => targetFilterKeys.includes(key)); @@ -145,4 +149,5 @@ export default { targetOperators, booleanOperators, customOperators, + getOperatorsByKeys, } \ No newline at end of file diff --git a/frontend/app/duck/funnels.js b/frontend/app/duck/funnels.js index cad648839..a30916777 100644 --- a/frontend/app/duck/funnels.js +++ b/frontend/app/duck/funnels.js @@ -7,6 +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'; const name = 'funnel'; const idKey = 'funnelId'; @@ -265,16 +266,20 @@ export const fetchIssueTypes = () => { } } -export const save = (instance) => { +export const save = (instance) => (dispatch, getState) => { +// export const save = (instance) => { + const filter = getState().getIn([ 'search', 'instance']).toData(); + filter.filters = filter.filters.map(searchFilterMap); + const _instance = instance instanceof Funnel ? instance : Funnel(instance); const url = _instance.exists() ? `/funnels/${ _instance[idKey] }` : `/funnels`; - return { + return dispatch({ types: array(_instance.exists() ? SAVE : UPDATE), - call: client => client.post(url, _instance.toData()), - } + call: client => client.post(url, { ..._instance.toData(), filter }), + }); } export const updateFunnelFilters = (funnelId, filter) => { diff --git a/frontend/app/duck/search.js b/frontend/app/duck/search.js index 86cc19e6a..027a03b88 100644 --- a/frontend/app/duck/search.js +++ b/frontend/app/duck/search.js @@ -7,6 +7,7 @@ import SavedFilter from 'Types/filter/savedFilter'; import { errors as errorsRoute, isRoute } from "App/routes"; import { fetchList as fetchSessionList } from './sessions'; import { fetchList as fetchErrorsList } from './errors'; +import { FilterCategory, FilterKey } from '../types/filter/filterType'; const ERRORS_ROUTE = errorsRoute(); @@ -88,13 +89,12 @@ export default mergeReducers( }), ); -const filterMap = ({value, key, operator, sourceOperator, source, custom, isEvent }) => ({ +export const filterMap = ({category, value, key, operator, sourceOperator, source, custom, isEvent }) => ({ value: value.filter(i => i !== '' && i !== null), custom, - type: key, - // key, + type: category === FilterCategory.METADATA ? FilterKey.METADATA : key, operator, - source, + source: category === FilterCategory.METADATA ? key : source, sourceOperator, isEvent }); diff --git a/frontend/app/types/filter/filter.js b/frontend/app/types/filter/filter.js index 74991b53a..b8093d5cb 100644 --- a/frontend/app/types/filter/filter.js +++ b/frontend/app/types/filter/filter.js @@ -45,7 +45,7 @@ export default Record({ suspicious: undefined, consoleLevel: undefined, strict: false, - eventsOrder: 'and', + eventsOrder: 'then', }, { idKey: 'searchId', methods: { diff --git a/frontend/app/types/filter/newFilter.js b/frontend/app/types/filter/newFilter.js index 5c9b5a942..74b739918 100644 --- a/frontend/app/types/filter/newFilter.js +++ b/frontend/app/types/filter/newFilter.js @@ -40,8 +40,8 @@ export const filtersMap = { [FilterKey.PLATFORM]: { key: FilterKey.PLATFORM, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.GEAR, label: 'Platform', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/platform', options: platformOptions }, [FilterKey.REVID]: { key: FilterKey.REVID, type: FilterType.MULTIPLE, category: FilterCategory.GEAR, label: 'RevId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/rev-id' }, [FilterKey.REFERRER]: { key: FilterKey.REFERRER, type: FilterType.MULTIPLE, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Referrer', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/referrer' }, - [FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.baseOperators, icon: 'filters/duration' }, - [FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'User Country', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/country', options: countryOptions }, + [FilterKey.DURATION]: { key: FilterKey.DURATION, type: FilterType.DURATION, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'Duration', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is']), icon: 'filters/duration' }, + [FilterKey.USER_COUNTRY]: { key: FilterKey.USER_COUNTRY, type: FilterType.MULTIPLE_DROPDOWN, category: FilterCategory.RECORDING_ATTRIBUTES, label: 'User Country', operator: 'is', operatorOptions: filterOptions.getOperatorsByKeys(['is', 'isAny', 'isNot']), icon: 'filters/country', options: countryOptions }, // [FilterKey.CONSOLE]: { key: FilterKey.CONSOLE, type: FilterType.MULTIPLE, category: FilterCategory.JAVASCRIPT, label: 'Console', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/console' }, [FilterKey.USERID]: { key: FilterKey.USERID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User Id', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' }, [FilterKey.USERANONYMOUSID]: { key: FilterKey.USERANONYMOUSID, type: FilterType.MULTIPLE, category: FilterCategory.USER, label: 'User AnonymousId', operator: 'is', operatorOptions: filterOptions.stringOperators, icon: 'filters/userid' },