Skip to content

Commit

Permalink
fix(dashboard): In app editor fixes & new in app previews (#7094)
Browse files Browse the repository at this point in the history
  • Loading branch information
desiprisg authored Nov 21, 2024
1 parent 525576c commit 62ad1a1
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 117 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const AvatarPicker = forwardRef<HTMLInputElement, AvatarPickerProps>(({ n
<PopoverTrigger asChild>
<Button variant="outline" size="icon" className="text-foreground-600 relative size-full overflow-hidden">
{value ? (
<Avatar className="p-px">
<Avatar className="bg-transparent p-px">
<AvatarImage src={value as string} />
</Avatar>
) : (
Expand Down Expand Up @@ -91,7 +91,12 @@ export const AvatarPicker = forwardRef<HTMLInputElement, AvatarPickerProps>(({ n
<TextSeparator text="or" />
<div className="grid grid-cols-6 gap-4">
{predefinedAvatars.map((url, index) => (
<Button key={index} variant="ghost" className="p-0" onClick={() => handlePredefinedAvatarClick(url)}>
<Button
key={index}
variant="ghost"
className="rounded-full p-0"
onClick={() => handlePredefinedAvatarClick(url)}
>
<Avatar>
<AvatarImage src={url} />
</Avatar>
Expand Down
229 changes: 139 additions & 90 deletions apps/dashboard/src/components/workflow-editor/in-app-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,113 +1,170 @@
import { HTMLAttributes, useMemo } from 'react';
import { parseMarkdownIntoTokens } from '@novu/js/internal';
import { ChannelTypeEnum, GeneratePreviewResponseDto, InAppRenderOutput } from '@novu/shared';

import { InboxArrowDown } from '@/components/icons/inbox-arrow-down';
import { InboxBell } from '@/components/icons/inbox-bell';
import { InboxEllipsis } from '@/components/icons/inbox-ellipsis';
import { InboxSettings } from '@/components/icons/inbox-settings';
import { Button } from '@/components/primitives/button';
import { Button, ButtonProps } from '@/components/primitives/button';
import { cn } from '@/utils/ui';
import { Skeleton } from '../primitives/skeleton';

type InAppPreviewProps = HTMLAttributes<HTMLDivElement> & {
truncateBody?: boolean;
data?: GeneratePreviewResponseDto;
isLoading?: boolean;
type InAppPreviewBellProps = HTMLAttributes<HTMLDivElement>;
export const InAppPreviewBell = (props: InAppPreviewBellProps) => {
const { className, ...rest } = props;
return (
<div className={cn('flex items-center justify-end p-2 text-neutral-300', className)} {...rest}>
<span className="relative rounded-lg bg-neutral-50 p-1">
<InboxBell className="relative size-5" />
<div className="bg-primary border-background absolute right-1 top-1 h-2 w-2 translate-y-[1px] rounded-full border border-solid" />
</span>
</div>
);
};

type InAppPreviewProps = HTMLAttributes<HTMLDivElement>;
export const InAppPreview = (props: InAppPreviewProps) => {
const { className, truncateBody: truncate = false, data, isLoading, ...rest } = props;
const { className, ...rest } = props;

return (
<div
className={cn(
'border-foreground-200 to-background/90 pointer-events-none relative flex h-full w-full flex-col rounded-xl rounded-b-none border border-b-0 border-dashed p-1',
'border-foreground-200 to-background/90 pointer-events-none relative mx-auto flex h-full w-full flex-col gap-4 rounded-xl px-2 py-3 shadow-sm',
className
)}
{...rest}
>
<div className="absolute -left-0.5 bottom-0 top-0 z-10 h-full w-[calc(100%+4px)] bg-gradient-to-t from-[rgb(255,255,255)] from-5% to-95%" />
<div className="z-20 flex h-6 items-center justify-end px-2 text-neutral-300">
<span className="relative p-1">
<InboxBell className="relative size-4" />
<div className="bg-primary border-background absolute right-1 top-1 h-2 w-2 translate-y-[1px] rounded-full border border-solid" />
</span>
/>
);
};

type InAppPreviewHeaderProps = HTMLAttributes<HTMLDivElement>;
export const InAppPreviewHeader = (props: InAppPreviewHeaderProps) => {
const { className, ...rest } = props;

return (
<div className={cn('z-20 flex items-center justify-between px-2 text-neutral-300', className)} {...rest}>
<div className="flex items-center gap-2">
<span className="text-xl font-medium">Inbox</span>
<InboxArrowDown />
</div>
<div className="my-0.5 w-full border-b border-b-neutral-100" />
<div className="z-20 flex items-center justify-between px-2 text-neutral-300">
<div className="flex items-center gap-2">
<span className="text-xl font-medium">Inbox</span>
<InboxArrowDown />
</div>
<div className="flex items-center gap-2">
<span className="p-0.5">
<InboxEllipsis />
</span>
<span className="p-0.5">
<InboxSettings />
</span>
</div>
<div className="flex items-center gap-2">
<span className="p-0.5">
<InboxEllipsis />
</span>
<span className="p-0.5">
<InboxSettings />
</span>
</div>
{isLoading && !data && (
<div className="bg-neutral-alpha-50 z-20 mt-2 rounded-lg px-2 py-2">
<div className="mb-2 flex items-center gap-2">
<Skeleton className="h-5 min-w-5 rounded-full" />
<Skeleton className="h-5 w-full" />
</div>
<Skeleton className="h-5 w-full" />
</div>
)}
{data && data.result?.type === ChannelTypeEnum.IN_APP && (
<div className="bg-neutral-alpha-50 z-20 mt-2 rounded-lg px-2 py-2">
<div className="mb-2 flex items-center gap-2">
{data.result.preview.avatar && (
<img src={data.result.preview.avatar} alt="avatar" className="bg-background h-5 min-w-5 rounded-full" />
)}
{data.result.preview.subject ? (
<Subject text={data.result.preview.subject} className={truncate ? 'truncate' : ''} />
) : (
<Body text={data.result.preview.body} className={truncate ? 'truncate' : ''} />
)}
</div>

{data.result.preview.subject && (
<Body text={data.result.preview.body} className={truncate ? 'truncate' : ''} />
)}

{(data.result.preview.primaryAction || data.result.preview.secondaryAction) && (
<div className="mt-3 flex items-center justify-start gap-1 overflow-hidden">
{data.result.preview.primaryAction && (
<Button
className="overflow-hidden text-xs font-medium shadow-none"
type="button"
variant="primary"
size="xs"
>
<span className="overflow-hidden text-ellipsis">
{(data.result.preview as InAppRenderOutput).primaryAction?.label}
</span>
</Button>
)}
{data.result.preview.secondaryAction && (
<Button variant="outline" className="overflow-hidden text-xs font-medium" type="button" size="xs">
<span className="overflow-hidden text-ellipsis">
{(data.result.preview as InAppRenderOutput).secondaryAction?.label}
</span>
</Button>
)}
</div>
)}
</div>
)}
</div>
);
};

type MarkdownProps = Omit<HTMLAttributes<HTMLParagraphElement>, 'children'> & { children: string };
type InAppPreviewAvatarProps = HTMLAttributes<HTMLImageElement> & {
src?: string;
isPending?: boolean;
};
export const InAppPreviewAvatar = (props: InAppPreviewAvatarProps) => {
const { className, isPending, src, ...rest } = props;

if (isPending) {
return <Skeleton className="size-8 shrink-0 rounded-full" />;
}

if (!src) {
return null;
}

return <img src={src} alt="avatar" className={cn('bg-background size-7 rounded-full')} {...rest} />;
};

type InAppPreviewNotificationProps = HTMLAttributes<HTMLDivElement>;
export const InAppPreviewNotification = (props: InAppPreviewNotificationProps) => {
const { className, ...rest } = props;

return <div className={cn('flex gap-2', className)} {...rest} />;
};

type InAppPreviewNotificationContentProps = HTMLAttributes<HTMLDivElement>;
export const InAppPreviewNotificationContent = (props: InAppPreviewNotificationContentProps) => {
const { className, ...rest } = props;

return <div className={cn('flex w-full flex-col gap-1 overflow-hidden', className)} {...rest} />;
};

type InAppPreviewSubjectProps = MarkdownProps & { isPending?: boolean };
export const InAppPreviewSubject = (props: InAppPreviewSubjectProps) => {
const { className, isPending, ...rest } = props;

if (isPending) {
return <Skeleton className="h-5 w-1/2" />;
}

return <Markdown className={cn('text-foreground-600 truncate text-xs font-medium', className)} {...rest} />;
};

type InAppPreviewBodyProps = MarkdownProps & { isPending?: boolean };
export const InAppPreviewBody = (props: InAppPreviewBodyProps) => {
const { className, isPending, ...rest } = props;

if (isPending) {
return (
<>
<Skeleton className="h-5 w-full" />
<Skeleton className="h-5 w-full" />
</>
);
}

return <Markdown className={cn('text-foreground-400 text-xs font-normal', className)} {...rest} />;
};

type InAppPreviewActionsProps = HTMLAttributes<HTMLDivElement>;
export const InAppPreviewActions = (props: InAppPreviewActionsProps) => {
const { className, ...rest } = props;
return <div className={cn('mt-3 flex flex-wrap gap-1 overflow-hidden', className)} {...rest} />;
};

type InAppPreviewPrimaryActionProps = ButtonProps & { isPending?: boolean };
export const InAppPreviewPrimaryAction = (props: InAppPreviewPrimaryActionProps) => {
const { className, isPending, children, ...rest } = props;

if (isPending) {
return <Skeleton className="h-5 w-[12ch]" />;
}

return (
<Button
className={cn('px-3 text-xs font-medium shadow-none', className)}
type="button"
variant="primary"
size="xs"
{...rest}
>
{children}
</Button>
);
};

type InAppPreviewSecondaryActionProps = ButtonProps & { isPending?: boolean };
export const InAppPreviewSecondaryAction = (props: InAppPreviewSecondaryActionProps) => {
const { className, isPending, children, ...rest } = props;

if (isPending) {
return <Skeleton className="h-5 w-[12ch]" />;
}

return (
<Button variant="outline" className={cn('px-3 text-xs font-medium', className)} type="button" size="xs" {...rest}>
{children}
</Button>
);
};

type MarkdownProps = Omit<HTMLAttributes<HTMLParagraphElement>, 'children'> & { children?: string };
const Markdown = (props: MarkdownProps) => {
const { children, ...rest } = props;

const tokens = useMemo(() => parseMarkdownIntoTokens(children), [children]);
const tokens = useMemo(() => parseMarkdownIntoTokens(children || ''), [children]);

return (
<p {...rest}>
Expand All @@ -121,11 +178,3 @@ const Markdown = (props: MarkdownProps) => {
</p>
);
};

const Subject = ({ text, className }: { text: string; className?: string }) => {
return <Markdown className={cn('text-foreground-600 text-xs font-medium', className)}>{text}</Markdown>;
};

const Body = ({ text, className }: { text: string; className?: string }) => {
return <Markdown className={cn('text-foreground-400 text-xs font-normal', className)}>{text}</Markdown>;
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { usePreviewStep } from '@/hooks';
import { InAppPreview } from '@/components/workflow-editor/in-app-preview';
import {
InAppPreview,
InAppPreviewAvatar,
InAppPreviewBody,
InAppPreviewHeader,
InAppPreviewNotification,
InAppPreviewNotificationContent,
InAppPreviewSubject,
} from '@/components/workflow-editor/in-app-preview';
import { useStepEditorContext } from '@/components/workflow-editor/steps/hooks';
import { InAppRenderOutput } from '@novu/shared';

export function ConfigureInAppPreview() {
const { previewStep, data, isPending: isPreviewPending } = usePreviewStep();
Expand All @@ -23,5 +32,26 @@ export function ConfigureInAppPreview() {
});
}, [workflowSlug, stepSlug, previewStep, step, isPendingStep]);

return <InAppPreview data={data} truncateBody isLoading={isPreviewPending} />;
if (!isPreviewPending && !data?.result) {
return null;
}

const preview = data?.result?.preview as InAppRenderOutput | undefined;

return (
<InAppPreview>
<InAppPreviewHeader />

<InAppPreviewNotification>
<InAppPreviewAvatar src={preview?.avatar} isPending={isPreviewPending} />

<InAppPreviewNotificationContent>
<InAppPreviewSubject isPending={isPreviewPending}>{preview?.subject}</InAppPreviewSubject>
<InAppPreviewBody isPending={isPreviewPending} className="line-clamp-2">
{preview?.body}
</InAppPreviewBody>
</InAppPreviewNotificationContent>
</InAppPreviewNotification>
</InAppPreview>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,12 @@ const ConfigureActionPopover = (props: ComponentProps<typeof PopoverTrigger> & {
<FormLabel>Button text</FormLabel>
</div>
<FormControl>
<InputField>
<InputField size="fit">
<Editor
fontFamily="inherit"
placeholder="Button text"
value={field.value}
onChange={field.onChange}
height="30px"
extensions={[autocompletion({ override: [completions(variables)] }), EditorView.lineWrapping]}
/>
</InputField>
Expand Down
Loading

0 comments on commit 62ad1a1

Please sign in to comment.