0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-24 00:47:50 +01:00

Nearly there, just needs a tiny refactor

This commit is contained in:
Robbie Coomber 2024-11-22 15:56:11 +00:00
parent 6e64697b08
commit 9b68150337
4 changed files with 198 additions and 105 deletions

View File

@ -50,6 +50,9 @@ export interface VerticalNestedDNDProps<ChildItem extends VDNDChildItem, Item ex
initialItems: Item[]
renderContainerItem: (item: Item, callbacks: { updateContainerItem: (item: Item) => void }) => JSX.Element | null
renderChildItem: (item: ChildItem, callbacks: { updateChildItem: (item: ChildItem) => void }) => JSX.Element | null
renderAddChildItem?: (item: Item, callbacks: { onAddChild: (id: UniqueIdentifier) => void }) => JSX.Element | null
renderAddContainerItem?: (callbacks: { onAddContainer: () => void }) => JSX.Element | null
renderAdditionalControls?: () => JSX.Element | null
createNewContainerItem(): Item
createNewChildItem(): ChildItem
onChange?(items: Item[]): void
@ -61,6 +64,9 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
renderChildItem,
createNewChildItem,
createNewContainerItem,
renderAddChildItem,
renderAddContainerItem,
renderAdditionalControls,
onChange,
}: VerticalNestedDNDProps<ChildItem, Item>): JSX.Element {
const [items, setItems] = useState(() => {
@ -365,12 +371,13 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
<DroppableContainer
key={containerId}
items={items[containerId].items || []}
onRemove={() => handleRemove(containerId)}
onRemove={() => handleRemoveContainer(containerId)}
renderContainerItem={renderContainerItem}
containerItemId={containerId}
item={items[containerId]}
onAddChild={handleAddChild}
updateContainerItem={updateContainerItem}
renderAddChildItem={renderAddChildItem}
>
<SortableContext
items={items[containerId].items || []}
@ -388,6 +395,7 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
getIndex={getIndex}
renderChildItem={renderChildItem}
updateChildItem={updateChildItem}
onRemove={handleRemoveChild}
item={value}
/>
)
@ -396,10 +404,15 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
</DroppableContainer>
))}
</SortableContext>
<div className="px-[calc(1.5rem+1px)]">
<LemonButton onClick={handleAddColumn} fullWidth={false} type="secondary">
Add container
</LemonButton>
<div className="px-[calc(1.5rem+1px)] flex flex-row justify-end space-x-2">
{renderAddContainerItem ? (
renderAddContainerItem({ onAddContainer: handleAddColumn })
) : (
<LemonButton onClick={handleAddColumn} fullWidth={false} type="primary">
Add container
</LemonButton>
)}
{renderAdditionalControls ? renderAdditionalControls() : null}
</div>
</div>
{createPortal(
@ -427,6 +440,7 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
renderChildItem={renderChildItem}
item={item}
updateChildItem={NOOP}
onRemove={NOOP}
/>
)
}
@ -452,16 +466,34 @@ export function VerticalNestedDND<ChildItem extends VDNDChildItem, Item extends
renderChildItem={renderChildItem}
item={item}
updateChildItem={NOOP}
onRemove={NOOP}
/>
))}
</Container>
)
}
function handleRemove(containerID: UniqueIdentifier): void {
function handleRemoveContainer(containerID: UniqueIdentifier): void {
setContainers((containers) => containers.filter((id) => id !== containerID))
}
function handleRemoveChild(childId: UniqueIdentifier): void {
setItems((items) => {
const containerId = findContainer(childId)
if (!containerId) {
return items
}
const container = items[containerId]
return {
...items,
[containerId]: {
...container,
items: container.items?.filter((item) => item.id !== childId),
},
}
})
}
function handleAddColumn(): void {
const newItem: Item = createNewContainerItem()
@ -540,6 +572,7 @@ interface SortableItemProps<Item extends VDNDChildItem> {
getIndex(id: UniqueIdentifier): number
renderChildItem(item: Item, callbacks: { updateChildItem: (item: Item) => void }): JSX.Element | null
updateChildItem(item: Item): void
onRemove(id: UniqueIdentifier): void
item: Item
}
@ -550,6 +583,7 @@ function SortableItem<Item extends VDNDChildItem>({
handle,
renderChildItem,
updateChildItem,
onRemove,
item,
}: SortableItemProps<Item>): JSX.Element {
const { setNodeRef, setActivatorNodeRef, listeners, isDragging, isSorting, transform, transition } = useSortable({
@ -572,6 +606,7 @@ function SortableItem<Item extends VDNDChildItem>({
listeners={listeners}
renderChildItem={renderChildItem}
updateChildItem={updateChildItem}
onRemove={onRemove}
item={item}
/>
)
@ -655,6 +690,10 @@ export interface ContainerProps<Item extends VNDNDContainerItem<any>> {
transform?: string
renderContainerItem(item: Item, callbacks: { updateContainerItem: (item: Item) => void }): JSX.Element | null
updateContainerItem(item: Item): void
renderAddChildItem?(
item: Item,
callbacks: { onAddChild: (containerId: UniqueIdentifier) => void }
): JSX.Element | null
item: Item
}
@ -679,6 +718,7 @@ export const Container = forwardRef(function Container_<Item extends VNDNDContai
renderContainerItem,
updateContainerItem,
item,
renderAddChildItem,
...props
}: ContainerProps<Item>,
ref: React.ForwardedRef<HTMLDivElement>
@ -700,7 +740,7 @@ export const Container = forwardRef(function Container_<Item extends VNDNDContai
onClick={onClick}
tabIndex={onClick ? 0 : undefined}
>
<div className="flex flex-row justify-between px-2 space-x-2">
<div className="flex flex-row justify-between px-2 space-x-2 items-start">
<Handle {...handleProps} />
<div className="flex-1">
{renderContainerItem ? (
@ -712,14 +752,18 @@ export const Container = forwardRef(function Container_<Item extends VNDNDContai
<Remove onClick={onRemove} />
</div>
{placeholder ? children : <ul className="space-y-2">{children}</ul>}
<div className="flex flex-row justify-between px-2 mb-2 space-x-2">
<LemonButton
onClick={onRemove ? () => onAddChild(item.id) : undefined}
fullWidth={false}
type="secondary"
>
Add child
</LemonButton>
<div className="flex flex-row justify-end px-2 mb-2 space-x-2">
{renderAddChildItem ? (
renderAddChildItem(item, { onAddChild })
) : (
<LemonButton
onClick={onRemove ? () => onAddChild(item.id) : undefined}
fullWidth={false}
type="secondary"
>
Add child
</LemonButton>
)}
</div>
</Component>
)
@ -742,7 +786,7 @@ export interface ChildItemProps<Item extends VDNDChildItem> {
wrapperStyle?: React.CSSProperties
childItemId: UniqueIdentifier
item: Item
onRemove?(): void
onRemove(id: UniqueIdentifier): void
renderChildItem(item: Item, callbacks: { updateChildItem: (item: Item) => void }): JSX.Element | null
updateChildItem(item: Item): void
}
@ -798,13 +842,13 @@ export const ChildItem = React.memo(
{...(!handle ? listeners : undefined)}
{...props}
tabIndex={!handle ? 0 : undefined}
className="flex flex-row justify-between w-full space-x-2"
className="flex flex-row justify-between w-full space-x-2 items-start"
>
<Handle {...handleProps} {...listeners} />
<div className="flex-1">
{renderChildItem ? renderChildItem(item, { updateChildItem }) : <span>Item {childItemId}</span>}
</div>
<Remove onClick={onRemove} />
<Remove onClick={() => onRemove(item.id)} />
</div>
</li>
)

View File

@ -1,5 +1,5 @@
import { BounceRatePageViewModeSetting } from 'scenes/settings/environment/BounceRatePageViewMode'
import { ChannelType } from 'scenes/settings/environment/ChannelType'
import { CustomChannelTypes } from 'scenes/settings/environment/CustomChannelTypes'
import { DeadClicksAutocaptureSettings } from 'scenes/settings/environment/DeadClicksAutocaptureSettings'
import { PersonsJoinMode } from 'scenes/settings/environment/PersonsJoinMode'
import { PersonsOnEvents } from 'scenes/settings/environment/PersonsOnEvents'
@ -215,8 +215,8 @@ export const SETTINGS_MAP: SettingSection[] = [
settings: [
{
id: 'channel-type',
title: 'Channel type',
component: <ChannelType />,
title: 'Custom channel type',
component: <CustomChannelTypes />,
},
],
flag: 'CUSTOM_CHANNEL_TYPE_RULES',

View File

@ -1,16 +1,24 @@
import { VerticalNestedDND } from 'lib/lemon-ui/VerticalNestedDND/VerticalNestedDND'
import { CustomChannelCondition, CustomChannelField, CustomChannelOperator, CustomChannelRule } from '~/queries/schema'
import { IconPlus } from '@posthog/icons'
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'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonInputSelect, LemonInputSelectOption } from 'lib/lemon-ui/LemonInputSelect'
import { LemonSelect } from 'lib/lemon-ui/LemonSelect'
import { VerticalNestedDND } from 'lib/lemon-ui/VerticalNestedDND/VerticalNestedDND'
import { genericOperatorMap, UnexpectedNeverError, uuid } from 'lib/utils'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import isEqual from 'lodash.isequal'
import { useMemo, useState } from 'react'
import { teamLogic } from 'scenes/teamLogic'
import {
CustomChannelCondition,
CustomChannelField,
CustomChannelOperator,
CustomChannelRule,
DefaultChannelTypes,
} from '~/queries/schema'
import { FilterLogicalOperator, PropertyFilterType, PropertyOperator } from '~/types'
const combinerOptions = [
{ label: 'All', value: FilterLogicalOperator.And },
@ -70,7 +78,7 @@ function opToPropertyOperator(op: CustomChannelOperator): PropertyOperator {
}
}
function keyToSessionproperty(key: CustomChannelField): string {
function keyToSessionProperty(key: CustomChannelField): string {
switch (key) {
case CustomChannelField.ReferringDomain:
return '$entry_referring_domain'
@ -85,86 +93,96 @@ function keyToSessionproperty(key: CustomChannelField): string {
}
}
export function ChannelType(): JSX.Element {
const sanitizeCustomChannelTypeRules = (rules: CustomChannelRule[]): CustomChannelRule[] => {
return (rules || [])
.map((rule) => {
return {
id: rule.id || uuid(),
channel_type: rule.channel_type,
combiner: rule.combiner,
items: (rule.items || [])
.map((condition) => ({
id: condition.id || uuid(),
key: condition.key,
op: condition.op,
value: condition.value,
}))
.filter((item) => item.key && item.op && item.value),
}
})
.filter((rule) => rule.channel_type && rule.items.length > 0)
}
export function CustomChannelTypes(): 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 [savedCustomChannelTypeRules, setSavedCustomChannelTypeRules] = useState(() =>
sanitizeCustomChannelTypeRules(
currentTeam?.modifiers?.customChannelTypeRules ??
currentTeam?.default_modifiers?.customChannelTypeRules ??
[]
)
)
const [customChannelTypeRules, setCustomChannelTypeRules] = useState(savedCustomChannelTypeRules)
const channelTypeOptions = useMemo((): LemonInputSelectOption[] => {
const optionsSet = new Set<string>()
customChannelTypeRules.forEach((rule) => {
optionsSet.add(rule.channel_type)
})
)
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]
)
Object.values(DefaultChannelTypes).forEach((channelType) => {
optionsSet.add(channelType)
})
return Array.from(optionsSet).map((channelType) => ({ label: channelType, key: channelType }))
}, [customChannelTypeRules])
return (
<ChannelTypeEditor
handleChange={debouncedHandleChange}
initialCustomChannelTypeRules={customChannelTypeRules}
/>
<div>
<ChannelTypeEditor
handleChange={setCustomChannelTypeRules}
initialCustomChannelTypeRules={customChannelTypeRules}
channelTypeOptions={channelTypeOptions}
onSave={() => {
updateCurrentTeam({
modifiers: { customChannelTypeRules: sanitizeCustomChannelTypeRules(customChannelTypeRules) },
})
reportCustomChannelTypeRulesUpdated(customChannelTypeRules.length)
setSavedCustomChannelTypeRules(customChannelTypeRules)
}}
isSaveDisabled={isEqual(customChannelTypeRules, savedCustomChannelTypeRules)}
/>
</div>
)
}
export interface ChannelTypeEditorProps {
handleChange: (rules: CustomChannelRule[]) => void
initialCustomChannelTypeRules: CustomChannelRule[]
channelTypeOptions: LemonInputSelectOption[]
isSaveDisabled: boolean
onSave: () => void
}
export function ChannelTypeEditor({
handleChange,
initialCustomChannelTypeRules,
channelTypeOptions,
isSaveDisabled,
onSave,
}: ChannelTypeEditorProps): JSX.Element {
return (
<VerticalNestedDND<CustomChannelCondition, CustomChannelRule>
initialItems={initialCustomChannelTypeRules}
renderContainerItem={(rule, { updateContainerItem }) => {
return (
<div className="flex flex-col">
<div>
Set Channel type to{' '}
<div className="flex flex-col space-y-2">
<div className="flex flex-row items-center space-x-2">
<span>Set Channel type to</span>
<LemonInputSelect
className="flex-1"
mode="single"
allowCustomValues={true}
value={[rule.channel_type]}
@ -174,22 +192,27 @@ export function ChannelTypeEditor({
channel_type: channelType[0],
})
}
options={channelTypeOptions}
placeholder="Enter a channel type name"
/>
</div>
<div>
{rule.items.length <= 1 ? (
'When'
) : (
<div>
When{' '}
<LemonSelect
value={rule.combiner}
options={combinerOptions}
onChange={(combiner) => updateContainerItem({ ...rule, combiner })}
/>
</div>
)}
</div>
{rule.items.length > 0 ? (
<div>
{rule.items.length == 1 ? (
'when this condition is met'
) : (
<div className="flex flex-row items-center space-x-2">
<span>When</span>
<LemonSelect
value={rule.combiner}
options={combinerOptions}
onChange={(combiner) => updateContainerItem({ ...rule, combiner })}
/>
<span>conditions are met</span>
</div>
)}
</div>
) : null}
</div>
)
}}
@ -211,18 +234,44 @@ export function ChannelTypeEditor({
{isNullary(rule.op) ? null : (
<PropertyValue
key={rule.key}
propertyKey={keyToSessionproperty(rule.key)}
propertyKey={keyToSessionProperty(rule.key)}
type={PropertyFilterType.Session}
onSet={(propertyValue: any) => {
updateChildItem({ ...rule, value: propertyValue })
}}
operator={opToPropertyOperator(rule.op)}
value={rule.value}
placeholder="Enter a value"
/>
)}
</div>
)
}}
renderAddChildItem={(rule, { onAddChild }) => {
return (
<LemonButton type="primary" onClick={() => onAddChild(rule.id)} icon={<IconPlus />}>
Add condition
</LemonButton>
)
}}
renderAddContainerItem={({ onAddContainer }) => {
return (
<LemonButton type="primary" onClick={onAddContainer} icon={<IconPlus />}>
Add rule
</LemonButton>
)
}}
renderAdditionalControls={() => {
return (
<LemonButton
onClick={onSave}
disabledReason={isSaveDisabled ? 'No changes to save' : undefined}
type="primary"
>
Save custom channel type rules
</LemonButton>
)
}}
createNewContainerItem={() => {
return {
id: uuid(),
@ -243,7 +292,7 @@ export function ChannelTypeEditor({
id: uuid(),
key: CustomChannelField.ReferringDomain,
op: CustomChannelOperator.Exact,
value: '',
value: [],
}
}}
onChange={handleChange}

View File

@ -147,7 +147,7 @@ def custom_condition_to_expr(
def custom_rule_to_expr(custom_rule: CustomChannelRule, source_exprs: ChannelTypeExprs) -> ast.Expr:
conditions: list[Union[ast.Expr | ast.Call]] = []
for condition in custom_rule.conditions:
for condition in custom_rule.items:
if condition.key == CustomChannelField.UTM_SOURCE:
expr = source_exprs.source
elif condition.key == CustomChannelField.UTM_MEDIUM: