fix: broken add notifications rule

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-02-27 09:37:43 +01:00
parent 8b18b86deb
commit a42adcdbfb
2 changed files with 84 additions and 83 deletions

View File

@@ -1,4 +1,3 @@
import { cn } from '@/utils/cn';
import { Slot } from '@radix-ui/react-slot'; import { Slot } from '@radix-ui/react-slot';
import { Link, type LinkComponentProps } from '@tanstack/react-router'; import { Link, type LinkComponentProps } from '@tanstack/react-router';
import type { VariantProps } from 'class-variance-authority'; import type { VariantProps } from 'class-variance-authority';
@@ -6,9 +5,10 @@ import { cva } from 'class-variance-authority';
import type { LucideIcon } from 'lucide-react'; import type { LucideIcon } from 'lucide-react';
import * as React from 'react'; import * as React from 'react';
import { Spinner, type SpinnerProps } from './spinner'; import { Spinner, type SpinnerProps } from './spinner';
import { cn } from '@/utils/cn';
const buttonVariants = cva( const buttonVariants = cva(
'inline-flex flex-shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:translate-y-[-0.5px] transition-all', 'inline-flex flex-shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background transition-all hover:translate-y-[-0.5px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{ {
variants: { variants: {
variant: { variant: {
@@ -33,7 +33,7 @@ const buttonVariants = cva(
variant: 'default', variant: 'default',
size: 'sm', size: 'sm',
}, },
}, }
); );
export interface ButtonProps export interface ButtonProps
@@ -52,7 +52,10 @@ export interface ButtonProps
function fixHeight({ function fixHeight({
autoHeight, autoHeight,
size, size,
}: { autoHeight?: boolean; size: ButtonProps['size'] }) { }: {
autoHeight?: boolean;
size: ButtonProps['size'];
}) {
if (autoHeight) { if (autoHeight) {
switch (size) { switch (size) {
case 'lg': case 'lg':
@@ -84,9 +87,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
responsive, responsive,
autoHeight, autoHeight,
loadingAbsolute, loadingAbsolute,
type = 'button',
...props ...props
}, },
ref, ref
) => { ) => {
const Comp = asChild ? Slot : 'button'; const Comp = asChild ? Slot : 'button';
const Icon = loading ? null : (icon ?? null); const Icon = loading ? null : (icon ?? null);
@@ -99,31 +103,32 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
className={cn( className={cn(
buttonVariants({ variant, size, className }), buttonVariants({ variant, size, className }),
fixHeight({ autoHeight, size }), fixHeight({ autoHeight, size }),
loadingAbsolute && 'relative', loadingAbsolute && 'relative'
)} )}
ref={ref}
disabled={loading || disabled} disabled={loading || disabled}
ref={ref}
type={type}
{...props} {...props}
> >
{loading && ( {loading && (
<div <div
className={cn( className={cn(
loadingAbsolute && loadingAbsolute &&
'absolute top-0 left-0 right-0 bottom-0 center-center backdrop-blur bg-background/10', 'center-center absolute top-0 right-0 bottom-0 left-0 bg-background/10 backdrop-blur'
)} )}
> >
<Spinner <Spinner
type={loadingType}
size={spinnerSize}
speed={loadingSpeed}
variant={
variant === 'default' || variant === 'cta' ? 'white' : 'default'
}
className={cn( className={cn(
'flex-shrink-0', 'flex-shrink-0',
size !== 'icon' && responsive && 'mr-0 sm:mr-2', size !== 'icon' && responsive && 'mr-0 sm:mr-2',
size !== 'icon' && !responsive && 'mr-2', size !== 'icon' && !responsive && 'mr-2'
)} )}
size={spinnerSize}
speed={loadingSpeed}
type={loadingType}
variant={
variant === 'default' || variant === 'cta' ? 'white' : 'default'
}
/> />
</div> </div>
)} )}
@@ -132,7 +137,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
className={cn( className={cn(
'h-4 w-4 flex-shrink-0', 'h-4 w-4 flex-shrink-0',
size !== 'icon' && responsive && 'mr-0 sm:mr-2', size !== 'icon' && responsive && 'mr-0 sm:mr-2',
size !== 'icon' && !responsive && 'mr-2', size !== 'icon' && !responsive && 'mr-2'
)} )}
/> />
)} )}
@@ -143,7 +148,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
)} )}
</Comp> </Comp>
); );
}, }
); );
Button.displayName = 'Button'; Button.displayName = 'Button';
@@ -180,24 +185,24 @@ const LinkButton = ({
<> <>
{loading && ( {loading && (
<Spinner <Spinner
type={loadingType}
size={spinnerSize}
speed={loadingSpeed}
variant={
variant === 'default' || variant === 'cta' ? 'white' : 'default'
}
className={cn( className={cn(
'flex-shrink-0', 'flex-shrink-0',
responsive && 'mr-0 sm:mr-2', responsive && 'mr-0 sm:mr-2',
!responsive && 'mr-2', !responsive && 'mr-2'
)} )}
size={spinnerSize}
speed={loadingSpeed}
type={loadingType}
variant={
variant === 'default' || variant === 'cta' ? 'white' : 'default'
}
/> />
)} )}
{Icon && ( {Icon && (
<Icon <Icon
className={cn( className={cn(
'mr-2 h-4 w-4 flex-shrink-0', 'mr-2 h-4 w-4 flex-shrink-0',
responsive && 'mr-0 sm:mr-2', responsive && 'mr-0 sm:mr-2'
)} )}
/> />
)} )}

View File

@@ -1,28 +1,7 @@
import type { RouterOutputs } from '@/trpc/client';
import { SheetContent } from '@/components/ui/sheet';
import { useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { popModal } from '.';
import { ModalHeader } from './Modal/Container';
import { ColorSquare } from '@/components/color-square';
import { InputWithLabel, WithLabel } from '@/components/forms/input-with-label';
import { PureFilterItem } from '@/components/report/sidebar/filters/FilterItem';
import { Button } from '@/components/ui/button';
import { Combobox } from '@/components/ui/combobox';
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
import { ComboboxEvents } from '@/components/ui/combobox-events';
import { Textarea } from '@/components/ui/textarea';
import { useAppParams } from '@/hooks/use-app-params';
import { useEventNames } from '@/hooks/use-event-names';
import { useEventProperties } from '@/hooks/use-event-properties';
import { useTRPC } from '@/integrations/trpc/react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { shortId } from '@openpanel/common'; import { shortId } from '@openpanel/common';
import { zCreateNotificationRule } from '@openpanel/validation'; import { zCreateNotificationRule } from '@openpanel/validation';
import { useMutation, useQuery } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { FilterIcon, PlusIcon, SaveIcon, TrashIcon } from 'lucide-react'; import { FilterIcon, PlusIcon, SaveIcon, TrashIcon } from 'lucide-react';
import { import {
Controller, Controller,
@@ -32,7 +11,24 @@ import {
useForm, useForm,
useWatch, useWatch,
} from 'react-hook-form'; } from 'react-hook-form';
import { toast } from 'sonner';
import type { z } from 'zod'; import type { z } from 'zod';
import { popModal } from '.';
import { ModalHeader } from './Modal/Container';
import { ColorSquare } from '@/components/color-square';
import { InputWithLabel, WithLabel } from '@/components/forms/input-with-label';
import { PureFilterItem } from '@/components/report/sidebar/filters/FilterItem';
import { Button } from '@/components/ui/button';
import { Combobox } from '@/components/ui/combobox';
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
import { ComboboxEvents } from '@/components/ui/combobox-events';
import { SheetContent } from '@/components/ui/sheet';
import { Textarea } from '@/components/ui/textarea';
import { useAppParams } from '@/hooks/use-app-params';
import { useEventNames } from '@/hooks/use-event-names';
import { useEventProperties } from '@/hooks/use-event-properties';
import { useTRPC } from '@/integrations/trpc/react';
import type { RouterOutputs } from '@/trpc/client';
interface Props { interface Props {
rule?: RouterOutputs['notification']['rules'][number]; rule?: RouterOutputs['notification']['rules'][number];
@@ -71,21 +67,21 @@ export default function AddNotificationRule({ rule }: Props) {
trpc.notification.createOrUpdateRule.mutationOptions({ trpc.notification.createOrUpdateRule.mutationOptions({
onSuccess() { onSuccess() {
toast.success( toast.success(
rule ? 'Notification rule updated' : 'Notification rule created', rule ? 'Notification rule updated' : 'Notification rule created'
); );
client.refetchQueries( client.refetchQueries(
trpc.notification.rules.queryFilter({ trpc.notification.rules.queryFilter({
projectId, projectId,
}), })
); );
popModal(); popModal();
}, },
}), })
); );
const integrationsQuery = useQuery( const integrationsQuery = useQuery(
trpc.integration.list.queryOptions({ trpc.integration.list.queryOptions({
organizationId: organizationId!, organizationId: organizationId!,
}), })
); );
const eventsArray = useFieldArray({ const eventsArray = useFieldArray({
@@ -106,18 +102,18 @@ export default function AddNotificationRule({ rule }: Props) {
return ( return (
<SheetContent className="[&>button.absolute]:hidden"> <SheetContent className="[&>button.absolute]:hidden">
<ModalHeader title={rule ? 'Edit rule' : 'Create rule'} /> <ModalHeader title={rule ? 'Edit rule' : 'Create rule'} />
<form onSubmit={form.handleSubmit(onSubmit)} className="col gap-4"> <form className="col gap-4" onSubmit={form.handleSubmit(onSubmit)}>
<InputWithLabel <InputWithLabel
error={form.formState.errors.name?.message}
label="Rule name" label="Rule name"
placeholder="Eg. Sign ups on android" placeholder="Eg. Sign ups on android"
error={form.formState.errors.name?.message}
{...form.register('name')} {...form.register('name')}
/> />
<WithLabel <WithLabel
label="Type"
// @ts-expect-error // @ts-expect-error
error={form.formState.errors.config?.type.message} error={form.formState.errors.config?.type.message}
label="Type"
> >
<Controller <Controller
control={form.control} control={form.control}
@@ -126,7 +122,6 @@ export default function AddNotificationRule({ rule }: Props) {
<Combobox <Combobox
{...field} {...field}
className="w-full" className="w-full"
placeholder="Select type"
// @ts-expect-error // @ts-expect-error
error={form.formState.errors.config?.type.message} error={form.formState.errors.config?.type.message}
items={[ items={[
@@ -139,6 +134,7 @@ export default function AddNotificationRule({ rule }: Props) {
value: 'funnel', value: 'funnel',
}, },
]} ]}
placeholder="Select type"
/> />
)} )}
/> />
@@ -148,16 +144,15 @@ export default function AddNotificationRule({ rule }: Props) {
{eventsArray.fields.map((field, index) => { {eventsArray.fields.map((field, index) => {
return ( return (
<EventField <EventField
key={field.id}
form={form} form={form}
index={index} index={index}
key={field.id}
remove={() => eventsArray.remove(index)} remove={() => eventsArray.remove(index)}
/> />
); );
})} })}
<Button <Button
className="self-start" className="self-start"
variant={'outline'}
icon={PlusIcon} icon={PlusIcon}
onClick={() => onClick={() =>
eventsArray.append({ eventsArray.append({
@@ -166,6 +161,7 @@ export default function AddNotificationRule({ rule }: Props) {
segment: 'event', segment: 'event',
}) })
} }
variant={'outline'}
> >
Add event Add event
</Button> </Button>
@@ -173,7 +169,6 @@ export default function AddNotificationRule({ rule }: Props) {
</WithLabel> </WithLabel>
<WithLabel <WithLabel
label="Template"
info={ info={
<div className="prose dark:prose-invert"> <div className="prose dark:prose-invert">
<p> <p>
@@ -197,7 +192,7 @@ export default function AddNotificationRule({ rule }: Props) {
profile property profile property
</li> </li>
<li> <li>
<div className="flex gap-x-2 flex-wrap"> <div className="flex flex-wrap gap-x-2">
And many more... And many more...
<code>profileId</code> <code>profileId</code>
<code>createdAt</code> <code>createdAt</code>
@@ -220,6 +215,7 @@ export default function AddNotificationRule({ rule }: Props) {
</ul> </ul>
</div> </div>
} }
label="Template"
> >
<Textarea <Textarea
{...form.register('template')} {...form.register('template')}
@@ -234,19 +230,19 @@ export default function AddNotificationRule({ rule }: Props) {
<WithLabel label="Integrations"> <WithLabel label="Integrations">
<ComboboxAdvanced <ComboboxAdvanced
{...field} {...field}
value={field.value ?? []}
className="w-full" className="w-full"
placeholder="Pick integrations"
items={integrations.map((integration) => ({ items={integrations.map((integration) => ({
label: integration.name, label: integration.name,
value: integration.id, value: integration.id,
}))} }))}
placeholder="Pick integrations"
value={field.value ?? []}
/> />
</WithLabel> </WithLabel>
)} )}
/> />
<Button type="submit" icon={SaveIcon}> <Button icon={SaveIcon} type="submit">
{rule ? 'Update' : 'Create'} {rule ? 'Update' : 'Create'}
</Button> </Button>
</form> </form>
@@ -276,27 +272,24 @@ function EventField({
const properties = useEventProperties({ projectId }); const properties = useEventProperties({ projectId });
return ( return (
<div className="border bg-def-100 rounded"> <div className="rounded border bg-def-100">
<div className="row gap-2 items-center p-2"> <div className="row items-center gap-2 p-2">
<ColorSquare>{index + 1}</ColorSquare> <ColorSquare>{index + 1}</ColorSquare>
<Controller <Controller
control={form.control} control={form.control}
name={`config.events.${index}.name`} name={`config.events.${index}.name`}
render={({ field }) => ( render={({ field }) => (
<ComboboxEvents <ComboboxEvents
searchable
className="flex-1" className="flex-1"
value={field.value}
placeholder="Select event"
onChange={field.onChange}
items={eventNames} items={eventNames}
onChange={field.onChange}
placeholder="Select event"
searchable
value={field.value}
/> />
)} )}
/> />
<Combobox <Combobox
searchable
placeholder="Select a filter"
value=""
items={properties.map((item) => ({ items={properties.map((item) => ({
label: item, label: item,
value: item, value: item,
@@ -309,27 +302,33 @@ function EventField({
value: [], value: [],
}); });
}} }}
placeholder="Select a filter"
searchable
value=""
> >
<Button variant={'outline'} icon={FilterIcon} size={'icon'} /> <Button icon={FilterIcon} size={'icon'} variant={'outline'} />
</Combobox> </Combobox>
<Button <Button
className="text-destructive"
icon={TrashIcon}
onClick={() => { onClick={() => {
remove(); remove();
}} }}
variant={'outline'}
className="text-destructive"
icon={TrashIcon}
size={'icon'} size={'icon'}
variant={'outline'}
/> />
</div> </div>
{filtersArray.fields.map((filter, index) => { {filtersArray.fields.map((filter, index) => {
return ( return (
<div key={filter.id} className="p-2 border-t"> <div className="border-t p-2" key={filter.id}>
<PureFilterItem <PureFilterItem
eventName={eventName} eventName={eventName}
filter={filter} filter={filter}
onRemove={() => { onChangeOperator={(operator) => {
filtersArray.remove(index); filtersArray.update(index, {
...filter,
operator,
});
}} }}
onChangeValue={(value) => { onChangeValue={(value) => {
filtersArray.update(index, { filtersArray.update(index, {
@@ -337,11 +336,8 @@ function EventField({
value, value,
}); });
}} }}
onChangeOperator={(operator) => { onRemove={() => {
filtersArray.update(index, { filtersArray.remove(index);
...filter,
operator,
});
}} }}
/> />
</div> </div>