From cf20c434aee1994e9781c87ce8eb7c69731698ec Mon Sep 17 00:00:00 2001 From: Robbie Coomber Date: Tue, 19 Nov 2024 11:56:31 +0000 Subject: [PATCH] WIP --- .../VerticalNestedDND.stories.tsx | 62 ++ .../VerticalNestedDND/VerticalNestedDND.tsx | 680 ++++++++++++++++++ .../lemon-ui/VerticalNestedDND/styles.scss | 21 + .../settings/environment/ChannelType.tsx | 658 +++++++++++++++-- package.json | 1 + pnpm-lock.yaml | 13 +- posthog/hogql/modifiers.py | 14 +- 7 files changed, 1395 insertions(+), 54 deletions(-) create mode 100644 frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.stories.tsx create mode 100644 frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx create mode 100644 frontend/src/lib/lemon-ui/VerticalNestedDND/styles.scss diff --git a/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.stories.tsx b/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.stories.tsx new file mode 100644 index 00000000000..747b2e47d3d --- /dev/null +++ b/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.stories.tsx @@ -0,0 +1,62 @@ +import { UniqueIdentifier } from '@dnd-kit/core' +import { Meta, StoryFn, StoryObj } from '@storybook/react' + +import { VerticalNestedDND, VerticalNestedDNDProps } from './VerticalNestedDND' + +type Story = StoryObj +const meta: Meta = { + title: 'Lemon UI/VerticalNestedDND', + component: VerticalNestedDND, + parameters: { + testOptions: { + waitForLoadersToDisappear: false, + }, + }, + tags: ['autodocs'], +} +export default meta + +interface ExampleSubItem { + id: UniqueIdentifier +} +interface ExampleItem { + id: UniqueIdentifier + items: ExampleSubItem[] +} + +const Template: StoryFn = (props: VerticalNestedDNDProps) => { + const starterData: ExampleItem[] = [ + { + id: 'A', + items: [ + { + id: 'A1', + }, + { + id: 'A2', + }, + { + id: 'A3', + }, + ], + }, + { + id: 'B', + items: [ + { + id: 'B1', + }, + { + id: 'B2', + }, + { + id: 'B3', + }, + ], + }, + ] + + return +} + +export const Base: Story = Template.bind({}) diff --git a/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx b/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx new file mode 100644 index 00000000000..a37a1d41c64 --- /dev/null +++ b/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx @@ -0,0 +1,680 @@ +import './styles.scss' + +import { + closestCenter, + CollisionDetection, + defaultDropAnimationSideEffects, + DndContext, + DraggableSyntheticListeners, + DragOverlay, + DropAnimation, + getFirstCollision, + MeasuringStrategy, + MouseSensor, + pointerWithin, + rectIntersection, + TouchSensor, + useSensor, + useSensors, +} from '@dnd-kit/core' +import type { UniqueIdentifier } from '@dnd-kit/core/dist/types' +import { + AnimateLayoutChanges, + arrayMove, + defaultAnimateLayoutChanges, + SortableContext, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable' +import type { Transform } from '@dnd-kit/utilities' +import { CSS } from '@dnd-kit/utilities' +import { IconBuilding, IconTrash } from '@posthog/icons' +import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton' +import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react' +import { createPortal, unstable_batchedUpdates } from 'react-dom' +export interface VDNDChildItem { + id: UniqueIdentifier +} + +export interface VNDNDContainerItem { + items: T[] + id: UniqueIdentifier +} + +export interface VerticalNestedDNDProps> { + initialItems: Item[] +} +const PLACEHOLDER_ID = 'placeholder' + +export function VerticalNestedDND>({ + initialItems, +}: VerticalNestedDNDProps): JSX.Element { + const [items, setItems] = useState(() => { + const items: Record = {} + initialItems.forEach((item) => { + items[item.id] = item + }) + return items + }) + const [clonedItems, setClonedItems] = useState | null>(null) + + const handle = true + + const [containers, setContainers] = useState(Object.keys(items) as UniqueIdentifier[]) + const [activeId, setActiveId] = useState(null) + const lastOverId = useRef(null) + const recentlyMovedToNewContainer = useRef(false) + const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor)) + const isSortingContainer = activeId ? containers.includes(activeId) : false + + const collisionDetectionStrategy: CollisionDetection = useCallback( + (args) => { + if (activeId && activeId in items) { + return closestCenter({ + ...args, + droppableContainers: args.droppableContainers.filter((container) => container.id in items), + }) + } + + // Start by finding any intersecting droppable + const pointerIntersections = pointerWithin(args) + const intersections = + pointerIntersections.length > 0 + ? // If there are droppables intersecting with the pointer, return those + pointerIntersections + : rectIntersection(args) + let overId = getFirstCollision(intersections, 'id') + + if (overId != null) { + if (overId in items) { + const containerItems = items[overId].items + + // If a container is matched and it contains items (columns 'A', 'B', 'C') + if (containerItems.length > 0) { + // Return the closest droppable within that container + overId = closestCenter({ + ...args, + droppableContainers: args.droppableContainers.filter( + (container) => + container.id !== overId && + containerItems.some((subItem) => subItem.id === container.id) + ), + })[0]?.id + } + } + + lastOverId.current = overId + + return [{ id: overId }] + } + + // When a draggable item moves to a new container, the layout may shift + // and the `overId` may become `null`. We manually set the cached `lastOverId` + // to the id of the draggable item that was moved to the new container, otherwise + // the previous `overId` will be returned which can cause items to incorrectly shift positions + if (recentlyMovedToNewContainer.current) { + lastOverId.current = activeId + } + + // If no droppable is matched, return the last match + return lastOverId.current ? [{ id: lastOverId.current }] : [] + }, + [activeId, items] + ) + const findContainer = (id: UniqueIdentifier): UniqueIdentifier | undefined => { + if (id in items) { + return id + } + + return Object.keys(items).find((key) => items[key].items.some((item) => item.id === id)) + } + + const getIndex = (id: UniqueIdentifier): number => { + const container = findContainer(id) + + if (!container) { + return -1 + } + + const index = items[container].items.findIndex((subItem) => subItem.id === id) + + return index + } + + const onDragCancel = (): void => { + if (clonedItems) { + // Reset items to their original state in case items have been + // Dragged across containers + setItems(clonedItems) + } + + setActiveId(null) + setClonedItems(null) + } + + useEffect(() => { + requestAnimationFrame(() => { + recentlyMovedToNewContainer.current = false + }) + }, [items]) + + return ( + { + setActiveId(active.id) + setClonedItems(items) + }} + onDragOver={({ active, over }) => { + const overId = over?.id + const activeIsContainer = active.id in items + + if (overId == null) { + return + } + + if (activeIsContainer) { + const overContainerId = findContainer(overId) + if (!overContainerId) { + return + } + if (activeId !== overContainerId) { + setContainers((containers) => { + const activeIndex = containers.indexOf(active.id) + const overIndex = containers.indexOf(overContainerId) + + return arrayMove(containers, activeIndex, overIndex) + }) + } + } else { + const overContainerId = findContainer(overId) + const activeContainerId = findContainer(active.id) + + if (!overContainerId || !activeContainerId) { + return + } + const activeContainer = items[activeContainerId] + const overContainer = items[overContainerId] + + if (activeContainerId !== overContainerId) { + setItems((items) => { + const activeItems = items[activeContainerId].items + const overItems = items[overContainerId].items + const overIndex = overItems.findIndex((subItem) => subItem.id === overId) + const activeIndex = activeItems.findIndex((subItem) => subItem.id === active.id) + + let newIndex: number + if (overId in items) { + newIndex = overItems.length + 1 + } else { + const isBelowOverItem = + over && + active.rect.current.translated && + active.rect.current.translated.top > over.rect.top + over.rect.height + + const modifier = isBelowOverItem ? 1 : 0 + + newIndex = overIndex >= 0 ? overIndex + modifier : overItems.length + 1 + } + + recentlyMovedToNewContainer.current = true + + const newActiveContainer = { + ...activeContainer, + items: activeItems.filter((item) => item.id !== active.id), + } + const newOverContainer = { + ...overContainer, + items: [ + ...overItems.slice(0, newIndex), + activeContainer.items[activeIndex], + ...overItems.slice(newIndex, overItems.length), + ], + } + + return { + ...items, + [activeContainerId]: newActiveContainer, + [overContainerId]: newOverContainer, + } + }) + } else if (overId !== active.id) { + setItems((items) => { + const overItems = items[overContainerId].items + const overIndex = overItems.findIndex((subItem) => subItem.id === overId) + const activeIndex = overItems.findIndex((subItem) => subItem.id === active.id) + + const isBelowOverItem = + over && + active.rect.current.translated && + active.rect.current.translated.top > over.rect.top + over.rect.height + + const modifier = isBelowOverItem ? 1 : 0 + + const newItems = arrayMove(overItems, activeIndex, overIndex + modifier) + const newOverContainer = { + ...overContainer, + items: newItems, + } + return { + ...items, + [overContainerId]: newOverContainer, + } + }) + } + } + }} + onDragEnd={({ active, over }) => { + if (active.id in items && over?.id) { + setContainers((containers) => { + const activeIndex = containers.indexOf(active.id) + const overIndex = containers.indexOf(over.id) + + return arrayMove(containers, activeIndex, overIndex) + }) + } + + const activeContainerId = findContainer(active.id) + + if (!activeContainerId) { + setActiveId(null) + return + } + + const overId = over?.id + + if (overId == null) { + setActiveId(null) + return + } + + const overContainerId = findContainer(overId) + + if (overContainerId) { + const activeIndex = items[activeContainerId].items.findIndex((subItem) => subItem.id === active.id) + const overIndex = items[overContainerId].items.findIndex((subItem) => subItem.id === overId) + + if (activeIndex !== overIndex) { + setItems((items) => { + const newOverContainer = { + ...items[overContainerId], + items: arrayMove(items[overContainerId].items, activeIndex, overIndex), + } + return { + ...items, + [overContainerId]: newOverContainer, + } + }) + } + } + + setActiveId(null) + }} + onDragCancel={onDragCancel} + > +
+ + {containers.map((containerId) => ( + handleRemove(containerId)} + > + + {items[containerId].items.map((value, index) => { + return ( + + ) + })} + + + ))} + +
+ {createPortal( + + {activeId + ? containers.includes(activeId) + ? renderContainerDragOverlay(activeId) + : renderSortableItemDragOverlay(activeId) + : null} + , + document.body + )} +
+ ) + + function renderSortableItemDragOverlay(id: UniqueIdentifier): JSX.Element { + return + } + + function renderContainerDragOverlay(containerId: UniqueIdentifier): JSX.Element { + return ( + + {items[containerId].items.map((item, index) => ( + + ))} + + ) + } + + function handleRemove(containerID: UniqueIdentifier): void { + setContainers((containers) => containers.filter((id) => id !== containerID)) + } + + function handleAddColumn(): void { + const newContainerId = getNextContainerId() + + unstable_batchedUpdates(() => { + setContainers((containers) => [...containers, newContainerId]) + const newItem: Item = { + id: newContainerId, + items: [], + } as any + setItems((items) => ({ + ...items, + [newContainerId]: newItem, + })) + }) + } + + function getNextContainerId(): string { + const containerIds = Object.keys(items) + const lastContainerId = containerIds[containerIds.length - 1] + + return String.fromCharCode(lastContainerId.charCodeAt(0) + 1) + } +} + +const dropAnimation: DropAnimation = { + sideEffects: defaultDropAnimationSideEffects({ + styles: { + active: { + opacity: '0.4', + }, + }, + }), +} +const animateLayoutChanges: AnimateLayoutChanges = (args) => defaultAnimateLayoutChanges({ ...args, wasDragging: true }) + +interface SortableItemProps { + containerId: UniqueIdentifier + id: UniqueIdentifier + index: number + handle: boolean + disabled?: boolean + getIndex(id: UniqueIdentifier): number +} + +function SortableItem({ disabled, id, index, handle }: SortableItemProps): JSX.Element { + const { + setNodeRef, + setActivatorNodeRef, + listeners, + isDragging, + isSorting, + over, + overIndex, + transform, + transition, + } = useSortable({ + id, + }) + const mounted = useMountStatus() + const mountedWhileDragging = isDragging && !mounted + + return ( + + ) +} + +function useMountStatus(): boolean { + const [isMounted, setIsMounted] = useState(false) + + useEffect(() => { + const timeout = setTimeout(() => setIsMounted(true), 500) + + return () => clearTimeout(timeout) + }, []) + + return isMounted +} + +function DroppableContainer({ + children, + columns = 1, + disabled, + id, + items, + style, + ...props +}: ContainerProps & { + disabled?: boolean + id: UniqueIdentifier + items: SubItem[] + style?: React.CSSProperties +}): JSX.Element { + const { active, attributes, isDragging, listeners, over, setNodeRef, transition, transform } = useSortable({ + id, + data: { + type: 'container', + children: items, + }, + animateLayoutChanges, + }) + const isOverContainer = over + ? (id === over.id && active?.data.current?.type !== 'container') || items.some((item) => item.id === over.id) + : false + + return ( + + {children} + + ) +} + +export interface ContainerProps { + children: React.ReactNode + columns?: number + label?: string + style?: React.CSSProperties + horizontal?: boolean + hover?: boolean + handleProps?: React.HTMLAttributes + scrollable?: boolean + shadow?: boolean + placeholder?: boolean + unstyled?: boolean + onClick?(): void + onRemove?(): void + isDragging?: boolean + transition?: string + transform?: string +} + +export const Container = forwardRef(function Container_( + { + children, + handleProps, + horizontal, + hover, + onClick, + onRemove, + label, + placeholder, + style, + scrollable, + shadow, + unstyled, + isDragging, + transform, + transition, + ...props + }: ContainerProps, + ref +) { + const Component = onClick ? 'button' : 'div' + + return ( + +
+ {label ? {label} : null} + + + + +
+ {placeholder ? children :
    {children}
} +
+ ) +}) + +export interface ItemProps { + dragOverlay?: boolean + color?: string + disabled?: boolean + dragging?: boolean + handleProps?: any + height?: number + index?: number + fadeIn?: boolean + transform?: Transform | null + listeners?: DraggableSyntheticListeners + sorting?: boolean + style?: React.CSSProperties + transition?: string | null + wrapperStyle?: React.CSSProperties + value: UniqueIdentifier + onRemove?(): void +} + +export const Item = React.memo( + React.forwardRef( + ( + { + color, + dragOverlay, + dragging, + disabled, + fadeIn, + handleProps, + height, + index, + listeners, + onRemove, + sorting, + style, + transition, + transform, + value, + wrapperStyle, + ...props + }, + ref + ) => { + const handle = true + useEffect(() => { + if (!dragOverlay) { + return + } + + document.body.style.cursor = 'grabbing' + + return () => { + document.body.style.cursor = '' + } + }, [dragOverlay]) + + return ( +
  • +
    + Item {value} + + {onRemove ? : null} + {handle ? : null} + +
    +
  • + ) + } + ) +) + +export function Remove(props: LemonButtonProps): JSX.Element { + return ( + + + + ) +} + +export const Handle = forwardRef(function Handle_(props, ref) { + return ( + + + + ) +}) diff --git a/frontend/src/lib/lemon-ui/VerticalNestedDND/styles.scss b/frontend/src/lib/lemon-ui/VerticalNestedDND/styles.scss new file mode 100644 index 00000000000..9e498671d32 --- /dev/null +++ b/frontend/src/lib/lemon-ui/VerticalNestedDND/styles.scss @@ -0,0 +1,21 @@ +.VerticalNestedDNDContainer { + box-sizing: initial; + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; + background: var(--bg-light); + border: 1px solid var(--border); + border-radius: var(--radius); +} + +.VerticalNestedDNDItem { + box-sizing: initial; + display: flex; + flex: 1; + flex-direction: column; + overflow: hidden; + background: var(--bg-light); + border: 1px solid var(--border); + border-radius: var(--radius); +} diff --git a/frontend/src/scenes/settings/environment/ChannelType.tsx b/frontend/src/scenes/settings/environment/ChannelType.tsx index ec8a9773e1f..668fc226df4 100644 --- a/frontend/src/scenes/settings/environment/ChannelType.tsx +++ b/frontend/src/scenes/settings/environment/ChannelType.tsx @@ -1,48 +1,614 @@ -import { useActions, useValues } from 'kea' -import { LemonButton } from 'lib/lemon-ui/LemonButton' -import { eventUsageLogic } from 'lib/utils/eventUsageLogic' -import { useState } from 'react' -import { teamLogic } from 'scenes/teamLogic' - -import { CustomChannelRule } from '~/queries/schema' -import { LemonInput } from 'lib/lemon-ui/LemonInput' - +// /* eslint-disable @typescript-eslint/explicit-function-return-type */ +// import { useActions, useValues } from 'kea' +// import { LemonButton } from 'lib/lemon-ui/LemonButton' +// import { eventUsageLogic } from 'lib/utils/eventUsageLogic' +// import React, { useEffect, useRef, useState } from 'react' +// import { teamLogic } from 'scenes/teamLogic' +// +// import { +// Active, +// Announcements, +// closestCenter, +// CollisionDetection, +// defaultDropAnimationSideEffects, +// DndContext, +// DragOverlay, +// DropAnimation, +// KeyboardCoordinateGetter, +// KeyboardSensor, +// MeasuringConfiguration, +// Modifiers, +// MouseSensor, +// PointerActivationConstraint, +// ScreenReaderInstructions, +// TouchSensor, +// UniqueIdentifier, +// useSensor, +// useSensors, +// } from '@dnd-kit/core' +// import { +// AnimateLayoutChanges, +// arrayMove, +// NewIndexGetter, +// rectSortingStrategy, +// SortableContext, +// sortableKeyboardCoordinates, +// SortingStrategy, +// useSortable, +// verticalListSortingStrategy, +// } from '@dnd-kit/sortable' +// +// import { CustomChannelRule } from '~/queries/schema' +// import { LemonInput } from 'lib/lemon-ui/LemonInput' +// import { createPortal } from 'react-dom' +// +// import { Item, List, Wrapper } from '../../components' +// import React, { forwardRef } from 'react' +// +// export interface Props { +// children: React.ReactNode +// columns?: number +// style?: React.CSSProperties +// horizontal?: boolean +// } +// +// export const List = forwardRef(({ children, columns = 1, horizontal, style }: Props, ref) => { +// return
      {children}
    +// }) +// +// export function createRange(length: number, initializer: (index: number) => any = defaultInitializer): T[] { +// return [...new Array(length)].map((_, index) => initializer(index)) +// } +// export function ChannelType(): JSX.Element { - const { updateCurrentTeam } = useActions(teamLogic) - const { currentTeam } = useValues(teamLogic) - const { reportCustomChannelTypeRulesUpdated } = useActions(eventUsageLogic) - - const savedCustomChannelTypeRules = - currentTeam?.modifiers?.customChannelTypeRules ?? currentTeam?.default_modifiers?.customChannelTypeRules ?? null - const [customChannelTypeRules, setCustomChannelTypeRules] = useState( - savedCustomChannelTypeRules ? JSON.stringify(savedCustomChannelTypeRules) : '' - ) - - const handleChange = (rules: string): void => { - let parsed: CustomChannelRule[] = [] - try { - parsed = JSON.parse(rules) - } catch (e) { - return - } - - updateCurrentTeam({ modifiers: { ...currentTeam?.modifiers, customChannelTypeRules: parsed } }) - reportCustomChannelTypeRulesUpdated(parsed.length) - } - - return ( - <> -

    Set your custom channel type

    - -
    - handleChange(customChannelTypeRules)}> - Save - -
    - - ) + return
    + // const { updateCurrentTeam } = useActions(teamLogic) + // const { currentTeam } = useValues(teamLogic) + // const { reportCustomChannelTypeRulesUpdated } = useActions(eventUsageLogic) + // + // const savedCustomChannelTypeRules = + // currentTeam?.modifiers?.customChannelTypeRules ?? currentTeam?.default_modifiers?.customChannelTypeRules ?? null + // const [customChannelTypeRules, setCustomChannelTypeRules] = useState( + // savedCustomChannelTypeRules ? JSON.stringify(savedCustomChannelTypeRules) : '' + // ) + // + // const handleChange = (rules: string): void => { + // let parsed: CustomChannelRule[] = [] + // try { + // parsed = JSON.parse(rules) + // } catch (e) { + // return + // } + // + // updateCurrentTeam({ modifiers: { ...currentTeam?.modifiers, customChannelTypeRules: parsed } }) + // reportCustomChannelTypeRulesUpdated(parsed.length) + // } + // + // return ( + // <> + //

    Set your custom channel type

    + // + //
    + // handleChange(customChannelTypeRules)}> + // Save + // + //
    + // + // ) } +// +// export interface ChannelTypeCustomRulesProps { +// customRules?: CustomChannelRule[] | null +// setCustomRules: (customRules: CustomChannelRule[]) => void +// } +// +// export function ChannelTypeCustomRules({ +// customRules: _customRules, +// setCustomRules: _setCustomRules, +// }: ChannelTypeCustomRulesProps): JSX.Element { +// const customRules = _customRules != null ? _customRules : [] +// const [localCustomRules, setLocalCustomRules] = useState(customRules) +// +// const updateCustomRules = (customRules: CustomChannelRule[]): void => { +// setLocalCustomRules(customRules) +// _setCustomRules(customRules) +// } +// +// const onAddFilter = (filter: CustomChannelRule): void => { +// updateCustomRules([...customRules, filter]) +// } +// const onEditFilter = (index: number, filter: CustomChannelRule): void => { +// const newCustomRules = customRules.map((f, i) => { +// if (i === index) { +// return filter +// } +// return f +// }) +// updateCustomRules(newCustomRules) +// } +// const onRemoveFilter = (index: number): void => { +// updateCustomRules(customRules.filter((_, i) => i !== index)) +// } +// +// function onSortEnd({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }): void { +// function move(arr: CustomChannelRule[], from: number, to: number): CustomChannelRule[] { +// const clone = [...arr] +// Array.prototype.splice.call(clone, to, 0, Array.prototype.splice.call(clone, from, 1)[0]) +// return clone.map((child, order) => ({ ...child, order })) +// } +// updateCustomRules(move(customRules, oldIndex, newIndex)) +// } +// +// return ( +//
    +//
    +// +//
    +//
    +// +//
    +//
    +// ) +// } +// +// export interface SortableProps { +// activationConstraint?: PointerActivationConstraint +// animateLayoutChanges?: AnimateLayoutChanges +// adjustScale?: boolean +// collisionDetection?: CollisionDetection +// coordinateGetter?: KeyboardCoordinateGetter +// Container?: any // To-do: Fix me +// dropAnimation?: DropAnimation | null +// getNewIndex?: NewIndexGetter +// handle?: boolean +// itemCount?: number +// items?: UniqueIdentifier[] +// measuring?: MeasuringConfiguration +// modifiers?: Modifiers +// renderItem?: any +// removable?: boolean +// reorderItems?: typeof arrayMove +// strategy?: SortingStrategy +// style?: React.CSSProperties +// useDragOverlay?: boolean +// getItemStyles?(args: { +// id: UniqueIdentifier +// index: number +// isSorting: boolean +// isDragOverlay: boolean +// overIndex: number +// isDragging: boolean +// }): React.CSSProperties +// wrapperStyle?(args: { +// active: Pick | null +// index: number +// isDragging: boolean +// id: UniqueIdentifier +// }): React.CSSProperties +// isDisabled?(id: UniqueIdentifier): boolean +// } +// +// const dropAnimationConfig: DropAnimation = { +// sideEffects: defaultDropAnimationSideEffects({ +// styles: { +// active: { +// opacity: '0.5', +// }, +// }, +// }), +// } +// +// const screenReaderInstructions: ScreenReaderInstructions = { +// draggable: ` +// To pick up a sortable item, press the space bar. +// While sorting, use the arrow keys to move the item. +// Press space again to drop the item in its new position, or press escape to cancel. +// `, +// } +// +// export function Sortable({ +// activationConstraint, +// animateLayoutChanges, +// adjustScale = false, +// Container = List, +// collisionDetection = closestCenter, +// coordinateGetter = sortableKeyboardCoordinates, +// dropAnimation = dropAnimationConfig, +// getItemStyles = () => ({}), +// getNewIndex, +// handle = false, +// itemCount = 16, +// items: initialItems, +// isDisabled = () => false, +// measuring, +// modifiers, +// removable, +// renderItem, +// reorderItems = arrayMove, +// strategy = rectSortingStrategy, +// style, +// useDragOverlay = true, +// wrapperStyle = () => ({}), +// }: SortableProps): JSX.Element { +// const [items, setItems] = useState( +// // @ts-expect-error +// () => initialItems ?? createRange(itemCount, (index) => index + 1) +// ) +// const [activeId, setActiveId] = useState(null) +// const sensors = useSensors( +// useSensor(MouseSensor, { +// activationConstraint, +// }), +// useSensor(TouchSensor, { +// activationConstraint, +// }), +// useSensor(KeyboardSensor, { +// // Disable smooth scrolling in Cypress automated tests +// scrollBehavior: 'Cypress' in window ? 'auto' : undefined, +// coordinateGetter, +// }) +// ) +// const isFirstAnnouncement = useRef(true) +// const getIndex = (id: UniqueIdentifier) => items.indexOf(id) +// const getPosition = (id: UniqueIdentifier) => getIndex(id) + 1 +// const activeIndex = activeId ? getIndex(activeId) : -1 +// const handleRemove = removable +// ? (id: UniqueIdentifier) => setItems((items) => items.filter((item) => item !== id)) +// : undefined +// const announcements: Announcements = { +// onDragStart({ active: { id } }) { +// return `Picked up sortable item ${String(id)}. Sortable item ${id} is in position ${getPosition(id)} of ${ +// items.length +// }` +// }, +// onDragOver({ active, over }) { +// // In this specific use-case, the picked up item's `id` is always the same as the first `over` id. +// // The first `onDragOver` event therefore doesn't need to be announced, because it is called +// // immediately after the `onDragStart` announcement and is redundant. +// if (isFirstAnnouncement.current === true) { +// isFirstAnnouncement.current = false +// return +// } +// +// if (over) { +// return `Sortable item ${active.id} was moved into position ${getPosition(over.id)} of ${items.length}` +// } +// +// return +// }, +// onDragEnd({ active, over }) { +// if (over) { +// return `Sortable item ${active.id} was dropped at position ${getPosition(over.id)} of ${items.length}` +// } +// +// return +// }, +// onDragCancel({ active: { id } }) { +// return `Sorting was cancelled. Sortable item ${id} was dropped and returned to position ${getPosition( +// id +// )} of ${items.length}.` +// }, +// } +// +// useEffect(() => { +// if (!activeId) { +// isFirstAnnouncement.current = true +// } +// }, [activeId]) +// +// return ( +// { +// if (!active) { +// return +// } +// +// setActiveId(active.id) +// }} +// onDragEnd={({ over }) => { +// setActiveId(null) +// +// if (over) { +// const overIndex = getIndex(over.id) +// if (activeIndex !== overIndex) { +// setItems((items) => reorderItems(items, activeIndex, overIndex)) +// } +// } +// }} +// onDragCancel={() => setActiveId(null)} +// measuring={measuring} +// modifiers={modifiers} +// > +// +// +// +// {items.map((value, index) => ( +// +// ))} +// +// +// +// {useDragOverlay +// ? createPortal( +// +// {activeId ? ( +// +// ) : null} +// , +// document.body +// ) +// : null} +// +// ) +// } +// +// interface SortableItemProps { +// animateLayoutChanges?: AnimateLayoutChanges +// disabled?: boolean +// getNewIndex?: NewIndexGetter +// id: UniqueIdentifier +// index: number +// handle: boolean +// useDragOverlay?: boolean +// onRemove?(id: UniqueIdentifier): void +// style(values: any): React.CSSProperties +// renderItem?(args: any): React.ReactElement +// wrapperStyle: SortableProps['wrapperStyle'] +// } +// +// export function SortableItem({ +// disabled, +// animateLayoutChanges, +// getNewIndex, +// handle, +// id, +// index, +// onRemove, +// style, +// renderItem, +// useDragOverlay, +// wrapperStyle, +// }: SortableItemProps): JSX.Element { +// const { +// active, +// attributes, +// isDragging, +// isSorting, +// listeners, +// overIndex, +// setNodeRef, +// setActivatorNodeRef, +// transform, +// transition, +// } = useSortable({ +// id, +// animateLayoutChanges, +// disabled, +// getNewIndex, +// }) +// +// return ( +// onRemove(id) : undefined} +// transform={transform} +// transition={transition} +// wrapperStyle={wrapperStyle?.({ index, isDragging, active, id })} +// listeners={listeners} +// data-index={index} +// data-id={id} +// dragOverlay={!useDragOverlay && isDragging} +// {...attributes} +// /> +// ) +// } +// +// +// export interface Props { +// dragOverlay?: boolean; +// color?: string; +// disabled?: boolean; +// dragging?: boolean; +// handle?: boolean; +// handleProps?: any; +// height?: number; +// index?: number; +// fadeIn?: boolean; +// transform?: Transform | null; +// listeners?: DraggableSyntheticListeners; +// sorting?: boolean; +// style?: React.CSSProperties; +// transition?: string | null; +// wrapperStyle?: React.CSSProperties; +// value: React.ReactNode; +// onRemove?(): void; +// renderItem?(args: { +// dragOverlay: boolean; +// dragging: boolean; +// sorting: boolean; +// index: number | undefined; +// fadeIn: boolean; +// listeners: DraggableSyntheticListeners; +// ref: React.Ref; +// style: React.CSSProperties | undefined; +// transform: Props['transform']; +// transition: Props['transition']; +// value: Props['value']; +// }): React.ReactElement; +// } +// +// export const Item = React.memo( +// React.forwardRef( +// ( +// { +// color, +// dragOverlay, +// dragging, +// disabled, +// fadeIn, +// handle, +// handleProps, +// height, +// index, +// listeners, +// onRemove, +// renderItem, +// sorting, +// style, +// transition, +// transform, +// value, +// wrapperStyle, +// ...props +// }, +// ref +// ) => { +// useEffect(() => { +// if (!dragOverlay) { +// return; +// } +// +// document.body.style.cursor = 'grabbing'; +// +// return () => { +// document.body.style.cursor = ''; +// }; +// }, [dragOverlay]); +// +// return renderItem ? ( +// renderItem({ +// dragOverlay: Boolean(dragOverlay), +// dragging: Boolean(dragging), +// sorting: Boolean(sorting), +// index, +// fadeIn: Boolean(fadeIn), +// listeners, +// ref, +// style, +// transform, +// transition, +// value, +// }) +// ) : ( +//
  • +//
    +// {value} +// +// {onRemove ? ( +// +// ) : null} +// {handle ? : null} +// +//
    +//
  • +// ); +// } +// ) +// ); diff --git a/package.json b/package.json index a1407c71269..80b462e6bc8 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "chartjs-plugin-stacked100": "^1.4.0", "chartjs-plugin-trendline": "^2.1.2", "chokidar": "^3.5.3", + "classnames": "^2.5.1", "clsx": "^1.1.1", "core-js": "^3.32.0", "cors": "^2.8.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f789b9f1710..f9f094b78e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,6 +154,9 @@ dependencies: chokidar: specifier: ^3.5.3 version: 3.5.3 + classnames: + specifier: ^2.5.1 + version: 2.5.1 clsx: specifier: ^1.1.1 version: 1.2.1 @@ -10313,8 +10316,8 @@ packages: resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==} dev: true - /classnames@2.3.2: - resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} + /classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} dev: false /clean-css@5.3.2: @@ -18239,7 +18242,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@babel/runtime': 7.24.0 - classnames: 2.3.2 + classnames: 2.5.1 dom-align: 1.12.3 lodash: 4.17.21 rc-util: 5.24.4(react-dom@18.2.0)(react@18.2.0) @@ -18255,7 +18258,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@babel/runtime': 7.24.0 - classnames: 2.3.2 + classnames: 2.5.1 rc-util: 5.24.4(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -18269,7 +18272,7 @@ packages: react-dom: '>=16.9.0' dependencies: '@babel/runtime': 7.24.0 - classnames: 2.3.2 + classnames: 2.5.1 rc-align: 4.0.12(react-dom@18.2.0)(react@18.2.0) rc-motion: 2.6.2(react-dom@18.2.0)(react@18.2.0) rc-util: 5.24.4(react-dom@18.2.0)(react@18.2.0) diff --git a/posthog/hogql/modifiers.py b/posthog/hogql/modifiers.py index afb644f978e..d232cf6dd8b 100644 --- a/posthog/hogql/modifiers.py +++ b/posthog/hogql/modifiers.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING, Optional import posthoganalytics +from pydantic import ValidationError from posthog.cloud_utils import is_cloud from posthog.schema import ( @@ -51,9 +52,16 @@ def create_default_modifiers_for_team( if isinstance(team.modifiers, dict): for key, value in team.modifiers.items(): if getattr(modifiers, key) is None: - if key == "customChannelTypeRules" and isinstance(value, list): - value = [CustomChannelRule(**rule) if isinstance(rule, dict) else rule for rule in value] - setattr(modifiers, key, value) + if key == "customChannelTypeRules": + # don't break all queries if customChannelTypeRules are invalid + try: + if isinstance(value, list): + value = [CustomChannelRule(**rule) if isinstance(rule, dict) else rule for rule in value] + setattr(modifiers, key, value) + except ValidationError: + pass + else: + setattr(modifiers, key, value) set_default_modifier_values(modifiers, team)