mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-24 00:47:50 +01:00
Almost there, just need a bit of styling
This commit is contained in:
parent
9eec0e71f9
commit
6e64697b08
@ -26,6 +26,7 @@ export interface PropertyValueProps {
|
||||
autoFocus?: boolean
|
||||
eventNames?: string[]
|
||||
addRelativeDateTimeOptions?: boolean
|
||||
forceSingleSelect?: boolean
|
||||
}
|
||||
|
||||
export function PropertyValue({
|
||||
@ -39,11 +40,12 @@ export function PropertyValue({
|
||||
autoFocus = false,
|
||||
eventNames = [],
|
||||
addRelativeDateTimeOptions = false,
|
||||
forceSingleSelect = false,
|
||||
}: PropertyValueProps): JSX.Element {
|
||||
const { formatPropertyValueForDisplay, describeProperty, options } = useValues(propertyDefinitionsModel)
|
||||
const { loadPropertyValues } = useActions(propertyDefinitionsModel)
|
||||
|
||||
const isMultiSelect = operator && isOperatorMulti(operator)
|
||||
const isMultiSelect = operator && isOperatorMulti(operator) && !forceSingleSelect
|
||||
const isDateTimeProperty = operator && isOperatorDate(operator)
|
||||
const propertyDefinitionType = propertyFilterTypeToPropertyDefinitionType(type)
|
||||
|
||||
|
@ -20,7 +20,7 @@ interface ExampleSubItem {
|
||||
}
|
||||
interface ExampleItem {
|
||||
id: UniqueIdentifier
|
||||
items: ExampleSubItem[]
|
||||
items?: ExampleSubItem[]
|
||||
}
|
||||
let counter = 0
|
||||
|
||||
|
@ -28,25 +28,28 @@ import {
|
||||
} from '@dnd-kit/sortable'
|
||||
import type { Transform } from '@dnd-kit/utilities'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { IconBuilding, IconTrash } from '@posthog/icons'
|
||||
import { IconTrash } from '@posthog/icons'
|
||||
import { IconDragHandle } from 'lib/lemon-ui/icons'
|
||||
import { LemonButton, LemonButtonProps } from 'lib/lemon-ui/LemonButton'
|
||||
import debounce from 'lodash.debounce'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal, unstable_batchedUpdates } from 'react-dom'
|
||||
|
||||
const NOOP = (): void => {}
|
||||
export interface VDNDChildItem {
|
||||
id: UniqueIdentifier
|
||||
}
|
||||
|
||||
export interface VNDNDContainerItem<T extends VDNDChildItem> {
|
||||
items: T[]
|
||||
items?: T[]
|
||||
id: UniqueIdentifier
|
||||
}
|
||||
|
||||
export interface VerticalNestedDNDProps<ChildItem extends VDNDChildItem, Item extends VNDNDContainerItem<ChildItem>> {
|
||||
initialItems: Item[]
|
||||
renderContainerItem: (item: Item) => JSX.Element | null
|
||||
renderChildItem: (item: ChildItem) => JSX.Element | null
|
||||
renderContainerItem: (item: Item, callbacks: { updateContainerItem: (item: Item) => void }) => JSX.Element | null
|
||||
renderChildItem: (item: ChildItem, callbacks: { updateChildItem: (item: ChildItem) => void }) => JSX.Element | null
|
||||
createNewContainerItem(): Item
|
||||
createNewChildItem(): ChildItem
|
||||
onChange?(items: Item[]): void
|
||||
@ -114,7 +117,7 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
const containerItems = items[overId].items
|
||||
|
||||
// If a container is matched and it contains items (columns 'A', 'B', 'C')
|
||||
if (containerItems.length > 0) {
|
||||
if (containerItems && containerItems.length > 0) {
|
||||
// Return the closest droppable within that container
|
||||
overId = closestCenter({
|
||||
...args,
|
||||
@ -150,12 +153,12 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
return id
|
||||
}
|
||||
|
||||
return Object.keys(items).find((key) => items[key].items.some((item) => item.id === id))
|
||||
return Object.keys(items).find((key) => items[key].items?.some((item) => item.id === id))
|
||||
}
|
||||
|
||||
const findChildItem = (id: UniqueIdentifier): ChildItem | undefined => {
|
||||
for (const containerId in items) {
|
||||
const item = items[containerId].items.find((item) => item.id === id)
|
||||
const item = items[containerId].items?.find((item) => item.id === id)
|
||||
if (item) {
|
||||
return item
|
||||
}
|
||||
@ -168,8 +171,12 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
if (!container) {
|
||||
return -1
|
||||
}
|
||||
const childItems = items[container].items
|
||||
if (!childItems) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return items[container].items.findIndex((ChildItem) => ChildItem.id === id)
|
||||
return childItems.findIndex((ChildItem) => ChildItem.id === id)
|
||||
}
|
||||
|
||||
const onDragCancel = (): void => {
|
||||
@ -235,8 +242,8 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
|
||||
if (activeContainerId !== overContainerId) {
|
||||
setItems((items) => {
|
||||
const activeItems = items[activeContainerId].items
|
||||
const overItems = items[overContainerId].items
|
||||
const activeItems = items[activeContainerId].items || []
|
||||
const overItems = items[overContainerId].items || []
|
||||
const overIndex = overItems.findIndex((ChildItem) => ChildItem.id === overId)
|
||||
const activeIndex = activeItems.findIndex((ChildItem) => ChildItem.id === active.id)
|
||||
|
||||
@ -264,7 +271,7 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
...overContainer,
|
||||
items: [
|
||||
...overItems.slice(0, newIndex),
|
||||
activeContainer.items[activeIndex],
|
||||
activeItems[activeIndex],
|
||||
...overItems.slice(newIndex, overItems.length),
|
||||
],
|
||||
}
|
||||
@ -277,7 +284,7 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
})
|
||||
} else if (overId !== active.id) {
|
||||
setItems((items) => {
|
||||
const overItems = items[overContainerId].items
|
||||
const overItems = items[overContainerId].items || []
|
||||
const overIndex = overItems.findIndex((ChildItem) => ChildItem.id === overId)
|
||||
const activeIndex = overItems.findIndex((ChildItem) => ChildItem.id === active.id)
|
||||
|
||||
@ -328,16 +335,17 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
const overContainerId = findContainer(overId)
|
||||
|
||||
if (overContainerId) {
|
||||
const activeIndex = items[activeContainerId].items.findIndex(
|
||||
(ChildItem) => ChildItem.id === active.id
|
||||
)
|
||||
const overIndex = items[overContainerId].items.findIndex((ChildItem) => ChildItem.id === overId)
|
||||
const overItems = items[overContainerId].items || []
|
||||
const activeItems = items[activeContainerId].items || []
|
||||
const activeIndex = activeItems.findIndex((ChildItem) => ChildItem.id === active.id)
|
||||
const overIndex = overItems.findIndex((ChildItem) => ChildItem.id === overId)
|
||||
|
||||
if (activeIndex !== overIndex) {
|
||||
setItems((items) => {
|
||||
const overItems = items[overContainerId].items || []
|
||||
const newOverContainer = {
|
||||
...items[overContainerId],
|
||||
items: arrayMove(items[overContainerId].items, activeIndex, overIndex),
|
||||
items: arrayMove(overItems, activeIndex, overIndex),
|
||||
}
|
||||
return {
|
||||
...items,
|
||||
@ -356,15 +364,19 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
{containers.map((containerId) => (
|
||||
<DroppableContainer
|
||||
key={containerId}
|
||||
items={items[containerId].items}
|
||||
items={items[containerId].items || []}
|
||||
onRemove={() => handleRemove(containerId)}
|
||||
renderContainerItem={renderContainerItem}
|
||||
containerItemId={containerId}
|
||||
item={items[containerId]}
|
||||
onAddChild={handleAddChild}
|
||||
updateContainerItem={updateContainerItem}
|
||||
>
|
||||
<SortableContext items={items[containerId].items} strategy={verticalListSortingStrategy}>
|
||||
{items[containerId].items.map((value, index) => {
|
||||
<SortableContext
|
||||
items={items[containerId].items || []}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{(items[containerId].items || []).map((value, index) => {
|
||||
return (
|
||||
<SortableItem
|
||||
disabled={isSortingContainer}
|
||||
@ -375,6 +387,7 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
containerId={containerId}
|
||||
getIndex={getIndex}
|
||||
renderChildItem={renderChildItem}
|
||||
updateChildItem={updateChildItem}
|
||||
item={value}
|
||||
/>
|
||||
)
|
||||
@ -407,7 +420,15 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
return <ChildItem childItemId={id} dragOverlay renderChildItem={renderChildItem} item={item} />
|
||||
return (
|
||||
<ChildItem
|
||||
childItemId={id}
|
||||
dragOverlay
|
||||
renderChildItem={renderChildItem}
|
||||
item={item}
|
||||
updateChildItem={NOOP}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function renderContainerDragOverlay(containerId: UniqueIdentifier): JSX.Element | null {
|
||||
@ -421,10 +442,17 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
shadow
|
||||
renderContainerItem={renderContainerItem}
|
||||
item={item}
|
||||
onAddChild={() => {}}
|
||||
onAddChild={NOOP}
|
||||
updateContainerItem={NOOP}
|
||||
>
|
||||
{items[containerId].items.map((item) => (
|
||||
<ChildItem key={item.id} childItemId={item.id} renderChildItem={renderChildItem} item={item} />
|
||||
{(items[containerId].items || []).map((item) => (
|
||||
<ChildItem
|
||||
key={item.id}
|
||||
childItemId={item.id}
|
||||
renderChildItem={renderChildItem}
|
||||
item={item}
|
||||
updateChildItem={NOOP}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
@ -455,7 +483,37 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
|
||||
...items,
|
||||
[containerId]: {
|
||||
...container,
|
||||
items: [...container.items, newChild],
|
||||
items: [...(container.items || []), newChild],
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateContainerItem(item: Item): void {
|
||||
setItems((items) => ({
|
||||
...items,
|
||||
[item.id]: item,
|
||||
}))
|
||||
}
|
||||
|
||||
function updateChildItem(item: ChildItem): void {
|
||||
const containerId = findContainer(item.id)
|
||||
|
||||
if (!containerId) {
|
||||
return
|
||||
}
|
||||
setItems((items) => {
|
||||
const container = items[containerId]
|
||||
return {
|
||||
...items,
|
||||
[containerId]: {
|
||||
...container,
|
||||
items: (container.items || []).map((childItem) => {
|
||||
if (childItem.id === item.id) {
|
||||
return item
|
||||
}
|
||||
return childItem
|
||||
}),
|
||||
},
|
||||
}
|
||||
})
|
||||
@ -480,7 +538,8 @@ interface SortableItemProps<Item extends VDNDChildItem> {
|
||||
handle: boolean
|
||||
disabled?: boolean
|
||||
getIndex(id: UniqueIdentifier): number
|
||||
renderChildItem(item: Item): JSX.Element | null
|
||||
renderChildItem(item: Item, callbacks: { updateChildItem: (item: Item) => void }): JSX.Element | null
|
||||
updateChildItem(item: Item): void
|
||||
item: Item
|
||||
}
|
||||
|
||||
@ -490,6 +549,7 @@ function SortableItem<Item extends VDNDChildItem>({
|
||||
index,
|
||||
handle,
|
||||
renderChildItem,
|
||||
updateChildItem,
|
||||
item,
|
||||
}: SortableItemProps<Item>): JSX.Element {
|
||||
const { setNodeRef, setActivatorNodeRef, listeners, isDragging, isSorting, transform, transition } = useSortable({
|
||||
@ -511,6 +571,7 @@ function SortableItem<Item extends VDNDChildItem>({
|
||||
fadeIn={mountedWhileDragging}
|
||||
listeners={listeners}
|
||||
renderChildItem={renderChildItem}
|
||||
updateChildItem={updateChildItem}
|
||||
item={item}
|
||||
/>
|
||||
)
|
||||
@ -592,7 +653,8 @@ export interface ContainerProps<Item extends VNDNDContainerItem<any>> {
|
||||
isDragging?: boolean
|
||||
transition?: string
|
||||
transform?: string
|
||||
renderContainerItem(item: Item): JSX.Element | null
|
||||
renderContainerItem(item: Item, callbacks: { updateContainerItem: (item: Item) => void }): JSX.Element | null
|
||||
updateContainerItem(item: Item): void
|
||||
item: Item
|
||||
}
|
||||
|
||||
@ -615,6 +677,7 @@ export const Container = forwardRef(function Container_<Item extends VNDNDContai
|
||||
transform,
|
||||
transition,
|
||||
renderContainerItem,
|
||||
updateContainerItem,
|
||||
item,
|
||||
...props
|
||||
}: ContainerProps<Item>,
|
||||
@ -639,8 +702,13 @@ export const Container = forwardRef(function Container_<Item extends VNDNDContai
|
||||
>
|
||||
<div className="flex flex-row justify-between px-2 space-x-2">
|
||||
<Handle {...handleProps} />
|
||||
<div>{renderContainerItem ? renderContainerItem(item) : <span>Container {containerItemId}</span>}</div>
|
||||
<div className="flex-1" />
|
||||
<div className="flex-1">
|
||||
{renderContainerItem ? (
|
||||
renderContainerItem(item, { updateContainerItem })
|
||||
) : (
|
||||
<span>Container {containerItemId}</span>
|
||||
)}
|
||||
</div>
|
||||
<Remove onClick={onRemove} />
|
||||
</div>
|
||||
{placeholder ? children : <ul className="space-y-2">{children}</ul>}
|
||||
@ -675,7 +743,8 @@ export interface ChildItemProps<Item extends VDNDChildItem> {
|
||||
childItemId: UniqueIdentifier
|
||||
item: Item
|
||||
onRemove?(): void
|
||||
renderChildItem(item: Item): JSX.Element | null
|
||||
renderChildItem(item: Item, callbacks: { updateChildItem: (item: Item) => void }): JSX.Element | null
|
||||
updateChildItem(item: Item): void
|
||||
}
|
||||
|
||||
export const ChildItem = React.memo(
|
||||
@ -698,6 +767,8 @@ export const ChildItem = React.memo(
|
||||
childItemId,
|
||||
wrapperStyle,
|
||||
renderChildItem,
|
||||
updateChildItem,
|
||||
item,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@ -730,8 +801,9 @@ export const ChildItem = React.memo(
|
||||
className="flex flex-row justify-between w-full space-x-2"
|
||||
>
|
||||
<Handle {...handleProps} {...listeners} />
|
||||
<div>{renderChildItem ? renderChildItem(childItemId) : <span>Item {childItemId}</span>}</div>
|
||||
<div className="flex-1" />
|
||||
<div className="flex-1">
|
||||
{renderChildItem ? renderChildItem(item, { updateChildItem }) : <span>Item {childItemId}</span>}
|
||||
</div>
|
||||
<Remove onClick={onRemove} />
|
||||
</div>
|
||||
</li>
|
||||
@ -749,8 +821,10 @@ export function Remove(props: LemonButtonProps): JSX.Element {
|
||||
|
||||
export const Handle = forwardRef<HTMLButtonElement, LemonButtonProps>(function Handle_(props, ref) {
|
||||
return (
|
||||
<LemonButton type="secondary" fullWidth={false} ref={ref} {...props}>
|
||||
<IconBuilding />
|
||||
<LemonButton type="tertiary" fullWidth={false} ref={ref} {...props} className="self-start">
|
||||
<div>
|
||||
<IconDragHandle />
|
||||
</div>
|
||||
</LemonButton>
|
||||
)
|
||||
})
|
||||
|
@ -3919,6 +3919,9 @@
|
||||
"CustomChannelCondition": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"$ref": "#/definitions/CustomChannelField"
|
||||
},
|
||||
@ -3939,7 +3942,7 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["key", "op"],
|
||||
"required": ["key", "op", "id"],
|
||||
"type": "object"
|
||||
},
|
||||
"CustomChannelField": {
|
||||
@ -3959,14 +3962,17 @@
|
||||
"combiner": {
|
||||
"$ref": "#/definitions/FilterLogicalOperator"
|
||||
},
|
||||
"conditions": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/CustomChannelCondition"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": ["conditions", "combiner", "channel_type"],
|
||||
"required": ["items", "combiner", "channel_type", "id"],
|
||||
"type": "object"
|
||||
},
|
||||
"CustomEventConversionGoal": {
|
||||
@ -5343,6 +5349,29 @@
|
||||
"Day": {
|
||||
"type": "integer"
|
||||
},
|
||||
"DefaultChannelTypes": {
|
||||
"enum": [
|
||||
"Cross Network",
|
||||
"Paid Search",
|
||||
"Paid Social",
|
||||
"Paid Video",
|
||||
"Paid Shopping",
|
||||
"Paid Unknown",
|
||||
"Direct",
|
||||
"Organic Search",
|
||||
"Organic Social",
|
||||
"Organic Video",
|
||||
"Organic Shopping",
|
||||
"Push",
|
||||
"SMS",
|
||||
"Audio",
|
||||
"Email",
|
||||
"Referral",
|
||||
"Affiliate",
|
||||
"Unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"DurationType": {
|
||||
"enum": ["duration", "active_seconds", "inactive_seconds"],
|
||||
"type": "string"
|
||||
|
@ -2558,10 +2558,33 @@ export interface CustomChannelCondition {
|
||||
key: CustomChannelField
|
||||
value?: string | string[]
|
||||
op: CustomChannelOperator
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface CustomChannelRule {
|
||||
conditions: CustomChannelCondition[]
|
||||
items: CustomChannelCondition[]
|
||||
combiner: FilterLogicalOperator
|
||||
channel_type: string
|
||||
id: string
|
||||
}
|
||||
|
||||
export enum DefaultChannelTypes {
|
||||
CrossNetwork = 'Cross Network',
|
||||
PaidSearch = 'Paid Search',
|
||||
PaidSocial = 'Paid Social',
|
||||
PaidVideo = 'Paid Video',
|
||||
PaidShopping = 'Paid Shopping',
|
||||
PaidUnknown = 'Paid Unknown',
|
||||
Direct = 'Direct',
|
||||
OrganicSearch = 'Organic Search',
|
||||
OrganicSocial = 'Organic Social',
|
||||
OrganicVideo = 'Organic Video',
|
||||
OrganicShopping = 'Organic Shopping',
|
||||
Push = 'Push',
|
||||
SMS = 'SMS',
|
||||
Audio = 'Audio',
|
||||
Email = 'Email',
|
||||
Referral = 'Referral',
|
||||
Affiliate = 'Affiliate',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
|
@ -1,614 +1,252 @@
|
||||
// /* 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 {
|
||||
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>
|
||||
// </>
|
||||
// )
|
||||
import { VerticalNestedDND } from 'lib/lemon-ui/VerticalNestedDND/VerticalNestedDND'
|
||||
import { CustomChannelCondition, CustomChannelField, CustomChannelOperator, CustomChannelRule } from '~/queries/schema'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { genericOperatorMap, UnexpectedNeverError, uuid } from 'lib/utils'
|
||||
import { FilterLogicalOperator, PropertyFilterType, PropertyOperator } from '~/types'
|
||||
import isEqual from 'lodash.isequal'
|
||||
import debounce from 'lodash.debounce'
|
||||
import { LemonSelect } from 'lib/lemon-ui/LemonSelect'
|
||||
import { PropertyValue } from 'lib/components/PropertyFilters/components/PropertyValue'
|
||||
import { LemonInputSelect } from 'lib/lemon-ui/LemonInputSelect'
|
||||
|
||||
const combinerOptions = [
|
||||
{ label: 'All', value: FilterLogicalOperator.And },
|
||||
{ label: 'Any', value: FilterLogicalOperator.Or },
|
||||
]
|
||||
|
||||
const keyOptions = [
|
||||
{
|
||||
label: 'Referring domain',
|
||||
value: CustomChannelField.ReferringDomain,
|
||||
},
|
||||
{
|
||||
label: 'UTM Source',
|
||||
value: CustomChannelField.UTMSource,
|
||||
},
|
||||
{
|
||||
label: 'UTM Medium',
|
||||
value: CustomChannelField.UTMMedium,
|
||||
},
|
||||
{
|
||||
label: 'UTM Campaign',
|
||||
value: CustomChannelField.UTMCampaign,
|
||||
},
|
||||
]
|
||||
|
||||
const opOptions = Object.values(CustomChannelOperator).map((op) => {
|
||||
return {
|
||||
label: genericOperatorMap[op],
|
||||
value: op,
|
||||
}
|
||||
})
|
||||
|
||||
const isNullary = (operator: CustomChannelOperator): boolean => {
|
||||
return [CustomChannelOperator.IsSet, CustomChannelOperator.IsNotSet].includes(operator)
|
||||
}
|
||||
|
||||
function opToPropertyOperator(op: CustomChannelOperator): PropertyOperator {
|
||||
switch (op) {
|
||||
case CustomChannelOperator.Exact:
|
||||
return PropertyOperator.Exact
|
||||
case CustomChannelOperator.IsNot:
|
||||
return PropertyOperator.IsNot
|
||||
case CustomChannelOperator.IsSet:
|
||||
return PropertyOperator.IsSet
|
||||
case CustomChannelOperator.IsNotSet:
|
||||
return PropertyOperator.IsNotSet
|
||||
case CustomChannelOperator.IContains:
|
||||
return PropertyOperator.IContains
|
||||
case CustomChannelOperator.NotIContains:
|
||||
return PropertyOperator.NotIContains
|
||||
case CustomChannelOperator.Regex:
|
||||
return PropertyOperator.Regex
|
||||
case CustomChannelOperator.NotRegex:
|
||||
return PropertyOperator.NotRegex
|
||||
default:
|
||||
throw new UnexpectedNeverError(op)
|
||||
}
|
||||
}
|
||||
|
||||
function keyToSessionproperty(key: CustomChannelField): string {
|
||||
switch (key) {
|
||||
case CustomChannelField.ReferringDomain:
|
||||
return '$entry_referring_domain'
|
||||
case CustomChannelField.UTMSource:
|
||||
return '$entry_utm_source'
|
||||
case CustomChannelField.UTMMedium:
|
||||
return '$entry_utm_medium'
|
||||
case CustomChannelField.UTMCampaign:
|
||||
return '$entry_utm_campaign'
|
||||
default:
|
||||
throw new UnexpectedNeverError(key)
|
||||
}
|
||||
}
|
||||
|
||||
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] = useState(() =>
|
||||
(savedCustomChannelTypeRules || []).map((rule) => {
|
||||
return {
|
||||
...rule,
|
||||
id: rule.id || uuid(),
|
||||
items: (rule.items || []).map((item) => {
|
||||
return {
|
||||
...item,
|
||||
id: item.id || uuid(),
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const lastSavedRef = useRef<CustomChannelRule[]>(customChannelTypeRules)
|
||||
|
||||
const debouncedHandleChange = useMemo(
|
||||
() =>
|
||||
debounce(
|
||||
function handleChange(rules: CustomChannelRule[]): void {
|
||||
// strip conditions where the value is empty, and strip empty rules
|
||||
rules = rules
|
||||
.map((rule) => {
|
||||
return {
|
||||
...rule,
|
||||
conditions: rule.items.filter((condition) => condition.value !== ''),
|
||||
}
|
||||
})
|
||||
.filter((rule) => {
|
||||
return rule.conditions.length > 0 && rule.channel_type !== ''
|
||||
})
|
||||
|
||||
// don't update if the rules are the same as the last saved rules
|
||||
if (isEqual(rules, lastSavedRef.current)) {
|
||||
return
|
||||
}
|
||||
|
||||
updateCurrentTeam({ modifiers: { ...currentTeam?.modifiers, customChannelTypeRules: rules } })
|
||||
reportCustomChannelTypeRulesUpdated(rules.length)
|
||||
},
|
||||
500,
|
||||
{ trailing: true, maxWait: 2000 }
|
||||
),
|
||||
[updateCurrentTeam, reportCustomChannelTypeRulesUpdated, currentTeam?.modifiers]
|
||||
)
|
||||
|
||||
return (
|
||||
<ChannelTypeEditor
|
||||
handleChange={debouncedHandleChange}
|
||||
initialCustomChannelTypeRules={customChannelTypeRules}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ChannelTypeEditorProps {
|
||||
handleChange: (rules: CustomChannelRule[]) => void
|
||||
initialCustomChannelTypeRules: CustomChannelRule[]
|
||||
}
|
||||
|
||||
export function ChannelTypeEditor({
|
||||
handleChange,
|
||||
initialCustomChannelTypeRules,
|
||||
}: ChannelTypeEditorProps): JSX.Element {
|
||||
return (
|
||||
<VerticalNestedDND<CustomChannelCondition, CustomChannelRule>
|
||||
initialItems={initialCustomChannelTypeRules}
|
||||
renderContainerItem={(rule, { updateContainerItem }) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
Set Channel type to{' '}
|
||||
<LemonInputSelect
|
||||
mode="single"
|
||||
allowCustomValues={true}
|
||||
value={[rule.channel_type]}
|
||||
onChange={(channelType) =>
|
||||
updateContainerItem({
|
||||
...rule,
|
||||
channel_type: channelType[0],
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{rule.items.length <= 1 ? (
|
||||
'When'
|
||||
) : (
|
||||
<div>
|
||||
When{' '}
|
||||
<LemonSelect
|
||||
value={rule.combiner}
|
||||
options={combinerOptions}
|
||||
onChange={(combiner) => updateContainerItem({ ...rule, combiner })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
renderChildItem={(rule, { updateChildItem }) => {
|
||||
return (
|
||||
<div className="w-full space-y-2">
|
||||
<div className="flex flex-row space-x-2">
|
||||
<LemonSelect<CustomChannelField>
|
||||
value={rule.key}
|
||||
options={keyOptions}
|
||||
onChange={(key) => updateChildItem({ ...rule, key })}
|
||||
/>
|
||||
<LemonSelect<CustomChannelOperator>
|
||||
value={rule.op}
|
||||
options={opOptions}
|
||||
onChange={(op) => updateChildItem({ ...rule, op })}
|
||||
/>
|
||||
</div>
|
||||
{isNullary(rule.op) ? null : (
|
||||
<PropertyValue
|
||||
key={rule.key}
|
||||
propertyKey={keyToSessionproperty(rule.key)}
|
||||
type={PropertyFilterType.Session}
|
||||
onSet={(propertyValue: any) => {
|
||||
updateChildItem({ ...rule, value: propertyValue })
|
||||
}}
|
||||
operator={opToPropertyOperator(rule.op)}
|
||||
value={rule.value}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
createNewContainerItem={() => {
|
||||
return {
|
||||
id: uuid(),
|
||||
items: [
|
||||
{
|
||||
id: uuid(),
|
||||
key: CustomChannelField.ReferringDomain,
|
||||
op: CustomChannelOperator.Exact,
|
||||
value: [],
|
||||
},
|
||||
],
|
||||
channel_type: '',
|
||||
combiner: FilterLogicalOperator.And,
|
||||
}
|
||||
}}
|
||||
createNewChildItem={() => {
|
||||
return {
|
||||
id: uuid(),
|
||||
key: CustomChannelField.ReferringDomain,
|
||||
op: CustomChannelOperator.Exact,
|
||||
value: '',
|
||||
}
|
||||
}}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
//
|
||||
// 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>
|
||||
// );
|
||||
// }
|
||||
// )
|
||||
// );
|
||||
|
@ -77,7 +77,7 @@
|
||||
"@microlink/react-json-view": "^1.21.3",
|
||||
"@monaco-editor/react": "4.6.0",
|
||||
"@posthog/hogvm": "^1.0.60",
|
||||
"@posthog/icons": "0.9.1",
|
||||
"@posthog/icons": "0.9.2",
|
||||
"@posthog/plugin-scaffold": "^1.4.4",
|
||||
"@react-hook/size": "^2.1.2",
|
||||
"@rrweb/types": "2.0.0-alpha.13",
|
||||
|
@ -53,8 +53,8 @@ dependencies:
|
||||
specifier: ^1.0.60
|
||||
version: 1.0.60(luxon@3.5.0)
|
||||
'@posthog/icons':
|
||||
specifier: 0.9.1
|
||||
version: 0.9.1(react-dom@18.2.0)(react@18.2.0)
|
||||
specifier: 0.9.2
|
||||
version: 0.9.2(react-dom@18.2.0)(react@18.2.0)
|
||||
'@posthog/plugin-scaffold':
|
||||
specifier: ^1.4.4
|
||||
version: 1.4.4
|
||||
@ -5441,8 +5441,8 @@ packages:
|
||||
luxon: 3.5.0
|
||||
dev: false
|
||||
|
||||
/@posthog/icons@0.9.1(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-9zlU1H7MZm2gSh1JsDzM25km6VDc/Y7HdNf6RyP5sUiHCHVMKhQQ8TA2IMq55v/uTFRc5Yen6BagOUvunD2kqQ==}
|
||||
/@posthog/icons@0.9.2(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-SL79DvkAnVw+urYVrSPQY/CvY71NoSmYFhTH+gZFdwTYVe4zL7x6uADjPfmtr/wdyG3xj6M6CGIJlP00lW3Srg==}
|
||||
peerDependencies:
|
||||
react: '>=16.14.0'
|
||||
react-dom: '>=16.14.0'
|
||||
|
@ -19,7 +19,6 @@ from posthog.hogql.database.models import (
|
||||
)
|
||||
from posthog.hogql.database.schema.channel_type import (
|
||||
create_channel_type_expr,
|
||||
POSSIBLE_CHANNEL_TYPES,
|
||||
ChannelTypeExprs,
|
||||
)
|
||||
from posthog.hogql.database.schema.util.where_clause_extractor import SessionMinTimestampWhereClauseExtractorV1
|
||||
@ -30,7 +29,7 @@ from posthog.models.sessions.sql import (
|
||||
SELECT_SESSION_PROP_STRING_VALUES_SQL,
|
||||
)
|
||||
from posthog.queries.insight import insight_sync_execute
|
||||
from posthog.schema import BounceRatePageViewMode
|
||||
from posthog.schema import BounceRatePageViewMode, DefaultChannelTypes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from posthog.models.team import Team
|
||||
@ -410,7 +409,11 @@ def get_lazy_session_table_values_v1(key: str, search_term: Optional[str], team:
|
||||
# the sessions table does not have a properties json object like the events and person tables
|
||||
|
||||
if key == "$channel_type":
|
||||
return [[name] for name in POSSIBLE_CHANNEL_TYPES if not search_term or search_term.lower() in name.lower()]
|
||||
return [
|
||||
[entry.value]
|
||||
for entry in DefaultChannelTypes
|
||||
if not search_term or search_term.lower() in entry.value.lower()
|
||||
]
|
||||
|
||||
field_definition = LAZY_SESSIONS_FIELDS.get(key)
|
||||
if not field_definition:
|
||||
|
@ -19,7 +19,6 @@ from posthog.hogql.database.models import (
|
||||
)
|
||||
from posthog.hogql.database.schema.channel_type import (
|
||||
create_channel_type_expr,
|
||||
POSSIBLE_CHANNEL_TYPES,
|
||||
ChannelTypeExprs,
|
||||
)
|
||||
from posthog.hogql.database.schema.sessions_v1 import null_if_empty
|
||||
@ -32,7 +31,7 @@ from posthog.models.raw_sessions.sql import (
|
||||
RAW_SELECT_SESSION_PROP_STRING_VALUES_SQL_WITH_FILTER,
|
||||
)
|
||||
from posthog.queries.insight import insight_sync_execute
|
||||
from posthog.schema import BounceRatePageViewMode, CustomChannelRule
|
||||
from posthog.schema import BounceRatePageViewMode, CustomChannelRule, DefaultChannelTypes
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from posthog.models.team import Team
|
||||
@ -503,7 +502,9 @@ def get_lazy_session_table_values_v2(key: str, search_term: Optional[str], team:
|
||||
else:
|
||||
custom_channel_types = []
|
||||
default_channel_types = [
|
||||
name for name in POSSIBLE_CHANNEL_TYPES if not search_term or search_term.lower() in name.lower()
|
||||
entry.value
|
||||
for entry in DefaultChannelTypes
|
||||
if not search_term or search_term.lower() in entry.value.lower()
|
||||
]
|
||||
# merge the list, keep the order, and remove duplicates
|
||||
return [[name] for name in list(dict.fromkeys(custom_channel_types + default_channel_types))]
|
||||
|
@ -347,3 +347,28 @@ class TestGetLazySessionProperties(ClickhouseTestMixin, APIBaseTest):
|
||||
results = get_lazy_session_table_properties_v1(None)
|
||||
for prop in results:
|
||||
get_lazy_session_table_values_v1(key=prop["id"], team=TEAM, search_term=None)
|
||||
|
||||
def test_custom_channel_types(self):
|
||||
results = get_lazy_session_table_values_v1(key="$channel_type", team=self.team, search_term=None)
|
||||
# the custom channel types should be first, there's should be no duplicates, and any custom rules for existing
|
||||
# channel types should be bumped to the top
|
||||
assert results == [
|
||||
["Cross Network"],
|
||||
["Paid Search"],
|
||||
["Paid Social"],
|
||||
["Paid Video"],
|
||||
["Paid Shopping"],
|
||||
["Paid Unknown"],
|
||||
["Direct"],
|
||||
["Organic Search"],
|
||||
["Organic Social"],
|
||||
["Organic Video"],
|
||||
["Organic Shopping"],
|
||||
["Push"],
|
||||
["SMS"],
|
||||
["Audio"],
|
||||
["Email"],
|
||||
["Referral"],
|
||||
["Affiliate"],
|
||||
["Unknown"],
|
||||
]
|
||||
|
@ -705,9 +705,9 @@ class TestGetLazySessionProperties(ClickhouseTestMixin, APIBaseTest):
|
||||
def test_custom_channel_types(self):
|
||||
self.team.modifiers = {
|
||||
"customChannelTypeRules": [
|
||||
{"conditions": [], "combiner": FilterLogicalOperator.AND_, "channel_type": "Test Channel Type"},
|
||||
{"conditions": [], "combiner": FilterLogicalOperator.AND_, "channel_type": "Paid Social"},
|
||||
{"conditions": [], "combiner": FilterLogicalOperator.AND_, "channel_type": "Test Channel Type"},
|
||||
{"items": [], "combiner": FilterLogicalOperator.AND_, "channel_type": "Test Channel Type", "id": "1"},
|
||||
{"items": [], "combiner": FilterLogicalOperator.AND_, "channel_type": "Paid Social", "id": "2"},
|
||||
{"items": [], "combiner": FilterLogicalOperator.AND_, "channel_type": "Test Channel Type", "id": "3"},
|
||||
]
|
||||
}
|
||||
self.team.save()
|
||||
|
@ -675,6 +675,27 @@ class Day(RootModel[int]):
|
||||
root: int
|
||||
|
||||
|
||||
class DefaultChannelTypes(StrEnum):
|
||||
CROSS_NETWORK = "Cross Network"
|
||||
PAID_SEARCH = "Paid Search"
|
||||
PAID_SOCIAL = "Paid Social"
|
||||
PAID_VIDEO = "Paid Video"
|
||||
PAID_SHOPPING = "Paid Shopping"
|
||||
PAID_UNKNOWN = "Paid Unknown"
|
||||
DIRECT = "Direct"
|
||||
ORGANIC_SEARCH = "Organic Search"
|
||||
ORGANIC_SOCIAL = "Organic Social"
|
||||
ORGANIC_VIDEO = "Organic Video"
|
||||
ORGANIC_SHOPPING = "Organic Shopping"
|
||||
PUSH = "Push"
|
||||
SMS = "SMS"
|
||||
AUDIO = "Audio"
|
||||
EMAIL = "Email"
|
||||
REFERRAL = "Referral"
|
||||
AFFILIATE = "Affiliate"
|
||||
UNKNOWN = "Unknown"
|
||||
|
||||
|
||||
class DurationType(StrEnum):
|
||||
DURATION = "duration"
|
||||
ACTIVE_SECONDS = "active_seconds"
|
||||
@ -2085,6 +2106,7 @@ class CustomChannelCondition(BaseModel):
|
||||
model_config = ConfigDict(
|
||||
extra="forbid",
|
||||
)
|
||||
id: str
|
||||
key: CustomChannelField
|
||||
op: CustomChannelOperator
|
||||
value: Optional[Union[str, list[str]]] = None
|
||||
@ -2096,7 +2118,8 @@ class CustomChannelRule(BaseModel):
|
||||
)
|
||||
channel_type: str
|
||||
combiner: FilterLogicalOperator
|
||||
conditions: list[CustomChannelCondition]
|
||||
id: str
|
||||
items: list[CustomChannelCondition]
|
||||
|
||||
|
||||
class DataWarehousePersonPropertyFilter(BaseModel):
|
||||
|
Loading…
Reference in New Issue
Block a user