Skip to content

Commit

Permalink
New filtering experience (#5189)
Browse files Browse the repository at this point in the history
* the new filtering experience

* revert bug
  • Loading branch information
mariusandra authored Jul 19, 2021
1 parent 44a7976 commit 5245ee6
Show file tree
Hide file tree
Showing 33 changed files with 1,410 additions and 897 deletions.
53 changes: 42 additions & 11 deletions frontend/src/lib/components/Popup/Popup.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import './Popup.scss'
import React, { ReactElement, useState } from 'react'
import React, { ReactElement, useContext, useEffect, useMemo, useState } from 'react'
import ReactDOM from 'react-dom'
import { usePopper } from 'react-popper'
import { useOutsideClickHandler } from 'lib/hooks/useOutsideClickHandler'
import { Placement } from '@popperjs/core'

interface PopupProps {
visible?: boolean
onClickOutside?: () => void
onClickOutside?: (event: Event) => void
children: React.ReactChild | ((props: { setRef?: (ref: HTMLElement) => void }) => JSX.Element)
overlay: React.ReactNode
placement?: Placement
fallbackPlacements?: Placement[]
}

// if we're inside a popup inside a popup, prevent the parent's onClickOutside from working
const PopupContext = React.createContext<number>(0)
const disabledPopups = new Map<number, number>()
let uniqueMemoizedIndex = 0

/** This is a custom popup control that uses `react-popper` to position DOM nodes */
export function Popup({
children,
overlay,
Expand All @@ -21,15 +28,32 @@ export function Popup({
placement = 'bottom-start',
fallbackPlacements = ['bottom-end', 'top-start', 'top-end'],
}: PopupProps): JSX.Element {
const popupId = useMemo(() => ++uniqueMemoizedIndex, [])

const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null)
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null)
const [arrowElement, setArrowElement] = useState<HTMLDivElement | null>(null)
useOutsideClickHandler([popperElement, referenceElement, arrowElement] as HTMLElement[], onClickOutside)

const parentPopupId = useContext(PopupContext)
const localRefs = [popperElement, referenceElement] as (HTMLElement | null)[]

useEffect(() => {
if (visible) {
disabledPopups.set(parentPopupId, (disabledPopups.get(parentPopupId) || 0) + 1)
return () => {
disabledPopups.set(parentPopupId, (disabledPopups.get(parentPopupId) || 0) - 1)
}
}
}, [visible, parentPopupId])

useOutsideClickHandler(localRefs, (event) => {
if (visible && !disabledPopups.get(popupId)) {
onClickOutside?.(event)
}
})

const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: placement,
modifiers: [
{ name: 'arrow', options: { element: arrowElement } },
fallbackPlacements
? {
name: 'flip',
Expand All @@ -55,12 +79,19 @@ export function Popup({
return (
<>
{clonedChildren}
{visible && (
<div className="popper-tooltip" ref={setPopperElement} style={styles.popper} {...attributes.popper}>
{overlay}
<div ref={setArrowElement} style={styles.arrow} />
</div>
)}
{visible
? ReactDOM.createPortal(
<div
className="popper-tooltip"
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
>
<PopupContext.Provider value={popupId}>{overlay}</PopupContext.Provider>
</div>,
document.querySelector('body') as HTMLElement
)
: null}
</>
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { SelectGradientOverflowProps } from 'lib/components/SelectGradientOverflow'
import { TabbedPropertyFilter } from './TabbedPropertyFilter'
import { TaxonomicPropertyFilter } from './TaxonomicPropertyFilter/TaxonomicPropertyFilter'
import { TaxonomicPropertyFilter } from './TaxonomicPropertyFilter'
import { UnifiedPropertyFilter } from './UnifiedPropertyFilter'

export interface PropertyFilterInternalProps {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
.taxonomic-property-filter {
width: 100%;

&.in-dropdown {
min-width: 300px;
width: 550px;
max-width: calc(100vw - 25px);
background: white;
}

.taxonomic-filter-row {
display: grid;
grid-column-gap: 0.5rem;
grid-row-gap: 0.25rem;
grid-template-columns: 70px minmax(180px, 1fr) minmax(100px, auto);

// only setting grid properties here, the rest are below
.taxonomic-where {
grid-column: 1;
grid-row: 1;
}

.taxonomic-button {
grid-column: 2;
grid-row: 1;
}

.taxonomic-operator {
grid-column: 3;
grid-row: 1;
}

.taxonomic-value-select {
grid-column: 2 / 4;
grid-row: 2;
}

// small screens
@media (max-width: 512px) {
grid-template-columns: 35px auto 70px;
.taxonomic-where {
.arrow {
display: none;
}
}
}

// bigger screens
@media (min-width: 1080px) {
grid-template-columns: 70px minmax(140px, 160px) minmax(100px, 120px) minmax(100px, 400px);
.taxonomic-where {
grid-column: 1;
grid-row: 1;
}
.taxonomic-button {
grid-column: 2;
grid-row: 1;
}
.taxonomic-operator {
grid-column: 3;
grid-row: 1;
}
.taxonomic-value-select {
grid-column: 4;
grid-row: 1;
}
}
// even more space for huge screens
@media (min-width: 1280px) {
grid-template-columns: 70px minmax(140px, 220px) minmax(80px, 160px) minmax(100px, 500px);
}
}

.taxonomic-where {
height: 32px; // matches antd Select height
display: flex;
align-items: center;
justify-content: flex-end;

.arrow {
color: #c4c4c4;
font-size: 18px;
font-weight: bold;
padding-left: 6px;
padding-right: 8px;
position: relative;
top: -4px;
user-select: none;
}
}

.taxonomic-button {
display: flex;
justify-content: space-between;
overflow: hidden;
.property-key-info {
width: auto;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
&.add-filter {
width: max-content;
}
}

.taxonomic-operator {
overflow: hidden;
}

.taxonomic-value-select {
overflow: hidden;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import './TaxonomicPropertyFilter.scss'
import React, { useMemo } from 'react'
import { Button, Col } from 'antd'
import { useActions, useValues } from 'kea'
import { propertyFilterLogic } from 'lib/components/PropertyFilters/propertyFilterLogic'
import { taxonomicPropertyFilterLogic } from './taxonomicPropertyFilterLogic'
import { SelectDownIcon } from 'lib/components/SelectDownIcon'
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
import { OperatorValueSelect } from 'lib/components/PropertyFilters/components/OperatorValueSelect'
import { isOperatorMulti, isOperatorRegex } from 'lib/utils'
import { Popup } from 'lib/components/Popup/Popup'
import { PropertyFilterInternalProps } from 'lib/components/PropertyFilters'
import { TaxonomicFilter } from 'lib/components/TaxonomicFilter/TaxonomicFilter'
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import {
propertyFilterTypeToTaxonomicFilterType,
taxonomicFilterTypeToPropertyFilterType,
} from 'lib/components/PropertyFilters/utils'

let uniqueMemoizedIndex = 0

export function TaxonomicPropertyFilter({
pageKey: pageKeyInput,
index,
onComplete,
disablePopover, // inside a dropdown if this is false
}: PropertyFilterInternalProps): JSX.Element {
const pageKey = useMemo(() => pageKeyInput || `filter-${uniqueMemoizedIndex++}`, [pageKeyInput])
const { setFilter } = useActions(propertyFilterLogic)

const logic = taxonomicPropertyFilterLogic({ pageKey, filterIndex: index })
const { filter, dropdownOpen, selectedCohortName } = useValues(logic)
const { openDropdown, closeDropdown, selectItem } = useActions(logic)

const showInitialSearchInline = !disablePopover && ((!filter?.type && !filter?.key) || filter?.type === 'cohort')
const showOperatorValueSelect = filter?.type && filter?.key && filter?.type !== 'cohort'

const taxonomicFilter = (
<TaxonomicFilter
groupType={propertyFilterTypeToTaxonomicFilterType(filter?.type)}
value={filter?.type === 'cohort' ? filter?.value : filter?.key}
onChange={(groupType, value) => {
selectItem(taxonomicFilterTypeToPropertyFilterType(groupType), value)
if (groupType === TaxonomicFilterGroupType.Cohorts) {
onComplete?.()
}
}}
groupTypes={[
TaxonomicFilterGroupType.EventProperties,
TaxonomicFilterGroupType.PersonProperties,
TaxonomicFilterGroupType.Cohorts,
TaxonomicFilterGroupType.Elements,
]}
/>
)

return (
<div className={`taxonomic-property-filter${!disablePopover ? ' in-dropdown' : ' row-on-page'}`}>
{showInitialSearchInline ? (
taxonomicFilter
) : (
<div className="taxonomic-filter-row">
<Col className="taxonomic-where">
{index === 0 ? (
<>
<span className="arrow">&#8627;</span>
<span className="text">where</span>
</>
) : (
<span className="stateful-badge and" style={{ fontSize: '90%' }}>
AND
</span>
)}
</Col>

<Popup
overlay={dropdownOpen ? taxonomicFilter : null}
placement={'bottom-start'}
fallbackPlacements={['bottom-end']}
visible={dropdownOpen}
onClickOutside={closeDropdown}
>
<Button
className={`taxonomic-button${!filter?.type && !filter?.key ? ' add-filter' : ''}`}
onClick={() => (dropdownOpen ? closeDropdown() : openDropdown())}
>
{filter?.type === 'cohort' ? (
<div>{selectedCohortName || `Cohort #${filter?.value}`}</div>
) : filter?.key ? (
<PropertyKeyInfo value={filter.key} disablePopover />
) : (
<div>Add filter</div>
)}
<SelectDownIcon />
</Button>
</Popup>

{showOperatorValueSelect && (
<OperatorValueSelect
type={filter?.type}
propkey={filter?.key}
operator={filter?.operator}
value={filter?.value}
placeholder="Enter value..."
onChange={(newOperator, newValue) => {
if (filter?.key && filter?.type) {
setFilter(index, filter?.key, newValue || null, newOperator, filter?.type)
}
if (
newOperator &&
newValue &&
!isOperatorMulti(newOperator) &&
!isOperatorRegex(newOperator)
) {
onComplete()
}
}}
columnOptions={[
{
className: 'taxonomic-operator',
},
{
className: 'taxonomic-value-select',
},
]}
/>
)}
</div>
)}
</div>
)
}
Loading

0 comments on commit 5245ee6

Please sign in to comment.