0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-24 00:47:50 +01:00
This commit is contained in:
Robbie Coomber 2024-11-19 11:56:31 +00:00
parent 3060b29d3e
commit cf20c434ae
7 changed files with 1395 additions and 54 deletions

View File

@ -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<typeof VerticalNestedDND>
const meta: Meta<typeof VerticalNestedDND> = {
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<typeof VerticalNestedDND> = (props: VerticalNestedDNDProps<ExampleSubItem, ExampleItem>) => {
const starterData: ExampleItem[] = [
{
id: 'A',
items: [
{
id: 'A1',
},
{
id: 'A2',
},
{
id: 'A3',
},
],
},
{
id: 'B',
items: [
{
id: 'B1',
},
{
id: 'B2',
},
{
id: 'B3',
},
],
},
]
return <VerticalNestedDND {...props} initialItems={starterData} />
}
export const Base: Story = Template.bind({})

View File

@ -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<T extends VDNDChildItem> {
items: T[]
id: UniqueIdentifier
}
export interface VerticalNestedDNDProps<SubItem extends VDNDChildItem, Item extends VNDNDContainerItem<SubItem>> {
initialItems: Item[]
}
const PLACEHOLDER_ID = 'placeholder'
export function VerticalNestedDND<SubItem extends VDNDChildItem, Item extends VNDNDContainerItem<SubItem>>({
initialItems,
}: VerticalNestedDNDProps<SubItem, Item>): JSX.Element {
const [items, setItems] = useState(() => {
const items: Record<UniqueIdentifier, Item> = {}
initialItems.forEach((item) => {
items[item.id] = item
})
return items
})
const [clonedItems, setClonedItems] = useState<Record<UniqueIdentifier, Item> | null>(null)
const handle = true
const [containers, setContainers] = useState(Object.keys(items) as UniqueIdentifier[])
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null)
const lastOverId = useRef<UniqueIdentifier | null>(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 (
<DndContext
sensors={sensors}
collisionDetection={collisionDetectionStrategy}
measuring={{
droppable: {
strategy: MeasuringStrategy.Always,
},
}}
onDragStart={({ active }) => {
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}
>
<div>
<SortableContext items={containers} strategy={verticalListSortingStrategy}>
{containers.map((containerId) => (
<DroppableContainer
key={containerId}
id={containerId}
items={items[containerId].items}
onRemove={() => handleRemove(containerId)}
>
<SortableContext items={items[containerId].items} strategy={verticalListSortingStrategy}>
{items[containerId].items.map((value, index) => {
return (
<SortableItem
disabled={isSortingContainer}
key={value.id}
id={value.id}
index={index}
handle={handle}
containerId={containerId}
getIndex={getIndex}
/>
)
})}
</SortableContext>
</DroppableContainer>
))}
</SortableContext>
</div>
{createPortal(
<DragOverlay dropAnimation={dropAnimation}>
{activeId
? containers.includes(activeId)
? renderContainerDragOverlay(activeId)
: renderSortableItemDragOverlay(activeId)
: null}
</DragOverlay>,
document.body
)}
</DndContext>
)
function renderSortableItemDragOverlay(id: UniqueIdentifier): JSX.Element {
return <Item value={id} dragOverlay />
}
function renderContainerDragOverlay(containerId: UniqueIdentifier): JSX.Element {
return (
<Container
label={`Column ${containerId}`}
style={{
height: '100%',
}}
shadow
unstyled={false}
>
{items[containerId].items.map((item, index) => (
<Item key={item.id} value={item.id} />
))}
</Container>
)
}
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 (
<Item
ref={disabled ? undefined : setNodeRef}
value={id}
dragging={isDragging}
sorting={isSorting}
handleProps={handle ? { ref: setActivatorNodeRef } : undefined}
index={index}
transition={transition}
transform={transform}
fadeIn={mountedWhileDragging}
listeners={listeners}
/>
)
}
function useMountStatus(): boolean {
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
const timeout = setTimeout(() => setIsMounted(true), 500)
return () => clearTimeout(timeout)
}, [])
return isMounted
}
function DroppableContainer<SubItem extends VDNDChildItem>({
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 (
<Container
ref={disabled ? undefined : setNodeRef}
isDragging={isDragging}
hover={isOverContainer}
transform={CSS.Translate.toString(transform)}
transition={transition}
handleProps={{
...attributes,
...listeners,
}}
columns={columns}
label={`Column ${id}`}
{...props}
>
{children}
</Container>
)
}
export interface ContainerProps {
children: React.ReactNode
columns?: number
label?: string
style?: React.CSSProperties
horizontal?: boolean
hover?: boolean
handleProps?: React.HTMLAttributes<any>
scrollable?: boolean
shadow?: boolean
placeholder?: boolean
unstyled?: boolean
onClick?(): void
onRemove?(): void
isDragging?: boolean
transition?: string
transform?: string
}
export const Container = forwardRef<HTMLDivElement, ContainerProps>(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 (
<Component
{...props}
className={`flex flex-col p-4 bg-bg-light border rounded overflow-hidden ${isDragging ? 'opacity-40' : ''}`}
style={{
transform,
transition,
}}
// @ts-expect-error
ref={ref}
onClick={onClick}
tabIndex={onClick ? 0 : undefined}
>
<div className="flex flex-row justify-between">
{label ? <span>{label}</span> : null}
<span className="flex flex-row space-x-1">
<Remove onClick={onRemove} />
<Handle {...handleProps} />
</span>
</div>
{placeholder ? children : <ul>{children}</ul>}
</Component>
)
})
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<HTMLLIElement, ItemProps>(
(
{
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 (
<li ref={ref} className={`${dragging ? 'opacity-40' : ''}`}>
<div
data-cypress="draggable-item"
{...(!handle ? listeners : undefined)}
{...props}
tabIndex={!handle ? 0 : undefined}
className="VerticalNestedDNDItem"
>
Item {value}
<span className="flex flex-row space-x-1">
{onRemove ? <Remove onClick={onRemove} /> : null}
{handle ? <Handle {...handleProps} {...listeners} /> : null}
</span>
</div>
</li>
)
}
)
)
export function Remove(props: LemonButtonProps): JSX.Element {
return (
<LemonButton type="secondary" fullWidth={false} {...props}>
<IconTrash />
</LemonButton>
)
}
export const Handle = forwardRef<HTMLButtonElement, LemonButtonProps>(function Handle_(props, ref) {
return (
<LemonButton type="secondary" fullWidth={false} ref={ref} {...props}>
<IconBuilding />
</LemonButton>
)
})

View File

@ -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);
}

View File

@ -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<HTMLUListElement, Props>(({ children, columns = 1, horizontal, style }: Props, ref) => {
// return <ul ref={ref}>{children}</ul>
// })
//
// export function createRange<T = number>(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<string>(
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 (
<>
<p>Set your custom channel type</p>
<LemonInput
value={customChannelTypeRules}
onChange={setCustomChannelTypeRules}
placeholder="Enter JSON array of custom channel type rules"
/>
<div className="mt-4">
<LemonButton type="primary" onClick={() => handleChange(customChannelTypeRules)}>
Save
</LemonButton>
</div>
</>
)
return <div />
// 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<string>(
// 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 (
// <>
// <p>Set your custom channel type</p>
// <LemonInput
// value={customChannelTypeRules}
// onChange={setCustomChannelTypeRules}
// placeholder="Enter JSON array of custom channel type rules"
// />
// <div className="mt-4">
// <LemonButton type="primary" onClick={() => handleChange(customChannelTypeRules)}>
// Save
// </LemonButton>
// </div>
// </>
// )
}
//
// 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<CustomChannelRule[]>(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 (
// <div className="flex flex-col gap-2">
// <div className="flex items-center gap-2 flex-wrap">
// <Sortable strategy={verticalListSortingStrategy} />
// </div>
// <div>
// <button>Add</button>
// </div>
// </div>
// )
// }
//
// 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<Active, 'id'> | 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<UniqueIdentifier[]>(
// // @ts-expect-error
// () => initialItems ?? createRange<UniqueIdentifier>(itemCount, (index) => index + 1)
// )
// const [activeId, setActiveId] = useState<UniqueIdentifier | null>(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 (
// <DndContext
// accessibility={{
// announcements,
// screenReaderInstructions,
// }}
// sensors={sensors}
// collisionDetection={collisionDetection}
// onDragStart={({ active }) => {
// 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}
// >
// <Wrapper style={style} center>
// <SortableContext items={items} strategy={strategy}>
// <Container>
// {items.map((value, index) => (
// <SortableItem
// key={value}
// id={value}
// handle={handle}
// index={index}
// style={getItemStyles}
// wrapperStyle={wrapperStyle}
// disabled={isDisabled(value)}
// renderItem={renderItem}
// onRemove={handleRemove}
// animateLayoutChanges={animateLayoutChanges}
// useDragOverlay={useDragOverlay}
// getNewIndex={getNewIndex}
// />
// ))}
// </Container>
// </SortableContext>
// </Wrapper>
// {useDragOverlay
// ? createPortal(
// <DragOverlay adjustScale={adjustScale} dropAnimation={dropAnimation}>
// {activeId ? (
// <Item
// value={items[activeIndex]}
// handle={handle}
// renderItem={renderItem}
// wrapperStyle={wrapperStyle({
// active: { id: activeId },
// index: activeIndex,
// isDragging: true,
// id: items[activeIndex],
// })}
// style={getItemStyles({
// id: items[activeIndex],
// index: activeIndex,
// isSorting: activeId !== null,
// isDragging: true,
// overIndex: -1,
// isDragOverlay: true,
// })}
// dragOverlay
// />
// ) : null}
// </DragOverlay>,
// document.body
// )
// : null}
// </DndContext>
// )
// }
//
// 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 (
// <Item
// ref={setNodeRef}
// value={id}
// disabled={disabled}
// dragging={isDragging}
// sorting={isSorting}
// handle={handle}
// handleProps={
// handle
// ? {
// ref: setActivatorNodeRef,
// }
// : undefined
// }
// renderItem={renderItem}
// index={index}
// style={style({
// index,
// id,
// isDragging,
// isSorting,
// overIndex,
// })}
// onRemove={onRemove ? () => 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<HTMLElement>;
// style: React.CSSProperties | undefined;
// transform: Props['transform'];
// transition: Props['transition'];
// value: Props['value'];
// }): React.ReactElement;
// }
//
// export const Item = React.memo(
// React.forwardRef<HTMLLIElement, Props>(
// (
// {
// 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,
// })
// ) : (
// <li
// className={classNames(
// styles.Wrapper,
// fadeIn && styles.fadeIn,
// sorting && styles.sorting,
// dragOverlay && styles.dragOverlay
// )}
// style={
// {
// ...wrapperStyle,
// transition: [transition, wrapperStyle?.transition]
// .filter(Boolean)
// .join(', '),
// '--translate-x': transform
// ? `${Math.round(transform.x)}px`
// : undefined,
// '--translate-y': transform
// ? `${Math.round(transform.y)}px`
// : undefined,
// '--scale-x': transform?.scaleX
// ? `${transform.scaleX}`
// : undefined,
// '--scale-y': transform?.scaleY
// ? `${transform.scaleY}`
// : undefined,
// '--index': index,
// '--color': color,
// } as React.CSSProperties
// }
// ref={ref}
// >
// <div
// className={classNames(
// styles.Item,
// dragging && styles.dragging,
// handle && styles.withHandle,
// dragOverlay && styles.dragOverlay,
// disabled && styles.disabled,
// color && styles.color
// )}
// style={style}
// data-cypress="draggable-item"
// {...(!handle ? listeners : undefined)}
// {...props}
// tabIndex={!handle ? 0 : undefined}
// >
// {value}
// <span className={styles.Actions}>
// {onRemove ? (
// <Remove className={styles.Remove} onClick={onRemove} />
// ) : null}
// {handle ? <Handle {...handleProps} {...listeners} /> : null}
// </span>
// </div>
// </li>
// );
// }
// )
// );

View File

@ -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",

View File

@ -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)

View File

@ -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)