Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
- Dashboard shows comparisons for all reports
- UTM Medium report and API shows (gclid) and (msclkid) for paid searches when no explicit utm medium present.
- Support for `case_sensitive: false` modifiers in Stats API V2 filters for case-insensitive searches.
- Add filter `is not` for goals in dashboard plausible/analytics#4983

### Removed

Expand Down
7 changes: 6 additions & 1 deletion assets/js/dashboard/components/filter-operator-selector.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
FILTER_OPERATIONS,
FILTER_OPERATIONS_DISPLAY_NAMES,
supportsContains,
supportsIsNot
supportsIsNot,
supportsHasDoneNot
} from '../util/filters'
import { Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
Expand Down Expand Up @@ -75,6 +76,10 @@ export default function FilterOperatorSelector(props) {
FILTER_OPERATIONS.isNot,
supportsIsNot(filterName)
)}
{renderTypeItem(
FILTER_OPERATIONS.has_not_done,
supportsHasDoneNot(filterName)
)}
{renderTypeItem(
FILTER_OPERATIONS.contains,
supportsContains(filterName)
Expand Down
7 changes: 3 additions & 4 deletions assets/js/dashboard/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import {
cleanLabels,
FILTER_MODAL_TO_FILTER_GROUP,
formatFilterGroup,
EVENT_PROPS_PREFIX,
plainFilterText,
styledFilterText
} from "./util/filters";
EVENT_PROPS_PREFIX
} from "./util/filters"
import { plainFilterText, styledFilterText } from "./util/filter-text"

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

Expand Down
5 changes: 2 additions & 3 deletions assets/js/dashboard/nav-menu/filter-pills-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ import { FilterPill } from './filter-pill'
import {
cleanLabels,
EVENT_PROPS_PREFIX,
FILTER_GROUP_TO_MODAL_TYPE,
plainFilterText,
styledFilterText
FILTER_GROUP_TO_MODAL_TYPE
} from '../util/filters'
import { styledFilterText, plainFilterText } from '../util/filter-text'
import { useAppNavigate } from '../navigation/use-app-navigate'
import classNames from 'classnames'

Expand Down
10 changes: 5 additions & 5 deletions assets/js/dashboard/nav-menu/filters-bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ test('user can see expected filters and clear them one by one or all together',
)

expect(queryFilterPills().map((m) => m.textContent)).toEqual([
'Country is Germany ',
'Goal is Subscribed to Newsletter ',
'Page is /docs or /blog '
'Country is Germany',
'Goal is Subscribed to Newsletter',
'Page is /docs or /blog'
])

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

expect(queryFilterPills().map((m) => m.textContent)).toEqual([
'Goal is Subscribed to Newsletter ',
'Page is /docs or /blog '
'Goal is Subscribed to Newsletter',
'Page is /docs or /blog'
])

await userEvent.click(
Expand Down
5 changes: 4 additions & 1 deletion assets/js/dashboard/stats/modals/filter-modal-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function FilterModalGroup({
[filterGroup, rows]
)

const showAddRow = filterGroup == 'props'
const showAddRow = ['props', 'goal'].includes(filterGroup)
const showTitle = filterGroup != 'props'

return (
Expand All @@ -42,7 +42,10 @@ export default function FilterModalGroup({
key={id}
filter={filter}
labels={labels}
canDelete={showAddRow}
showDelete={rows.length > 1}
onUpdate={(newFilter, labelUpdate) => onUpdateRowValue(id, newFilter, labelUpdate)}
onDelete={() => onDeleteRow(id)}
/>
)
)}
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/stats/modals/filter-modal-props-row.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export default function FilterModalPropsRow({
/>
</div>
{showDelete && (
<div className="col-span-1 flex flex-col justify-center">
<div className="col-span-1 flex flex-col mt-2">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
className="ml-2 text-red-600 h-5 w-5 cursor-pointer"
Expand Down
29 changes: 27 additions & 2 deletions assets/js/dashboard/stats/modals/filter-modal-row.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/** @format */

import React, { useMemo } from 'react'
import { TrashIcon } from '@heroicons/react/20/solid'
import classNames from 'classnames'

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

export default function FilterModalRow({ filter, labels, onUpdate }) {
export default function FilterModalRow({
filter,
labels,
canDelete,
showDelete,
onUpdate,
onDelete
}) {
const { query } = useQueryContext()
const site = useSiteContext()
const [operation, filterKey, clauses] = filter
Expand Down Expand Up @@ -64,7 +73,12 @@ export default function FilterModalRow({ filter, labels, onUpdate }) {
}

return (
<div className="grid grid-cols-11 mt-1">
<div
className={classNames('grid mt-1', {
'grid-cols-12': canDelete,
'grid-cols-11': !canDelete
})}
>
<div className="col-span-3">
<FilterOperatorSelector
forFilter={filterKey}
Expand All @@ -83,6 +97,17 @@ export default function FilterModalRow({ filter, labels, onUpdate }) {
placeholder={`Select ${withIndefiniteArticle(formattedFilters[filterKey])}`}
/>
</div>
{showDelete && (
<div className="col-span-1 flex flex-col mt-2">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
className="ml-2 text-red-600 h-5 w-5 cursor-pointer"
onClick={onDelete}
>
<TrashIcon />
</a>
</div>
)}
</div>
)
}
Expand Down
4 changes: 2 additions & 2 deletions assets/js/dashboard/stats/reports/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import {
cleanLabels,
replaceFilterByPrefix,
isRealTimeDashboard,
hasGoalFilter,
plainFilterText
hasGoalFilter
} from '../../util/filters'
import { plainFilterText } from '../../util/filter-text'
import { useQueryContext } from '../../query-context'

const MAX_ITEMS = 9
Expand Down
24 changes: 24 additions & 0 deletions assets/js/dashboard/util/filter-text.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react'
import { DashboardQuery, Filter, FilterClauseLabels } from '../query'
import { plainFilterText, styledFilterText } from './filter-text'
import { render, screen } from '@testing-library/react'

describe('styledFilterText() and plainFilterText()', () => {
it.each<[Filter, FilterClauseLabels, string]>([
[['is', 'page', ['/docs', '/blog']], {}, 'Page is /docs or /blog'],
[['is', 'country', ['US']], { US: 'United States' }, 'Country is United States'],
[['is', 'goal', ['Signup']], {}, 'Goal is Signup'],
[['is', 'props:browser_language', ['en-US']], {}, 'Property browser_language is en-US'],
[['has_not_done', 'goal', ['Signup', 'Login']], {}, 'Goal is not Signup or Login'],
])(
'when filter is %p and labels are %p, functions return %p',
(filter, labels, expectedPlainText) => {
const query = { labels } as unknown as DashboardQuery

expect(plainFilterText(query, filter)).toBe(expectedPlainText)

render(<p data-testid="filter-text">{styledFilterText(query, filter)}</p>)
expect(screen.getByTestId('filter-text')).toHaveTextContent(expectedPlainText)
}
)
})
77 changes: 77 additions & 0 deletions assets/js/dashboard/util/filter-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* @format */

import React, { ReactNode, isValidElement, Fragment } from 'react'
import { DashboardQuery, Filter } from '../query'
import {
EVENT_PROPS_PREFIX,
FILTER_OPERATIONS_DISPLAY_NAMES,
formattedFilters,
getLabel,
getPropertyKeyFromFilterKey
} from './filters'

export function styledFilterText(
query: DashboardQuery,
[operation, filterKey, clauses]: Filter
) {
if (filterKey.startsWith(EVENT_PROPS_PREFIX)) {
const propKey = getPropertyKeyFromFilterKey(filterKey)
return (
<>
Property <b>{propKey}</b> {FILTER_OPERATIONS_DISPLAY_NAMES[operation]}{' '}
{formatClauses(clauses)}
</>
)
}

const formattedFilter = (
formattedFilters as Record<string, string | undefined>
)[filterKey]
const clausesLabels = clauses.map((value) =>
getLabel(query.labels, filterKey, value)
)

if (!formattedFilter) {
throw new Error(`Unknown filter: ${filterKey}`)
}

return (
<>
{capitalize(formattedFilter)} {FILTER_OPERATIONS_DISPLAY_NAMES[operation]}{' '}
{formatClauses(clausesLabels)}
</>
)
}

export function plainFilterText(query: DashboardQuery, filter: Filter) {
return reactNodeToString(styledFilterText(query, filter))
}

function formatClauses(labels: Array<string | number>): ReactNode[] {
return labels.map((label, index) => (
<Fragment key={index}>
{index > 0 && ' or '}
<b>{label}</b>
</Fragment>
))
}

function capitalize(str: string): string {
return str[0].toUpperCase() + str.slice(1)
}

function reactNodeToString(reactNode: ReactNode): string {
let string = ''
if (typeof reactNode === 'string') {
string = reactNode
} else if (typeof reactNode === 'number') {
string = reactNode.toString()
} else if (reactNode instanceof Array) {
reactNode.forEach(function (child) {
string += reactNodeToString(child)
})
} else if (isValidElement(reactNode)) {
string += reactNodeToString(reactNode.props.children)
}
return string
}
Loading
Loading