Sendgrid module updated
This commit is contained in:
parent
b1bc467da6
commit
373063d631
28 changed files with 1066 additions and 569 deletions
|
@ -19,7 +19,7 @@
|
||||||
"@monaco-editor/react": "^4.4.6",
|
"@monaco-editor/react": "^4.4.6",
|
||||||
"@saleor/app-sdk": "0.37.3",
|
"@saleor/app-sdk": "0.37.3",
|
||||||
"@saleor/apps-shared": "workspace:*",
|
"@saleor/apps-shared": "workspace:*",
|
||||||
"@saleor/macaw-ui": "0.8.0-pre.76",
|
"@saleor/macaw-ui": "0.8.0-pre.79",
|
||||||
"@sendgrid/client": "^7.7.0",
|
"@sendgrid/client": "^7.7.0",
|
||||||
"@sendgrid/mail": "^7.7.0",
|
"@sendgrid/mail": "^7.7.0",
|
||||||
"@tanstack/react-query": "^4.24.4",
|
"@tanstack/react-query": "^4.24.4",
|
||||||
|
|
17
apps/emails-and-messages/src/components/basic-layout.tsx
Normal file
17
apps/emails-and-messages/src/components/basic-layout.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { Box } from "@saleor/macaw-ui/next";
|
||||||
|
import { Breadcrumbs } from "./breadcrumbs";
|
||||||
|
|
||||||
|
interface BasicLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
isLoading?: boolean;
|
||||||
|
breadcrumbs?: { name: string; href?: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BasicLayout = ({ children, breadcrumbs, isLoading = false }: BasicLayoutProps) => {
|
||||||
|
return (
|
||||||
|
<Box padding={10} display={"grid"} gap={13}>
|
||||||
|
{breadcrumbs?.length && <Breadcrumbs items={breadcrumbs} />}
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,31 +1,56 @@
|
||||||
import { Box, Text } from "@saleor/macaw-ui/next";
|
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { TextLink } from "./text-link";
|
||||||
|
|
||||||
interface BreadcrumbsProps {
|
interface BreadcrumbsProps {
|
||||||
items: Array<{ name: string; href?: string }>;
|
items: Array<{ name: string; href?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays breadcrumbs for the current page.
|
||||||
|
* On desktop full path is visible. On mobile only last item is shown.
|
||||||
|
*/
|
||||||
export const Breadcrumbs = (props: BreadcrumbsProps) => {
|
export const Breadcrumbs = (props: BreadcrumbsProps) => {
|
||||||
if (props.items.length === 0) {
|
if (props.items.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: do I have to recreate the array here?
|
const items = [...props.items];
|
||||||
const i = [...props.items];
|
const lastItem = items.pop()!; // can enforce the type since array is at least one element long
|
||||||
const lastItem = i.pop()!; // can enforce the type since array is at least one element long
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display={"flex"} gap={6}>
|
<Box display={"flex"} gap={6}>
|
||||||
{i.map((item) => (
|
{items.map((item) => (
|
||||||
<>
|
<Box key={`${item.name}`} display={{ mobile: "none", desktop: "flex" }} gap={6}>
|
||||||
<Text variant="hero" display={{ mobile: "none", desktop: "block" }}>
|
{!item.href ? (
|
||||||
{item.name}
|
<Text
|
||||||
</Text>
|
key={`${item.name}_name`}
|
||||||
<Text variant="hero" display={{ mobile: "none", desktop: "block" }}>
|
variant="hero"
|
||||||
|
display={{ mobile: "none", desktop: "block" }}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<TextLink
|
||||||
|
key={`${item.name}_name`}
|
||||||
|
href={item.href}
|
||||||
|
variant="hero"
|
||||||
|
display={{ mobile: "none", desktop: "block" }}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</TextLink>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Text
|
||||||
|
key={`${item.name}_separator`}
|
||||||
|
variant="hero"
|
||||||
|
display={{ mobile: "none", desktop: "block" }}
|
||||||
|
>
|
||||||
{">"}
|
{">"}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</Box>
|
||||||
))}
|
))}
|
||||||
<Text variant="hero" display="block">
|
<Text key={`${lastItem.name}_name`} variant="hero" display="block">
|
||||||
{lastItem.name}
|
{lastItem.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
53
apps/emails-and-messages/src/components/chip-text.tsx
Normal file
53
apps/emails-and-messages/src/components/chip-text.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { Text, Chip, ChipProps } from "@saleor/macaw-ui/next";
|
||||||
|
|
||||||
|
interface ChipTextProps {
|
||||||
|
variant?: "default" | "warning" | "error" | "success";
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChipText = ({ variant = "default", content }: ChipTextProps) => {
|
||||||
|
const commonProps: ChipProps = {
|
||||||
|
__maxWidth: "max-content",
|
||||||
|
display: "flex",
|
||||||
|
borderStyle: "solid",
|
||||||
|
borderWidth: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Choose colors for variants
|
||||||
|
switch (variant) {
|
||||||
|
case "default":
|
||||||
|
return (
|
||||||
|
<Chip {...commonProps} borderColor={"neutralSubdued"}>
|
||||||
|
<Text color="textNeutralDefault" size="small" variant="caption">
|
||||||
|
{content}
|
||||||
|
</Text>
|
||||||
|
</Chip>
|
||||||
|
);
|
||||||
|
case "warning":
|
||||||
|
return (
|
||||||
|
<Chip {...commonProps} borderColor={"brandHighlight"}>
|
||||||
|
<Text color="textNeutralDefault" size="small" variant="caption">
|
||||||
|
{content}
|
||||||
|
</Text>
|
||||||
|
</Chip>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "error":
|
||||||
|
return (
|
||||||
|
<Chip {...commonProps} borderColor={"criticalDefault"}>
|
||||||
|
<Text color="textNeutralDefault" size="small" variant="caption">
|
||||||
|
{content}
|
||||||
|
</Text>
|
||||||
|
</Chip>
|
||||||
|
);
|
||||||
|
|
||||||
|
case "success":
|
||||||
|
return (
|
||||||
|
<Chip {...commonProps} borderColor={"neutralDefault"}>
|
||||||
|
<Text color="textNeutralDefault" size="small" variant="caption">
|
||||||
|
{content}
|
||||||
|
</Text>
|
||||||
|
</Chip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
|
@ -3,7 +3,11 @@ import { BoxWithBorder } from "./box-with-border";
|
||||||
import { BoxFooter } from "./box-footer";
|
import { BoxFooter } from "./box-footer";
|
||||||
import { defaultPadding } from "./ui-defaults";
|
import { defaultPadding } from "./ui-defaults";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { SendgridConfiguration } from "../modules/sendgrid/configuration/sendgrid-config";
|
import { SendgridConfiguration } from "../modules/sendgrid/configuration/sendgrid-config-schema";
|
||||||
|
import { TextLink } from "./text-link";
|
||||||
|
import { ChipText } from "./chip-text";
|
||||||
|
import Image from "next/image";
|
||||||
|
import sendgrid from "../public/sendgrid.png";
|
||||||
|
|
||||||
const NoExistingConfigurations = () => {
|
const NoExistingConfigurations = () => {
|
||||||
const { replace } = useRouter();
|
const { replace } = useRouter();
|
||||||
|
@ -53,17 +57,32 @@ export const MessagingProvidersBox = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxWithBorder>
|
<BoxWithBorder>
|
||||||
<Box padding={defaultPadding} display={"grid"} gridTemplateColumns={4}>
|
<Box padding={defaultPadding} display={"grid"} gridTemplateColumns={4} gap={defaultPadding}>
|
||||||
<Text variant="caption">Provider</Text>
|
<Text variant="caption" color="textNeutralSubdued">
|
||||||
<Text variant="caption">Name</Text>
|
Provider
|
||||||
<Text variant="caption">Status</Text>
|
</Text>
|
||||||
|
<Text variant="caption" color="textNeutralSubdued">
|
||||||
|
Configuration name
|
||||||
|
</Text>
|
||||||
|
<Text variant="caption" color="textNeutralSubdued">
|
||||||
|
Status
|
||||||
|
</Text>
|
||||||
<Box />
|
<Box />
|
||||||
{configurations.map((configuration) => (
|
{configurations.map((configuration) => (
|
||||||
<>
|
<>
|
||||||
<Text>Sendgrid</Text>
|
<Box display="flex" gap={defaultPadding}>
|
||||||
<Text>{configuration.configurationName}</Text>
|
<Image alt="Sendgrid logo" src={sendgrid} height={20} width={20} />
|
||||||
<Text>{configuration.active}</Text>
|
<Text>Sendgrid</Text>
|
||||||
<Text onClick={() => redirectToEditConfiguration(configuration.id)}>Edit</Text>
|
</Box>
|
||||||
|
|
||||||
|
<Text>{configuration.name}</Text>
|
||||||
|
<ChipText
|
||||||
|
content={configuration.active ? "Active" : "Inactive"}
|
||||||
|
variant={configuration.active ? "success" : "error"}
|
||||||
|
/>
|
||||||
|
<Box display="flex" justifyContent={"flex-end"}>
|
||||||
|
<TextLink href={`/configuration/sendgrid/edit/${configuration.id}`}>Edit</TextLink>
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -1,160 +0,0 @@
|
||||||
const x = () => (
|
|
||||||
<SectionWithDescription
|
|
||||||
title="Events"
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<Text display="block">
|
|
||||||
Provide unique name for your configuration - you can create more than one. For example
|
|
||||||
- production and development.
|
|
||||||
</Text>
|
|
||||||
<Text display="block">Then, pass your API Key. Obtain it here.</Text>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BoxWithBorder backgroundColor={"surfaceCriticalSubdued"} borderColor={"criticalSubdued"}>
|
|
||||||
<Box padding={defaultPadding}>
|
|
||||||
<Text variant="heading" display="block">
|
|
||||||
Remove provider
|
|
||||||
</Text>
|
|
||||||
<Text display="block">You can remove provider configuration.</Text>
|
|
||||||
<Text display="block">
|
|
||||||
This operation will remove all settings related to this configuration. Data will be
|
|
||||||
permanently removed from the App.{" "}
|
|
||||||
</Text>
|
|
||||||
<Text display="block">
|
|
||||||
This operation cant be undone. You still can create new configuration.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
<BoxFooter borderColor={"criticalSubdued"}>
|
|
||||||
<Button color={"textNeutralSubdued"} backgroundColor={"interactiveCriticalDefault"}>
|
|
||||||
Remove provider
|
|
||||||
</Button>
|
|
||||||
</BoxFooter>
|
|
||||||
</BoxWithBorder>
|
|
||||||
</SectionWithDescription>
|
|
||||||
<SectionWithDescription
|
|
||||||
title="Events"
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<Text display="block">
|
|
||||||
Provide unique name for your configuration - you can create more than one. For example
|
|
||||||
- production and development.
|
|
||||||
</Text>
|
|
||||||
<Text display="block">Then, pass your API Key. Obtain it here.</Text>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BoxWithBorder>
|
|
||||||
<Box
|
|
||||||
padding={defaultPadding}
|
|
||||||
display={"flex"}
|
|
||||||
flexDirection={"column"}
|
|
||||||
gap={defaultPadding}
|
|
||||||
>
|
|
||||||
<Box display="grid" gridTemplateColumns={3} gap={defaultPadding}>
|
|
||||||
<Input value={"Order created"} disabled label="Configuration name" />
|
|
||||||
<Combobox
|
|
||||||
options={[
|
|
||||||
{ label: "Template 1", value: "1" },
|
|
||||||
{ label: "Template 2", value: "2" },
|
|
||||||
]}
|
|
||||||
label="Template"
|
|
||||||
/>
|
|
||||||
<Toggle>
|
|
||||||
<Text variant="body">Enabled</Text>
|
|
||||||
</Toggle>
|
|
||||||
</Box>
|
|
||||||
<Box display="grid" gridTemplateColumns={3} gap={defaultPadding}>
|
|
||||||
<Input value={"Gift card sent"} disabled label="Configuration name" />
|
|
||||||
<Combobox
|
|
||||||
options={[
|
|
||||||
{ label: "Template 1", value: "1" },
|
|
||||||
{ label: "Template 2", value: "2" },
|
|
||||||
]}
|
|
||||||
label="Template"
|
|
||||||
/>
|
|
||||||
<Toggle>
|
|
||||||
<Text variant="body">Enabled</Text>
|
|
||||||
</Toggle>
|
|
||||||
</Box>
|
|
||||||
<Box display="grid" gridTemplateColumns={3} gap={defaultPadding}>
|
|
||||||
<Input value={"Confirm account"} disabled label="Configuration name" />
|
|
||||||
<Combobox
|
|
||||||
options={[
|
|
||||||
{ label: "Template 1", value: "1" },
|
|
||||||
{ label: "Template 2", value: "2" },
|
|
||||||
]}
|
|
||||||
label="Template"
|
|
||||||
/>
|
|
||||||
<Toggle>
|
|
||||||
<Text variant="body">Enabled</Text>
|
|
||||||
</Toggle>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<BoxFooter>
|
|
||||||
<Button>Save provider</Button>
|
|
||||||
</BoxFooter>
|
|
||||||
</BoxWithBorder>
|
|
||||||
</SectionWithDescription>
|
|
||||||
<SectionWithDescription
|
|
||||||
title="Connect Sendgrid"
|
|
||||||
description={
|
|
||||||
<>
|
|
||||||
<Text display="block">
|
|
||||||
Provide unique name for your configuration - you can create more than one. For example
|
|
||||||
- production and development.
|
|
||||||
</Text>
|
|
||||||
<Text display="block">Then, pass your API Key. Obtain it here.</Text>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BoxWithBorder>
|
|
||||||
<Box padding={defaultPadding} display={"flex"} flexDirection={"column"} gap={10}>
|
|
||||||
<Input
|
|
||||||
label="Configuration name"
|
|
||||||
helperText="Used to distinguish between multiple configurations"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="API key"
|
|
||||||
helperText="Your API key, ensure it has permission XYZ enabled"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<BoxFooter>
|
|
||||||
<Button>Save provider</Button>
|
|
||||||
</BoxFooter>
|
|
||||||
</BoxWithBorder>
|
|
||||||
</SectionWithDescription>
|
|
||||||
<SectionWithDescription
|
|
||||||
title="Messaging providers"
|
|
||||||
description={
|
|
||||||
<Text>
|
|
||||||
Manage providers configuration to connect Saleor events with 3rd party services.
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<BoxWithBorder
|
|
||||||
padding={10}
|
|
||||||
display={"grid"}
|
|
||||||
alignItems={"center"}
|
|
||||||
justifyContent={"center"}
|
|
||||||
>
|
|
||||||
<Text>No providers configured yet</Text>
|
|
||||||
<Button>Add first provider</Button>
|
|
||||||
</BoxWithBorder>
|
|
||||||
</SectionWithDescription>
|
|
||||||
<SectionWithDescription title="Choose provider">
|
|
||||||
<Box display="grid" gridTemplateColumns={2} gap={6}>
|
|
||||||
<ProviderSelectionBox
|
|
||||||
providerName="Sendgrid"
|
|
||||||
providerDescription="Use dynamic templates created in Sendgrid dashboard to send messages. Event data will be forwarded to Sendgrid."
|
|
||||||
onClick={() => console.log("clicked sendgrid")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ProviderSelectionBox
|
|
||||||
providerName="SMTP & MJML"
|
|
||||||
providerDescription="Provide your own SMTP credentials and map Saleor event to custom MJML templates."
|
|
||||||
onClick={() => console.log("clicked mjml")}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</SectionWithDescription>
|
|
||||||
)
|
|
14
apps/emails-and-messages/src/components/text-link.tsx
Normal file
14
apps/emails-and-messages/src/components/text-link.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { TextProps, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface TextLinkProps extends TextProps {
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TextLink = (props: TextLinkProps) => {
|
||||||
|
return (
|
||||||
|
<Text textDecoration={"underline"} variant="bodyStrong" {...props}>
|
||||||
|
<Link href={props.href}>{props.children}</Link>
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,7 +1,8 @@
|
||||||
|
import { SendgridConfigurationChannels } from "../modules/sendgrid/configuration/sendgrid-config-schema";
|
||||||
|
|
||||||
interface IsAvailableInChannelArgs {
|
interface IsAvailableInChannelArgs {
|
||||||
channel: string;
|
channel: string;
|
||||||
restrictedToChannels: string[];
|
channelConfiguration: SendgridConfigurationChannels;
|
||||||
excludedChannels: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,14 +15,13 @@ interface IsAvailableInChannelArgs {
|
||||||
*/
|
*/
|
||||||
export const isAvailableInChannel = ({
|
export const isAvailableInChannel = ({
|
||||||
channel,
|
channel,
|
||||||
restrictedToChannels,
|
channelConfiguration,
|
||||||
excludedChannels,
|
|
||||||
}: IsAvailableInChannelArgs): boolean => {
|
}: IsAvailableInChannelArgs): boolean => {
|
||||||
if (channel in excludedChannels) {
|
if (!channelConfiguration.override) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
if (restrictedToChannels.length > 0 && !(channel in restrictedToChannels)) {
|
if (channelConfiguration.mode === "restrict") {
|
||||||
return false;
|
return channel in channelConfiguration.channels;
|
||||||
}
|
}
|
||||||
return true;
|
return !(channel in channelConfiguration.channels);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { SendgridConfigurator, PrivateMetadataSendgridConfigurator } from "./sendgrid-configurator";
|
import { SendgridConfigurator, PrivateMetadataSendgridConfigurator } from "./sendgrid-configurator";
|
||||||
import { Client } from "urql";
|
import { Client } from "urql";
|
||||||
import { logger as pinoLogger } from "../../../lib/logger";
|
import { logger as pinoLogger } from "../../../lib/logger";
|
||||||
import { SendgridConfig, SendgridConfiguration } from "./sendgrid-config";
|
import { SendgridConfig, SendgridConfiguration } from "./sendgrid-config-schema";
|
||||||
import { FilterConfigurationsArgs, SendgridConfigContainer } from "./sendgrid-config-container";
|
import { FilterConfigurationsArgs, SendgridConfigContainer } from "./sendgrid-config-container";
|
||||||
import { createSettingsManager } from "../../../lib/metadata-manager";
|
import { createSettingsManager } from "../../../lib/metadata-manager";
|
||||||
|
|
||||||
|
|
|
@ -4,30 +4,24 @@ import { messageEventTypes } from "../../event-handlers/message-event-types";
|
||||||
import {
|
import {
|
||||||
SendgridConfig as SendgridConfigurationRoot,
|
SendgridConfig as SendgridConfigurationRoot,
|
||||||
SendgridConfiguration,
|
SendgridConfiguration,
|
||||||
} from "./sendgrid-config";
|
sendgridConfigurationEventSchema,
|
||||||
|
sendgridConfigurationSchema,
|
||||||
|
} from "./sendgrid-config-schema";
|
||||||
|
|
||||||
export const getDefaultEventsConfiguration = (): SendgridConfiguration["events"] =>
|
export const getDefaultEventsConfiguration = (): SendgridConfiguration["events"] =>
|
||||||
messageEventTypes.map((eventType) => ({
|
messageEventTypes.map((eventType) => sendgridConfigurationEventSchema.parse({ eventType }));
|
||||||
active: true,
|
|
||||||
eventType: eventType,
|
|
||||||
template: "",
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const getDefaultEmptyConfiguration = (): SendgridConfiguration => {
|
export const getDefaultEmptyConfiguration = (): SendgridConfiguration => {
|
||||||
const defaultConfig: SendgridConfiguration = {
|
const defaultConfig: SendgridConfiguration = sendgridConfigurationSchema.parse({
|
||||||
id: "",
|
id: "id",
|
||||||
active: true,
|
name: "name",
|
||||||
configurationName: "",
|
apiKey: "key",
|
||||||
senderName: undefined,
|
|
||||||
senderEmail: undefined,
|
|
||||||
apiKey: "",
|
|
||||||
sandboxMode: false,
|
|
||||||
events: getDefaultEventsConfiguration(),
|
|
||||||
channels: {
|
channels: {
|
||||||
excludedFrom: [],
|
excludedFrom: [],
|
||||||
restrictedTo: [],
|
restrictedTo: [],
|
||||||
},
|
},
|
||||||
};
|
events: getDefaultEventsConfiguration(),
|
||||||
|
});
|
||||||
|
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,46 +1,26 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { messageEventTypes } from "../../event-handlers/message-event-types";
|
import { messageEventTypes } from "../../event-handlers/message-event-types";
|
||||||
|
import {
|
||||||
|
sendgridConfigurationChannelsSchema,
|
||||||
|
sendgridConfigurationEventSchema,
|
||||||
|
sendgridConfigurationSchema,
|
||||||
|
} from "./sendgrid-config-schema";
|
||||||
|
|
||||||
export const sendgridConfigurationEventObjectSchema = z.object({
|
export const sendgridCreateConfigurationInputSchema = sendgridConfigurationSchema.pick({
|
||||||
active: z.boolean(),
|
name: true,
|
||||||
eventType: z.enum(messageEventTypes),
|
|
||||||
template: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const sendgridConfigurationBaseObjectSchema = z.object({
|
|
||||||
active: z.boolean(),
|
|
||||||
configurationName: z.string().min(1),
|
|
||||||
sandboxMode: z.boolean(),
|
|
||||||
apiKey: z.string().min(1),
|
|
||||||
senderName: z.string().min(1).optional(),
|
|
||||||
senderEmail: z.string().email().min(5).optional(),
|
|
||||||
channels: z.object({
|
|
||||||
excludedFrom: z.array(z.string()),
|
|
||||||
restrictedTo: z.array(z.string()),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const sendgridCreateConfigurationSchema = sendgridConfigurationBaseObjectSchema.pick({
|
|
||||||
configurationName: true,
|
|
||||||
apiKey: true,
|
apiKey: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SendgridCreateConfigurationSchemaType = z.infer<
|
export type SendgridCreateConfigurationInput = z.infer<
|
||||||
typeof sendgridCreateConfigurationSchema
|
typeof sendgridCreateConfigurationInputSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const sendgridUpdateOrCreateConfigurationSchema =
|
export const sendgridConfigurationIdInputSchema = sendgridConfigurationSchema.pick({
|
||||||
sendgridConfigurationBaseObjectSchema.merge(
|
id: true,
|
||||||
z.object({
|
|
||||||
id: z.string().optional(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
export const sendgridGetConfigurationInputSchema = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
});
|
|
||||||
export const sendgridDeleteConfigurationInputSchema = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type SendgridGetConfigurationIdInput = z.infer<typeof sendgridConfigurationIdInputSchema>;
|
||||||
|
|
||||||
export const sendgridGetConfigurationsInputSchema = z
|
export const sendgridGetConfigurationsInputSchema = z
|
||||||
.object({
|
.object({
|
||||||
ids: z.array(z.string()).optional(),
|
ids: z.array(z.string()).optional(),
|
||||||
|
@ -48,20 +28,61 @@ export const sendgridGetConfigurationsInputSchema = z
|
||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
|
export type SendgridGetConfigurationsInput = z.infer<typeof sendgridGetConfigurationsInputSchema>;
|
||||||
|
|
||||||
export const sendgridUpdateEventConfigurationInputSchema = z
|
export const sendgridUpdateEventConfigurationInputSchema = z
|
||||||
.object({
|
.object({
|
||||||
configurationId: z.string(),
|
configurationId: z.string(),
|
||||||
})
|
})
|
||||||
.merge(sendgridConfigurationEventObjectSchema);
|
.merge(sendgridConfigurationEventSchema);
|
||||||
|
|
||||||
|
export type SendgridUpdateEventConfigurationInput = z.infer<
|
||||||
|
typeof sendgridUpdateEventConfigurationInputSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const sendgridGetEventConfigurationInputSchema = z.object({
|
export const sendgridGetEventConfigurationInputSchema = z.object({
|
||||||
configurationId: z.string(),
|
configurationId: z.string(),
|
||||||
eventType: z.enum(messageEventTypes),
|
eventType: z.enum(messageEventTypes),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sendgridUpdateBasicInformationSchema = sendgridConfigurationBaseObjectSchema
|
export type SendgridGetEventConfigurationInput = z.infer<
|
||||||
.pick({
|
typeof sendgridGetEventConfigurationInputSchema
|
||||||
configurationName: true,
|
>;
|
||||||
active: true,
|
|
||||||
|
export const sendgridUpdateBasicInformationSchema = sendgridConfigurationSchema.pick({
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SendgridUpdateBasicInformation = z.infer<typeof sendgridUpdateBasicInformationSchema>;
|
||||||
|
|
||||||
|
export const sendgridUpdateApiConnectionSchema = sendgridConfigurationSchema.pick({
|
||||||
|
id: true,
|
||||||
|
apiKey: true,
|
||||||
|
sandboxMode: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SendgridUpdateApiConnection = z.infer<typeof sendgridUpdateApiConnectionSchema>;
|
||||||
|
|
||||||
|
export const sendgridUpdateSenderSchema = sendgridConfigurationSchema.pick({
|
||||||
|
id: true,
|
||||||
|
sender: true,
|
||||||
|
});
|
||||||
|
export type SendgridUpdateSender = z.infer<typeof sendgridUpdateSenderSchema>;
|
||||||
|
|
||||||
|
export const sendgridUpdateChannelsSchema = sendgridConfigurationChannelsSchema.merge(
|
||||||
|
sendgridConfigurationSchema.pick({
|
||||||
|
id: true,
|
||||||
})
|
})
|
||||||
.merge(z.object({ id: z.string() }));
|
);
|
||||||
|
|
||||||
|
export type SendgridUpdateChannels = z.infer<typeof sendgridUpdateChannelsSchema>;
|
||||||
|
|
||||||
|
export const sendgridUpdateEventSchema = sendgridConfigurationEventSchema.merge(
|
||||||
|
sendgridConfigurationSchema.pick({
|
||||||
|
id: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export type SendgridUpdateEvent = z.infer<typeof sendgridUpdateEventSchema>;
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
import { messageEventTypes } from "../../event-handlers/message-event-types";
|
||||||
|
|
||||||
|
export const sendgridConfigurationEventSchema = z.object({
|
||||||
|
active: z.boolean().default(false),
|
||||||
|
eventType: z.enum(messageEventTypes),
|
||||||
|
template: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SendgridEventConfiguration = z.infer<typeof sendgridConfigurationEventSchema>;
|
||||||
|
|
||||||
|
export const sendgridConfigurationChannelsSchema = z.object({
|
||||||
|
override: z.boolean().default(false),
|
||||||
|
channels: z.array(z.string()).default([]),
|
||||||
|
mode: z.enum(["exclude", "restrict"]).default("restrict"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SendgridConfigurationChannels = z.infer<typeof sendgridConfigurationChannelsSchema>;
|
||||||
|
|
||||||
|
export const sendgridConfigurationSchema = z.object({
|
||||||
|
id: z.string().min(1),
|
||||||
|
active: z.boolean().default(true),
|
||||||
|
name: z.string().min(1),
|
||||||
|
sandboxMode: z.boolean().default(false),
|
||||||
|
apiKey: z.string().min(1),
|
||||||
|
sender: z.string().min(1).optional(),
|
||||||
|
channels: sendgridConfigurationChannelsSchema,
|
||||||
|
events: z.array(sendgridConfigurationEventSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SendgridConfiguration = z.infer<typeof sendgridConfigurationSchema>;
|
||||||
|
|
||||||
|
export const sendgridConfigSchema = z.object({
|
||||||
|
configurations: z.array(sendgridConfigurationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SendgridConfig = z.infer<typeof sendgridConfigSchema>;
|
|
@ -1,26 +0,0 @@
|
||||||
import { MessageEventTypes } from "../../event-handlers/message-event-types";
|
|
||||||
|
|
||||||
export interface SendgridEventConfiguration {
|
|
||||||
active: boolean;
|
|
||||||
eventType: MessageEventTypes;
|
|
||||||
template: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SendgridConfiguration {
|
|
||||||
id: string;
|
|
||||||
active: boolean;
|
|
||||||
configurationName: string;
|
|
||||||
sandboxMode: boolean;
|
|
||||||
senderName?: string;
|
|
||||||
senderEmail?: string;
|
|
||||||
apiKey: string;
|
|
||||||
events: SendgridEventConfiguration[];
|
|
||||||
channels: {
|
|
||||||
excludedFrom: string[];
|
|
||||||
restrictedTo: string[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SendgridConfig = {
|
|
||||||
configurations: SendgridConfiguration[];
|
|
||||||
};
|
|
|
@ -1,18 +1,21 @@
|
||||||
import { logger as pinoLogger } from "../../../lib/logger";
|
import { logger as pinoLogger } from "../../../lib/logger";
|
||||||
import {
|
import {
|
||||||
sendgridCreateConfigurationSchema,
|
sendgridConfigurationIdInputSchema,
|
||||||
sendgridDeleteConfigurationInputSchema,
|
sendgridCreateConfigurationInputSchema,
|
||||||
sendgridGetConfigurationInputSchema,
|
|
||||||
sendgridGetConfigurationsInputSchema,
|
sendgridGetConfigurationsInputSchema,
|
||||||
sendgridGetEventConfigurationInputSchema,
|
sendgridGetEventConfigurationInputSchema,
|
||||||
|
sendgridUpdateApiConnectionSchema,
|
||||||
sendgridUpdateBasicInformationSchema,
|
sendgridUpdateBasicInformationSchema,
|
||||||
|
sendgridUpdateChannelsSchema,
|
||||||
sendgridUpdateEventConfigurationInputSchema,
|
sendgridUpdateEventConfigurationInputSchema,
|
||||||
sendgridUpdateOrCreateConfigurationSchema,
|
sendgridUpdateEventSchema,
|
||||||
|
sendgridUpdateSenderSchema,
|
||||||
} from "./sendgrid-config-input-schema";
|
} from "./sendgrid-config-input-schema";
|
||||||
import { SendgridConfigurationService } from "./get-sendgrid-configuration.service";
|
import { SendgridConfigurationService } from "./get-sendgrid-configuration.service";
|
||||||
import { router } from "../../trpc/trpc-server";
|
import { router } from "../../trpc/trpc-server";
|
||||||
import { protectedClientProcedure } from "../../trpc/protected-client-procedure";
|
import { protectedClientProcedure } from "../../trpc/protected-client-procedure";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { getDefaultEmptyConfiguration } from "./sendgrid-config-container";
|
||||||
|
|
||||||
// Allow access only for the dashboard users and attaches the
|
// Allow access only for the dashboard users and attaches the
|
||||||
// configuration service to the context
|
// configuration service to the context
|
||||||
|
@ -36,7 +39,7 @@ export const sendgridConfigurationRouter = router({
|
||||||
}),
|
}),
|
||||||
getConfiguration: protectedWithConfigurationService
|
getConfiguration: protectedWithConfigurationService
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(sendgridGetConfigurationInputSchema)
|
.input(sendgridConfigurationIdInputSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
logger.debug(input, "sendgridConfigurationRouter.get called");
|
logger.debug(input, "sendgridConfigurationRouter.get called");
|
||||||
|
@ -52,15 +55,19 @@ export const sendgridConfigurationRouter = router({
|
||||||
}),
|
}),
|
||||||
createConfiguration: protectedWithConfigurationService
|
createConfiguration: protectedWithConfigurationService
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(sendgridCreateConfigurationSchema)
|
.input(sendgridCreateConfigurationInputSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
logger.debug(input, "sendgridConfigurationRouter.create called");
|
logger.debug(input, "sendgridConfigurationRouter.create called");
|
||||||
return await ctx.configurationService.createConfiguration(input);
|
const newConfiguration = {
|
||||||
|
...getDefaultEmptyConfiguration(),
|
||||||
|
...input,
|
||||||
|
};
|
||||||
|
return await ctx.configurationService.createConfiguration(newConfiguration);
|
||||||
}),
|
}),
|
||||||
deleteConfiguration: protectedWithConfigurationService
|
deleteConfiguration: protectedWithConfigurationService
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(sendgridDeleteConfigurationInputSchema)
|
.input(sendgridConfigurationIdInputSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
logger.debug(input, "sendgridConfigurationRouter.delete called");
|
logger.debug(input, "sendgridConfigurationRouter.delete called");
|
||||||
|
@ -74,33 +81,33 @@ export const sendgridConfigurationRouter = router({
|
||||||
await ctx.configurationService.deleteConfiguration(input);
|
await ctx.configurationService.deleteConfiguration(input);
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
updateOrCreateConfiguration: protectedWithConfigurationService
|
// updateOrCreateConfiguration: protectedWithConfigurationService
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
// .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(sendgridUpdateOrCreateConfigurationSchema)
|
// .input(sendgridUpdateOrCreateConfigurationSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
// .mutation(async ({ ctx, input }) => {
|
||||||
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
// const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
logger.debug(input, "sendgridConfigurationRouter.update or create called");
|
// logger.debug(input, "sendgridConfigurationRouter.update or create called");
|
||||||
|
|
||||||
const { id } = input;
|
// const { id } = input;
|
||||||
if (!id) {
|
// if (!id) {
|
||||||
return await ctx.configurationService.createConfiguration(input);
|
// return await ctx.configurationService.createConfiguration(input);
|
||||||
} else {
|
// } else {
|
||||||
const existingConfiguration = await ctx.configurationService.getConfiguration({ id });
|
// const existingConfiguration = await ctx.configurationService.getConfiguration({ id });
|
||||||
if (!existingConfiguration) {
|
// if (!existingConfiguration) {
|
||||||
throw new TRPCError({
|
// throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
// code: "BAD_REQUEST",
|
||||||
message: "Configuration not found",
|
// message: "Configuration not found",
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
const configuration = {
|
// const configuration = {
|
||||||
id,
|
// id,
|
||||||
...input,
|
// ...input,
|
||||||
events: existingConfiguration.events,
|
// events: existingConfiguration.events,
|
||||||
};
|
// };
|
||||||
await ctx.configurationService.updateConfiguration(configuration);
|
// await ctx.configurationService.updateConfiguration(configuration);
|
||||||
return configuration;
|
// return configuration;
|
||||||
}
|
// }
|
||||||
}),
|
// }),
|
||||||
getEventConfiguration: protectedWithConfigurationService
|
getEventConfiguration: protectedWithConfigurationService
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(sendgridGetEventConfigurationInputSchema)
|
.input(sendgridGetEventConfigurationInputSchema)
|
||||||
|
@ -175,4 +182,92 @@ export const sendgridConfigurationRouter = router({
|
||||||
await ctx.configurationService.updateConfiguration({ ...configuration, ...input });
|
await ctx.configurationService.updateConfiguration({ ...configuration, ...input });
|
||||||
return configuration;
|
return configuration;
|
||||||
}),
|
}),
|
||||||
|
updateApiConnection: protectedWithConfigurationService
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(sendgridUpdateApiConnectionSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const configuration = await ctx.configurationService.getConfiguration({
|
||||||
|
id: input.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!configuration) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Configuration not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.configurationService.updateConfiguration({ ...configuration, ...input });
|
||||||
|
return configuration;
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateSender: protectedWithConfigurationService
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(sendgridUpdateSenderSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const configuration = await ctx.configurationService.getConfiguration({
|
||||||
|
id: input.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!configuration) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Configuration not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await ctx.configurationService.updateConfiguration({ ...configuration, ...input });
|
||||||
|
return configuration;
|
||||||
|
}),
|
||||||
|
updateChannels: protectedWithConfigurationService
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(sendgridUpdateChannelsSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const configuration = await ctx.configurationService.getConfiguration({
|
||||||
|
id: input.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!configuration) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Configuration not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
configuration.channels = {
|
||||||
|
override: input.override,
|
||||||
|
channels: input.channels,
|
||||||
|
mode: input.mode,
|
||||||
|
};
|
||||||
|
await ctx.configurationService.updateConfiguration(configuration);
|
||||||
|
return configuration;
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateEvent: protectedWithConfigurationService
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(sendgridUpdateEventSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const configuration = await ctx.configurationService.getConfiguration({
|
||||||
|
id: input.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!configuration) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Configuration not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const event = configuration.events.find((e) => e.eventType === input.eventType);
|
||||||
|
|
||||||
|
if (!event) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Configuration event not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
event.template = input.template;
|
||||||
|
event.active = input.active;
|
||||||
|
|
||||||
|
await ctx.configurationService.updateConfiguration(configuration);
|
||||||
|
return configuration;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SendgridConfig } from "./sendgrid-config";
|
import { SendgridConfig } from "./sendgrid-config-schema";
|
||||||
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||||
|
|
||||||
export interface SendgridConfigurator {
|
export interface SendgridConfigurator {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { logger as pinoLogger } from "../../lib/logger";
|
import { logger as pinoLogger } from "../../lib/logger";
|
||||||
import { SendgridConfiguration } from "./configuration/sendgrid-config";
|
import { SendgridConfiguration } from "./configuration/sendgrid-config-schema";
|
||||||
import { MailService } from "@sendgrid/mail";
|
import { MailService } from "@sendgrid/mail";
|
||||||
import { MessageEventTypes } from "../event-handlers/message-event-types";
|
import { MessageEventTypes } from "../event-handlers/message-event-types";
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export const fetchTemplates =
|
||||||
};
|
};
|
||||||
const templates =
|
const templates =
|
||||||
resJson.result?.map((r) => ({
|
resJson.result?.map((r) => ({
|
||||||
value: r.id,
|
value: r.id.toString(),
|
||||||
label: r.name,
|
label: r.name,
|
||||||
})) || [];
|
})) || [];
|
||||||
return templates;
|
return templates;
|
||||||
|
@ -65,7 +65,7 @@ export const fetchSenders =
|
||||||
};
|
};
|
||||||
const senders =
|
const senders =
|
||||||
resJson.results?.map((r) => ({
|
resJson.results?.map((r) => ({
|
||||||
value: r.id,
|
value: r.id.toString(),
|
||||||
label: `${r.nickname} (${r.from_email})`,
|
label: `${r.nickname} (${r.from_email})`,
|
||||||
nickname: r.nickname,
|
nickname: r.nickname,
|
||||||
from_email: r.from_email,
|
from_email: r.from_email,
|
||||||
|
|
|
@ -1,76 +1,64 @@
|
||||||
import { useRouter } from "next/router";
|
import { SendgridConfiguration } from "../configuration/sendgrid-config-schema";
|
||||||
import { SendgridConfiguration } from "../configuration/sendgrid-config";
|
|
||||||
import { BoxWithBorder } from "../../../components/box-with-border";
|
import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
import { Box, Button, Input, RadioGroup, Text } from "@saleor/macaw-ui/next";
|
import { Box, Button, Input, Text } from "@saleor/macaw-ui/next";
|
||||||
import { defaultPadding } from "../../../components/ui-defaults";
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
import { sendgridUpdateBasicInformationSchema } from "../configuration/sendgrid-config-input-schema";
|
import { SendgridUpdateApiConnection } from "../configuration/sendgrid-config-input-schema";
|
||||||
import { z } from "zod";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { BoxFooter } from "../../../components/box-footer";
|
import { BoxFooter } from "../../../components/box-footer";
|
||||||
import { SectionWithDescription } from "../../../components/section-with-description";
|
import { SectionWithDescription } from "../../../components/section-with-description";
|
||||||
|
|
||||||
interface ApiConnectionSectionProps {
|
interface ApiConnectionSectionProps {
|
||||||
configuration?: SendgridConfiguration;
|
configuration: SendgridConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ApiConnectionSection = ({ configuration }: ApiConnectionSectionProps) => {
|
export const ApiConnectionSection = ({ configuration }: ApiConnectionSectionProps) => {
|
||||||
const { replace } = useRouter();
|
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
|
|
||||||
const { handleSubmit, control, setError } = useForm<
|
const { handleSubmit, control, setError, register } = useForm<SendgridUpdateApiConnection>({
|
||||||
z.infer<typeof sendgridUpdateBasicInformationSchema>
|
|
||||||
>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
configurationName: configuration?.configurationName,
|
id: configuration.id,
|
||||||
active: configuration?.active,
|
apiKey: configuration.apiKey,
|
||||||
|
sandboxMode: configuration.sandboxMode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: createConfiguration } =
|
const trpcContext = trpcClient.useContext();
|
||||||
trpcClient.sendgridConfiguration.updateBasicInformation.useMutation({
|
const { mutate } = trpcClient.sendgridConfiguration.updateApiConnection.useMutation({
|
||||||
onSuccess: async (data, variables) => {
|
onSuccess: async (data, variables) => {
|
||||||
notifySuccess("Configuration saved");
|
notifySuccess("Configuration saved");
|
||||||
// TODO: redirect to configuration details based on id
|
trpcContext.sendgridConfiguration.invalidate();
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
let isFieldErrorSet = false;
|
let isFieldErrorSet = false;
|
||||||
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
||||||
for (const fieldName in fieldErrors) {
|
for (const fieldName in fieldErrors) {
|
||||||
for (const message of fieldErrors[fieldName] || []) {
|
for (const message of fieldErrors[fieldName] || []) {
|
||||||
isFieldErrorSet = true;
|
isFieldErrorSet = true;
|
||||||
setError(fieldName as keyof z.infer<typeof sendgridUpdateBasicInformationSchema>, {
|
setError(fieldName as keyof SendgridUpdateApiConnection, {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const formErrors = error.data?.zodError?.formErrors || [];
|
}
|
||||||
const formErrorMessage = formErrors.length ? formErrors.join("\n") : undefined;
|
const formErrors = error.data?.zodError?.formErrors || [];
|
||||||
|
const formErrorMessage = formErrors.length ? formErrors.join("\n") : undefined;
|
||||||
|
|
||||||
notifyError(
|
notifyError(
|
||||||
"Could not save the configuration",
|
"Could not save the configuration",
|
||||||
isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
||||||
formErrorMessage
|
formErrorMessage
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!configuration) {
|
|
||||||
return (
|
|
||||||
<BoxWithBorder padding={10} display={"grid"} alignItems={"center"} justifyContent={"center"}>
|
|
||||||
<Text>Loading</Text>
|
|
||||||
</BoxWithBorder>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionWithDescription title="API Connection">
|
<SectionWithDescription title="API Connection">
|
||||||
<BoxWithBorder>
|
<BoxWithBorder>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit((data, event) => {
|
onSubmit={handleSubmit((data, event) => {
|
||||||
createConfiguration({
|
mutate({
|
||||||
...data,
|
...data,
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
|
@ -96,25 +84,11 @@ export const ApiConnectionSection = ({ configuration }: ApiConnectionSectionProp
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
|
||||||
name="sandboxMode"
|
<label>
|
||||||
control={control}
|
<input type="checkbox" {...register("sandboxMode")} />
|
||||||
render={({
|
<Text paddingLeft={defaultPadding}>Sandbox mode</Text>
|
||||||
field: { onChange, value },
|
</label>
|
||||||
fieldState: { error },
|
|
||||||
formState: { errors },
|
|
||||||
}) => (
|
|
||||||
// TODO: add validation
|
|
||||||
<RadioGroup value={value?.toString()} onChange={(e) => console.log(e)}>
|
|
||||||
<RadioGroup.Item id="default-unchecked" value="live">
|
|
||||||
<Text>Live</Text>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
<RadioGroup.Item id="default-checked" value="sandbox">
|
|
||||||
<Text>Sandbox</Text>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
</RadioGroup>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<BoxFooter>
|
<BoxFooter>
|
||||||
<Button type="submit">Save provider</Button>
|
<Button type="submit">Save provider</Button>
|
||||||
|
|
|
@ -1,69 +1,60 @@
|
||||||
import { useRouter } from "next/router";
|
import { SendgridConfiguration } from "../configuration/sendgrid-config-schema";
|
||||||
import { SendgridConfiguration } from "../configuration/sendgrid-config";
|
|
||||||
import { BoxWithBorder } from "../../../components/box-with-border";
|
import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
import { Box, Button, Input, RadioGroup, Text } from "@saleor/macaw-ui/next";
|
import { Box, Button, Input, RadioGroup, Text } from "@saleor/macaw-ui/next";
|
||||||
import { defaultPadding } from "../../../components/ui-defaults";
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
import { sendgridUpdateBasicInformationSchema } from "../configuration/sendgrid-config-input-schema";
|
import {
|
||||||
|
SendgridUpdateBasicInformation,
|
||||||
|
sendgridUpdateBasicInformationSchema,
|
||||||
|
} from "../configuration/sendgrid-config-input-schema";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { BoxFooter } from "../../../components/box-footer";
|
import { BoxFooter } from "../../../components/box-footer";
|
||||||
import { SectionWithDescription } from "../../../components/section-with-description";
|
import { SectionWithDescription } from "../../../components/section-with-description";
|
||||||
|
|
||||||
interface BasicInformationSectionProps {
|
interface BasicInformationSectionProps {
|
||||||
configuration?: SendgridConfiguration;
|
configuration: SendgridConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BasicInformationSection = ({ configuration }: BasicInformationSectionProps) => {
|
export const BasicInformationSection = ({ configuration }: BasicInformationSectionProps) => {
|
||||||
const { replace } = useRouter();
|
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
|
const { handleSubmit, control, setError, register } = useForm<SendgridUpdateBasicInformation>({
|
||||||
const { handleSubmit, control, setError } = useForm<
|
|
||||||
z.infer<typeof sendgridUpdateBasicInformationSchema>
|
|
||||||
>({
|
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
configurationName: configuration?.configurationName,
|
id: configuration.id,
|
||||||
active: configuration?.active,
|
name: configuration.name,
|
||||||
|
active: configuration.active,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: createConfiguration } =
|
const trpcContext = trpcClient.useContext();
|
||||||
trpcClient.sendgridConfiguration.updateBasicInformation.useMutation({
|
const { mutate } = trpcClient.sendgridConfiguration.updateBasicInformation.useMutation({
|
||||||
onSuccess: async (data, variables) => {
|
onSuccess: async (data, variables) => {
|
||||||
notifySuccess("Configuration saved");
|
notifySuccess("Configuration saved");
|
||||||
// TODO: redirect to configuration details based on id
|
trpcContext.sendgridConfiguration.invalidate();
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
let isFieldErrorSet = false;
|
let isFieldErrorSet = false;
|
||||||
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
||||||
for (const fieldName in fieldErrors) {
|
for (const fieldName in fieldErrors) {
|
||||||
for (const message of fieldErrors[fieldName] || []) {
|
for (const message of fieldErrors[fieldName] || []) {
|
||||||
isFieldErrorSet = true;
|
isFieldErrorSet = true;
|
||||||
setError(fieldName as keyof z.infer<typeof sendgridUpdateBasicInformationSchema>, {
|
setError(fieldName as keyof z.infer<typeof sendgridUpdateBasicInformationSchema>, {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const formErrors = error.data?.zodError?.formErrors || [];
|
}
|
||||||
const formErrorMessage = formErrors.length ? formErrors.join("\n") : undefined;
|
const formErrors = error.data?.zodError?.formErrors || [];
|
||||||
|
const formErrorMessage = formErrors.length ? formErrors.join("\n") : undefined;
|
||||||
|
|
||||||
notifyError(
|
notifyError(
|
||||||
"Could not save the configuration",
|
"Could not save the configuration",
|
||||||
isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
||||||
formErrorMessage
|
formErrorMessage
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!configuration) {
|
|
||||||
return (
|
|
||||||
<BoxWithBorder padding={10} display={"grid"} alignItems={"center"} justifyContent={"center"}>
|
|
||||||
<Text>Loading</Text>
|
|
||||||
</BoxWithBorder>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionWithDescription
|
<SectionWithDescription
|
||||||
|
@ -78,14 +69,14 @@ export const BasicInformationSection = ({ configuration }: BasicInformationSecti
|
||||||
<BoxWithBorder>
|
<BoxWithBorder>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit((data, event) => {
|
onSubmit={handleSubmit((data, event) => {
|
||||||
createConfiguration({
|
mutate({
|
||||||
...data,
|
...data,
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Box padding={defaultPadding} display={"flex"} flexDirection={"column"} gap={10}>
|
<Box padding={defaultPadding} display={"flex"} flexDirection={"column"} gap={10}>
|
||||||
<Controller
|
<Controller
|
||||||
name="configurationName"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
render={({
|
render={({
|
||||||
field: { onChange, value },
|
field: { onChange, value },
|
||||||
|
@ -104,25 +95,10 @@ export const BasicInformationSection = ({ configuration }: BasicInformationSecti
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<label>
|
||||||
name="active"
|
<input type="checkbox" placeholder="Enabled" {...register("active")} />
|
||||||
control={control}
|
<Text paddingLeft={defaultPadding}>Active</Text>
|
||||||
render={({
|
</label>
|
||||||
field: { onChange, value },
|
|
||||||
fieldState: { error },
|
|
||||||
formState: { errors },
|
|
||||||
}) => (
|
|
||||||
// TODO: add validation
|
|
||||||
<RadioGroup value={value?.toString()} onChange={(e) => console.log(e)}>
|
|
||||||
<RadioGroup.Item id="default-unchecked" value="trueeee">
|
|
||||||
<Text>Active</Text>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
<RadioGroup.Item id="default-checked" value="falseeeee">
|
|
||||||
<Text>Disabled</Text>
|
|
||||||
</RadioGroup.Item>
|
|
||||||
</RadioGroup>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
<BoxFooter>
|
<BoxFooter>
|
||||||
<Button type="submit">Save provider</Button>
|
<Button type="submit">Save provider</Button>
|
||||||
|
|
|
@ -0,0 +1,198 @@
|
||||||
|
import {
|
||||||
|
SendgridConfiguration,
|
||||||
|
SendgridConfigurationChannels,
|
||||||
|
} from "../configuration/sendgrid-config-schema";
|
||||||
|
import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
|
import { Box, Button, ProductsIcons, Switch, TableEditIcon, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
|
import { SendgridUpdateChannels } from "../configuration/sendgrid-config-input-schema";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { BoxFooter } from "../../../components/box-footer";
|
||||||
|
import { SectionWithDescription } from "../../../components/section-with-description";
|
||||||
|
|
||||||
|
interface ChannelsSectionProps {
|
||||||
|
configuration: SendgridConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OverrideMessageArgs {
|
||||||
|
availableChannels: string[];
|
||||||
|
channelConfiguration: SendgridConfigurationChannels;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Move to a separate component
|
||||||
|
const overrideMessage = ({
|
||||||
|
availableChannels,
|
||||||
|
channelConfiguration: { channels, mode, override },
|
||||||
|
}: OverrideMessageArgs) => {
|
||||||
|
if (!override) {
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
Configuration will be used with <Text variant="bodyStrong"> all</Text> channels.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === "exclude") {
|
||||||
|
const leftChannels = availableChannels.filter((channel) => !channels.includes(channel));
|
||||||
|
if (!leftChannels.length) {
|
||||||
|
return <Text>Theres no channel which will be used with this configuration.</Text>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
Configuration will be used with channels:
|
||||||
|
<Text variant="bodyStrong">{leftChannels.join(", ")}</Text>.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Text>
|
||||||
|
Configuration will be used with channels:{" "}
|
||||||
|
<Text variant="bodyStrong">{channels.join(", ")}</Text>.
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ChannelsSection = ({ configuration }: ChannelsSectionProps) => {
|
||||||
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
|
|
||||||
|
const { handleSubmit, control, setError, setValue, getValues, register } =
|
||||||
|
useForm<SendgridUpdateChannels>({
|
||||||
|
defaultValues: {
|
||||||
|
id: configuration.id,
|
||||||
|
...configuration.channels,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: channels } = trpcClient.channels.fetch.useQuery();
|
||||||
|
|
||||||
|
const trpcContext = trpcClient.useContext();
|
||||||
|
const { mutate } = trpcClient.sendgridConfiguration.updateChannels.useMutation({
|
||||||
|
onSuccess: async (data, variables) => {
|
||||||
|
notifySuccess("Configuration saved");
|
||||||
|
trpcContext.sendgridConfiguration.invalidate();
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
let isFieldErrorSet = false;
|
||||||
|
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
||||||
|
for (const fieldName in fieldErrors) {
|
||||||
|
for (const message of fieldErrors[fieldName] || []) {
|
||||||
|
isFieldErrorSet = true;
|
||||||
|
setError(fieldName as keyof SendgridUpdateChannels, {
|
||||||
|
type: "manual",
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const formErrors = error.data?.zodError?.formErrors || [];
|
||||||
|
const formErrorMessage = formErrors.length ? formErrors.join("\n") : undefined;
|
||||||
|
|
||||||
|
notifyError(
|
||||||
|
"Could not save the configuration",
|
||||||
|
isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
||||||
|
formErrorMessage
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionWithDescription
|
||||||
|
title="Channels"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<Text display="block">
|
||||||
|
By default, provider will work for every channel. You can change this behavior with
|
||||||
|
"excluding" or "including" strategy.
|
||||||
|
</Text>
|
||||||
|
<Text display="block">
|
||||||
|
<Text variant="bodyStrong">Excluding</Text> - all current channels and new created
|
||||||
|
channels will work, excluding selected
|
||||||
|
</Text>
|
||||||
|
<Text display="block">
|
||||||
|
<Text variant="bodyStrong">Including</Text> - only selected channels will work, new
|
||||||
|
created channels will not work
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((data, event) => {
|
||||||
|
mutate({
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<BoxWithBorder>
|
||||||
|
<Box
|
||||||
|
padding={defaultPadding}
|
||||||
|
display={"flex"}
|
||||||
|
flexDirection={"column"}
|
||||||
|
gap={defaultPadding}
|
||||||
|
>
|
||||||
|
<Box display={"flex"} flexDirection={"column"} gap={defaultPadding}>
|
||||||
|
<Text variant="heading">Current behaviour</Text>
|
||||||
|
{overrideMessage({
|
||||||
|
availableChannels: channels?.map((channel) => channel.slug) || [],
|
||||||
|
channelConfiguration: configuration.channels,
|
||||||
|
})}
|
||||||
|
<Text variant="heading">Settings</Text>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" {...register("override")} />
|
||||||
|
<Text paddingLeft={defaultPadding}>Override channels</Text>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="mode"
|
||||||
|
control={control}
|
||||||
|
render={({
|
||||||
|
field: { onChange, value },
|
||||||
|
fieldState: { error },
|
||||||
|
formState: { errors },
|
||||||
|
}) => (
|
||||||
|
<Switch
|
||||||
|
defaultValue={configuration.channels.mode}
|
||||||
|
__maxWidth={"max-content"}
|
||||||
|
onValueChange={onChange}
|
||||||
|
>
|
||||||
|
<Switch.Item id="1" value="restrict">
|
||||||
|
<TableEditIcon size="medium" />
|
||||||
|
<Text>Include</Text>
|
||||||
|
</Switch.Item>
|
||||||
|
<Switch.Item id="2" value="exclude">
|
||||||
|
<ProductsIcons size="medium" />
|
||||||
|
<Text>Exclude</Text>
|
||||||
|
</Switch.Item>
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{channels?.map((channel) => (
|
||||||
|
<label key={channel.slug}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
defaultChecked={!!getValues("channels").includes(channel.slug)}
|
||||||
|
onChange={(event) => {
|
||||||
|
if (event.target.checked) {
|
||||||
|
setValue("channels", [...getValues("channels"), channel.slug]);
|
||||||
|
} else {
|
||||||
|
setValue(
|
||||||
|
"channels",
|
||||||
|
getValues("channels").filter((slug) => slug !== channel.slug)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text paddingLeft={defaultPadding}>{channel.name}</Text>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<BoxFooter>
|
||||||
|
<Button type="submit">Save provider</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</BoxWithBorder>
|
||||||
|
</form>
|
||||||
|
</SectionWithDescription>
|
||||||
|
);
|
||||||
|
};
|
|
@ -3,42 +3,87 @@ import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
import { SectionWithDescription } from "../../../components/section-with-description";
|
import { SectionWithDescription } from "../../../components/section-with-description";
|
||||||
import { defaultPadding } from "../../../components/ui-defaults";
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
import { BoxFooter } from "../../../components/box-footer";
|
import { BoxFooter } from "../../../components/box-footer";
|
||||||
import { SendgridConfiguration } from "../configuration/sendgrid-config";
|
import { SendgridConfiguration } from "../configuration/sendgrid-config-schema";
|
||||||
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { SendgridGetConfigurationIdInput } from "../configuration/sendgrid-config-input-schema";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
interface DangerousSectionProps {
|
interface DangerousSectionProps {
|
||||||
configuration?: SendgridConfiguration;
|
configuration: SendgridConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DangerousSection = ({ configuration }: DangerousSectionProps) => {
|
export const DangerousSection = ({ configuration }: DangerousSectionProps) => {
|
||||||
const onRemoveConfiguration = () => {
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
console.log("remove", configuration?.id);
|
const { replace } = useRouter();
|
||||||
};
|
const { handleSubmit, setError } = useForm<SendgridGetConfigurationIdInput>({
|
||||||
|
defaultValues: {
|
||||||
|
id: configuration.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate } = trpcClient.sendgridConfiguration.deleteConfiguration.useMutation({
|
||||||
|
onSuccess: async (data, variables) => {
|
||||||
|
notifySuccess("Configuration saved");
|
||||||
|
replace("/configuration");
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
let isFieldErrorSet = false;
|
||||||
|
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
||||||
|
for (const fieldName in fieldErrors) {
|
||||||
|
for (const message of fieldErrors[fieldName] || []) {
|
||||||
|
isFieldErrorSet = true;
|
||||||
|
setError(fieldName as keyof SendgridGetConfigurationIdInput, {
|
||||||
|
type: "manual",
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const formErrors = error.data?.zodError?.formErrors || [];
|
||||||
|
const formErrorMessage = formErrors.length ? formErrors.join("\n") : undefined;
|
||||||
|
|
||||||
|
notifyError(
|
||||||
|
"Could not save the configuration",
|
||||||
|
isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
||||||
|
formErrorMessage
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionWithDescription title="Danger zone">
|
<SectionWithDescription title="Danger zone">
|
||||||
<BoxWithBorder backgroundColor={"surfaceCriticalSubdued"} borderColor={"criticalSubdued"}>
|
<form
|
||||||
<Box padding={defaultPadding}>
|
onSubmit={handleSubmit((data, event) => {
|
||||||
<Text variant="heading" display="block">
|
mutate({
|
||||||
Remove provider
|
...data,
|
||||||
</Text>
|
});
|
||||||
<Text display="block">You can remove provider configuration.</Text>
|
})}
|
||||||
<Text display="block">
|
>
|
||||||
This operation will remove all settings related to this configuration. Data will be
|
<BoxWithBorder backgroundColor={"surfaceCriticalSubdued"} borderColor={"criticalSubdued"}>
|
||||||
permanently removed from the App.{" "}
|
<Box padding={defaultPadding}>
|
||||||
</Text>
|
<Text variant="heading" display="block">
|
||||||
<Text display="block">This operation cant be undone.</Text>
|
Remove provider
|
||||||
<Text display="block">You still can create new configuration.</Text>
|
</Text>
|
||||||
</Box>
|
<Text display="block">You can remove provider configuration.</Text>
|
||||||
<BoxFooter borderColor={"criticalSubdued"}>
|
<Text display="block">
|
||||||
<Button
|
This operation will remove all settings related to this configuration. Data will be
|
||||||
color={"textNeutralSubdued"}
|
permanently removed from the App.{" "}
|
||||||
backgroundColor={"interactiveCriticalDefault"}
|
</Text>
|
||||||
onClick={onRemoveConfiguration}
|
<Text display="block">This operation cant be undone.</Text>
|
||||||
>
|
<Text display="block">You still can create new configuration.</Text>
|
||||||
Remove provider
|
</Box>
|
||||||
</Button>
|
<BoxFooter borderColor={"criticalSubdued"}>
|
||||||
</BoxFooter>
|
<Button
|
||||||
</BoxWithBorder>
|
color={"textNeutralSubdued"}
|
||||||
|
backgroundColor={"interactiveCriticalDefault"}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Remove provider
|
||||||
|
</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</BoxWithBorder>
|
||||||
|
</form>
|
||||||
</SectionWithDescription>
|
</SectionWithDescription>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
import {
|
||||||
|
SendgridConfiguration,
|
||||||
|
SendgridEventConfiguration,
|
||||||
|
} from "../configuration/sendgrid-config-schema";
|
||||||
|
import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
|
import { Box, Button, Combobox, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
|
import { SendgridUpdateEvent } from "../configuration/sendgrid-config-input-schema";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { BoxFooter } from "../../../components/box-footer";
|
||||||
|
import { SectionWithDescription } from "../../../components/section-with-description";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { fetchTemplates } from "../sendgrid-api";
|
||||||
|
|
||||||
|
interface EventBoxProps {
|
||||||
|
configuration: SendgridConfiguration;
|
||||||
|
event: SendgridEventConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventBox = ({ event, configuration }: EventBoxProps) => {
|
||||||
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
|
|
||||||
|
const { data: templatesChoices } = useQuery({
|
||||||
|
queryKey: ["sendgridTemplates"],
|
||||||
|
queryFn: fetchTemplates({ apiKey: configuration.apiKey }),
|
||||||
|
enabled: !!configuration.apiKey?.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleSubmit, control, setError, register } = useForm<SendgridUpdateEvent>({
|
||||||
|
defaultValues: {
|
||||||
|
id: configuration.id,
|
||||||
|
...event,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const trpcContext = trpcClient.useContext();
|
||||||
|
const { mutate } = trpcClient.sendgridConfiguration.updateEvent.useMutation({
|
||||||
|
onSuccess: async (data, variables) => {
|
||||||
|
notifySuccess("Configuration saved");
|
||||||
|
trpcContext.sendgridConfiguration.invalidate();
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
let isFieldErrorSet = false;
|
||||||
|
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
||||||
|
for (const fieldName in fieldErrors) {
|
||||||
|
for (const message of fieldErrors[fieldName] || []) {
|
||||||
|
isFieldErrorSet = true;
|
||||||
|
setError(fieldName as keyof SendgridUpdateEvent, {
|
||||||
|
type: "manual",
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const formErrors = error.data?.zodError?.formErrors || [];
|
||||||
|
const formErrorMessage = formErrors.length ? formErrors.join("\n") : undefined;
|
||||||
|
|
||||||
|
notifyError(
|
||||||
|
"Could not save the configuration",
|
||||||
|
isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
||||||
|
formErrorMessage
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((data, event) => {
|
||||||
|
mutate({
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<BoxWithBorder>
|
||||||
|
<Box
|
||||||
|
padding={defaultPadding}
|
||||||
|
display={"flex"}
|
||||||
|
flexDirection={"column"}
|
||||||
|
gap={defaultPadding}
|
||||||
|
>
|
||||||
|
<Text variant="heading">{event.eventType}</Text>
|
||||||
|
{!templatesChoices ? (
|
||||||
|
<Combobox label="Template" disabled options={[]} />
|
||||||
|
) : (
|
||||||
|
<Controller
|
||||||
|
name="template"
|
||||||
|
control={control}
|
||||||
|
render={({
|
||||||
|
field: { onChange, value },
|
||||||
|
fieldState: { error },
|
||||||
|
formState: { errors },
|
||||||
|
}) => (
|
||||||
|
<Combobox
|
||||||
|
label="Template"
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event?.value)}
|
||||||
|
options={templatesChoices.map((sender) => ({
|
||||||
|
label: sender.label,
|
||||||
|
value: sender.value,
|
||||||
|
}))}
|
||||||
|
error={!!error}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" placeholder="Enabled" {...register("active")} />
|
||||||
|
<Text paddingLeft={defaultPadding}>Active</Text>
|
||||||
|
</label>
|
||||||
|
</Box>
|
||||||
|
<BoxFooter>
|
||||||
|
<Button type="submit">Save event</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</BoxWithBorder>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface EventsSectionProps {
|
||||||
|
configuration: SendgridConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EventsSection = ({ configuration }: EventsSectionProps) => {
|
||||||
|
return (
|
||||||
|
<SectionWithDescription
|
||||||
|
title="Events"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<Text display="block">
|
||||||
|
Provide unique name for your configuration - you can create more than one. For example -
|
||||||
|
production and development.
|
||||||
|
</Text>
|
||||||
|
<Text display="block">Then, pass your API Key. Obtain it here.</Text>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box display="flex" flexDirection={"column"} gap={defaultPadding}>
|
||||||
|
{configuration.events.map((event) => (
|
||||||
|
<EventBox key={event.eventType} configuration={configuration} event={event} />
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</SectionWithDescription>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,99 +1,101 @@
|
||||||
import { useRouter } from "next/router";
|
import { SendgridConfiguration } from "../configuration/sendgrid-config-schema";
|
||||||
import { SendgridConfiguration } from "../configuration/sendgrid-config";
|
|
||||||
import { BoxWithBorder } from "../../../components/box-with-border";
|
import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
import { Box, Button, Combobox, Text } from "@saleor/macaw-ui/next";
|
import { Box, Button, Combobox } from "@saleor/macaw-ui/next";
|
||||||
import { defaultPadding } from "../../../components/ui-defaults";
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
import { trpcClient } from "../../trpc/trpc-client";
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
import { sendgridUpdateBasicInformationSchema } from "../configuration/sendgrid-config-input-schema";
|
import { SendgridUpdateSender } from "../configuration/sendgrid-config-input-schema";
|
||||||
import { z } from "zod";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { BoxFooter } from "../../../components/box-footer";
|
import { BoxFooter } from "../../../components/box-footer";
|
||||||
import { SectionWithDescription } from "../../../components/section-with-description";
|
import { SectionWithDescription } from "../../../components/section-with-description";
|
||||||
|
import { fetchSenders } from "../sendgrid-api";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
interface SenderSectionProps {
|
interface SenderSectionProps {
|
||||||
configuration?: SendgridConfiguration;
|
configuration: SendgridConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SenderSection = ({ configuration }: SenderSectionProps) => {
|
export const SenderSection = ({ configuration }: SenderSectionProps) => {
|
||||||
const { replace } = useRouter();
|
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
|
|
||||||
const { handleSubmit, control, setError } = useForm<
|
const { data: sendersChoices, isLoading: isSendersChoicesLoading } = useQuery({
|
||||||
z.infer<typeof sendgridUpdateBasicInformationSchema>
|
queryKey: ["sendgridSenders"],
|
||||||
>({
|
queryFn: fetchSenders({ apiKey: configuration.apiKey }),
|
||||||
|
enabled: !!configuration.apiKey?.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleSubmit, control, setError } = useForm<SendgridUpdateSender>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
configurationName: configuration?.configurationName,
|
id: configuration.id,
|
||||||
active: configuration?.active,
|
sender: configuration.sender,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: createConfiguration } =
|
const trpcContext = trpcClient.useContext();
|
||||||
trpcClient.sendgridConfiguration.updateBasicInformation.useMutation({
|
const { mutate } = trpcClient.sendgridConfiguration.updateSender.useMutation({
|
||||||
onSuccess: async (data, variables) => {
|
onSuccess: async (data, variables) => {
|
||||||
notifySuccess("Configuration saved");
|
notifySuccess("Configuration saved");
|
||||||
// TODO: redirect to configuration details based on id
|
trpcContext.sendgridConfiguration.invalidate();
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
let isFieldErrorSet = false;
|
let isFieldErrorSet = false;
|
||||||
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
||||||
for (const fieldName in fieldErrors) {
|
for (const fieldName in fieldErrors) {
|
||||||
for (const message of fieldErrors[fieldName] || []) {
|
for (const message of fieldErrors[fieldName] || []) {
|
||||||
isFieldErrorSet = true;
|
isFieldErrorSet = true;
|
||||||
setError(fieldName as keyof z.infer<typeof sendgridUpdateBasicInformationSchema>, {
|
setError(fieldName as keyof SendgridUpdateSender, {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const formErrors = error.data?.zodError?.formErrors || [];
|
}
|
||||||
const formErrorMessage = formErrors.length ? formErrors.join("\n") : undefined;
|
const formErrors = error.data?.zodError?.formErrors || [];
|
||||||
|
const formErrorMessage = formErrors.length ? formErrors.join("\n") : undefined;
|
||||||
|
|
||||||
notifyError(
|
notifyError(
|
||||||
"Could not save the configuration",
|
"Could not save the configuration",
|
||||||
isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
||||||
formErrorMessage
|
formErrorMessage
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!configuration) {
|
|
||||||
return (
|
|
||||||
<BoxWithBorder padding={10} display={"grid"} alignItems={"center"} justifyContent={"center"}>
|
|
||||||
<Text>Loading</Text>
|
|
||||||
</BoxWithBorder>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionWithDescription title="Sender">
|
<SectionWithDescription title="Sender">
|
||||||
<BoxWithBorder>
|
<BoxWithBorder>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit((data, event) => {
|
onSubmit={handleSubmit((data, event) => {
|
||||||
createConfiguration({
|
mutate({
|
||||||
...data,
|
...data,
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Box padding={defaultPadding} display={"flex"} flexDirection={"column"} gap={10}>
|
<Box padding={defaultPadding} display={"flex"} flexDirection={"column"} gap={10}>
|
||||||
<Controller
|
{sendersChoices?.length ? (
|
||||||
name="sender"
|
<Controller
|
||||||
control={control}
|
name="sender"
|
||||||
render={({
|
control={control}
|
||||||
field: { onChange, value },
|
render={({
|
||||||
fieldState: { error },
|
field: { onChange, value },
|
||||||
formState: { errors },
|
fieldState: { error },
|
||||||
}) => (
|
formState: { errors },
|
||||||
<Combobox
|
}) => (
|
||||||
label="Sender"
|
<Combobox
|
||||||
value={value}
|
label="Sender"
|
||||||
options={[
|
value={value}
|
||||||
{ label: "test", value: "test" },
|
defaultValue={configuration.sender}
|
||||||
{ label: "test", value: "test" },
|
onChange={(event) => onChange(event?.value)}
|
||||||
]}
|
options={sendersChoices.map((sender) => ({
|
||||||
/>
|
label: sender.label,
|
||||||
)}
|
value: sender.value,
|
||||||
/>
|
}))}
|
||||||
|
error={!!error}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Combobox label="Sender" options={[]} disabled={true} />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<BoxFooter>
|
<BoxFooter>
|
||||||
<Button type="submit">Save provider</Button>
|
<Button type="submit">Save provider</Button>
|
||||||
|
|
|
@ -1,37 +1,95 @@
|
||||||
import { Box, Text } from "@saleor/macaw-ui/next";
|
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import { Breadcrumbs } from "../../../../components/breadcrumbs";
|
|
||||||
import { trpcClient } from "../../../../modules/trpc/trpc-client";
|
import { trpcClient } from "../../../../modules/trpc/trpc-client";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { BasicInformationSection } from "../../../../modules/sendgrid/ui/basic-information-section";
|
import { BasicInformationSection } from "../../../../modules/sendgrid/ui/basic-information-section";
|
||||||
import { DangerousSection } from "../../../../modules/sendgrid/ui/dangrous-section";
|
import { DangerousSection } from "../../../../modules/sendgrid/ui/dangrous-section";
|
||||||
import { SectionWithDescription } from "../../../../components/section-with-description";
|
|
||||||
import { ApiConnectionSection } from "../../../../modules/sendgrid/ui/api-connection-section";
|
import { ApiConnectionSection } from "../../../../modules/sendgrid/ui/api-connection-section";
|
||||||
import { SenderSection } from "../../../../modules/sendgrid/ui/sender-section";
|
import { SenderSection } from "../../../../modules/sendgrid/ui/sender-section";
|
||||||
|
import { EventsSection } from "../../../../modules/sendgrid/ui/events-section";
|
||||||
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
|
import { BasicLayout } from "../../../../components/basic-layout";
|
||||||
|
import { ChannelsSection } from "../../../../modules/sendgrid/ui/channels-section";
|
||||||
|
|
||||||
|
const LoadingView = () => {
|
||||||
|
return (
|
||||||
|
<BasicLayout
|
||||||
|
breadcrumbs={[
|
||||||
|
{ name: "Configuration", href: "/" },
|
||||||
|
{ name: "Sendgrid provider" },
|
||||||
|
{ name: "..." },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text variant="hero">Loading...</Text>
|
||||||
|
</BasicLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NotFoundView = () => {
|
||||||
|
return (
|
||||||
|
<BasicLayout
|
||||||
|
breadcrumbs={[
|
||||||
|
{ name: "Configuration", href: "/" },
|
||||||
|
{ name: "Sendgrid provider" },
|
||||||
|
{ name: "Not found" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text variant="hero">Could not find the requested configuration.</Text>
|
||||||
|
</BasicLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const EditSendgridConfigurationPage: NextPage = () => {
|
const EditSendgridConfigurationPage: NextPage = () => {
|
||||||
|
const { notifyError } = useDashboardNotification();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
const { data: configuration, isLoading } =
|
||||||
|
trpcClient.sendgridConfiguration.getConfiguration.useQuery(
|
||||||
|
{
|
||||||
|
id: id as string,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!id,
|
||||||
|
onSettled(data, error) {
|
||||||
|
if (error) {
|
||||||
|
console.log("Error: ", error);
|
||||||
|
}
|
||||||
|
if (error?.data?.code === "NOT_FOUND" || !data) {
|
||||||
|
notifyError("The requested configuration does not exist.");
|
||||||
|
router.replace("/configuration");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const { data: configuration } = trpcClient.sendgridConfiguration.getConfiguration.useQuery({
|
if (isLoading) {
|
||||||
id: id as string,
|
return <LoadingView />;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
if (!configuration) {
|
||||||
|
return <NotFoundView />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box padding={10} display={"grid"} gap={13}>
|
<BasicLayout
|
||||||
<Breadcrumbs
|
breadcrumbs={[
|
||||||
items={[{ name: "Configuration", href: "/" }, { name: "Sendgrid" }, { name: "Sendgrid" }]}
|
{ name: "Configuration", href: "/configuration" },
|
||||||
/>
|
{ name: "Sendgrid provider" },
|
||||||
|
{ name: configuration.name },
|
||||||
|
]}
|
||||||
|
>
|
||||||
<Box display={"grid"} gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
<Box display={"grid"} gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Text>Connect Sendgrid with Saleor.</Text>
|
<Text>Connect Sendgrid with Saleor.</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{!!configuration && <BasicInformationSection configuration={configuration} />}
|
<BasicInformationSection configuration={configuration} />
|
||||||
{!!configuration && <ApiConnectionSection configuration={configuration} />}
|
<ApiConnectionSection configuration={configuration} />
|
||||||
{!!configuration && <SenderSection configuration={configuration} />}
|
<SenderSection configuration={configuration} />
|
||||||
{!!configuration && <DangerousSection configuration={configuration} />}
|
<EventsSection configuration={configuration} />
|
||||||
</Box>
|
<ChannelsSection configuration={configuration} />
|
||||||
|
<DangerousSection configuration={configuration} />
|
||||||
|
</BasicLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Box, Button, Input, Text } from "@saleor/macaw-ui/next";
|
import { Box, Button, Input, Text } from "@saleor/macaw-ui/next";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import { Breadcrumbs } from "../../../components/breadcrumbs";
|
|
||||||
import { SectionWithDescription } from "../../../components/section-with-description";
|
import { SectionWithDescription } from "../../../components/section-with-description";
|
||||||
import { BoxWithBorder } from "../../../components/box-with-border";
|
import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
import { defaultPadding } from "../../../components/ui-defaults";
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
|
@ -8,18 +7,21 @@ import { BoxFooter } from "../../../components/box-footer";
|
||||||
import { trpcClient } from "../../../modules/trpc/trpc-client";
|
import { trpcClient } from "../../../modules/trpc/trpc-client";
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared/src/use-dashboard-notification";
|
import { useDashboardNotification } from "@saleor/apps-shared/src/use-dashboard-notification";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { SendgridCreateConfigurationSchemaType } from "../../../modules/sendgrid/configuration/sendgrid-config-input-schema";
|
import { SendgridCreateConfigurationInput } from "../../../modules/sendgrid/configuration/sendgrid-config-input-schema";
|
||||||
|
import { BasicLayout } from "../../../components/basic-layout";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
const NewSendgridConfigurationPage: NextPage = () => {
|
const NewSendgridConfigurationPage: NextPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
|
|
||||||
const { handleSubmit, control, setError } = useForm<SendgridCreateConfigurationSchemaType>();
|
const { handleSubmit, control, setError } = useForm<SendgridCreateConfigurationInput>();
|
||||||
|
|
||||||
const { mutate: createConfiguration } =
|
const { mutate: createConfiguration } =
|
||||||
trpcClient.sendgridConfiguration.createConfiguration.useMutation({
|
trpcClient.sendgridConfiguration.createConfiguration.useMutation({
|
||||||
onSuccess: async (data, variables) => {
|
onSuccess: async (data, variables) => {
|
||||||
notifySuccess("Configuration saved");
|
notifySuccess("Configuration saved");
|
||||||
// TODO: redirect to configuration details based on id
|
router.push(`/configuration/sendgrid/edit/${data.id}`);
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
let isFieldErrorSet = false;
|
let isFieldErrorSet = false;
|
||||||
|
@ -27,7 +29,7 @@ const NewSendgridConfigurationPage: NextPage = () => {
|
||||||
for (const fieldName in fieldErrors) {
|
for (const fieldName in fieldErrors) {
|
||||||
for (const message of fieldErrors[fieldName] || []) {
|
for (const message of fieldErrors[fieldName] || []) {
|
||||||
isFieldErrorSet = true;
|
isFieldErrorSet = true;
|
||||||
setError(fieldName as keyof SendgridCreateConfigurationSchemaType, {
|
setError(fieldName as keyof SendgridCreateConfigurationInput, {
|
||||||
type: "manual",
|
type: "manual",
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
|
@ -45,14 +47,13 @@ const NewSendgridConfigurationPage: NextPage = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box padding={10} display={"grid"} gap={13}>
|
<BasicLayout
|
||||||
<Breadcrumbs
|
breadcrumbs={[
|
||||||
items={[
|
{ name: "Configuration", href: "/" },
|
||||||
{ name: "Configuration", href: "/" },
|
{ name: "Add provider" },
|
||||||
{ name: "Add provider" },
|
{ name: "Sendgrid" },
|
||||||
{ name: "Sendgrid" },
|
]}
|
||||||
]}
|
>
|
||||||
/>
|
|
||||||
<Box display={"grid"} gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
<Box display={"grid"} gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
||||||
<Box>
|
<Box>
|
||||||
<Text>Connect Sendgrid with Saleor.</Text>
|
<Text>Connect Sendgrid with Saleor.</Text>
|
||||||
|
@ -77,7 +78,7 @@ const NewSendgridConfigurationPage: NextPage = () => {
|
||||||
>
|
>
|
||||||
<Box padding={defaultPadding} display={"flex"} flexDirection={"column"} gap={10}>
|
<Box padding={defaultPadding} display={"flex"} flexDirection={"column"} gap={10}>
|
||||||
<Controller
|
<Controller
|
||||||
name="configurationName"
|
name="name"
|
||||||
control={control}
|
control={control}
|
||||||
render={({
|
render={({
|
||||||
field: { onChange, value },
|
field: { onChange, value },
|
||||||
|
@ -123,7 +124,7 @@ const NewSendgridConfigurationPage: NextPage = () => {
|
||||||
</form>
|
</form>
|
||||||
</BoxWithBorder>
|
</BoxWithBorder>
|
||||||
</SectionWithDescription>
|
</SectionWithDescription>
|
||||||
</Box>
|
</BasicLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
BIN
apps/emails-and-messages/src/public/sendgrid.png
Normal file
BIN
apps/emails-and-messages/src/public/sendgrid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 332 B |
9
apps/emails-and-messages/src/public/smtp.svg
Normal file
9
apps/emails-and-messages/src/public/smtp.svg
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<svg width="20" height="19" viewBox="0 0 20 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4 4.625C4.55228 4.625 5 4.17728 5 3.625C5 3.07272 4.55228 2.625 4 2.625C3.44772 2.625 3 3.07272 3 3.625C3 4.17728 3.44772 4.625 4 4.625Z" fill="#4C5C6D"/>
|
||||||
|
<path d="M9 3.625C9 4.17728 8.55229 4.625 8 4.625C7.44772 4.625 7 4.17728 7 3.625C7 3.07272 7.44772 2.625 8 2.625C8.55229 2.625 9 3.07272 9 3.625Z" fill="#4C5C6D"/>
|
||||||
|
<path d="M12 4.625C12.5523 4.625 13 4.17728 13 3.625C13 3.07272 12.5523 2.625 12 2.625C11.4477 2.625 11 3.07272 11 3.625C11 4.17728 11.4477 4.625 12 4.625Z" fill="#4C5C6D"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 0C1.10254 0 0.375 0.727538 0.375 1.625V5.625C0.375 6.52246 1.10254 7.25 2 7.25H18C18.8975 7.25 19.625 6.52246 19.625 5.625V1.625C19.625 0.727537 18.8975 0 18 0H2ZM1.625 1.625C1.625 1.41789 1.79289 1.25 2 1.25H18C18.2071 1.25 18.375 1.41789 18.375 1.625V5.625C18.375 5.83211 18.2071 6 18 6H2C1.79289 6 1.625 5.83211 1.625 5.625V1.625Z" fill="#4C5C6D"/>
|
||||||
|
<path d="M5.5 9C5.84518 9 6.125 9.27982 6.125 9.625C6.125 9.97018 5.84518 10.25 5.5 10.25H2.00001C1.7929 10.25 1.62501 10.4179 1.62501 10.625V14.625C1.62501 14.8321 1.7929 15 2.00001 15H5.5C5.84518 15 6.125 15.2798 6.125 15.625C6.125 15.9702 5.84518 16.25 5.5 16.25H2.00001C1.10254 16.25 0.375008 15.5225 0.375008 14.625V10.625C0.375008 9.72754 1.10254 9 2.00001 9H5.5Z" fill="#4C5C6D"/>
|
||||||
|
<path d="M5 12.625C5 13.1773 4.55228 13.625 4 13.625C3.44772 13.625 3 13.1773 3 12.625C3 12.0727 3.44772 11.625 4 11.625C4.55228 11.625 5 12.0727 5 12.625Z" fill="#4C5C6D"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.375 10.625C7.375 9.72754 8.10254 9 9 9H18C18.8975 9 19.625 9.72754 19.625 10.625V11.6053C19.6254 11.6183 19.6254 11.6313 19.625 11.6443V16.625C19.625 17.5225 18.8975 18.25 18 18.25H9C8.10254 18.25 7.375 17.5225 7.375 16.625V11.6251C7.375 11.6249 7.375 11.6253 7.375 11.6251V10.625ZM8.625 12.5957V16.625C8.625 16.8321 8.79289 17 9 17H18C18.2071 17 18.375 16.8321 18.375 16.625V12.5958L14.1726 14.506C13.7453 14.7002 13.255 14.7002 12.8277 14.506L8.625 12.5957ZM18.375 11.2228L13.6553 13.3681C13.5567 13.4129 13.4436 13.4129 13.345 13.3681L8.625 11.2226V10.625C8.625 10.4179 8.79289 10.25 9 10.25H18C18.2071 10.25 18.375 10.4179 18.375 10.625V11.2228Z" fill="#4C5C6D"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -325,7 +325,7 @@ importers:
|
||||||
'@monaco-editor/react': ^4.4.6
|
'@monaco-editor/react': ^4.4.6
|
||||||
'@saleor/app-sdk': 0.37.3
|
'@saleor/app-sdk': 0.37.3
|
||||||
'@saleor/apps-shared': workspace:*
|
'@saleor/apps-shared': workspace:*
|
||||||
'@saleor/macaw-ui': 0.8.0-pre.76
|
'@saleor/macaw-ui': 0.8.0-pre.79
|
||||||
'@sendgrid/client': ^7.7.0
|
'@sendgrid/client': ^7.7.0
|
||||||
'@sendgrid/mail': ^7.7.0
|
'@sendgrid/mail': ^7.7.0
|
||||||
'@tanstack/react-query': ^4.24.4
|
'@tanstack/react-query': ^4.24.4
|
||||||
|
@ -375,7 +375,7 @@ importers:
|
||||||
'@monaco-editor/react': 4.4.6_biqbaboplfbrettd7655fr4n2y
|
'@monaco-editor/react': 4.4.6_biqbaboplfbrettd7655fr4n2y
|
||||||
'@saleor/app-sdk': 0.37.3_yucv4tfv7v7nrkw2uguegj6e7e
|
'@saleor/app-sdk': 0.37.3_yucv4tfv7v7nrkw2uguegj6e7e
|
||||||
'@saleor/apps-shared': link:../../packages/shared
|
'@saleor/apps-shared': link:../../packages/shared
|
||||||
'@saleor/macaw-ui': 0.8.0-pre.76_5ndqzdd6t4rivxsukjv3i3ak2q
|
'@saleor/macaw-ui': 0.8.0-pre.79_5ndqzdd6t4rivxsukjv3i3ak2q
|
||||||
'@sendgrid/client': 7.7.0
|
'@sendgrid/client': 7.7.0
|
||||||
'@sendgrid/mail': 7.7.0
|
'@sendgrid/mail': 7.7.0
|
||||||
'@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y
|
'@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y
|
||||||
|
@ -4804,8 +4804,8 @@ packages:
|
||||||
- '@types/react'
|
- '@types/react'
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@saleor/macaw-ui/0.8.0-pre.76_5ndqzdd6t4rivxsukjv3i3ak2q:
|
/@saleor/macaw-ui/0.8.0-pre.79_5ndqzdd6t4rivxsukjv3i3ak2q:
|
||||||
resolution: {integrity: sha512-z5zlgdiLcJTR4al4FP6Z3JBzcH1VWQQRrUVH/TraqvHfIxC5XCPz1ZSve1/KUyXafbjUaM0ih81B9vqfipbXRA==}
|
resolution: {integrity: sha512-E+kqNPPyD5QR+DHLIrhbfN++g7DC+LyrINYeSnDXYad03BZz1AXcY9tagNMIt8ie4qrmQ/gMrsEQpN0yMUlNgg==}
|
||||||
engines: {node: '>=16 <19', pnpm: '>=8'}
|
engines: {node: '>=16 <19', pnpm: '>=8'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
Loading…
Reference in a new issue