Skip to content

Commit

Permalink
Merge branch 'next' into inbox-starter-onboarding
Browse files Browse the repository at this point in the history
  • Loading branch information
scopsy committed Dec 3, 2024
2 parents 7218b4e + df29243 commit 1f235a8
Show file tree
Hide file tree
Showing 37 changed files with 416 additions and 269 deletions.
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@novu/api",
"version": "2.1.0",
"version": "2.1.1",
"description": "description",
"author": "",
"private": "true",
Expand Down
2 changes: 0 additions & 2 deletions apps/api/src/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,6 @@ API_RATE_LIMIT_MAXIMUM_UNLIMITED_TRIGGER=
API_RATE_LIMIT_MAXIMUM_UNLIMITED_CONFIGURATION=
API_RATE_LIMIT_MAXIMUM_UNLIMITED_GLOBAL=

PR_PREVIEW_ROOT_URL=dev-web-novu.netlify.app

HUBSPOT_INVITE_NUDGE_EMAIL_USER_LIST_ID=
HUBSPOT_PRIVATE_APP_ACCESS_TOKEN=

Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,9 @@ modules.push(
client: new Client({
secretKey: process.env.NOVU_INTERNAL_SECRET_KEY,
strictAuthentication:
process.env.NODE_ENV === 'production' || process.env.NOVU_STRICT_AUTHENTICATION_ENABLED === 'true',
process.env.NODE_ENV === 'production' ||
process.env.NODE_ENV === 'dev' ||
process.env.NOVU_STRICT_AUTHENTICATION_ENABLED === 'true',
}),
controllerDecorators: [ApiExcludeController()],
workflows: [usageLimitsWorkflow],
Expand Down
69 changes: 15 additions & 54 deletions apps/api/src/config/cors.config.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { spy } from 'sinon';
import { expect } from 'chai';
import { corsOptionsDelegate, isPermittedDeployPreviewOrigin } from './cors.config';
import { corsOptionsDelegate } from './cors.config';

describe('CORS Configuration', () => {
describe('Local Environment', () => {
Expand Down Expand Up @@ -32,7 +32,6 @@ describe('CORS Configuration', () => {
process.env.FRONT_BASE_URL = 'https://test.com';
process.env.LEGACY_STAGING_DASHBOARD_URL = 'https://test-legacy-staging-dashboard.com';
process.env.WIDGET_BASE_URL = 'https://widget.com';
process.env.PR_PREVIEW_ROOT_URL = 'https://pr-preview.com';
});

afterEach(() => {
Expand All @@ -43,14 +42,26 @@ describe('CORS Configuration', () => {
const callbackSpy = spy();

// @ts-expect-error - corsOptionsDelegate is not typed correctly
corsOptionsDelegate({ url: '/v1/test' }, callbackSpy);
corsOptionsDelegate(
{
url: '/v1/test',
headers: {
origin: 'https://test.novu.com',
},
},
callbackSpy
);

expect(callbackSpy.calledOnce).to.be.ok;
expect(callbackSpy.firstCall.firstArg).to.be.null;
expect(callbackSpy.firstCall.lastArg.origin.length).to.equal(3);
expect(callbackSpy.firstCall.lastArg.origin.length).to.equal(environment === 'dev' ? 4 : 3);
expect(callbackSpy.firstCall.lastArg.origin[0]).to.equal(process.env.FRONT_BASE_URL);
expect(callbackSpy.firstCall.lastArg.origin[1]).to.equal(process.env.LEGACY_STAGING_DASHBOARD_URL);
expect(callbackSpy.firstCall.lastArg.origin[2]).to.equal(process.env.WIDGET_BASE_URL);

if (environment === 'dev') {
expect(callbackSpy.firstCall.lastArg.origin[3]).to.equal('https://test.novu.com');
}
});

it('widget routes should be wildcarded', () => {
Expand All @@ -74,56 +85,6 @@ describe('CORS Configuration', () => {
expect(callbackSpy.firstCall.firstArg).to.be.null;
expect(callbackSpy.firstCall.lastArg.origin).to.equal('*');
});

if (environment === 'dev') {
it('should allow all origins for dev environment from pr preview', () => {
const callbackSpy = spy();

// @ts-expect-error - corsOptionsDelegate is not typed correctly
corsOptionsDelegate(
{
url: '/v1/test',
headers: {
origin: `https://test--${process.env.PR_PREVIEW_ROOT_URL}`,
},
},
callbackSpy
);

expect(callbackSpy.calledOnce).to.be.ok;
expect(callbackSpy.firstCall.firstArg).to.be.null;
expect(callbackSpy.firstCall.lastArg.origin).to.equal('*');
});
}
});
});

describe('isPermittedDeployPreviewOrigin', () => {
afterEach(() => {
process.env.NODE_ENV = 'test';
});

it('should return false when NODE_ENV is not dev', () => {
process.env.NODE_ENV = 'production';
expect(isPermittedDeployPreviewOrigin('https://someorigin.com')).to.be.false;
});

it('should return false when PR_PREVIEW_ROOT_URL is not set', () => {
process.env.NODE_ENV = 'dev';
delete process.env.PR_PREVIEW_ROOT_URL;
expect(isPermittedDeployPreviewOrigin('https://someorigin.com')).to.be.false;
});

it('should return false for origins not matching PR_PREVIEW_ROOT_URL (string)', () => {
process.env.NODE_ENV = 'dev';
process.env.PR_PREVIEW_ROOT_URL = 'https://pr-preview.com';
expect(isPermittedDeployPreviewOrigin('https://anotherorigin.com')).to.be.false;
});

it('should return true for origin matching PR_PREVIEW_ROOT_URL', () => {
process.env.NODE_ENV = 'dev';
process.env.PR_PREVIEW_ROOT_URL = 'https://pr-preview.com';
expect(isPermittedDeployPreviewOrigin('https://netlify-https://pr-preview.com')).to.be.true;
});
});
});
32 changes: 6 additions & 26 deletions apps/api/src/config/cors.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ export const corsOptionsDelegate: Parameters<INestApplication['enableCors']>[0]
methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
};

const origin = extractOrigin(req);

if (enableWildcard(req)) {
corsOptions.origin = '*';
} else {
Expand All @@ -29,27 +27,17 @@ export const corsOptionsDelegate: Parameters<INestApplication['enableCors']>[0]
if (process.env.WIDGET_BASE_URL) {
corsOptions.origin.push(process.env.WIDGET_BASE_URL);
}
// Enable preview deployments in staging environment for Netlify and Vercel
if (process.env.NODE_ENV === 'dev') {
corsOptions.origin.push(origin(req));
}
}

const shouldDisableCorsForPreviewUrls = isPermittedDeployPreviewOrigin(origin);

Logger.verbose(`Should allow deploy preview? ${shouldDisableCorsForPreviewUrls ? 'Yes' : 'No'}.`, {
curEnv: process.env.NODE_ENV,
previewUrlRoot: process.env.PR_PREVIEW_ROOT_URL,
origin,
});

callback(null as unknown as Error, corsOptions);
};

function enableWildcard(req: Request): boolean {
return (
isSandboxEnvironment() ||
isWidgetRoute(req.url) ||
isInboxRoute(req.url) ||
isBlueprintRoute(req.url) ||
isPermittedDeployPreviewOrigin(extractOrigin(req))
);
return isSandboxEnvironment() || isWidgetRoute(req.url) || isInboxRoute(req.url) || isBlueprintRoute(req.url);
}

function isWidgetRoute(url: string): boolean {
Expand All @@ -68,14 +56,6 @@ function isSandboxEnvironment(): boolean {
return ['test', 'local'].includes(process.env.NODE_ENV);
}

export function isPermittedDeployPreviewOrigin(origin: string | string[]): boolean {
if (!process.env.PR_PREVIEW_ROOT_URL || process.env.NODE_ENV !== 'dev') {
return false;
}

return origin.includes(process.env.PR_PREVIEW_ROOT_URL);
}

function extractOrigin(req: Request): string {
function origin(req: Request): string {
return (req.headers as any)?.origin || '';
}
15 changes: 10 additions & 5 deletions apps/dashboard/src/components/primitives/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,40 @@ import { cva, type VariantProps } from 'class-variance-authority';
import * as React from 'react';

const badgeVariants = cva(
'inline-flex items-center [&>svg]:shrink-0 gap-1 h-fit border text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
'inline-flex items-center [&>svg]:shrink-0 gap-1 h-fit border transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{
variants: {
variant: {
neutral: 'border-neutral-500 bg-neutral-500',
neutral: 'border-neutral-100 bg-neutral-100 text-neutral-500',
destructive: 'border-transparent bg-destructive/10 text-destructive',
success: 'border-transparent bg-success/10 text-success',
warning: 'border-transparent bg-warning/10 text-warning',
soft: 'bg-neutral-alpha-200 text-foreground-500 border-transparent',
outline: 'border-neutral-alpha-200 bg-transparent font-normal text-foreground-600 shadow-sm',
},
size: {
kind: {
default: 'rounded-md px-2 py-1',
pill: 'rounded-full px-2',
'pill-stroke': 'rounded-full px-2',
tag: 'rounded-md py-0.5 px-2',
},
size: {
default: 'text-xs',
'2xs': 'text-[10px] leading-[14px] font-medium',
},
},
defaultVariants: {
variant: 'neutral',
kind: 'default',
size: 'default',
},
}
);

export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}

function Badge({ className, variant, size, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant, size }), className)} {...props} />;
function Badge({ className, variant, kind, size, ...props }: BadgeProps) {
return <div className={cn(badgeVariants({ variant, kind, size }), className)} {...props} />;
}

export { Badge };
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/primitives/tag-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const TagInput = forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {
</PopoverAnchor>
<div className="flex flex-wrap gap-2">
{tags.map((tag, index) => (
<Badge key={index} variant="outline" size="tag" className="gap-1">
<Badge key={index} variant="outline" kind="tag" className="gap-1">
<span style={{ wordBreak: 'break-all' }}>{tag}</span>
<button type="button" onClick={() => removeTag(tag)}>
<RiCloseFill className="-mr-0.5 size-3" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function GettingStartedMenuItem() {

<Badge
variant="soft"
size="pill"
kind="pill"
className="bg-primary/10 text-primary inline-flex items-center gap-0.5 px-1 py-0.5 leading-4"
>
<motion.div
Expand Down
7 changes: 3 additions & 4 deletions apps/dashboard/src/components/welcome/progress-section.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Card, CardContent } from '../primitives/card';
import { RiArrowRightDoubleFill, RiCheckLine, RiLoader3Line } from 'react-icons/ri';
import { useEnvironment } from '../../context/environment/hooks';
import { useOnboardingSteps, StepIdEnum } from '../../hooks/use-onboarding-steps';
import { Link } from 'react-router-dom';
import { Link, useParams } from 'react-router-dom';
import { buildRoute, LEGACY_ROUTES, ROUTES } from '../../utils/routes';
import { motion } from 'motion/react';
import { mainCard, leftSection, textItem, stepsList, stepItem, logo } from './progress-section.animations';
Expand All @@ -21,7 +20,7 @@ interface StepItemProps {
}

export function ProgressSection() {
const { currentEnvironment } = useEnvironment();
const { environmentSlug } = useParams<{ environmentSlug?: string }>();
const { steps } = useOnboardingSteps();

return (
Expand All @@ -31,7 +30,7 @@ export function ProgressSection() {

<motion.div className="flex flex-1 flex-col gap-3 p-6" variants={stepsList}>
{steps.map((step, index) => (
<StepItem key={index} step={step} environmentSlug={currentEnvironment?.slug} />
<StepItem key={index} step={step} environmentSlug={environmentSlug} />
))}
</motion.div>

Expand Down
4 changes: 2 additions & 2 deletions apps/dashboard/src/components/welcome/resources-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ export function ResourcesList({ resources, title, icon }: ResourcesListProps) {
</motion.div>

<ScrollArea className="w-full whitespace-nowrap">
<motion.div className="flex gap-4 pb-7 pl-1" variants={containerVariants} initial="hidden" animate="show">
<motion.div className="flex gap-4 pb-1 pl-1" variants={containerVariants} initial="hidden" animate="show">
{resources.map((resource, index) => (
<motion.div key={index} variants={itemVariants}>
<Link to={resource.url} target="_blank" rel="noopener" onClick={() => handleResourceClick(resource)}>
<Card className="w-60 shrink-0 overflow-hidden border-none shadow-[0px_12px_32px_0px_rgba(0,0,0,0.02),0px_0px_0px_1px_rgba(0,0,0,0.05)] transition-all duration-200 hover:translate-y-[1px] hover:cursor-pointer hover:shadow-md">
<Card className="w-60 shrink-0 overflow-hidden border-none shadow-[0px_12px_12px_0px_rgba(0,0,0,0.02),0px_0px_0px_1px_rgba(0,0,0,0.05)] transition-all duration-200">
<motion.div
className="bg-foreground-50 h-[95px] overflow-hidden"
whileHover={{ scale: 1.03 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const MenuItem = ({
/>
<span className="text-xs">{children}</span>
{disabled && (
<Badge size="pill" variant="soft" className="ml-auto opacity-40">
<Badge kind="pill" variant="soft" className="ml-auto opacity-40">
coming soon
</Badge>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import React from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { FaCode } from 'react-icons/fa6';
import { WorkflowOriginEnum } from '@novu/shared';

import { ArrowRight, RouteFill } from '@/components/icons';
import {
Breadcrumb,
Expand All @@ -14,6 +17,7 @@ import { useEnvironment } from '@/context/environment/hooks';
import { buildRoute, ROUTES } from '@/utils/routes';
import { useFetchWorkflow } from '@/hooks';
import TruncatedText from '@/components/truncated-text';
import { Badge } from '@/components/primitives/badge';

export const EditorBreadcrumbs = () => {
const { workflowSlug = '' } = useParams<{ workflowSlug: string }>();
Expand All @@ -29,6 +33,11 @@ export const EditorBreadcrumbs = () => {
{
label: 'Workflows',
href: workflowsRoute,
node: (
<Badge kind="pill" size="2xs" className="no-underline">
BETA
</Badge>
),
},
];

Expand All @@ -43,21 +52,30 @@ export const EditorBreadcrumbs = () => {
</Button>
<Breadcrumb>
<BreadcrumbList>
{breadcrumbs.map(({ label, href }) => (
{breadcrumbs.map(({ label, href, node }) => (
<React.Fragment key={`${href}_${label}`}>
<BreadcrumbItem>
<BreadcrumbItem className="flex items-center gap-1">
<BreadcrumbLink to={href}>{label}</BreadcrumbLink>
{node}
</BreadcrumbItem>
<BreadcrumbSeparator />
</React.Fragment>
))}
<BreadcrumbItem>
<BreadcrumbPage>
<RouteFill />
<div className="flex max-w-[32ch]">
<TruncatedText>{workflow?.name}</TruncatedText>
</div>
</BreadcrumbPage>
{workflow && (
<BreadcrumbPage className="flex items-center gap-1">
{workflow.origin === WorkflowOriginEnum.EXTERNAL ? (
<Badge variant="warning" kind="pill" size="2xs">
<FaCode className="size-3.5" />
</Badge>
) : (
<RouteFill className="size-4" />
)}
<div className="flex max-w-[32ch]">
<TruncatedText>{workflow?.name}</TruncatedText>
</div>
</BreadcrumbPage>
)}
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
Expand Down
7 changes: 6 additions & 1 deletion apps/dashboard/src/components/workflow-editor/nodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { STEP_TYPE_TO_COLOR } from '@/utils/color';
import { useWorkflow } from '@/components/workflow-editor/workflow-provider';
import { WorkflowOriginEnum } from '@novu/shared';
import { createStep } from '@/components/workflow-editor/steps/step-provider';
import { getEncodedId, STEP_DIVIDER } from '@/utils/step';

export type NodeData = {
name?: string;
Expand Down Expand Up @@ -52,7 +53,11 @@ const StepNode = (props: StepNodeProps) => {
stepSlug: string;
}>();

return <Node aria-selected={stepSlug === data.stepSlug} className={cn('group', className)} {...rest} />;
const isSelected =
getEncodedId({ slug: stepSlug ?? '', divider: STEP_DIVIDER }) ===
getEncodedId({ slug: data.stepSlug ?? '', divider: STEP_DIVIDER });

return <Node aria-selected={isSelected} className={cn('group', className)} {...rest} />;
};

export const EmailNode = ({ data }: NodeProps<NodeType>) => {
Expand Down
Loading

0 comments on commit 1f235a8

Please sign in to comment.