Skip to content

Commit

Permalink
feat(novui,web): V2 flow list (#5682)
Browse files Browse the repository at this point in the history
* feat: Define `Table` component
* feat: Setup basis for Workflows list page
* feat: Add semantic tokens
  • Loading branch information
antonjoel82 authored Jun 6, 2024
1 parent cf02130 commit cac9cba
Show file tree
Hide file tree
Showing 26 changed files with 1,061 additions and 106 deletions.
3 changes: 2 additions & 1 deletion apps/web/src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import { TenantsPage } from './pages/tenants/TenantsPage';
import { UpdateTenantPage } from './pages/tenants/UpdateTenantPage';
import { TranslationRoutes } from './pages/TranslationPages';
import { useSettingsRoutes } from './SettingsRoutes';
import { WorkflowsListPage } from './studio/components/workflows/WorkflowsListPage';

export const AppRoutes = () => {
const isImprovedOnboardingEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_IMPROVED_ONBOARDING_ENABLED);
Expand Down Expand Up @@ -140,7 +141,7 @@ export const AppRoutes = () => {
)}
<Route path={ROUTES.STUDIO}>
<Route path="" element={<Navigate to={ROUTES.STUDIO_FLOWS} replace />} />
<Route path={ROUTES.STUDIO_FLOWS} element={<WorkflowListPage />} />
<Route path={ROUTES.STUDIO_FLOWS} element={<WorkflowsListPage />} />
</Route>

<Route path="/translations/*" element={<TranslationRoutes />} />
Expand Down
10 changes: 10 additions & 0 deletions apps/web/src/studio/components/workflows/WorkflowsListPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { PageContainer } from '../../layout';
import { WorkflowsTable } from './table';

export const WorkflowsListPage = () => {
return (
<PageContainer title="Workflows">
<WorkflowsTable />
</PageContainer>
);
};
1 change: 1 addition & 0 deletions apps/web/src/studio/components/workflows/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './WorkflowsListPage';
114 changes: 114 additions & 0 deletions apps/web/src/studio/components/workflows/table/WorkflowsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/* cspell:disable */

import { createColumnHelper, Table } from '@novu/novui';
import { css } from '@novu/novui/css';
import format from 'date-fns/format';
import { FC } from 'react';
import { WorkflowTableRow } from './WorkflowsTable.types';
import { GroupCell, NameCell, StatusCell } from './WorkflowsTableCellRenderers';

// TODO: remove this test data
const TEST_FLOW = {
_id: 'adaewr',
name: 'test naming',
active: false,
type: 'REGULAR',
draft: true,
critical: false,
isBlueprint: false,
_notificationGroupId: 'dsasdfasdf',
tags: [],
triggers: [
{
type: 'event',
identifier: 'test-naming',
variables: [],
reservedVariables: [],
subscriberVariables: [],
_id: 'sdfsdf',
},
],
steps: [],
preferenceSettings: {
email: true,
sms: true,
in_app: true,
chat: true,
push: true,
},
_environmentId: 'sdfsdf',
_organizationId: 'sdf',
_creatorId: 'sdfsdfsdf',
deleted: false,
createdAt: '2024-06-04T19:11:11.600Z',
updatedAt: '2024-06-05T17:55:39.022Z',
__v: 0,
notificationGroup: {
_id: 'sdfsdf',
name: 'General',
_organizationId: 'sdfsdf',
_environmentId: 'sdfdfsdfsf',
createdAt: '2024-05-17T22:26:08.177Z',
updatedAt: '2024-05-17T22:26:08.177Z',
__v: 0,
},
workflowIntegrationStatus: {
hasActiveIntegrations: true,
channels: {
in_app: {
hasActiveIntegrations: false,
},
email: {
hasActiveIntegrations: true,
hasPrimaryIntegrations: true,
},
sms: {
hasActiveIntegrations: true,
hasPrimaryIntegrations: true,
},
chat: {
hasActiveIntegrations: false,
},
push: {
hasActiveIntegrations: false,
},
},
},
};

interface IWorkflowsTableProps {
temp?: string;
}

const columnHelper = createColumnHelper<WorkflowTableRow>();

const WORKFLOW_COLUMNS = [
columnHelper.accessor('name', {
header: 'Name & Trigger ID',
cell: NameCell,
}),
columnHelper.accessor('notificationGroup.name', {
header: 'Group',
cell: GroupCell,
}),
columnHelper.accessor('createdAt', {
header: 'Created at',
cell: ({ getValue }) => format(new Date(getValue() ?? ''), 'dd/MM/yyyy HH:mm'),
}),
columnHelper.accessor('active', {
header: 'Status',
cell: StatusCell,
}),
];

export const WorkflowsTable: FC<IWorkflowsTableProps> = () => {
return (
<div className={css({ display: 'flex', flex: '1' })}>
<Table<typeof TEST_FLOW>
columns={WORKFLOW_COLUMNS}
data={[TEST_FLOW, { ...TEST_FLOW, active: true }]}
className={css({ w: '100%' })}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { INotificationTemplateExtended } from '../../../../hooks/useTemplates';

export type WorkflowTableRow = INotificationTemplateExtended;
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { CellRendererComponent } from '@novu/novui';
import { css } from '@novu/novui/css';
import { IconBolt, IconCable, IconFlashOff } from '@novu/novui/icons';
import { Center, Flex, HStack, styled } from '@novu/novui/jsx';
import { text } from '@novu/novui/recipes';
import { ColorToken } from '@novu/novui/tokens';
import { WorkflowTableRow } from './WorkflowsTable.types';

export const GroupCell: CellRendererComponent<WorkflowTableRow, string> = (props) => {
return (
<Center
className={css({
color: 'typography.text.main',
rounded: '50',
border: 'solid',
py: '25',
px: '75',
borderColor: 'badge.border',
width: '[fit-content]',
bg: '[transparent]',
})}
>
{props.getValue()}
</Center>
);
};

const Text = styled('p', text);

export const NameCell: CellRendererComponent<WorkflowTableRow, string> = ({ getValue, row: { original } }) => {
return (
<HStack gap="50">
{
<IconCable
className={css({ width: '200', height: '200', color: 'icon.secondary' })}
title="workflow-row-label"
/>
}
<Flex direction={'column'}>
<Text variant={'main'}>{getValue()}</Text>
<Text variant={'secondary'}>{original.triggers ? original.triggers[0].identifier : 'Unknown'}</Text>
</Flex>
</HStack>
);
};

export const StatusCell: CellRendererComponent<WorkflowTableRow, boolean> = ({ getValue }) => {
const isActive = getValue();
getValue();
const color: ColorToken = isActive ? 'status.active' : 'status.inactive';

return (
<HStack gap="0">
{isActive ? (
<IconBolt size="16" className={css({ color })} title="workflow-status-indicator" />
) : (
<IconFlashOff size="16" className={css({ color })} title="workflow-status-indicator" />
)}
<Text variant={'main'} color={color}>
{isActive ? 'Active' : 'Inactive'}
</Text>
</HStack>
);
};
1 change: 1 addition & 0 deletions apps/web/src/studio/components/workflows/table/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './WorkflowsTable';
44 changes: 44 additions & 0 deletions apps/web/src/studio/layout/PageContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { FC, PropsWithChildren, ReactNode } from 'react';
import { css, cx } from '@novu/novui/css';
import { PageHeader } from './PageHeader';
import { PageMeta } from './PageMeta';
import { Container } from '@novu/novui/jsx';
import { CoreProps } from '@novu/novui';

export interface IPageContainerProps extends CoreProps {
// TODO: this should be LocalizedMessage, but PageContainer and PageHeader don't accept it
title: string;
header?: ReactNode;
}

export const PageContainer: FC<PropsWithChildren<IPageContainerProps>> = ({ title, children, header, className }) => {
return (
<Container
className={cx(
css({
overflowY: 'auto !important',
borderRadius: '0',
px: 'paddings.page.horizontal',
py: 'paddings.page.vertical',
m: '0',
h: '100%',
bg: 'surface.page',
}),
className
)}
>
<PageMeta title={title} />
<PageHeader title={title} className={css({ mb: 'margins.layout.page.titleBottom' })} />
{!!header && (
<section
className={css({
mx: 'paddings.page.horizontal',
})}
>
{header}
</section>
)}
<section>{children}</section>
</Container>
);
};
20 changes: 20 additions & 0 deletions apps/web/src/studio/layout/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CoreProps } from '@novu/novui';
import { styled, Flex } from '@novu/novui/jsx';
import { title as titleRecipe } from '@novu/novui/recipes';
import { LocalizedMessage } from '@novu/shared-web';

const Title = styled('h1', titleRecipe);

export interface IPageHeaderProps extends CoreProps {
actions?: JSX.Element;
title: LocalizedMessage;
}

export const PageHeader: React.FC<IPageHeaderProps> = ({ title, actions, className }) => {
return (
<Flex direction={'row'} justifyContent="space-between" className={className}>
<Title variant={'page'}>{title}</Title>
{actions && <div>{actions}</div>}
</Flex>
);
};
13 changes: 13 additions & 0 deletions apps/web/src/studio/layout/PageMeta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Helmet } from 'react-helmet-async';

interface IPageMetaProps {
title?: string;
}

export const PageMeta: React.FC<IPageMetaProps> = ({ title }) => {
return (
<Helmet>
<title>{title ? `${title} | ` : ``}Novu</title>
</Helmet>
);
};
1 change: 1 addition & 0 deletions apps/web/src/studio/layout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './PageContainer';
1 change: 1 addition & 0 deletions libs/novui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
"dependencies": {
"@mantine/core": "^7.10.0",
"@mantine/hooks": "^7.10.0",
"@tanstack/react-table": "^8.17.3",
"react-icons": "^5.0.1"
}
}
2 changes: 2 additions & 0 deletions libs/novui/panda.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ export default defineConfig({

// Enables JSX util generation!
jsxFramework: 'react',

validation: 'error',
});
3 changes: 2 additions & 1 deletion libs/novui/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './Test';
export * from './NovuiProvider';
export * from './table';
export * from './Test';
60 changes: 60 additions & 0 deletions libs/novui/src/components/table/Table.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useState } from 'react';
import { StoryFn, Meta } from '@storybook/react';
import { Badge, Switch } from '@mantine/core';

import { Table } from './Table';
import { ColumnDef } from '@tanstack/react-table';

export default {
title: 'Components/Table',
component: Table,
argTypes: {
data: {
control: false,
},
columns: {
control: false,
},
},
} as Meta<typeof Table>;

const SwitchCell = (props) => {
const [status, setStatus] = useState(props.status);
const switchHandler = () => {
setStatus((prev) => (prev === 'Enabled' ? 'Disabled' : 'Enabled'));
};

return <Switch label={status} onChange={switchHandler} checked={status === 'Enabled'} />;
};

const BadgeCell = (props) => {
return (
<Badge variant="outline" size="md" radius="xs">
{props.getValue()}
</Badge>
);
};

interface IExampleData {
name: string;
category: string;
creationDate: string;
status: string;
}

const columns: ColumnDef<IExampleData>[] = [
{ accessorKey: 'name', header: 'Name' },
{ accessorKey: 'category', header: 'Category', cell: BadgeCell },
{ accessorKey: 'creationDate', header: 'Date Created' },
{ accessorKey: 'status', header: 'Status', cell: SwitchCell },
];

const data: IExampleData[] = [
{ name: 'Great', category: 'Fun', status: 'Disabled', creationDate: '01/01/2021 16:36' },
{ name: 'Whats up?', category: 'Done', status: 'Enabled', creationDate: '01/01/2021 16:36' },
];

const Template: StoryFn<typeof Table> = ({ ...args }) => <Table columns={columns} data={data} {...args} />;

export const PrimaryUse = Template.bind({});
PrimaryUse.args = {};
Loading

0 comments on commit cac9cba

Please sign in to comment.