feat: add dashboard UI for Core Extensions app
- Created dashboard page with webhook status - Added email configuration accordion - Shows connection status to Saleor - Displays app information and quick actions - Auto-redirects to dashboard when in Saleor iframe
This commit is contained in:
@@ -0,0 +1,267 @@
|
|||||||
|
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
Divider,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
CircularProgress,
|
||||||
|
Alert,
|
||||||
|
Accordion,
|
||||||
|
AccordionSummary,
|
||||||
|
AccordionDetails,
|
||||||
|
} from "@saleor/macaw-ui";
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface WebhookStatus {
|
||||||
|
name: string;
|
||||||
|
event: string;
|
||||||
|
active: boolean;
|
||||||
|
targetUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppInfo {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardPage: NextPage = () => {
|
||||||
|
const { appBridgeState } = useAppBridge();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
// Simulate loading app data
|
||||||
|
setTimeout(() => setLoading(false), 1000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const webhooks: WebhookStatus[] = [
|
||||||
|
{
|
||||||
|
name: "Order Created",
|
||||||
|
event: "ORDER_CREATED",
|
||||||
|
active: true,
|
||||||
|
targetUrl: "N8N Workflow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Order Fulfilled",
|
||||||
|
event: "ORDER_FULFILLED",
|
||||||
|
active: true,
|
||||||
|
targetUrl: "N8N Workflow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Order Cancelled",
|
||||||
|
event: "ORDER_CANCELLED",
|
||||||
|
active: true,
|
||||||
|
targetUrl: "N8N Workflow",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const appInfo: AppInfo = {
|
||||||
|
name: "Core Extensions",
|
||||||
|
version: "1.0.0",
|
||||||
|
id: "saleor-core-extensions",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<Box display="flex" justifyContent="center" alignItems="center" height="100vh">
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box padding={8}>
|
||||||
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
|
<CircularProgress size={24} />
|
||||||
|
<Text>Loading dashboard...</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box padding={8}>
|
||||||
|
{/* Header */}
|
||||||
|
<Box marginBottom={8}>
|
||||||
|
<Text size={11} as="h1">
|
||||||
|
Core Extensions Dashboard
|
||||||
|
</Text>
|
||||||
|
<Text color="textSecondary" marginTop={2}>
|
||||||
|
Email automation for ManoonOils
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Connection Status */}
|
||||||
|
<Box marginBottom={8}>
|
||||||
|
<Text size={8} as="h2" marginBottom={4}>
|
||||||
|
Connection Status
|
||||||
|
</Text>
|
||||||
|
{appBridgeState?.ready ? (
|
||||||
|
<Alert severity="success">
|
||||||
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
|
<Chip size="small" color="success">
|
||||||
|
Connected
|
||||||
|
</Chip>
|
||||||
|
<Text>Connected to Saleor Dashboard</Text>
|
||||||
|
</Box>
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<Alert severity="warning">
|
||||||
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
|
<Chip size="small" color="warning">
|
||||||
|
Standalone
|
||||||
|
</Chip>
|
||||||
|
<Text>Running outside Saleor Dashboard</Text>
|
||||||
|
</Box>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Webhooks Section */}
|
||||||
|
<Box marginY={8}>
|
||||||
|
<Text size={8} as="h2" marginBottom={4}>
|
||||||
|
Webhooks Configuration
|
||||||
|
</Text>
|
||||||
|
<Text marginBottom={4} color="textSecondary">
|
||||||
|
Active webhooks forwarding to N8N
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<List>
|
||||||
|
{webhooks.map((webhook) => (
|
||||||
|
<ListItem key={webhook.event}>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
width="100%"
|
||||||
|
>
|
||||||
|
<ListItemText
|
||||||
|
primary={webhook.name}
|
||||||
|
secondary={webhook.event}
|
||||||
|
/>
|
||||||
|
<Box display="flex" alignItems="center" gap={2}>
|
||||||
|
<Text color="textSecondary" size={2}>
|
||||||
|
{webhook.targetUrl}
|
||||||
|
</Text>
|
||||||
|
<Chip size="small" color={webhook.active ? "success" : "error"}>
|
||||||
|
{webhook.active ? "Active" : "Inactive"}
|
||||||
|
</Chip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Email Configuration */}
|
||||||
|
<Box marginY={8}>
|
||||||
|
<Text size={8} as="h2" marginBottom={4}>
|
||||||
|
Email Configuration
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary>
|
||||||
|
<Text>Customer Emails</Text>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Box display="flex" flexDirection="column" gap={2}>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Text>From:</Text>
|
||||||
|
<Text>support@mail.manoonoils.com</Text>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Text>Provider:</Text>
|
||||||
|
<Text>Resend</Text>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Text>Templates:</Text>
|
||||||
|
<Text>Order Confirmation, Order Shipped, Order Cancelled</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion>
|
||||||
|
<AccordionSummary>
|
||||||
|
<Text>Admin Notifications</Text>
|
||||||
|
</AccordionSummary>
|
||||||
|
<AccordionDetails>
|
||||||
|
<Box display="flex" flexDirection="column" gap={2}>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Text>Recipients:</Text>
|
||||||
|
<Text>me@hytham.me, tamara@hytham.me</Text>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Text>Subject:</Text>
|
||||||
|
<Text>New Order! 🎉 #{orderNumber}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Text>Trigger:</Text>
|
||||||
|
<Text>All order events</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</AccordionDetails>
|
||||||
|
</Accordion>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* App Info */}
|
||||||
|
<Box marginY={8}>
|
||||||
|
<Text size={8} as="h2" marginBottom={4}>
|
||||||
|
App Information
|
||||||
|
</Text>
|
||||||
|
<Box display="flex" flexDirection="column" gap={2}>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Text>Name:</Text>
|
||||||
|
<Text>{appInfo.name}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Text>Version:</Text>
|
||||||
|
<Text>{appInfo.version}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" justifyContent="space-between">
|
||||||
|
<Text>ID:</Text>
|
||||||
|
<Text>{appInfo.id}</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<Box marginTop={8} display="flex" gap={4}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
window.open("https://n8n.nodecrew.me", "_blank");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open N8N
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
window.open("https://resend.com/emails", "_blank");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Resend Emails
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardPage;
|
||||||
+72
-182
@@ -1,206 +1,96 @@
|
|||||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
import { Box, Button, Input, Text } from "@saleor/macaw-ui";
|
import { Box, Text, Button, CircularProgress } from "@saleor/macaw-ui";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import Link from "next/link";
|
import { useEffect, useState } from "react";
|
||||||
import { MouseEventHandler, useEffect, useState } from "react";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
const AddToSaleorForm = () => (
|
|
||||||
<Box
|
|
||||||
as={"form"}
|
|
||||||
display={"flex"}
|
|
||||||
alignItems={"center"}
|
|
||||||
gap={4}
|
|
||||||
onSubmit={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const saleorUrl = new FormData(event.currentTarget as HTMLFormElement).get("saleor-url");
|
|
||||||
const manifestUrl = new URL("/api/manifest", window.location.origin);
|
|
||||||
const redirectUrl = new URL(
|
|
||||||
`/dashboard/apps/install?manifestUrl=${manifestUrl}`,
|
|
||||||
saleorUrl as string
|
|
||||||
).href;
|
|
||||||
|
|
||||||
window.open(redirectUrl, "_blank");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Input type="url" required label="Saleor URL" name="saleor-url" />
|
|
||||||
<Button type="submit">Add to Saleor</Button>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is page publicly accessible from your app.
|
|
||||||
* You should probably remove it.
|
|
||||||
*/
|
|
||||||
const IndexPage: NextPage = () => {
|
const IndexPage: NextPage = () => {
|
||||||
const { appBridgeState, appBridge } = useAppBridge();
|
const { appBridgeState } = useAppBridge();
|
||||||
|
const router = useRouter();
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleLinkClick: MouseEventHandler<HTMLAnchorElement> = (e) => {
|
useEffect(() => {
|
||||||
/**
|
// If inside Saleor Dashboard, redirect to dashboard
|
||||||
* In iframe, link can't be opened in new tab, so Dashboard must be a proxy
|
|
||||||
*/
|
|
||||||
if (appBridgeState?.ready) {
|
if (appBridgeState?.ready) {
|
||||||
e.preventDefault();
|
router.push("/dashboard");
|
||||||
|
}
|
||||||
|
}, [appBridgeState?.ready, router]);
|
||||||
|
|
||||||
appBridge?.dispatch(
|
if (!mounted) {
|
||||||
actions.Redirect({
|
return (
|
||||||
newContext: true,
|
<Box display="flex" justifyContent="center" alignItems="center" height="100vh">
|
||||||
to: e.currentTarget.href,
|
<CircularProgress />
|
||||||
})
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// If inside Saleor Dashboard, show loading while redirecting
|
||||||
* Otherwise, assume app is accessed outside of Dashboard, so href attribute on <a> will work
|
if (appBridgeState?.ready) {
|
||||||
*/
|
|
||||||
};
|
|
||||||
|
|
||||||
const isLocalHost = global.location.href.includes("localhost");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box padding={8}>
|
<Box display="flex" justifyContent="center" alignItems="center" height="100vh">
|
||||||
<Text size={11}>Welcome to Saleor App Template (Next.js) 🚀</Text>
|
<CircularProgress />
|
||||||
<Text as={"p"} marginY={4}>
|
</Box>
|
||||||
Saleor App Template is a minimalistic boilerplate that provides a working example of a
|
);
|
||||||
Saleor app.
|
}
|
||||||
</Text>
|
|
||||||
{appBridgeState?.ready && mounted && (
|
|
||||||
<Link href="/actions">
|
|
||||||
<Button variant="secondary">See what your app can do →</Button>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Text as={"p"} marginTop={8}>
|
// Standalone landing page
|
||||||
Explore the App Template by visiting:
|
return (
|
||||||
|
<Box padding={8} maxWidth={800} margin="0 auto">
|
||||||
|
<Text size={11} as="h1" marginBottom={4}>
|
||||||
|
Core Extensions
|
||||||
</Text>
|
</Text>
|
||||||
<ul>
|
<Text size={6} color="textSecondary" marginBottom={8}>
|
||||||
<li>
|
Email automation for ManoonOils
|
||||||
<code>/src/pages/api/manifest</code> - the{" "}
|
|
||||||
<a
|
|
||||||
href="https://docs.saleor.io/docs/3.x/developer/extending/apps/manifest"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
App Manifest
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>/src/pages/api/webhooks/order-created</code> - an example <code>ORDER_CREATED</code>{" "}
|
|
||||||
webhook handler.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>/graphql</code> - the pre-defined GraphQL queries.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>/generated/graphql.ts</code> - the code generated for those queries by{" "}
|
|
||||||
<a target="_blank" rel="noreferrer" href="https://the-guild.dev/graphql/codegen">
|
|
||||||
GraphQL Code Generator
|
|
||||||
</a>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<Text size={8} marginTop={8} as={"h2"}>
|
|
||||||
Resources
|
|
||||||
</Text>
|
</Text>
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
onClick={handleLinkClick}
|
|
||||||
target="_blank"
|
|
||||||
href="https://docs.saleor.io/docs/3.x/developer/extending/apps/key-concepts"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<Text color={"info1"}>Apps documentation </Text>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
onClick={handleLinkClick}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
href="https://docs.saleor.io/docs/3.x/developer/extending/apps/developing-with-tunnels"
|
|
||||||
>
|
|
||||||
<Text color={"info1"}>Tunneling the app</Text>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
onClick={handleLinkClick}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
href="https://github.com/saleor/app-examples"
|
|
||||||
>
|
|
||||||
<Text color={"info1"}>App Examples repository</Text>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
<Box
|
||||||
<a
|
as="div"
|
||||||
onClick={handleLinkClick}
|
backgroundColor="surfaceNeutralPlain"
|
||||||
target="_blank"
|
padding={8}
|
||||||
rel="noreferrer"
|
borderRadius={4}
|
||||||
href="https://github.com/saleor/saleor-app-sdk"
|
marginBottom={8}
|
||||||
>
|
>
|
||||||
<Text color={"info1"}>Saleor App SDK</Text>
|
<Text size={7} marginBottom={4}>
|
||||||
</a>
|
Features
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
onClick={handleLinkClick}
|
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/saleor/saleor-cli"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<Text color={"info1"}>Saleor CLI</Text>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
onClick={handleLinkClick}
|
|
||||||
target="_blank"
|
|
||||||
href="https://github.com/saleor/apps"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<Text color={"info1"}>Saleor App Store - official apps by Saleor Team</Text>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
onClick={handleLinkClick}
|
|
||||||
target="_blank"
|
|
||||||
href="https://macaw-ui-next.vercel.app/?path=/docs/getting-started-installation--docs"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<Text color={"info1"}>Macaw UI - official Saleor UI library</Text>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
onClick={handleLinkClick}
|
|
||||||
target="_blank"
|
|
||||||
href="https://nextjs.org/docs"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<Text color={"info1"}>Next.js documentation</Text>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{mounted && !isLocalHost && !appBridgeState?.ready && (
|
|
||||||
<>
|
|
||||||
<Text marginBottom={4} as={"p"}>
|
|
||||||
Install this app in your Dashboard and get extra powers!
|
|
||||||
</Text>
|
</Text>
|
||||||
<AddToSaleorForm />
|
<Box as="ul" display="flex" flexDirection="column" gap={2}>
|
||||||
</>
|
<li>✉️ Automated order confirmation emails</li>
|
||||||
)}
|
<li>📦 Shipping notification emails</li>
|
||||||
|
<li>❌ Order cancellation emails</li>
|
||||||
|
<li>🔔 Admin notifications for all orders</li>
|
||||||
|
<li>🎨 Styled HTML email templates</li>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" flexDirection="column" gap={4}>
|
||||||
|
<Text>
|
||||||
|
This app connects Saleor to N8N for automated email workflows using Resend.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Box display="flex" gap={4} marginTop={4}>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => {
|
||||||
|
window.open("https://dashboard.manoonoils.com/apps", "_blank");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open in Saleor Dashboard
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
router.push("/dashboard");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Dashboard
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user