Skip to content

Commit 545ccca

Browse files
peterzimonEvanHahn
andauthored
Added static Automations page behind private feature flag (TryGhost#27510)
closes https://linear.app/tryghost/issue/NY-1239 - introduces the Automations area in the admin sidebar (after Comments) as a static skeleton so backend devs have a visual target to wire dynamic behaviour against - gated entirely behind the existing private `automations` flag so it stays invisible until we are ready to ship - new view lives in apps/posts (alongside Comments and Tags) and is built with Shade primitives (ListHeader) and components (Table, Badge) — no API calls, no data hooks Co-authored-by: Evan Hahn <evan@ghost.org>
1 parent 32e82d6 commit 545ccca

7 files changed

Lines changed: 170 additions & 0 deletions

File tree

apps/admin/src/layout/app-sidebar/nav-content.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ function NavContent({ ...props }: React.ComponentProps<typeof SidebarGroup>) {
7777
const memberCount = useMemberCount();
7878
const routing = useEmberRouting();
7979
const commentModerationEnabled = useFeatureFlag('commentModeration');
80+
const automationsEnabled = useFeatureFlag('automations');
8081
const isMembersRouteActive = useIsActiveLink({path: 'members', activeOnSubpath: true});
8182

8283
const showTags = currentUser && canManageTags(currentUser);
@@ -212,6 +213,18 @@ function NavContent({ ...props }: React.ComponentProps<typeof SidebarGroup>) {
212213
</NavMenuItem.Link>
213214
</NavMenuItem>
214215
)}
216+
217+
{showMembers && automationsEnabled && (
218+
<NavMenuItem>
219+
<NavMenuItem.Link
220+
to="automations"
221+
activeOnSubpath
222+
>
223+
<LucideIcon.Zap />
224+
<NavMenuItem.Label>Automations</NavMenuItem.Label>
225+
</NavMenuItem.Link>
226+
</NavMenuItem>
227+
)}
215228
</SidebarMenu>
216229
</SidebarGroupContent>
217230
</SidebarGroup>

apps/posts/src/routes.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ export const routes: RouteObject[] = [
6969
path: 'comments',
7070
lazy: lazyComponent(() => import('@views/comments/comments'))
7171
},
72+
{
73+
path: 'automations',
74+
lazy: lazyComponent(() => import('@views/Automations/automations'))
75+
},
7276

7377
// Error handling
7478
{
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import AutomationsContent from './components/automations-content';
2+
import AutomationsHeader from './components/automations-header';
3+
import AutomationsLayout from './components/automations-layout';
4+
import AutomationsList from './components/automations-list';
5+
import React from 'react';
6+
7+
const Automations: React.FC = () => {
8+
return (
9+
<AutomationsLayout>
10+
<AutomationsHeader />
11+
<AutomationsContent>
12+
<AutomationsList />
13+
</AutomationsContent>
14+
</AutomationsLayout>
15+
);
16+
};
17+
18+
export default Automations;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import React from 'react';
2+
import {cn} from '@tryghost/shade/utils';
3+
4+
const AutomationsContent: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({children, className, ...props}) => {
5+
return (
6+
<section className={cn('flex gap-6 flex-col px-4 lg:px-8 py-2 size-full grow', className)} {...props}>
7+
{children}
8+
</section>
9+
);
10+
};
11+
12+
export default AutomationsContent;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import React from 'react';
2+
import {ListHeader} from '@tryghost/shade/primitives';
3+
4+
const AutomationsHeader: React.FC = () => {
5+
return (
6+
<ListHeader className='py-4 sidebar:py-6'>
7+
<ListHeader.Left>
8+
<ListHeader.Title>Automations</ListHeader.Title>
9+
</ListHeader.Left>
10+
</ListHeader>
11+
);
12+
};
13+
14+
export default AutomationsHeader;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import MainLayout from '@components/layout/main-layout';
2+
import React from 'react';
3+
4+
const AutomationsLayout: React.FC<{children: React.ReactNode}> = ({children}) => {
5+
return (
6+
<MainLayout>
7+
<div className="grid w-full grow">
8+
<div className="flex h-full flex-col" data-testid="automations-page">
9+
{children}
10+
</div>
11+
</div>
12+
</MainLayout>
13+
);
14+
};
15+
16+
export default AutomationsLayout;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import React from 'react';
2+
import {Badge, Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade/components';
3+
import {formatNumber, formatTimestamp} from '@tryghost/shade/utils';
4+
5+
type AutomationStatus = 'active' | 'inactive';
6+
7+
interface AutomationRow {
8+
id: string;
9+
name: string;
10+
steps: number;
11+
status: AutomationStatus;
12+
lastRun: string;
13+
}
14+
15+
// TODO(NY-1196): This is sample data, which we'll replace with real data once
16+
// we have the API in place.
17+
const automations: AutomationRow[] = [
18+
{
19+
id: 'free-members-welcome-email',
20+
name: 'Free members welcome email',
21+
steps: 2,
22+
status: 'active',
23+
lastRun: new Date(Date.now() - (2 * 60 * 60 * 1000)).toISOString()
24+
},
25+
{
26+
id: 'paid-members-welcome-email',
27+
name: 'Paid members welcome email',
28+
steps: 3,
29+
status: 'active',
30+
lastRun: new Date(Date.now() - (24 * 60 * 60 * 1000)).toISOString()
31+
}
32+
];
33+
34+
const AutomationsStatusBadge: React.FC<{status: AutomationStatus}> = ({status}) => {
35+
switch (status) {
36+
case 'active':
37+
return <Badge variant="success">Active</Badge>;
38+
case 'inactive':
39+
return <Badge variant="secondary">Inactive</Badge>;
40+
default: {
41+
const invalidStatus: never = status;
42+
throw new Error(`Unhandled status: ${invalidStatus}`);
43+
}
44+
}
45+
};
46+
47+
const AutomationsList: React.FC = () => {
48+
return (
49+
<Table className="flex table-fixed flex-col lg:table" data-testid="automations-list">
50+
<TableHeader className="hidden lg:visible! lg:table-header-group!">
51+
<TableRow>
52+
<TableHead className="w-auto px-4">Name</TableHead>
53+
<TableHead className="w-24 px-4">Steps</TableHead>
54+
<TableHead className="w-28 px-4">Status</TableHead>
55+
<TableHead className="w-32 px-4">Last run</TableHead>
56+
</TableRow>
57+
</TableHeader>
58+
<TableBody className="flex flex-col lg:table-row-group">
59+
{automations.map(automation => (
60+
<TableRow
61+
key={automation.id}
62+
className="group relative grid w-full cursor-pointer grid-cols-2 items-center gap-x-4 p-2 hover:bg-muted/50 lg:table-row lg:p-0"
63+
data-testid="automation-list-row"
64+
>
65+
<TableCell className="lg:w-auto lg:p-4">
66+
<a
67+
className="before:absolute before:top-0 before:left-0 before:z-10 before:h-full before:w-full"
68+
href={`#/automations/${automation.id}`}
69+
>
70+
<span className="block truncate text-lg font-medium">
71+
{automation.name}
72+
</span>
73+
</a>
74+
</TableCell>
75+
<TableCell className="lg:p-4">
76+
<span className="text-muted-foreground">
77+
{formatNumber(automation.steps)} {automation.steps === 1 ? 'step' : 'steps'}
78+
</span>
79+
</TableCell>
80+
<TableCell className="lg:p-4">
81+
<AutomationsStatusBadge status={automation.status} />
82+
</TableCell>
83+
<TableCell className="lg:p-4">
84+
<span className="text-muted-foreground">{formatTimestamp(automation.lastRun)}</span>
85+
</TableCell>
86+
</TableRow>
87+
))}
88+
</TableBody>
89+
</Table>
90+
);
91+
};
92+
93+
export default AutomationsList;

0 commit comments

Comments
 (0)