Skip to content

Commit c0a762a

Browse files
authored
Excluding goals in dashboard (#4983)
* Simple frontend for has_done_not * Simple UI for goal filter adding or removal * Better alignment on trash icons, avoid moving around if row expands * Refactor filter text functions, share code * has_not_done, special casing for has not done when formatting filter text * Changelog * Fix lint * prettier format * Add tests * Lowercase Goal * Update changelog * has_not_done for goals is now named `is not` in the UI * prettier * Document and test serializeApiFilters
1 parent 117cf46 commit c0a762a

File tree

14 files changed

+211
-79
lines changed

14 files changed

+211
-79
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
77
- Dashboard shows comparisons for all reports
88
- UTM Medium report and API shows (gclid) and (msclkid) for paid searches when no explicit utm medium present.
99
- Support for `case_sensitive: false` modifiers in Stats API V2 filters for case-insensitive searches.
10+
- Add filter `is not` for goals in dashboard plausible/analytics#4983
1011

1112
### Removed
1213

assets/js/dashboard/components/filter-operator-selector.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
FILTER_OPERATIONS,
77
FILTER_OPERATIONS_DISPLAY_NAMES,
88
supportsContains,
9-
supportsIsNot
9+
supportsIsNot,
10+
supportsHasDoneNot
1011
} from '../util/filters'
1112
import { Menu, Transition } from '@headlessui/react'
1213
import { ChevronDownIcon } from '@heroicons/react/20/solid'
@@ -75,6 +76,10 @@ export default function FilterOperatorSelector(props) {
7576
FILTER_OPERATIONS.isNot,
7677
supportsIsNot(filterName)
7778
)}
79+
{renderTypeItem(
80+
FILTER_OPERATIONS.has_not_done,
81+
supportsHasDoneNot(filterName)
82+
)}
7883
{renderTypeItem(
7984
FILTER_OPERATIONS.contains,
8085
supportsContains(filterName)

assets/js/dashboard/filters.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ import {
1212
cleanLabels,
1313
FILTER_MODAL_TO_FILTER_GROUP,
1414
formatFilterGroup,
15-
EVENT_PROPS_PREFIX,
16-
plainFilterText,
17-
styledFilterText
18-
} from "./util/filters";
15+
EVENT_PROPS_PREFIX
16+
} from "./util/filters"
17+
import { plainFilterText, styledFilterText } from "./util/filter-text"
1918

2019
const WRAPSTATE = { unwrapped: 0, waiting: 1, wrapped: 2 }
2120

assets/js/dashboard/nav-menu/filter-pills-list.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,9 @@ import { FilterPill } from './filter-pill'
66
import {
77
cleanLabels,
88
EVENT_PROPS_PREFIX,
9-
FILTER_GROUP_TO_MODAL_TYPE,
10-
plainFilterText,
11-
styledFilterText
9+
FILTER_GROUP_TO_MODAL_TYPE
1210
} from '../util/filters'
11+
import { styledFilterText, plainFilterText } from '../util/filter-text'
1312
import { useAppNavigate } from '../navigation/use-app-navigate'
1413
import classNames from 'classnames'
1514

assets/js/dashboard/nav-menu/filters-bar.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ test('user can see expected filters and clear them one by one or all together',
6161
)
6262

6363
expect(queryFilterPills().map((m) => m.textContent)).toEqual([
64-
'Country is Germany ',
65-
'Goal is Subscribed to Newsletter ',
66-
'Page is /docs or /blog '
64+
'Country is Germany',
65+
'Goal is Subscribed to Newsletter',
66+
'Page is /docs or /blog'
6767
])
6868

6969
await userEvent.click(
@@ -74,8 +74,8 @@ test('user can see expected filters and clear them one by one or all together',
7474
)
7575

7676
expect(queryFilterPills().map((m) => m.textContent)).toEqual([
77-
'Goal is Subscribed to Newsletter ',
78-
'Page is /docs or /blog '
77+
'Goal is Subscribed to Newsletter',
78+
'Page is /docs or /blog'
7979
])
8080

8181
await userEvent.click(

assets/js/dashboard/stats/modals/filter-modal-group.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function FilterModalGroup({
2020
[filterGroup, rows]
2121
)
2222

23-
const showAddRow = filterGroup == 'props'
23+
const showAddRow = ['props', 'goal'].includes(filterGroup)
2424
const showTitle = filterGroup != 'props'
2525

2626
return (
@@ -42,7 +42,10 @@ export default function FilterModalGroup({
4242
key={id}
4343
filter={filter}
4444
labels={labels}
45+
canDelete={showAddRow}
46+
showDelete={rows.length > 1}
4547
onUpdate={(newFilter, labelUpdate) => onUpdateRowValue(id, newFilter, labelUpdate)}
48+
onDelete={() => onDeleteRow(id)}
4649
/>
4750
)
4851
)}

assets/js/dashboard/stats/modals/filter-modal-props-row.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export default function FilterModalPropsRow({
112112
/>
113113
</div>
114114
{showDelete && (
115-
<div className="col-span-1 flex flex-col justify-center">
115+
<div className="col-span-1 flex flex-col mt-2">
116116
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
117117
<a
118118
className="ml-2 text-red-600 h-5 w-5 cursor-pointer"

assets/js/dashboard/stats/modals/filter-modal-row.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/** @format */
22

33
import React, { useMemo } from 'react'
4+
import { TrashIcon } from '@heroicons/react/20/solid'
5+
import classNames from 'classnames'
46

57
import FilterOperatorSelector from '../../components/filter-operator-selector'
68
import Combobox from '../../components/combobox'
@@ -16,7 +18,14 @@ import { apiPath } from '../../util/url'
1618
import { useQueryContext } from '../../query-context'
1719
import { useSiteContext } from '../../site-context'
1820

19-
export default function FilterModalRow({ filter, labels, onUpdate }) {
21+
export default function FilterModalRow({
22+
filter,
23+
labels,
24+
canDelete,
25+
showDelete,
26+
onUpdate,
27+
onDelete
28+
}) {
2029
const { query } = useQueryContext()
2130
const site = useSiteContext()
2231
const [operation, filterKey, clauses] = filter
@@ -64,7 +73,12 @@ export default function FilterModalRow({ filter, labels, onUpdate }) {
6473
}
6574

6675
return (
67-
<div className="grid grid-cols-11 mt-1">
76+
<div
77+
className={classNames('grid mt-1', {
78+
'grid-cols-12': canDelete,
79+
'grid-cols-11': !canDelete
80+
})}
81+
>
6882
<div className="col-span-3">
6983
<FilterOperatorSelector
7084
forFilter={filterKey}
@@ -83,6 +97,17 @@ export default function FilterModalRow({ filter, labels, onUpdate }) {
8397
placeholder={`Select ${withIndefiniteArticle(formattedFilters[filterKey])}`}
8498
/>
8599
</div>
100+
{showDelete && (
101+
<div className="col-span-1 flex flex-col mt-2">
102+
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
103+
<a
104+
className="ml-2 text-red-600 h-5 w-5 cursor-pointer"
105+
onClick={onDelete}
106+
>
107+
<TrashIcon />
108+
</a>
109+
</div>
110+
)}
86111
</div>
87112
)
88113
}

assets/js/dashboard/stats/reports/list.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ import {
1414
cleanLabels,
1515
replaceFilterByPrefix,
1616
isRealTimeDashboard,
17-
hasGoalFilter,
18-
plainFilterText
17+
hasGoalFilter
1918
} from '../../util/filters'
19+
import { plainFilterText } from '../../util/filter-text'
2020
import { useQueryContext } from '../../query-context'
2121

2222
const MAX_ITEMS = 9
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import React from 'react'
2+
import { DashboardQuery, Filter, FilterClauseLabels } from '../query'
3+
import { plainFilterText, styledFilterText } from './filter-text'
4+
import { render, screen } from '@testing-library/react'
5+
6+
describe('styledFilterText() and plainFilterText()', () => {
7+
it.each<[Filter, FilterClauseLabels, string]>([
8+
[['is', 'page', ['/docs', '/blog']], {}, 'Page is /docs or /blog'],
9+
[['is', 'country', ['US']], { US: 'United States' }, 'Country is United States'],
10+
[['is', 'goal', ['Signup']], {}, 'Goal is Signup'],
11+
[['is', 'props:browser_language', ['en-US']], {}, 'Property browser_language is en-US'],
12+
[['has_not_done', 'goal', ['Signup', 'Login']], {}, 'Goal is not Signup or Login'],
13+
])(
14+
'when filter is %p and labels are %p, functions return %p',
15+
(filter, labels, expectedPlainText) => {
16+
const query = { labels } as unknown as DashboardQuery
17+
18+
expect(plainFilterText(query, filter)).toBe(expectedPlainText)
19+
20+
render(<p data-testid="filter-text">{styledFilterText(query, filter)}</p>)
21+
expect(screen.getByTestId('filter-text')).toHaveTextContent(expectedPlainText)
22+
}
23+
)
24+
})

0 commit comments

Comments
 (0)