From 373063d631e0a425d8c9a7fd15ef438b7b59ccee Mon Sep 17 00:00:00 2001 From: Krzysztof Wolski Date: Fri, 28 Apr 2023 23:44:30 +0200 Subject: [PATCH] Sendgrid module updated --- apps/emails-and-messages/package.json | 2 +- .../src/components/basic-layout.tsx | 17 ++ .../src/components/breadcrumbs.tsx | 47 ++++- .../src/components/chip-text.tsx | 53 +++++ .../components/messaging-providers-box.tsx | 37 +++- .../src/components/temp.tsx | 160 -------------- .../src/components/text-link.tsx | 14 ++ .../src/lib/is-available-in-channel.ts | 18 +- .../get-sendgrid-configuration.service.ts | 2 +- .../sendgrid-config-container.ts | 26 +-- .../sendgrid-config-input-schema.ts | 101 +++++---- .../configuration/sendgrid-config-schema.ts | 37 ++++ .../sendgrid/configuration/sendgrid-config.ts | 26 --- .../sendgrid-configuration.router.ts | 163 +++++++++++--- .../configuration/sendgrid-configurator.ts | 2 +- .../src/modules/sendgrid/send-sendgrid.ts | 2 +- .../src/modules/sendgrid/sendgrid-api.ts | 4 +- .../sendgrid/ui/api-connection-section.tsx | 106 ++++------ .../sendgrid/ui/basic-information-section.tsx | 108 ++++------ .../modules/sendgrid/ui/channels-section.tsx | 198 ++++++++++++++++++ .../modules/sendgrid/ui/dangrous-section.tsx | 101 ++++++--- .../modules/sendgrid/ui/events-section.tsx | 145 +++++++++++++ .../modules/sendgrid/ui/sender-section.tsx | 132 ++++++------ .../configuration/sendgrid/edit/[id].tsx | 86 ++++++-- .../src/pages/configuration/sendgrid/new.tsx | 31 +-- .../src/public/sendgrid.png | Bin 0 -> 332 bytes apps/emails-and-messages/src/public/smtp.svg | 9 + pnpm-lock.yaml | 8 +- 28 files changed, 1066 insertions(+), 569 deletions(-) create mode 100644 apps/emails-and-messages/src/components/basic-layout.tsx create mode 100644 apps/emails-and-messages/src/components/chip-text.tsx delete mode 100644 apps/emails-and-messages/src/components/temp.tsx create mode 100644 apps/emails-and-messages/src/components/text-link.tsx create mode 100644 apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-schema.ts delete mode 100644 apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config.ts create mode 100644 apps/emails-and-messages/src/modules/sendgrid/ui/channels-section.tsx create mode 100644 apps/emails-and-messages/src/modules/sendgrid/ui/events-section.tsx create mode 100644 apps/emails-and-messages/src/public/sendgrid.png create mode 100644 apps/emails-and-messages/src/public/smtp.svg diff --git a/apps/emails-and-messages/package.json b/apps/emails-and-messages/package.json index aacf571..d21ff8b 100644 --- a/apps/emails-and-messages/package.json +++ b/apps/emails-and-messages/package.json @@ -19,7 +19,7 @@ "@monaco-editor/react": "^4.4.6", "@saleor/app-sdk": "0.37.3", "@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/mail": "^7.7.0", "@tanstack/react-query": "^4.24.4", diff --git a/apps/emails-and-messages/src/components/basic-layout.tsx b/apps/emails-and-messages/src/components/basic-layout.tsx new file mode 100644 index 0000000..52876a3 --- /dev/null +++ b/apps/emails-and-messages/src/components/basic-layout.tsx @@ -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 ( + + {breadcrumbs?.length && } + {children} + + ); +}; diff --git a/apps/emails-and-messages/src/components/breadcrumbs.tsx b/apps/emails-and-messages/src/components/breadcrumbs.tsx index 661e761..5624656 100644 --- a/apps/emails-and-messages/src/components/breadcrumbs.tsx +++ b/apps/emails-and-messages/src/components/breadcrumbs.tsx @@ -1,31 +1,56 @@ import { Box, Text } from "@saleor/macaw-ui/next"; +import Link from "next/link"; +import { TextLink } from "./text-link"; interface BreadcrumbsProps { 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) => { if (props.items.length === 0) { return null; } - // TODO: do I have to recreate the array here? - const i = [...props.items]; - const lastItem = i.pop()!; // can enforce the type since array is at least one element long + const items = [...props.items]; + const lastItem = items.pop()!; // can enforce the type since array is at least one element long return ( - {i.map((item) => ( - <> - - {item.name} - - + {items.map((item) => ( + + {!item.href ? ( + + {item.name} + + ) : ( + + {item.name} + + )} + + {">"} - + ))} - + {lastItem.name} diff --git a/apps/emails-and-messages/src/components/chip-text.tsx b/apps/emails-and-messages/src/components/chip-text.tsx new file mode 100644 index 0000000..57d4856 --- /dev/null +++ b/apps/emails-and-messages/src/components/chip-text.tsx @@ -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 ( + + + {content} + + + ); + case "warning": + return ( + + + {content} + + + ); + + case "error": + return ( + + + {content} + + + ); + + case "success": + return ( + + + {content} + + + ); + } +}; diff --git a/apps/emails-and-messages/src/components/messaging-providers-box.tsx b/apps/emails-and-messages/src/components/messaging-providers-box.tsx index 5a3da8a..eb3d7cf 100644 --- a/apps/emails-and-messages/src/components/messaging-providers-box.tsx +++ b/apps/emails-and-messages/src/components/messaging-providers-box.tsx @@ -3,7 +3,11 @@ import { BoxWithBorder } from "./box-with-border"; import { BoxFooter } from "./box-footer"; import { defaultPadding } from "./ui-defaults"; 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 { replace } = useRouter(); @@ -53,17 +57,32 @@ export const MessagingProvidersBox = ({ return ( - - Provider - Name - Status + + + Provider + + + Configuration name + + + Status + {configurations.map((configuration) => ( <> - Sendgrid - {configuration.configurationName} - {configuration.active} - redirectToEditConfiguration(configuration.id)}>Edit + + Sendgrid logo + Sendgrid + + + {configuration.name} + + + Edit + ))} diff --git a/apps/emails-and-messages/src/components/temp.tsx b/apps/emails-and-messages/src/components/temp.tsx deleted file mode 100644 index 5d6454c..0000000 --- a/apps/emails-and-messages/src/components/temp.tsx +++ /dev/null @@ -1,160 +0,0 @@ -const x = () => ( - - - Provide unique name for your configuration - you can create more than one. For example - - production and development. - - Then, pass your API Key. Obtain it here. - - } - > - - - - Remove provider - - You can remove provider configuration. - - This operation will remove all settings related to this configuration. Data will be - permanently removed from the App.{" "} - - - This operation cant be undone. You still can create new configuration. - - - - - - - - - - Provide unique name for your configuration - you can create more than one. For example - - production and development. - - Then, pass your API Key. Obtain it here. - - } - > - - - - - - - Enabled - - - - - - - Enabled - - - - - - - Enabled - - - - - - - - - - - Provide unique name for your configuration - you can create more than one. For example - - production and development. - - Then, pass your API Key. Obtain it here. - - } - > - - - - - - - - - - - - Manage providers configuration to connect Saleor events with 3rd party services. - - } - > - - No providers configured yet - - - - - - console.log("clicked sendgrid")} - /> - - console.log("clicked mjml")} - /> - - -) diff --git a/apps/emails-and-messages/src/components/text-link.tsx b/apps/emails-and-messages/src/components/text-link.tsx new file mode 100644 index 0000000..f435ad7 --- /dev/null +++ b/apps/emails-and-messages/src/components/text-link.tsx @@ -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 ( + + {props.children} + + ); +}; diff --git a/apps/emails-and-messages/src/lib/is-available-in-channel.ts b/apps/emails-and-messages/src/lib/is-available-in-channel.ts index f26a38b..f9fcd70 100644 --- a/apps/emails-and-messages/src/lib/is-available-in-channel.ts +++ b/apps/emails-and-messages/src/lib/is-available-in-channel.ts @@ -1,7 +1,8 @@ +import { SendgridConfigurationChannels } from "../modules/sendgrid/configuration/sendgrid-config-schema"; + interface IsAvailableInChannelArgs { channel: string; - restrictedToChannels: string[]; - excludedChannels: string[]; + channelConfiguration: SendgridConfigurationChannels; } /** @@ -14,14 +15,13 @@ interface IsAvailableInChannelArgs { */ export const isAvailableInChannel = ({ channel, - restrictedToChannels, - excludedChannels, + channelConfiguration, }: IsAvailableInChannelArgs): boolean => { - if (channel in excludedChannels) { - return false; + if (!channelConfiguration.override) { + return true; } - if (restrictedToChannels.length > 0 && !(channel in restrictedToChannels)) { - return false; + if (channelConfiguration.mode === "restrict") { + return channel in channelConfiguration.channels; } - return true; + return !(channel in channelConfiguration.channels); }; diff --git a/apps/emails-and-messages/src/modules/sendgrid/configuration/get-sendgrid-configuration.service.ts b/apps/emails-and-messages/src/modules/sendgrid/configuration/get-sendgrid-configuration.service.ts index fd7b576..fd75b0c 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/configuration/get-sendgrid-configuration.service.ts +++ b/apps/emails-and-messages/src/modules/sendgrid/configuration/get-sendgrid-configuration.service.ts @@ -1,7 +1,7 @@ import { SendgridConfigurator, PrivateMetadataSendgridConfigurator } from "./sendgrid-configurator"; import { Client } from "urql"; 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 { createSettingsManager } from "../../../lib/metadata-manager"; diff --git a/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-container.ts b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-container.ts index a649981..95d278f 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-container.ts +++ b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-container.ts @@ -4,30 +4,24 @@ import { messageEventTypes } from "../../event-handlers/message-event-types"; import { SendgridConfig as SendgridConfigurationRoot, SendgridConfiguration, -} from "./sendgrid-config"; + sendgridConfigurationEventSchema, + sendgridConfigurationSchema, +} from "./sendgrid-config-schema"; export const getDefaultEventsConfiguration = (): SendgridConfiguration["events"] => - messageEventTypes.map((eventType) => ({ - active: true, - eventType: eventType, - template: "", - })); + messageEventTypes.map((eventType) => sendgridConfigurationEventSchema.parse({ eventType })); export const getDefaultEmptyConfiguration = (): SendgridConfiguration => { - const defaultConfig: SendgridConfiguration = { - id: "", - active: true, - configurationName: "", - senderName: undefined, - senderEmail: undefined, - apiKey: "", - sandboxMode: false, - events: getDefaultEventsConfiguration(), + const defaultConfig: SendgridConfiguration = sendgridConfigurationSchema.parse({ + id: "id", + name: "name", + apiKey: "key", channels: { excludedFrom: [], restrictedTo: [], }, - }; + events: getDefaultEventsConfiguration(), + }); return defaultConfig; }; diff --git a/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-input-schema.ts b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-input-schema.ts index 615a2af..6465818 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-input-schema.ts +++ b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-input-schema.ts @@ -1,46 +1,26 @@ import { z } from "zod"; import { messageEventTypes } from "../../event-handlers/message-event-types"; +import { + sendgridConfigurationChannelsSchema, + sendgridConfigurationEventSchema, + sendgridConfigurationSchema, +} from "./sendgrid-config-schema"; -export const sendgridConfigurationEventObjectSchema = z.object({ - active: z.boolean(), - 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, +export const sendgridCreateConfigurationInputSchema = sendgridConfigurationSchema.pick({ + name: true, apiKey: true, }); -export type SendgridCreateConfigurationSchemaType = z.infer< - typeof sendgridCreateConfigurationSchema +export type SendgridCreateConfigurationInput = z.infer< + typeof sendgridCreateConfigurationInputSchema >; -export const sendgridUpdateOrCreateConfigurationSchema = - sendgridConfigurationBaseObjectSchema.merge( - z.object({ - id: z.string().optional(), - }) - ); -export const sendgridGetConfigurationInputSchema = z.object({ - id: z.string(), -}); -export const sendgridDeleteConfigurationInputSchema = z.object({ - id: z.string(), +export const sendgridConfigurationIdInputSchema = sendgridConfigurationSchema.pick({ + id: true, }); + +export type SendgridGetConfigurationIdInput = z.infer; + export const sendgridGetConfigurationsInputSchema = z .object({ ids: z.array(z.string()).optional(), @@ -48,20 +28,61 @@ export const sendgridGetConfigurationsInputSchema = z }) .optional(); +export type SendgridGetConfigurationsInput = z.infer; + export const sendgridUpdateEventConfigurationInputSchema = z .object({ configurationId: z.string(), }) - .merge(sendgridConfigurationEventObjectSchema); + .merge(sendgridConfigurationEventSchema); + +export type SendgridUpdateEventConfigurationInput = z.infer< + typeof sendgridUpdateEventConfigurationInputSchema +>; export const sendgridGetEventConfigurationInputSchema = z.object({ configurationId: z.string(), eventType: z.enum(messageEventTypes), }); -export const sendgridUpdateBasicInformationSchema = sendgridConfigurationBaseObjectSchema - .pick({ - configurationName: true, - active: true, +export type SendgridGetEventConfigurationInput = z.infer< + typeof sendgridGetEventConfigurationInputSchema +>; + +export const sendgridUpdateBasicInformationSchema = sendgridConfigurationSchema.pick({ + id: true, + name: true, + active: true, +}); + +export type SendgridUpdateBasicInformation = z.infer; + +export const sendgridUpdateApiConnectionSchema = sendgridConfigurationSchema.pick({ + id: true, + apiKey: true, + sandboxMode: true, +}); + +export type SendgridUpdateApiConnection = z.infer; + +export const sendgridUpdateSenderSchema = sendgridConfigurationSchema.pick({ + id: true, + sender: true, +}); +export type SendgridUpdateSender = z.infer; + +export const sendgridUpdateChannelsSchema = sendgridConfigurationChannelsSchema.merge( + sendgridConfigurationSchema.pick({ + id: true, }) - .merge(z.object({ id: z.string() })); +); + +export type SendgridUpdateChannels = z.infer; + +export const sendgridUpdateEventSchema = sendgridConfigurationEventSchema.merge( + sendgridConfigurationSchema.pick({ + id: true, + }) +); + +export type SendgridUpdateEvent = z.infer; diff --git a/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-schema.ts b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-schema.ts new file mode 100644 index 0000000..380bd6d --- /dev/null +++ b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-schema.ts @@ -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; + +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; + +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; + +export const sendgridConfigSchema = z.object({ + configurations: z.array(sendgridConfigurationSchema), +}); + +export type SendgridConfig = z.infer; diff --git a/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config.ts b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config.ts deleted file mode 100644 index d231e30..0000000 --- a/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config.ts +++ /dev/null @@ -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[]; -}; diff --git a/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-configuration.router.ts b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-configuration.router.ts index cd02720..3325af1 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-configuration.router.ts +++ b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-configuration.router.ts @@ -1,18 +1,21 @@ import { logger as pinoLogger } from "../../../lib/logger"; import { - sendgridCreateConfigurationSchema, - sendgridDeleteConfigurationInputSchema, - sendgridGetConfigurationInputSchema, + sendgridConfigurationIdInputSchema, + sendgridCreateConfigurationInputSchema, sendgridGetConfigurationsInputSchema, sendgridGetEventConfigurationInputSchema, + sendgridUpdateApiConnectionSchema, sendgridUpdateBasicInformationSchema, + sendgridUpdateChannelsSchema, sendgridUpdateEventConfigurationInputSchema, - sendgridUpdateOrCreateConfigurationSchema, + sendgridUpdateEventSchema, + sendgridUpdateSenderSchema, } from "./sendgrid-config-input-schema"; import { SendgridConfigurationService } from "./get-sendgrid-configuration.service"; import { router } from "../../trpc/trpc-server"; import { protectedClientProcedure } from "../../trpc/protected-client-procedure"; import { TRPCError } from "@trpc/server"; +import { getDefaultEmptyConfiguration } from "./sendgrid-config-container"; // Allow access only for the dashboard users and attaches the // configuration service to the context @@ -36,7 +39,7 @@ export const sendgridConfigurationRouter = router({ }), getConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(sendgridGetConfigurationInputSchema) + .input(sendgridConfigurationIdInputSchema) .query(async ({ ctx, input }) => { const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); logger.debug(input, "sendgridConfigurationRouter.get called"); @@ -52,15 +55,19 @@ export const sendgridConfigurationRouter = router({ }), createConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(sendgridCreateConfigurationSchema) + .input(sendgridCreateConfigurationInputSchema) .mutation(async ({ ctx, input }) => { const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); logger.debug(input, "sendgridConfigurationRouter.create called"); - return await ctx.configurationService.createConfiguration(input); + const newConfiguration = { + ...getDefaultEmptyConfiguration(), + ...input, + }; + return await ctx.configurationService.createConfiguration(newConfiguration); }), deleteConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(sendgridDeleteConfigurationInputSchema) + .input(sendgridConfigurationIdInputSchema) .mutation(async ({ ctx, input }) => { const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); logger.debug(input, "sendgridConfigurationRouter.delete called"); @@ -74,33 +81,33 @@ export const sendgridConfigurationRouter = router({ await ctx.configurationService.deleteConfiguration(input); return null; }), - updateOrCreateConfiguration: protectedWithConfigurationService - .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(sendgridUpdateOrCreateConfigurationSchema) - .mutation(async ({ ctx, input }) => { - const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); - logger.debug(input, "sendgridConfigurationRouter.update or create called"); + // updateOrCreateConfiguration: protectedWithConfigurationService + // .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) + // .input(sendgridUpdateOrCreateConfigurationSchema) + // .mutation(async ({ ctx, input }) => { + // const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); + // logger.debug(input, "sendgridConfigurationRouter.update or create called"); - const { id } = input; - if (!id) { - return await ctx.configurationService.createConfiguration(input); - } else { - const existingConfiguration = await ctx.configurationService.getConfiguration({ id }); - if (!existingConfiguration) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Configuration not found", - }); - } - const configuration = { - id, - ...input, - events: existingConfiguration.events, - }; - await ctx.configurationService.updateConfiguration(configuration); - return configuration; - } - }), + // const { id } = input; + // if (!id) { + // return await ctx.configurationService.createConfiguration(input); + // } else { + // const existingConfiguration = await ctx.configurationService.getConfiguration({ id }); + // if (!existingConfiguration) { + // throw new TRPCError({ + // code: "BAD_REQUEST", + // message: "Configuration not found", + // }); + // } + // const configuration = { + // id, + // ...input, + // events: existingConfiguration.events, + // }; + // await ctx.configurationService.updateConfiguration(configuration); + // return configuration; + // } + // }), getEventConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .input(sendgridGetEventConfigurationInputSchema) @@ -175,4 +182,92 @@ export const sendgridConfigurationRouter = router({ await ctx.configurationService.updateConfiguration({ ...configuration, ...input }); 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; + }), }); diff --git a/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-configurator.ts b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-configurator.ts index dea0caa..48663f7 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-configurator.ts +++ b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-configurator.ts @@ -1,4 +1,4 @@ -import { SendgridConfig } from "./sendgrid-config"; +import { SendgridConfig } from "./sendgrid-config-schema"; import { SettingsManager } from "@saleor/app-sdk/settings-manager"; export interface SendgridConfigurator { diff --git a/apps/emails-and-messages/src/modules/sendgrid/send-sendgrid.ts b/apps/emails-and-messages/src/modules/sendgrid/send-sendgrid.ts index c692723..357b6c4 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/send-sendgrid.ts +++ b/apps/emails-and-messages/src/modules/sendgrid/send-sendgrid.ts @@ -1,5 +1,5 @@ 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 { MessageEventTypes } from "../event-handlers/message-event-types"; diff --git a/apps/emails-and-messages/src/modules/sendgrid/sendgrid-api.ts b/apps/emails-and-messages/src/modules/sendgrid/sendgrid-api.ts index 5f2d821..7ebaad4 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/sendgrid-api.ts +++ b/apps/emails-and-messages/src/modules/sendgrid/sendgrid-api.ts @@ -30,7 +30,7 @@ export const fetchTemplates = }; const templates = resJson.result?.map((r) => ({ - value: r.id, + value: r.id.toString(), label: r.name, })) || []; return templates; @@ -65,7 +65,7 @@ export const fetchSenders = }; const senders = resJson.results?.map((r) => ({ - value: r.id, + value: r.id.toString(), label: `${r.nickname} (${r.from_email})`, nickname: r.nickname, from_email: r.from_email, diff --git a/apps/emails-and-messages/src/modules/sendgrid/ui/api-connection-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/api-connection-section.tsx index e9f4513..04108d1 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/ui/api-connection-section.tsx +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/api-connection-section.tsx @@ -1,76 +1,64 @@ -import { useRouter } from "next/router"; -import { SendgridConfiguration } from "../configuration/sendgrid-config"; +import { SendgridConfiguration } from "../configuration/sendgrid-config-schema"; 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 { useDashboardNotification } from "@saleor/apps-shared"; import { trpcClient } from "../../trpc/trpc-client"; -import { sendgridUpdateBasicInformationSchema } from "../configuration/sendgrid-config-input-schema"; -import { z } from "zod"; +import { SendgridUpdateApiConnection } 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 ApiConnectionSectionProps { - configuration?: SendgridConfiguration; + configuration: SendgridConfiguration; } export const ApiConnectionSection = ({ configuration }: ApiConnectionSectionProps) => { - const { replace } = useRouter(); const { notifySuccess, notifyError } = useDashboardNotification(); - const { handleSubmit, control, setError } = useForm< - z.infer - >({ + const { handleSubmit, control, setError, register } = useForm({ defaultValues: { - configurationName: configuration?.configurationName, - active: configuration?.active, + id: configuration.id, + apiKey: configuration.apiKey, + sandboxMode: configuration.sandboxMode, }, }); - const { mutate: createConfiguration } = - trpcClient.sendgridConfiguration.updateBasicInformation.useMutation({ - onSuccess: async (data, variables) => { - notifySuccess("Configuration saved"); - // TODO: redirect to configuration details based on id - }, - 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 z.infer, { - type: "manual", - message, - }); - } + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.sendgridConfiguration.updateApiConnection.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 SendgridUpdateApiConnection, { + type: "manual", + 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( - "Could not save the configuration", - isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration", - formErrorMessage - ); - }, - }); - - if (!configuration) { - return ( - - Loading - - ); - } + notifyError( + "Could not save the configuration", + isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration", + formErrorMessage + ); + }, + }); return (
{ - createConfiguration({ + mutate({ ...data, }); })} @@ -96,25 +84,11 @@ export const ApiConnectionSection = ({ configuration }: ApiConnectionSectionProp /> )} /> - ( - // TODO: add validation - console.log(e)}> - - Live - - - Sandbox - - - )} - /> + + diff --git a/apps/emails-and-messages/src/modules/sendgrid/ui/basic-information-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/basic-information-section.tsx index 81283da..743f6c0 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/ui/basic-information-section.tsx +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/basic-information-section.tsx @@ -1,69 +1,60 @@ -import { useRouter } from "next/router"; -import { SendgridConfiguration } from "../configuration/sendgrid-config"; +import { SendgridConfiguration } from "../configuration/sendgrid-config-schema"; import { BoxWithBorder } from "../../../components/box-with-border"; import { Box, Button, Input, RadioGroup, 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 { sendgridUpdateBasicInformationSchema } from "../configuration/sendgrid-config-input-schema"; +import { + SendgridUpdateBasicInformation, + sendgridUpdateBasicInformationSchema, +} from "../configuration/sendgrid-config-input-schema"; import { z } from "zod"; import { Controller, useForm } from "react-hook-form"; import { BoxFooter } from "../../../components/box-footer"; import { SectionWithDescription } from "../../../components/section-with-description"; interface BasicInformationSectionProps { - configuration?: SendgridConfiguration; + configuration: SendgridConfiguration; } export const BasicInformationSection = ({ configuration }: BasicInformationSectionProps) => { - const { replace } = useRouter(); const { notifySuccess, notifyError } = useDashboardNotification(); - - const { handleSubmit, control, setError } = useForm< - z.infer - >({ + const { handleSubmit, control, setError, register } = useForm({ defaultValues: { - configurationName: configuration?.configurationName, - active: configuration?.active, + id: configuration.id, + name: configuration.name, + active: configuration.active, }, }); - const { mutate: createConfiguration } = - trpcClient.sendgridConfiguration.updateBasicInformation.useMutation({ - onSuccess: async (data, variables) => { - notifySuccess("Configuration saved"); - // TODO: redirect to configuration details based on id - }, - 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 z.infer, { - type: "manual", - message, - }); - } + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.sendgridConfiguration.updateBasicInformation.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 z.infer, { + type: "manual", + 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( - "Could not save the configuration", - isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration", - formErrorMessage - ); - }, - }); - - if (!configuration) { - return ( - - Loading - - ); - } + notifyError( + "Could not save the configuration", + isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration", + formErrorMessage + ); + }, + }); return ( { - createConfiguration({ + mutate({ ...data, }); })} > )} /> - ( - // TODO: add validation - console.log(e)}> - - Active - - - Disabled - - - )} - /> + diff --git a/apps/emails-and-messages/src/modules/sendgrid/ui/channels-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/channels-section.tsx new file mode 100644 index 0000000..2e79bb0 --- /dev/null +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/channels-section.tsx @@ -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 ( + + Configuration will be used with all channels. + + ); + } + + if (mode === "exclude") { + const leftChannels = availableChannels.filter((channel) => !channels.includes(channel)); + if (!leftChannels.length) { + return Theres no channel which will be used with this configuration.; + } + return ( + + Configuration will be used with channels: + {leftChannels.join(", ")}. + + ); + } + return ( + + Configuration will be used with channels:{" "} + {channels.join(", ")}. + + ); +}; + +export const ChannelsSection = ({ configuration }: ChannelsSectionProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + + const { handleSubmit, control, setError, setValue, getValues, register } = + useForm({ + 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 ( + + + By default, provider will work for every channel. You can change this behavior with + "excluding" or "including" strategy. + + + Excluding - all current channels and new created + channels will work, excluding selected + + + Including - only selected channels will work, new + created channels will not work + + + } + > + { + mutate({ + ...data, + }); + })} + > + + + + Current behaviour + {overrideMessage({ + availableChannels: channels?.map((channel) => channel.slug) || [], + channelConfiguration: configuration.channels, + })} + Settings + + + ( + + + + Include + + + + Exclude + + + )} + /> + + {channels?.map((channel) => ( + + ))} + + + + + + + + + ); +}; diff --git a/apps/emails-and-messages/src/modules/sendgrid/ui/dangrous-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/dangrous-section.tsx index 393c178..6d8ece9 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/ui/dangrous-section.tsx +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/dangrous-section.tsx @@ -3,42 +3,87 @@ import { BoxWithBorder } from "../../../components/box-with-border"; import { SectionWithDescription } from "../../../components/section-with-description"; import { defaultPadding } from "../../../components/ui-defaults"; 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 { - configuration?: SendgridConfiguration; + configuration: SendgridConfiguration; } export const DangerousSection = ({ configuration }: DangerousSectionProps) => { - const onRemoveConfiguration = () => { - console.log("remove", configuration?.id); - }; + const { notifySuccess, notifyError } = useDashboardNotification(); + const { replace } = useRouter(); + const { handleSubmit, setError } = useForm({ + 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 ( - - - - Remove provider - - You can remove provider configuration. - - This operation will remove all settings related to this configuration. Data will be - permanently removed from the App.{" "} - - This operation cant be undone. - You still can create new configuration. - - - - - +
{ + mutate({ + ...data, + }); + })} + > + + + + Remove provider + + You can remove provider configuration. + + This operation will remove all settings related to this configuration. Data will be + permanently removed from the App.{" "} + + This operation cant be undone. + You still can create new configuration. + + + + + +
); }; diff --git a/apps/emails-and-messages/src/modules/sendgrid/ui/events-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/events-section.tsx new file mode 100644 index 0000000..e3f7b92 --- /dev/null +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/events-section.tsx @@ -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({ + 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 ( +
{ + mutate({ + ...data, + }); + })} + > + + + {event.eventType} + {!templatesChoices ? ( + + ) : ( + ( + onChange(event?.value)} + options={templatesChoices.map((sender) => ({ + label: sender.label, + value: sender.value, + }))} + error={!!error} + /> + )} + /> + )} + + + + + + + +
+ ); +}; + +interface EventsSectionProps { + configuration: SendgridConfiguration; +} + +export const EventsSection = ({ configuration }: EventsSectionProps) => { + return ( + + + Provide unique name for your configuration - you can create more than one. For example - + production and development. + + Then, pass your API Key. Obtain it here. + + } + > + + {configuration.events.map((event) => ( + + ))} + + + ); +}; diff --git a/apps/emails-and-messages/src/modules/sendgrid/ui/sender-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/sender-section.tsx index de89bc6..f8d34ca 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/ui/sender-section.tsx +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/sender-section.tsx @@ -1,99 +1,101 @@ -import { useRouter } from "next/router"; -import { SendgridConfiguration } from "../configuration/sendgrid-config"; +import { SendgridConfiguration } from "../configuration/sendgrid-config-schema"; 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 { useDashboardNotification } from "@saleor/apps-shared"; import { trpcClient } from "../../trpc/trpc-client"; -import { sendgridUpdateBasicInformationSchema } from "../configuration/sendgrid-config-input-schema"; -import { z } from "zod"; +import { SendgridUpdateSender } 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 { fetchSenders } from "../sendgrid-api"; +import { useQuery } from "@tanstack/react-query"; interface SenderSectionProps { - configuration?: SendgridConfiguration; + configuration: SendgridConfiguration; } export const SenderSection = ({ configuration }: SenderSectionProps) => { - const { replace } = useRouter(); const { notifySuccess, notifyError } = useDashboardNotification(); - const { handleSubmit, control, setError } = useForm< - z.infer - >({ + const { data: sendersChoices, isLoading: isSendersChoicesLoading } = useQuery({ + queryKey: ["sendgridSenders"], + queryFn: fetchSenders({ apiKey: configuration.apiKey }), + enabled: !!configuration.apiKey?.length, + }); + + const { handleSubmit, control, setError } = useForm({ defaultValues: { - configurationName: configuration?.configurationName, - active: configuration?.active, + id: configuration.id, + sender: configuration.sender, }, }); - const { mutate: createConfiguration } = - trpcClient.sendgridConfiguration.updateBasicInformation.useMutation({ - onSuccess: async (data, variables) => { - notifySuccess("Configuration saved"); - // TODO: redirect to configuration details based on id - }, - 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 z.infer, { - type: "manual", - message, - }); - } + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.sendgridConfiguration.updateSender.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 SendgridUpdateSender, { + type: "manual", + 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( - "Could not save the configuration", - isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration", - formErrorMessage - ); - }, - }); - - if (!configuration) { - return ( - - Loading - - ); - } + notifyError( + "Could not save the configuration", + isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration", + formErrorMessage + ); + }, + }); return (
{ - createConfiguration({ + mutate({ ...data, }); })} > - ( - - )} - /> + {sendersChoices?.length ? ( + ( + onChange(event?.value)} + options={sendersChoices.map((sender) => ({ + label: sender.label, + value: sender.value, + }))} + error={!!error} + /> + )} + /> + ) : ( + + )} diff --git a/apps/emails-and-messages/src/pages/configuration/sendgrid/edit/[id].tsx b/apps/emails-and-messages/src/pages/configuration/sendgrid/edit/[id].tsx index e398310..60e343d 100644 --- a/apps/emails-and-messages/src/pages/configuration/sendgrid/edit/[id].tsx +++ b/apps/emails-and-messages/src/pages/configuration/sendgrid/edit/[id].tsx @@ -1,37 +1,95 @@ import { Box, Text } from "@saleor/macaw-ui/next"; import { NextPage } from "next"; -import { Breadcrumbs } from "../../../../components/breadcrumbs"; import { trpcClient } from "../../../../modules/trpc/trpc-client"; import { useRouter } from "next/router"; import { BasicInformationSection } from "../../../../modules/sendgrid/ui/basic-information-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 { 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 ( + + Loading... + + ); +}; + +const NotFoundView = () => { + return ( + + Could not find the requested configuration. + + ); +}; const EditSendgridConfigurationPage: NextPage = () => { + const { notifyError } = useDashboardNotification(); const router = useRouter(); 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({ - id: id as string, - }); + if (isLoading) { + return ; + } + + if (!configuration) { + return ; + } return ( - - + Connect Sendgrid with Saleor. - {!!configuration && } - {!!configuration && } - {!!configuration && } - {!!configuration && } - + + + + + + + ); }; diff --git a/apps/emails-and-messages/src/pages/configuration/sendgrid/new.tsx b/apps/emails-and-messages/src/pages/configuration/sendgrid/new.tsx index 06caccd..eabd7f5 100644 --- a/apps/emails-and-messages/src/pages/configuration/sendgrid/new.tsx +++ b/apps/emails-and-messages/src/pages/configuration/sendgrid/new.tsx @@ -1,6 +1,5 @@ import { Box, Button, Input, Text } from "@saleor/macaw-ui/next"; import { NextPage } from "next"; -import { Breadcrumbs } from "../../../components/breadcrumbs"; import { SectionWithDescription } from "../../../components/section-with-description"; import { BoxWithBorder } from "../../../components/box-with-border"; import { defaultPadding } from "../../../components/ui-defaults"; @@ -8,18 +7,21 @@ import { BoxFooter } from "../../../components/box-footer"; import { trpcClient } from "../../../modules/trpc/trpc-client"; import { useDashboardNotification } from "@saleor/apps-shared/src/use-dashboard-notification"; 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 router = useRouter(); const { notifySuccess, notifyError } = useDashboardNotification(); - const { handleSubmit, control, setError } = useForm(); + const { handleSubmit, control, setError } = useForm(); const { mutate: createConfiguration } = trpcClient.sendgridConfiguration.createConfiguration.useMutation({ onSuccess: async (data, variables) => { notifySuccess("Configuration saved"); - // TODO: redirect to configuration details based on id + router.push(`/configuration/sendgrid/edit/${data.id}`); }, onError(error) { let isFieldErrorSet = false; @@ -27,7 +29,7 @@ const NewSendgridConfigurationPage: NextPage = () => { for (const fieldName in fieldErrors) { for (const message of fieldErrors[fieldName] || []) { isFieldErrorSet = true; - setError(fieldName as keyof SendgridCreateConfigurationSchemaType, { + setError(fieldName as keyof SendgridCreateConfigurationInput, { type: "manual", message, }); @@ -45,14 +47,13 @@ const NewSendgridConfigurationPage: NextPage = () => { }); return ( - - + Connect Sendgrid with Saleor. @@ -77,7 +78,7 @@ const NewSendgridConfigurationPage: NextPage = () => { > {
-
+ ); }; diff --git a/apps/emails-and-messages/src/public/sendgrid.png b/apps/emails-and-messages/src/public/sendgrid.png new file mode 100644 index 0000000000000000000000000000000000000000..ede13eb1cec8e92e0aeed674210454a1ec70180d GIT binary patch literal 332 zcmeAS@N?(olHy`uVBq!ia0vp^3P3E+!3HGT!p`>qDb50q$YKTtZeb8+WSBKa0w~B> z9OUlAu|M+GDqhz zp)F+#CcZEYOJpxg;8hV@AZ)wT{c)i1`5n`Dwnv>)S*BFDujpLO(5i!ynFR6Wzot^uRpZBKDV?;`?I{pZ)Ov!m@R) zu$$WVK7MANW8yP1LV{OJ{kcwaY1`UG*IQVD${m + + + + + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d91023a..e1986e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -325,7 +325,7 @@ importers: '@monaco-editor/react': ^4.4.6 '@saleor/app-sdk': 0.37.3 '@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/mail': ^7.7.0 '@tanstack/react-query': ^4.24.4 @@ -375,7 +375,7 @@ importers: '@monaco-editor/react': 4.4.6_biqbaboplfbrettd7655fr4n2y '@saleor/app-sdk': 0.37.3_yucv4tfv7v7nrkw2uguegj6e7e '@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/mail': 7.7.0 '@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y @@ -4804,8 +4804,8 @@ packages: - '@types/react' dev: false - /@saleor/macaw-ui/0.8.0-pre.76_5ndqzdd6t4rivxsukjv3i3ak2q: - resolution: {integrity: sha512-z5zlgdiLcJTR4al4FP6Z3JBzcH1VWQQRrUVH/TraqvHfIxC5XCPz1ZSve1/KUyXafbjUaM0ih81B9vqfipbXRA==} + /@saleor/macaw-ui/0.8.0-pre.79_5ndqzdd6t4rivxsukjv3i3ak2q: + resolution: {integrity: sha512-E+kqNPPyD5QR+DHLIrhbfN++g7DC+LyrINYeSnDXYad03BZz1AXcY9tagNMIt8ie4qrmQ/gMrsEQpN0yMUlNgg==} engines: {node: '>=16 <19', pnpm: '>=8'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0