diff --git a/frontend/__snapshots__/filters-action-filter--bordered.png b/frontend/__snapshots__/filters-action-filter--bordered.png index 6cf0fe7ba0cfa..8c563c33a451b 100644 Binary files a/frontend/__snapshots__/filters-action-filter--bordered.png and b/frontend/__snapshots__/filters-action-filter--bordered.png differ diff --git a/frontend/__snapshots__/filters-action-filter--funnel-like.png b/frontend/__snapshots__/filters-action-filter--funnel-like.png index b5275dc45677d..7dc3c323eea7b 100644 Binary files a/frontend/__snapshots__/filters-action-filter--funnel-like.png and b/frontend/__snapshots__/filters-action-filter--funnel-like.png differ diff --git a/frontend/__snapshots__/filters-action-filter--property-filters-with-popover.png b/frontend/__snapshots__/filters-action-filter--property-filters-with-popover.png index ff318774f46a3..5b009d401dda5 100644 Binary files a/frontend/__snapshots__/filters-action-filter--property-filters-with-popover.png and b/frontend/__snapshots__/filters-action-filter--property-filters-with-popover.png differ diff --git a/frontend/__snapshots__/filters-action-filter--single-filter.png b/frontend/__snapshots__/filters-action-filter--single-filter.png index a4fbbaf443ef2..e59fed9c418b9 100644 Binary files a/frontend/__snapshots__/filters-action-filter--single-filter.png and b/frontend/__snapshots__/filters-action-filter--single-filter.png differ diff --git a/frontend/__snapshots__/filters-action-filter--sortable.png b/frontend/__snapshots__/filters-action-filter--sortable.png index ff318774f46a3..5b009d401dda5 100644 Binary files a/frontend/__snapshots__/filters-action-filter--sortable.png and b/frontend/__snapshots__/filters-action-filter--sortable.png differ diff --git a/frontend/__snapshots__/filters-action-filter--standard.png b/frontend/__snapshots__/filters-action-filter--standard.png index ff318774f46a3..5b009d401dda5 100644 Binary files a/frontend/__snapshots__/filters-action-filter--standard.png and b/frontend/__snapshots__/filters-action-filter--standard.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png index 21e554f56ab8e..b1e294f7d5d07 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png index 9574d6e03d310..1b7538c076975 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-historical-trends-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png index abf96cf1c2c1d..bc79f804ca560 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png index ca136afb5c010..1f0c46fe5c31e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png index cc7a5e5bc6233..118d11b614a3f 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png index afe5f32e5f79b..a70e35a6c7f2c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-left-to-right-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png index e4de49978fb59..20983c9ce3d2c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png index ade798e8b8585..945122d0b64fa 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-time-to-convert-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png index eea650b28efaf..1d63db0eb5a55 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png index 6414315f95b96..e3c5c6ec54b5e 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-breakdown-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png index 12e046675a97c..ad880846e725c 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit--webkit.png differ diff --git a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png index 4a767b6971349..18412c611e842 100644 Binary files a/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png and b/frontend/__snapshots__/scenes-app-insights--funnel-top-to-bottom-edit.png differ diff --git a/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-no-pinned-recordings.png b/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-no-pinned-recordings.png index a3d4147b50f67..55b81bc98fcce 100644 Binary files a/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-no-pinned-recordings.png and b/frontend/__snapshots__/scenes-app-recordings--recordings-play-list-no-pinned-recordings.png differ diff --git a/frontend/src/lib/sortable.ts b/frontend/src/lib/sortable.ts new file mode 100644 index 0000000000000..3b62cb4ea7db9 --- /dev/null +++ b/frontend/src/lib/sortable.ts @@ -0,0 +1,71 @@ +// Adapted from https://github.com/clauderic/dnd-kit/pull/805 to fix an issue where variable +// height items in a sortable container were not always firing collisions correctly. +// Should be possible to remove this custom collision detection algorithm once a proper fix +// is merged into dnd-kit. + +import { CollisionDetection, DroppableContainer, UniqueIdentifier } from '@dnd-kit/core' + +export const verticalSortableListCollisionDetection: CollisionDetection = (args) => { + if (args.collisionRect.top < (args.active.rect.current?.initial?.top ?? 0)) { + return highestDroppableContainerMajorityCovered(args) + } else { + return lowestDroppableContainerMajorityCovered(args) + } +} + +// Look for the first (/ furthest up / highest) droppable container that is at least +// 50% covered by the top edge of the dragging container. +const highestDroppableContainerMajorityCovered: CollisionDetection = ({ droppableContainers, collisionRect }) => { + const ascendingDroppabaleContainers = droppableContainers.sort(sortByRectTop) + + for (const droppableContainer of ascendingDroppabaleContainers) { + const { + rect: { current: droppableRect }, + } = droppableContainer + + if (droppableRect) { + const coveredPercentage = + (droppableRect.top + droppableRect.height - collisionRect.top) / droppableRect.height + + if (coveredPercentage > 0.5) { + return [collision(droppableContainer)] + } + } + } + + // if we haven't found anything then we are off the top, so return the first item + return [collision(ascendingDroppabaleContainers[0])] +} + +// Look for the last (/ furthest down / lowest) droppable container that is at least +// 50% covered by the bottom edge of the dragging container. +const lowestDroppableContainerMajorityCovered: CollisionDetection = ({ droppableContainers, collisionRect }) => { + const descendingDroppabaleContainers = droppableContainers.sort(sortByRectTop).reverse() + + for (const droppableContainer of descendingDroppabaleContainers) { + const { + rect: { current: droppableRect }, + } = droppableContainer + + if (droppableRect) { + const coveredPercentage = (collisionRect.bottom - droppableRect.top) / droppableRect.height + + if (coveredPercentage > 0.5) { + return [collision(droppableContainer)] + } + } + } + + // if we haven't found anything then we are off the bottom, so return the last item + return [collision(descendingDroppabaleContainers[0])] +} + +const sortByRectTop = (a: DroppableContainer, b: DroppableContainer): number => + (a?.rect.current?.top || 0) - (b?.rect.current?.top || 0) + +const collision = (dropppableContainer?: DroppableContainer): { id: UniqueIdentifier; value?: DroppableContainer } => { + return { + id: dropppableContainer?.id ?? '', + value: dropppableContainer, + } +} diff --git a/frontend/src/scenes/experiments/SecondaryMetrics.tsx b/frontend/src/scenes/experiments/SecondaryMetrics.tsx index bed6b3a3f3336..75d22a10d19ec 100644 --- a/frontend/src/scenes/experiments/SecondaryMetrics.tsx +++ b/frontend/src/scenes/experiments/SecondaryMetrics.tsx @@ -196,7 +196,7 @@ export function SecondaryMetrics({ buttonCopy="Add funnel step" seriesIndicatorType="numeric" sortable - showNestedArrow={true} + showNestedArrow propertiesTaxonomicGroupTypes={[ TaxonomicFilterGroupType.EventProperties, TaxonomicFilterGroupType.PersonProperties, @@ -204,7 +204,7 @@ export function SecondaryMetrics({ TaxonomicFilterGroupType.Cohorts, TaxonomicFilterGroupType.Elements, ]} - readOnly={true} + readOnly /> )} {metric.filters.insight === InsightType.TRENDS && ( diff --git a/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx b/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx index fe7d61cc7a320..5a12d9e52bbbf 100644 --- a/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx +++ b/frontend/src/scenes/insights/EditorFilters/FunnelsQuerySteps.tsx @@ -58,7 +58,7 @@ export function FunnelsQuerySteps({ insightProps }: EditorFilterProps): JSX.Elem seriesIndicatorType="numeric" entitiesLimit={FUNNEL_STEP_COUNT_LIMIT} sortable - showNestedArrow={true} + showNestedArrow propertiesTaxonomicGroupTypes={[ TaxonomicFilterGroupType.EventProperties, TaxonomicFilterGroupType.PersonProperties, diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx index 3277b592153df..ea099b6ec9764 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilter.tsx @@ -10,7 +10,6 @@ import { InsightType, Optional, } from '~/types' -import { SortableActionFilterContainer, SortableActionFilterRow } from './ActionFilterRow/SortableActionFilterRow' import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types' import { RenameModal } from 'scenes/insights/filters/ActionFilter/RenameModal' import { eventUsageLogic } from 'lib/utils/eventUsageLogic' @@ -18,6 +17,10 @@ import { teamLogic } from '../../../teamLogic' import clsx from 'clsx' import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' import { IconPlusMini } from 'lib/lemon-ui/icons' +import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' +import { DndContext } from '@dnd-kit/core' +import { restrictToParentElement, restrictToVerticalAxis } from '@dnd-kit/modifiers' +import { verticalSortableListCollisionDetection } from 'lib/sortable' export interface ActionFilterProps { setFilters: (filters: FilterType) => void @@ -159,6 +162,7 @@ export const ActionFilter = React.forwardRef( } const reachedLimit: boolean = Boolean(entitiesLimit && localFilters.length >= entitiesLimit) + const sortedItemIds = localFilters.map((i) => i.uuid) return (
( )} {localFilters ? ( - sortable ? ( - - {localFilters.map((filter, index) => ( - - ))} - - ) : ( - localFilters.map((filter, index) => ( - - )) - ) +
    + { + if (over && active.id !== over.id) { + onSortEnd({ + oldIndex: sortedItemIds.indexOf(active.id.toString()), + newIndex: sortedItemIds.indexOf(over.id.toString()), + }) + } + }} + modifiers={[restrictToVerticalAxis, restrictToParentElement]} + collisionDetection={verticalSortableListCollisionDetection} + > + + {localFilters.map((filter, index) => ( + + ))} + + +
) : null} {!singleFilter && (
diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.scss b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.scss index 854c50ea3469f..cd25fb8ed77f1 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.scss +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.scss @@ -1,4 +1,6 @@ .ActionFilterRow { + background: var(--bg-3000); + .ActionFilterRow-content { display: flex; align-items: flex-start; diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx index 66b0380403cd1..90e01a83d6df5 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx +++ b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/ActionFilterRow.tsx @@ -30,7 +30,6 @@ import { actionsModel } from '~/models/actionsModel' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { TaxonomicPopover, TaxonomicStringPopover } from 'lib/components/TaxonomicPopover/TaxonomicPopover' import { IconCopy, IconDelete, IconEdit, IconFilter, IconWithCount } from 'lib/lemon-ui/icons' -import { SortableHandle as sortableHandle } from 'react-sortable-hoc' import { SortableDragIcon } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonSelect, LemonSelectOption, LemonSelectOptions } from '@posthog/lemon-ui' @@ -40,12 +39,16 @@ import { LemonDropdown } from 'lib/lemon-ui/LemonDropdown' import { HogQLEditor } from 'lib/components/HogQLEditor/HogQLEditor' import { entityFilterLogicType } from '../entityFilterLogicType' import { isAllEventsEntityFilter } from 'scenes/insights/utils' +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { LocalFilter } from '../entityFilterLogic' +import { DraggableSyntheticListeners } from '@dnd-kit/core' -const DragHandle = sortableHandle(() => ( - +const DragHandle = (props: DraggableSyntheticListeners | undefined): JSX.Element => ( + -)) +) export enum MathAvailability { All, @@ -68,7 +71,7 @@ const getValue = ( export interface ActionFilterRowProps { logic: BuiltLogic - filter: ActionFilter + filter: LocalFilter index: number typeKey: string mathAvailability: MathAvailability @@ -149,6 +152,8 @@ export function ActionFilterRow({ const [isHogQLDropdownVisible, setIsHogQLDropdownVisible] = useState(false) + const { setNodeRef, attributes, transform, transition, listeners, isDragging } = useSortable({ id: filter.uuid }) + const propertyFiltersVisible = typeof filter.order === 'number' ? entityFilterVisible[filter.order] : false let name: string | null | undefined, value: PropertyFilterValue @@ -302,7 +307,7 @@ export function ActionFilterRow({ ) const rowStartElements = [ - sortable && filterCount > 1 ? : null, + sortable && filterCount > 1 ? : null, showSeriesIndicator &&
{seriesIndicator}
, ].filter(Boolean) @@ -316,7 +321,17 @@ export function ActionFilterRow({ : [] return ( -
+
  • {renderRow ? ( renderRow({ @@ -461,7 +476,7 @@ export function ActionFilterRow({ />
    )} -
  • + ) } diff --git a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/SortableActionFilterRow.tsx b/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/SortableActionFilterRow.tsx deleted file mode 100644 index 019b79473b6e0..0000000000000 --- a/frontend/src/scenes/insights/filters/ActionFilter/ActionFilterRow/SortableActionFilterRow.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { SortableContainer as sortableContainer, SortableElement as sortableElement } from 'react-sortable-hoc' -import { ActionFilterRow, ActionFilterRowProps } from './ActionFilterRow' - -interface SortableActionFilterRowProps extends ActionFilterRowProps { - filterIndex: number // sortable requires, yet eats, the index prop -} - -export const SortableActionFilterRow = sortableElement( - ({ filterCount, filterIndex, ...props }: SortableActionFilterRowProps) => { - return - } -) - -export const SortableActionFilterContainer = sortableContainer(({ children }: { children: React.ReactNode }) => { - return
    {children}
    -}) diff --git a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.test.ts b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.test.ts index 5e5fb2560fa66..dfeb640ded0a8 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.test.ts +++ b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.test.ts @@ -5,11 +5,13 @@ import filtersJson from './__mocks__/filters.json' import eventDefinitionsJson from './__mocks__/event_definitions.json' import { FilterType } from '~/types' import { useMocks } from '~/mocks/jest' +import * as libUtils from 'lib/utils' describe('entityFilterLogic', () => { let logic: ReturnType beforeEach(() => { + ;(libUtils as any).uuid = jest.fn().mockReturnValue('generated-uuid') useMocks({ get: { '/api/projects/:team/actions/': { diff --git a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts index 2a59fecc356d8..9c59d413c9a21 100644 --- a/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts +++ b/frontend/src/scenes/insights/filters/ActionFilter/entityFilterLogic.ts @@ -2,10 +2,11 @@ import { kea, props, key, path, connect, actions, reducers, selectors, listeners import { EntityTypes, FilterType, Entity, EntityType, ActionFilter, EntityFilter, AnyPropertyFilter } from '~/types' import type { entityFilterLogicType } from './entityFilterLogicType' import { eventUsageLogic, GraphSeriesAddedSource } from 'lib/utils/eventUsageLogic' -import { convertPropertyGroupToProperties } from 'lib/utils' +import { convertPropertyGroupToProperties, uuid } from 'lib/utils' export type LocalFilter = ActionFilter & { order: number + uuid: string } export type BareEntity = Pick @@ -17,9 +18,10 @@ export function toLocalFilters(filters: Partial): LocalFilter[] { filter.properties && Array.isArray(filter.properties) ? { ...filter, + uuid: uuid(), properties: convertPropertyGroupToProperties(filter.properties), } - : filter + : { ...filter, uuid: uuid() } ) } @@ -197,6 +199,7 @@ export const entityFilterLogic = kea([ const order = precedingEntity ? precedingEntity.order + 1 : 0 const newFilter = { id: null, + uuid: uuid(), type: EntityTypes.EVENTS, order: order, ...props.addFilterDefaultOptions, @@ -218,6 +221,7 @@ export const entityFilterLogic = kea([ } newFilters.splice(order, 0, { ...filter, + uuid: uuid(), custom_name: undefined, order: order + 1, } as LocalFilter) diff --git a/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Shown-Mobile-1-chromium-linux.png b/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Shown-Mobile-1-chromium-linux.png index 07f625a3d6d99..24af8b2279910 100644 Binary files a/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Shown-Mobile-1-chromium-linux.png and b/playwright/e2e-vrt/layout/Navigation.spec.ts-snapshots/Navigation-App-Page-With-Side-Bar-Shown-Mobile-1-chromium-linux.png differ