Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New filtering experience #5189

Merged
merged 3 commits into from
Jul 19, 2021
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
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