From 9b681503372deb5766e2b254e404e0bc565fb60e Mon Sep 17 00:00:00 2001 From: Robbie Coomber Date: Fri, 22 Nov 2024 15:56:11 +0000 Subject: [PATCH] Nearly there, just needs a tiny refactor --- .../VerticalNestedDND/VerticalNestedDND.tsx | 80 +++++-- frontend/src/scenes/settings/SettingsMap.tsx | 6 +- ...ChannelType.tsx => CustomChannelTypes.tsx} | 215 +++++++++++------- posthog/hogql/database/schema/channel_type.py | 2 +- 4 files changed, 198 insertions(+), 105 deletions(-) rename frontend/src/scenes/settings/environment/{ChannelType.tsx => CustomChannelTypes.tsx} (54%) diff --git a/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx b/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx index b6db0dd6954..77d6e0c4bf7 100644 --- a/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx +++ b/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx @@ -50,6 +50,9 @@ export interface VerticalNestedDNDProps 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): JSX.Element { const [items, setItems] = useState(() => { @@ -365,12 +371,13 @@ export function VerticalNestedDND handleRemove(containerId)} + onRemove={() => handleRemoveContainer(containerId)} renderContainerItem={renderContainerItem} containerItemId={containerId} item={items[containerId]} onAddChild={handleAddChild} updateContainerItem={updateContainerItem} + renderAddChildItem={renderAddChildItem} > ) @@ -396,10 +404,15 @@ export function VerticalNestedDND ))} -
- - Add container - +
+ {renderAddContainerItem ? ( + renderAddContainerItem({ onAddContainer: handleAddColumn }) + ) : ( + + Add container + + )} + {renderAdditionalControls ? renderAdditionalControls() : null}
{createPortal( @@ -427,6 +440,7 @@ export function VerticalNestedDND ) } @@ -452,16 +466,34 @@ export function VerticalNestedDND ))} ) } - 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 { 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({ handle, renderChildItem, updateChildItem, + onRemove, item, }: SortableItemProps): JSX.Element { const { setNodeRef, setActivatorNodeRef, listeners, isDragging, isSorting, transform, transition } = useSortable({ @@ -572,6 +606,7 @@ function SortableItem({ listeners={listeners} renderChildItem={renderChildItem} updateChildItem={updateChildItem} + onRemove={onRemove} item={item} /> ) @@ -655,6 +690,10 @@ export interface ContainerProps> { 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_, ref: React.ForwardedRef @@ -700,7 +740,7 @@ export const Container = forwardRef(function Container_ -
+
{renderContainerItem ? ( @@ -712,14 +752,18 @@ export const Container = forwardRef(function Container_
{placeholder ? children :
    {children}
} -
- onAddChild(item.id) : undefined} - fullWidth={false} - type="secondary" - > - Add child - +
+ {renderAddChildItem ? ( + renderAddChildItem(item, { onAddChild }) + ) : ( + onAddChild(item.id) : undefined} + fullWidth={false} + type="secondary" + > + Add child + + )}
) @@ -742,7 +786,7 @@ export interface ChildItemProps { 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" >
{renderChildItem ? renderChildItem(item, { updateChildItem }) : Item {childItemId}}
- + onRemove(item.id)} />
) diff --git a/frontend/src/scenes/settings/SettingsMap.tsx b/frontend/src/scenes/settings/SettingsMap.tsx index 0ace49b1ac2..3e4946ea1f8 100644 --- a/frontend/src/scenes/settings/SettingsMap.tsx +++ b/frontend/src/scenes/settings/SettingsMap.tsx @@ -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: , + title: 'Custom channel type', + component: , }, ], flag: 'CUSTOM_CHANNEL_TYPE_RULES', diff --git a/frontend/src/scenes/settings/environment/ChannelType.tsx b/frontend/src/scenes/settings/environment/CustomChannelTypes.tsx similarity index 54% rename from frontend/src/scenes/settings/environment/ChannelType.tsx rename to frontend/src/scenes/settings/environment/CustomChannelTypes.tsx index c3f4088f49b..f0fe1b38958 100644 --- a/frontend/src/scenes/settings/environment/ChannelType.tsx +++ b/frontend/src/scenes/settings/environment/CustomChannelTypes.tsx @@ -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() + customChannelTypeRules.forEach((rule) => { + optionsSet.add(rule.channel_type) }) - ) - - const lastSavedRef = useRef(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 ( - +
+ { + updateCurrentTeam({ + modifiers: { customChannelTypeRules: sanitizeCustomChannelTypeRules(customChannelTypeRules) }, + }) + reportCustomChannelTypeRulesUpdated(customChannelTypeRules.length) + setSavedCustomChannelTypeRules(customChannelTypeRules) + }} + isSaveDisabled={isEqual(customChannelTypeRules, savedCustomChannelTypeRules)} + /> +
) } 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 ( initialItems={initialCustomChannelTypeRules} renderContainerItem={(rule, { updateContainerItem }) => { return ( -
-
- Set Channel type to{' '} +
+
+ Set Channel type to
-
- {rule.items.length <= 1 ? ( - 'When' - ) : ( -
- When{' '} - updateContainerItem({ ...rule, combiner })} - /> -
- )} -
+ {rule.items.length > 0 ? ( +
+ {rule.items.length == 1 ? ( + 'when this condition is met' + ) : ( +
+ When + updateContainerItem({ ...rule, combiner })} + /> + conditions are met +
+ )} +
+ ) : null}
) }} @@ -211,18 +234,44 @@ export function ChannelTypeEditor({ {isNullary(rule.op) ? null : ( { updateChildItem({ ...rule, value: propertyValue }) }} operator={opToPropertyOperator(rule.op)} value={rule.value} + placeholder="Enter a value" /> )}
) }} + renderAddChildItem={(rule, { onAddChild }) => { + return ( + onAddChild(rule.id)} icon={}> + Add condition + + ) + }} + renderAddContainerItem={({ onAddContainer }) => { + return ( + }> + Add rule + + ) + }} + renderAdditionalControls={() => { + return ( + + Save custom channel type rules + + ) + }} createNewContainerItem={() => { return { id: uuid(), @@ -243,7 +292,7 @@ export function ChannelTypeEditor({ id: uuid(), key: CustomChannelField.ReferringDomain, op: CustomChannelOperator.Exact, - value: '', + value: [], } }} onChange={handleChange} diff --git a/posthog/hogql/database/schema/channel_type.py b/posthog/hogql/database/schema/channel_type.py index c0579f86f83..e0807f6a5e1 100644 --- a/posthog/hogql/database/schema/channel_type.py +++ b/posthog/hogql/database/schema/channel_type.py @@ -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: