From 44741aa74eb62700627b94af5c38635329f1e71d Mon Sep 17 00:00:00 2001 From: Krzysztof Wolski Date: Wed, 24 May 2023 10:38:09 +0200 Subject: [PATCH] Update E&M Provider UI (#459) * update to macaw next * update ui to new design --- apps/emails-and-messages/package.json | 9 +- .../src/components/basic-layout.tsx | 17 + .../src/components/box-footer.tsx | 19 + .../src/components/box-with-border.tsx | 15 + .../src/components/breadcrumbs.tsx | 55 +++ .../src/components/chip-text.tsx | 30 ++ .../react-hook-form-macaw/Combobox.tsx | 36 ++ .../react-hook-form-macaw/Input.tsx | 36 ++ .../react-hook-form-macaw/Multiselect.tsx | 42 ++ .../react-hook-form-macaw/README.md | 3 + .../components/section-with-description.tsx | 27 ++ .../src/components/text-link.tsx | 35 ++ .../src/components/ui-defaults.ts | 1 + .../src/lib/get-base-url.ts | 10 +- .../src/lib/set-backend-errors.ts | 38 ++ .../src/lib/theme-synchronizer.test.tsx | 48 --- .../ui/messaging-providers-box.tsx | 133 +++++++ .../ui/provider-selection-box.tsx | 18 +- .../src/modules/app-configuration/urls.ts | 6 +- .../channels/channel-configuration-schema.ts | 19 + .../channels/is-available-in-channel.test.ts | 95 +++++ .../channels}/is-available-in-channel.ts | 18 +- .../channels/ui/assigned-channels-message.tsx | 52 +++ .../ui/universal-channels-section.tsx | 118 ++++++ .../event-handlers/send-event-messages.ts | 20 +- .../configuration/mjml-config-container.ts | 142 ------- .../configuration/mjml-config-input-schema.ts | 56 --- .../modules/mjml/configuration/mjml-config.ts | 34 -- .../src/modules/mjml/urls.ts | 7 - .../get-sendgrid-configuration.service.ts | 2 +- .../sendgrid-config-container.ts | 31 +- .../sendgrid-config-input-schema.ts | 97 +++-- .../configuration/sendgrid-config-schema.ts | 32 ++ .../sendgrid/configuration/sendgrid-config.ts | 26 -- .../sendgrid-configuration.router.ts | 181 +++++++-- .../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 | 79 ++++ .../ui/sendgrid-basic-information-section.tsx | 88 +++++ .../sendgrid/ui/sendgrid-channels-section.tsx | 33 ++ .../sendgrid/ui/sendgrid-dangrous-section.tsx | 73 ++++ .../sendgrid/ui/sendgrid-events-section.tsx | 119 ++++++ .../sendgrid/ui/sendgrid-sender-section.tsx | 84 ++++ .../src/modules/sendgrid/urls.ts | 4 +- .../compile-handlebars-template.ts | 0 .../modules/{mjml => smtp}/compile-mjml.ts | 0 .../get-smtp-configuration.service.ts} | 34 +- .../configuration/smtp-config-container.ts | 142 +++++++ .../configuration/smtp-config-input-schema.ts | 89 +++++ .../smtp/configuration/smtp-config-schema.ts | 37 ++ .../smtp-configuration.router.ts} | 205 +++++++--- .../configuration/smtp-configurator.ts} | 16 +- .../{mjml => smtp}/default-templates.ts | 0 .../{mjml => smtp}/html-to-plaintext.ts | 0 .../{mjml => smtp}/send-email-with-smtp.ts | 0 .../{mjml/send-mjml.ts => smtp/send-smtp.ts} | 28 +- .../src/modules/smtp/ui/code-edtor.tsx | 42 ++ .../src/modules/smtp/ui/event-form.tsx | 163 ++++++++ .../src/modules/smtp/ui/mjml-preview.tsx | 17 + .../ui/smtp-basic-information-section.tsx | 88 +++++ .../modules/smtp/ui/smtp-channels-section.tsx | 36 ++ .../modules/smtp/ui/smtp-dangrous-section.tsx | 76 ++++ .../modules/smtp/ui/smtp-events-section.tsx | 106 ++++++ .../src/modules/smtp/ui/smtp-section.tsx | 109 ++++++ .../modules/smtp/ui/smtp-sender-section.tsx | 79 ++++ .../src/modules/smtp/urls.ts | 8 + .../src/modules/trpc/trpc-app-router.ts | 4 +- apps/emails-and-messages/src/pages/_app.tsx | 85 +---- .../src/pages/api/register.ts | 1 - .../pages/configuration/choose-provider.tsx | 46 +++ .../src/pages/configuration/index.tsx | 61 +++ .../sendgrid/[configurationId].tsx | 100 +++++ .../src/pages/configuration/sendgrid/new.tsx | 120 ++++++ .../configuration/smtp/[configurationId].tsx | 97 +++++ .../[configurationId]/event/[eventType].tsx | 98 +++++ .../src/pages/configuration/smtp/new.tsx | 134 +++++++ apps/emails-and-messages/src/pages/index.tsx | 6 +- .../src/public/breadcrumb-separator.svg | 8 + .../src/public/sendgrid.png | Bin 0 -> 332 bytes apps/emails-and-messages/src/public/smtp.svg | 9 + .../src/styles/globals.css | 21 +- pnpm-lock.yaml | 359 ++++++++++++------ 83 files changed, 3651 insertions(+), 769 deletions(-) create mode 100644 apps/emails-and-messages/src/components/basic-layout.tsx create mode 100644 apps/emails-and-messages/src/components/box-footer.tsx create mode 100644 apps/emails-and-messages/src/components/box-with-border.tsx create mode 100644 apps/emails-and-messages/src/components/breadcrumbs.tsx create mode 100644 apps/emails-and-messages/src/components/chip-text.tsx create mode 100644 apps/emails-and-messages/src/components/react-hook-form-macaw/Combobox.tsx create mode 100644 apps/emails-and-messages/src/components/react-hook-form-macaw/Input.tsx create mode 100644 apps/emails-and-messages/src/components/react-hook-form-macaw/Multiselect.tsx create mode 100644 apps/emails-and-messages/src/components/react-hook-form-macaw/README.md create mode 100644 apps/emails-and-messages/src/components/section-with-description.tsx create mode 100644 apps/emails-and-messages/src/components/text-link.tsx create mode 100644 apps/emails-and-messages/src/components/ui-defaults.ts create mode 100644 apps/emails-and-messages/src/lib/set-backend-errors.ts delete mode 100644 apps/emails-and-messages/src/lib/theme-synchronizer.test.tsx create mode 100644 apps/emails-and-messages/src/modules/app-configuration/ui/messaging-providers-box.tsx create mode 100644 apps/emails-and-messages/src/modules/channels/channel-configuration-schema.ts create mode 100644 apps/emails-and-messages/src/modules/channels/is-available-in-channel.test.ts rename apps/emails-and-messages/src/{lib => modules/channels}/is-available-in-channel.ts (51%) create mode 100644 apps/emails-and-messages/src/modules/channels/ui/assigned-channels-message.tsx create mode 100644 apps/emails-and-messages/src/modules/channels/ui/universal-channels-section.tsx delete mode 100644 apps/emails-and-messages/src/modules/mjml/configuration/mjml-config-container.ts delete mode 100644 apps/emails-and-messages/src/modules/mjml/configuration/mjml-config-input-schema.ts delete mode 100644 apps/emails-and-messages/src/modules/mjml/configuration/mjml-config.ts delete mode 100644 apps/emails-and-messages/src/modules/mjml/urls.ts 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/api-connection-section.tsx create mode 100644 apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-basic-information-section.tsx create mode 100644 apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-channels-section.tsx create mode 100644 apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-dangrous-section.tsx create mode 100644 apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-events-section.tsx create mode 100644 apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-sender-section.tsx rename apps/emails-and-messages/src/modules/{mjml => smtp}/compile-handlebars-template.ts (100%) rename apps/emails-and-messages/src/modules/{mjml => smtp}/compile-mjml.ts (100%) rename apps/emails-and-messages/src/modules/{mjml/configuration/get-mjml-configuration.service.ts => smtp/configuration/get-smtp-configuration.service.ts} (71%) create mode 100644 apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-container.ts create mode 100644 apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-input-schema.ts create mode 100644 apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-schema.ts rename apps/emails-and-messages/src/modules/{mjml/configuration/mjml-configuration.router.ts => smtp/configuration/smtp-configuration.router.ts} (53%) rename apps/emails-and-messages/src/modules/{mjml/configuration/mjml-configurator.ts => smtp/configuration/smtp-configurator.ts} (61%) rename apps/emails-and-messages/src/modules/{mjml => smtp}/default-templates.ts (100%) rename apps/emails-and-messages/src/modules/{mjml => smtp}/html-to-plaintext.ts (100%) rename apps/emails-and-messages/src/modules/{mjml => smtp}/send-email-with-smtp.ts (100%) rename apps/emails-and-messages/src/modules/{mjml/send-mjml.ts => smtp/send-smtp.ts} (86%) create mode 100644 apps/emails-and-messages/src/modules/smtp/ui/code-edtor.tsx create mode 100644 apps/emails-and-messages/src/modules/smtp/ui/event-form.tsx create mode 100644 apps/emails-and-messages/src/modules/smtp/ui/mjml-preview.tsx create mode 100644 apps/emails-and-messages/src/modules/smtp/ui/smtp-basic-information-section.tsx create mode 100644 apps/emails-and-messages/src/modules/smtp/ui/smtp-channels-section.tsx create mode 100644 apps/emails-and-messages/src/modules/smtp/ui/smtp-dangrous-section.tsx create mode 100644 apps/emails-and-messages/src/modules/smtp/ui/smtp-events-section.tsx create mode 100644 apps/emails-and-messages/src/modules/smtp/ui/smtp-section.tsx create mode 100644 apps/emails-and-messages/src/modules/smtp/ui/smtp-sender-section.tsx create mode 100644 apps/emails-and-messages/src/modules/smtp/urls.ts create mode 100644 apps/emails-and-messages/src/pages/configuration/choose-provider.tsx create mode 100644 apps/emails-and-messages/src/pages/configuration/index.tsx create mode 100644 apps/emails-and-messages/src/pages/configuration/sendgrid/[configurationId].tsx create mode 100644 apps/emails-and-messages/src/pages/configuration/sendgrid/new.tsx create mode 100644 apps/emails-and-messages/src/pages/configuration/smtp/[configurationId].tsx create mode 100644 apps/emails-and-messages/src/pages/configuration/smtp/[configurationId]/event/[eventType].tsx create mode 100644 apps/emails-and-messages/src/pages/configuration/smtp/new.tsx create mode 100644 apps/emails-and-messages/src/public/breadcrumb-separator.svg 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 6dfd4e9..c7b5d27 100644 --- a/apps/emails-and-messages/package.json +++ b/apps/emails-and-messages/package.json @@ -16,10 +16,11 @@ "schemaVersion": "3.11.7" }, "dependencies": { + "@hookform/resolvers": "^2.9.10", "@monaco-editor/react": "^4.4.6", "@saleor/app-sdk": "0.38.0", "@saleor/apps-shared": "workspace:*", - "@saleor/macaw-ui": "0.8.0-pre.72", + "@saleor/macaw-ui": "0.8.0-pre.83", "@sendgrid/client": "^7.7.0", "@sendgrid/mail": "^7.7.0", "@sentry/nextjs": "^7.52.1", @@ -29,7 +30,7 @@ "@trpc/react-query": "^10.13.0", "@trpc/server": "^10.13.0", "@urql/exchange-auth": "^1.0.0", - "@vitejs/plugin-react": "^3.0.1", + "@vitejs/plugin-react": "^4.0.0", "clsx": "^1.2.1", "graphql": "^16.6.0", "graphql-tag": "^2.12.6", @@ -49,8 +50,8 @@ "react-query": "^3.39.3", "urql": "^3.0.3", "usehooks-ts": "^2.9.1", - "vite": "^4.3.1", - "vitest": "^0.30.1", + "vite": "^4.3.8", + "vitest": "^0.31.1", "zod": "^3.20.2" }, "devDependencies": { 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..ed6dace --- /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/box-footer.tsx b/apps/emails-and-messages/src/components/box-footer.tsx new file mode 100644 index 0000000..043e203 --- /dev/null +++ b/apps/emails-and-messages/src/components/box-footer.tsx @@ -0,0 +1,19 @@ +import { Box, BoxProps } from "@saleor/macaw-ui/next"; +import { defaultPadding } from "./ui-defaults"; + +export const BoxFooter = (props: BoxProps) => { + return ( + + {props.children} + + ); +}; diff --git a/apps/emails-and-messages/src/components/box-with-border.tsx b/apps/emails-and-messages/src/components/box-with-border.tsx new file mode 100644 index 0000000..b6879fb --- /dev/null +++ b/apps/emails-and-messages/src/components/box-with-border.tsx @@ -0,0 +1,15 @@ +import { Box, BoxProps } from "@saleor/macaw-ui/next"; + +export const BoxWithBorder = (props: BoxProps) => { + return ( + + {props.children} + + ); +}; diff --git a/apps/emails-and-messages/src/components/breadcrumbs.tsx b/apps/emails-and-messages/src/components/breadcrumbs.tsx new file mode 100644 index 0000000..a202c72 --- /dev/null +++ b/apps/emails-and-messages/src/components/breadcrumbs.tsx @@ -0,0 +1,55 @@ +import { Box, Text, TextProps } from "@saleor/macaw-ui/next"; +import { TextLink } from "./text-link"; +import icon from "../public/breadcrumb-separator.svg"; +import Image from "next/image"; + +type BreadcrumbItem = { name: string; href?: string }; + +interface BreadcrumbProps extends BreadcrumbItem { + isLast?: boolean; +} + +const Breadcrumb = ({ name, href, isLast }: BreadcrumbProps) => { + const textProps: TextProps = { + variant: "hero", + display: isLast ? "block" : { mobile: "none", desktop: "block" }, + }; + + if (!!href) { + return ( + + {name} + + ); + } + return {name}; +}; + +interface BreadcrumbsProps { + items: Array; +} + +/** + * 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; + } + + const items = [...props.items]; + const lastItem = items.pop()!; // can enforce the type since array is at least one element long + + return ( + + {items.map((item) => ( + + + Separator icon + + ))} + + + ); +}; 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..d7c8d89 --- /dev/null +++ b/apps/emails-and-messages/src/components/chip-text.tsx @@ -0,0 +1,30 @@ +import { Text, Chip, ChipProps } from "@saleor/macaw-ui/next"; + +const colorPropsMapping: Record = { + default: "neutralSubdued", + warning: "brandHighlight", + error: "criticalDefault", + success: "neutralDefault", +}; + +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, + }; + + return ( + + + {content} + + + ); +}; diff --git a/apps/emails-and-messages/src/components/react-hook-form-macaw/Combobox.tsx b/apps/emails-and-messages/src/components/react-hook-form-macaw/Combobox.tsx new file mode 100644 index 0000000..eeaee63 --- /dev/null +++ b/apps/emails-and-messages/src/components/react-hook-form-macaw/Combobox.tsx @@ -0,0 +1,36 @@ +import { Combobox as $Combobox, type ComboboxProps as $ComboboxProps } from "@saleor/macaw-ui/next"; +import { Control, Controller, FieldPath, FieldValues } from "react-hook-form"; + +export type ComboboxProps = Omit<$ComboboxProps, "name"> & { + name: FieldPath; + control: Control; +}; + +export function Combobox({ + type, + required, + name, + control, + options, + ...rest +}: ComboboxProps): JSX.Element { + return ( + ( + <$Combobox + {...rest} + {...field} + options={options} + value={value || ""} + name={name} + required={required} + type={type} + error={!!error} + helperText={rest.helperText} + /> + )} + /> + ); +} diff --git a/apps/emails-and-messages/src/components/react-hook-form-macaw/Input.tsx b/apps/emails-and-messages/src/components/react-hook-form-macaw/Input.tsx new file mode 100644 index 0000000..6e829db --- /dev/null +++ b/apps/emails-and-messages/src/components/react-hook-form-macaw/Input.tsx @@ -0,0 +1,36 @@ +import { Input as $Input, type InputProps as $InputProps } from "@saleor/macaw-ui/next"; +import { Control, Controller, FieldPath, FieldValues } from "react-hook-form"; + +export type TextFieldElementProps = Omit< + $InputProps, + "name" +> & { + name: FieldPath; + control: Control; +}; + +export function Input({ + type, + required, + name, + control, + ...rest +}: TextFieldElementProps): JSX.Element { + return ( + ( + <$Input + {...rest} + {...field} + name={name} + required={required} + type={type} + error={!!error} + helperText={rest.helperText} + /> + )} + /> + ); +} diff --git a/apps/emails-and-messages/src/components/react-hook-form-macaw/Multiselect.tsx b/apps/emails-and-messages/src/components/react-hook-form-macaw/Multiselect.tsx new file mode 100644 index 0000000..d14a14d --- /dev/null +++ b/apps/emails-and-messages/src/components/react-hook-form-macaw/Multiselect.tsx @@ -0,0 +1,42 @@ +import { + Multiselect as $Multiselect, + type MultiselectProps as $MultiselectProps, +} from "@saleor/macaw-ui/next"; +import { Control, Controller, FieldPath, FieldValues, UseFormSetError } from "react-hook-form"; + +export type MultiselectProps = Omit< + $MultiselectProps, + "name" +> & { + name: FieldPath; + control: Control; +}; + +export function Multiselect({ + type, + required, + name, + control, + options, + ...rest +}: MultiselectProps): JSX.Element { + return ( + ( + <$Multiselect + {...rest} + {...field} + options={options} + value={value || []} + name={name} + required={required} + type={type} + error={!!error} + helperText={rest.helperText} + /> + )} + /> + ); +} diff --git a/apps/emails-and-messages/src/components/react-hook-form-macaw/README.md b/apps/emails-and-messages/src/components/react-hook-form-macaw/README.md new file mode 100644 index 0000000..f4f6036 --- /dev/null +++ b/apps/emails-and-messages/src/components/react-hook-form-macaw/README.md @@ -0,0 +1,3 @@ +Bindings for Macaw, code based on https://github.com/jsun969/react-hook-form-antd/ + +Components will be moved to the separate library inside apps monorepo. diff --git a/apps/emails-and-messages/src/components/section-with-description.tsx b/apps/emails-and-messages/src/components/section-with-description.tsx new file mode 100644 index 0000000..329128d --- /dev/null +++ b/apps/emails-and-messages/src/components/section-with-description.tsx @@ -0,0 +1,27 @@ +import { Box, Text } from "@saleor/macaw-ui/next"; + +interface SectionWithDescriptionProps { + title: string; + description?: React.ReactNode; + children?: React.ReactNode; +} +export const SectionWithDescription = (props: SectionWithDescriptionProps) => { + return ( + + + + {props.title} + + {props.description} + + {!!props.children && ( + + {props.children} + + )} + + ); +}; 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..f24a424 --- /dev/null +++ b/apps/emails-and-messages/src/components/text-link.tsx @@ -0,0 +1,35 @@ +import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge"; +import { TextProps, Text } from "@saleor/macaw-ui/next"; +import Link from "next/link"; + +interface TextLinkProps extends TextProps { + href: string; + openNewTab?: boolean; +} + +export const TextLink = ({ href, openNewTab, children, props }: TextLinkProps) => { + const { appBridge } = useAppBridge(); + + const onClick = (event: React.MouseEvent) => { + event.preventDefault(); + + appBridge?.dispatch( + actions.Redirect({ + to: href, + newContext: true, + }) + ); + }; + + return ( + + {openNewTab ? ( + + {children} + + ) : ( + {children} + )} + + ); +}; diff --git a/apps/emails-and-messages/src/components/ui-defaults.ts b/apps/emails-and-messages/src/components/ui-defaults.ts new file mode 100644 index 0000000..cb54a9c --- /dev/null +++ b/apps/emails-and-messages/src/components/ui-defaults.ts @@ -0,0 +1 @@ +export const defaultPadding = 6; diff --git a/apps/emails-and-messages/src/lib/get-base-url.ts b/apps/emails-and-messages/src/lib/get-base-url.ts index 1f31897..c90c3af 100644 --- a/apps/emails-and-messages/src/lib/get-base-url.ts +++ b/apps/emails-and-messages/src/lib/get-base-url.ts @@ -1,4 +1,12 @@ export const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => { - const { host, "x-forwarded-proto": protocol = "http" } = headers; + const { host, "x-forwarded-proto": xForwardedProto = "http" } = headers; + + const xForwardedProtos = Array.isArray(xForwardedProto) + ? xForwardedProto.join(",") + : xForwardedProto; + const protocols = xForwardedProtos.split(","); + // prefer https over other protocols + const protocol = protocols.find((el) => el === "https") || protocols[0]; + return `${protocol}://${host}`; }; diff --git a/apps/emails-and-messages/src/lib/set-backend-errors.ts b/apps/emails-and-messages/src/lib/set-backend-errors.ts new file mode 100644 index 0000000..0e9e6a1 --- /dev/null +++ b/apps/emails-and-messages/src/lib/set-backend-errors.ts @@ -0,0 +1,38 @@ +import { TRPCClientErrorLike } from "@trpc/client"; +import { AppRouter } from "../modules/trpc/trpc-app-router"; +import { FieldValues, UseFormSetError } from "react-hook-form"; + +type SetBackendErrorsProps = { + error: TRPCClientErrorLike; + setError?: UseFormSetError; + notifyError: (title: string, text?: string, apiMessage?: string) => void; +}; + +export function setBackendErrors({ + error, + setError, + notifyError, +}: SetBackendErrorsProps) { + let isFieldErrorSet = false; + const fieldErrors = error.data?.zodError?.fieldErrors || {}; + + for (const fieldName in fieldErrors) { + for (const message of fieldErrors[fieldName] || []) { + isFieldErrorSet = true; + if (!!setError) { + setError(fieldName as keyof UseFormSetError, { + 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 + ); +} diff --git a/apps/emails-and-messages/src/lib/theme-synchronizer.test.tsx b/apps/emails-and-messages/src/lib/theme-synchronizer.test.tsx deleted file mode 100644 index c2d8b70..0000000 --- a/apps/emails-and-messages/src/lib/theme-synchronizer.test.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; -import { AppBridgeState } from "@saleor/app-sdk/app-bridge"; -import { render, waitFor } from "@testing-library/react"; -import { ThemeSynchronizer } from "./theme-synchronizer"; - -const appBridgeState: AppBridgeState = { - ready: true, - token: "token", - domain: "some-domain.saleor.cloud", - theme: "dark", - path: "/", - locale: "en", - id: "app-id", - saleorApiUrl: "https://some-domain.saleor.cloud/graphql/", -}; - -const mockThemeChange = vi.fn(); - -vi.mock("@saleor/app-sdk/app-bridge", () => { - return { - useAppBridge() { - return { - appBridgeState: appBridgeState, - }; - }, - }; -}); - -vi.mock("@saleor/macaw-ui", () => { - return { - useTheme() { - return { - setTheme: mockThemeChange, - themeType: "light", - }; - }, - }; -}); - -describe("ThemeSynchronizer", () => { - it("Updates MacawUI theme when AppBridgeState theme changes", () => { - render(); - - return waitFor(() => { - expect(mockThemeChange).toHaveBeenCalledWith("dark"); - }); - }); -}); diff --git a/apps/emails-and-messages/src/modules/app-configuration/ui/messaging-providers-box.tsx b/apps/emails-and-messages/src/modules/app-configuration/ui/messaging-providers-box.tsx new file mode 100644 index 0000000..eada09c --- /dev/null +++ b/apps/emails-and-messages/src/modules/app-configuration/ui/messaging-providers-box.tsx @@ -0,0 +1,133 @@ +import { Box, Button, Text } from "@saleor/macaw-ui/next"; +import { BoxWithBorder } from "../../../components/box-with-border"; +import { BoxFooter } from "../../../components/box-footer"; +import { defaultPadding } from "../../../components/ui-defaults"; +import { useRouter } from "next/router"; +import { ChipText } from "../../../components/chip-text"; +import Image from "next/image"; +import sendgrid from "../../../public/sendgrid.png"; +import smtp from "../../../public/smtp.svg"; +import { smtpUrls } from "../../smtp/urls"; +import { sendgridUrls } from "../../sendgrid/urls"; +import { appUrls } from "../urls"; +import React from "react"; + +const NoExistingConfigurations = () => { + const { replace } = useRouter(); + + const redirectToProvidersSelection = () => { + replace("/configuration/choose-provider"); + }; + + return ( + + No providers configured yet + + + ); +}; + +type ProviderType = "sendgrid" | "smtp"; + +const providerLabels: Record = { + sendgrid: "Sendgrid", + smtp: "SMTP", +}; + +export type ConfigurationListItem = { + id: string; + name: string; + active: boolean; + provider: ProviderType; +}; + +interface MessagingProvidersSectionProps { + configurations: ConfigurationListItem[]; + isLoading: boolean; +} + +export const MessagingProvidersBox = ({ + configurations, + isLoading: loading, +}: MessagingProvidersSectionProps) => { + const { push } = useRouter(); + + if (loading) { + return ( + + Loading + + ); + } + + if (configurations.length === 0) { + return ; + } + + const redirectToProvidersSelection = () => { + push(appUrls.chooseProvider()); + }; + + const getEditLink = (configuration: ConfigurationListItem) => { + switch (configuration.provider) { + case "smtp": + return smtpUrls.configuration(configuration.id); + case "sendgrid": + return sendgridUrls.configuration(configuration.id); + } + }; + + const getProviderLogo = (configuration: ConfigurationListItem) => { + switch (configuration.provider) { + case "smtp": + return SMTP logo; + case "sendgrid": + return Sendgrid logo; + } + }; + + return ( + + + + Provider + + + Configuration name + + + Status + + + {configurations.map((configuration) => ( + + + {getProviderLogo(configuration)} + {providerLabels[configuration.provider]} + + + {configuration.name} + + + + + + ))} + + + + + + ); +}; diff --git a/apps/emails-and-messages/src/modules/app-configuration/ui/provider-selection-box.tsx b/apps/emails-and-messages/src/modules/app-configuration/ui/provider-selection-box.tsx index 3124a9f..8f533d5 100644 --- a/apps/emails-and-messages/src/modules/app-configuration/ui/provider-selection-box.tsx +++ b/apps/emails-and-messages/src/modules/app-configuration/ui/provider-selection-box.tsx @@ -11,24 +11,14 @@ interface ProviderSelectionBoxProps { export const ProviderSelectionBox = (props: ProviderSelectionBoxProps) => { return ( - - + + {props.providerName} - + {props.providerDescription} - + diff --git a/apps/emails-and-messages/src/modules/app-configuration/urls.ts b/apps/emails-and-messages/src/modules/app-configuration/urls.ts index e0c8c3a..e07de5f 100644 --- a/apps/emails-and-messages/src/modules/app-configuration/urls.ts +++ b/apps/emails-and-messages/src/modules/app-configuration/urls.ts @@ -1,4 +1,4 @@ -export const channelUrls = { - configuration: (id?: string) => - id ? `/configuration/channels/${id}` : "/configuration/channels", +export const appUrls = { + configuration: () => "/configuration/", + chooseProvider: () => "/configuration/choose-provider", }; diff --git a/apps/emails-and-messages/src/modules/channels/channel-configuration-schema.ts b/apps/emails-and-messages/src/modules/channels/channel-configuration-schema.ts new file mode 100644 index 0000000..d0a583c --- /dev/null +++ b/apps/emails-and-messages/src/modules/channels/channel-configuration-schema.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export const channelMode = ["exclude", "restrict"] as const; + +export const channelConfigurationSchema = z.object({ + override: z.boolean().default(false), + channels: z.array(z.string()).default([]), + mode: z.enum(channelMode).default("restrict"), +}); + +export type ChannelConfiguration = z.infer; + +export const updateChannelsInputSchema = channelConfigurationSchema.merge( + z.object({ + id: z.string().min(1), + }) +); + +export type UpdateChannelsInput = z.infer; diff --git a/apps/emails-and-messages/src/modules/channels/is-available-in-channel.test.ts b/apps/emails-and-messages/src/modules/channels/is-available-in-channel.test.ts new file mode 100644 index 0000000..5f9b1bd --- /dev/null +++ b/apps/emails-and-messages/src/modules/channels/is-available-in-channel.test.ts @@ -0,0 +1,95 @@ +import { vi, expect, describe, it } from "vitest"; +import { isAvailableInChannel } from "./is-available-in-channel"; + +describe("isAvailableInChannel", function () { + it("Passes when no overrides are set", () => { + expect( + isAvailableInChannel({ + channel: "default-channel", + channelConfiguration: { + channels: [], + mode: "restrict", + override: false, + }, + }) + ).toEqual(true); + }); + + describe("Restrict mode", () => { + it("Fails if no channel is specified", () => { + expect( + isAvailableInChannel({ + channel: "default-channel", + channelConfiguration: { + channels: [], + mode: "restrict", + override: true, + }, + }) + ).toEqual(false); + }); + it("Fails if tested channel is not on the list", () => { + expect( + isAvailableInChannel({ + channel: "default-channel", + channelConfiguration: { + channels: ["another-channel"], + mode: "restrict", + override: true, + }, + }) + ).toEqual(false); + }); + it("Passes if tested channel is on the list", () => { + expect( + isAvailableInChannel({ + channel: "default-channel", + channelConfiguration: { + channels: ["default-channel"], + mode: "restrict", + override: true, + }, + }) + ).toEqual(true); + }); + }); + + describe("Exclude mode", () => { + it("Passes if no channel is specified", () => { + expect( + isAvailableInChannel({ + channel: "default-channel", + channelConfiguration: { + channels: [], + mode: "exclude", + override: true, + }, + }) + ).toEqual(true); + }); + it("Passes if other channels are specified", () => { + expect( + isAvailableInChannel({ + channel: "default-channel", + channelConfiguration: { + channels: ["other-channel", "different-channel"], + mode: "exclude", + override: true, + }, + }) + ).toEqual(true); + }); + it("Fails if channel is on the list", () => { + expect( + isAvailableInChannel({ + channel: "default-channel", + channelConfiguration: { + channels: ["default-channel", "different-channel"], + mode: "exclude", + override: true, + }, + }) + ).toEqual(false); + }); + }); +}); diff --git a/apps/emails-and-messages/src/lib/is-available-in-channel.ts b/apps/emails-and-messages/src/modules/channels/is-available-in-channel.ts similarity index 51% rename from apps/emails-and-messages/src/lib/is-available-in-channel.ts rename to apps/emails-and-messages/src/modules/channels/is-available-in-channel.ts index f26a38b..4fd9dc9 100644 --- a/apps/emails-and-messages/src/lib/is-available-in-channel.ts +++ b/apps/emails-and-messages/src/modules/channels/is-available-in-channel.ts @@ -1,7 +1,8 @@ +import { ChannelConfiguration } from "./channel-configuration-schema"; + interface IsAvailableInChannelArgs { channel: string; - restrictedToChannels: string[]; - excludedChannels: string[]; + channelConfiguration: ChannelConfiguration; } /** @@ -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 channelConfiguration.channels.includes(channel); } - return true; + return !channelConfiguration.channels.includes(channel); }; diff --git a/apps/emails-and-messages/src/modules/channels/ui/assigned-channels-message.tsx b/apps/emails-and-messages/src/modules/channels/ui/assigned-channels-message.tsx new file mode 100644 index 0000000..8931c2c --- /dev/null +++ b/apps/emails-and-messages/src/modules/channels/ui/assigned-channels-message.tsx @@ -0,0 +1,52 @@ +import { ChannelConfiguration } from "../channel-configuration-schema"; +import { Text } from "@saleor/macaw-ui/next"; + +interface OverrideMessageArgs { + availableChannels: string[]; + channelConfiguration: ChannelConfiguration; +} + +export const AssignedChannelsMessage = ({ + 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(", ")}. + + ); + } + + if (channels.length === 0) { + return ( + + No channels assigned. The configuration will not be used! + + ); + } + return ( + + Configuration will be used with channels:{" "} + {channels.join(", ")}. + + ); +}; diff --git a/apps/emails-and-messages/src/modules/channels/ui/universal-channels-section.tsx b/apps/emails-and-messages/src/modules/channels/ui/universal-channels-section.tsx new file mode 100644 index 0000000..80b4a3d --- /dev/null +++ b/apps/emails-and-messages/src/modules/channels/ui/universal-channels-section.tsx @@ -0,0 +1,118 @@ +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 { trpcClient } from "../../trpc/trpc-client"; +import { Controller, useForm } from "react-hook-form"; +import { BoxFooter } from "../../../components/box-footer"; +import { SectionWithDescription } from "../../../components/section-with-description"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Multiselect } from "../../../components/react-hook-form-macaw/Multiselect"; +import { AssignedChannelsMessage } from "./assigned-channels-message"; +import { + ChannelConfiguration, + UpdateChannelsInput, + updateChannelsInputSchema, +} from "../channel-configuration-schema"; + +interface UniversalChannelsSectionProps { + configurationId: string; + channelConfiguration: ChannelConfiguration; + onSubmit: (formData: UpdateChannelsInput) => void; +} + +export const UniversalChannelsSection = ({ + configurationId, + channelConfiguration, + onSubmit, +}: UniversalChannelsSectionProps) => { + const { handleSubmit, control, register } = useForm({ + defaultValues: { + id: configurationId, + ...channelConfiguration, + }, + resolver: zodResolver(updateChannelsInputSchema), + }); + + const { data: channels } = trpcClient.channels.fetch.useQuery(); + + 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 + + + } + > +
{ + onSubmit(data); + })} + > + + + + Current behaviour + channel.slug) || []} + channelConfiguration={channelConfiguration} + /> + Settings + + + ( + + + + Include + + + + Exclude + + + )} + /> + ({ + label: channel.name, + value: channel.slug, + })) || [] + } + /> + + + + + + +
+
+ ); +}; diff --git a/apps/emails-and-messages/src/modules/event-handlers/send-event-messages.ts b/apps/emails-and-messages/src/modules/event-handlers/send-event-messages.ts index f6487c9..cf3b2b5 100644 --- a/apps/emails-and-messages/src/modules/event-handlers/send-event-messages.ts +++ b/apps/emails-and-messages/src/modules/event-handlers/send-event-messages.ts @@ -1,8 +1,8 @@ import { AuthData } from "@saleor/app-sdk/APL"; import { Client } from "urql"; import { createLogger } from "@saleor/apps-shared"; -import { MjmlConfigurationService } from "../mjml/configuration/get-mjml-configuration.service"; -import { sendMjml } from "../mjml/send-mjml"; +import { SmtpConfigurationService } from "../smtp/configuration/get-smtp-configuration.service"; +import { sendSmtp } from "../smtp/send-smtp"; import { SendgridConfigurationService } from "../sendgrid/configuration/get-sendgrid-configuration.service"; import { sendSendgrid } from "../sendgrid/send-sendgrid"; import { MessageEventTypes } from "./message-event-types"; @@ -30,27 +30,27 @@ export const sendEventMessages = async ({ logger.debug("Function called"); - const mjmlConfigurationService = new MjmlConfigurationService({ + const smtpConfigurationService = new SmtpConfigurationService({ apiClient: client, saleorApiUrl: authData.saleorApiUrl, }); - const availableMjmlConfigurations = await mjmlConfigurationService.getConfigurations({ + const availableSmtpConfigurations = await smtpConfigurationService.getConfigurations({ active: true, availableInChannel: channel, }); - for (const mjmlConfiguration of availableMjmlConfigurations) { - const mjmlStatus = await sendMjml({ + for (const smtpConfiguration of availableSmtpConfigurations) { + const smtpStatus = await sendSmtp({ event, payload, recipientEmail, - mjmlConfiguration, + smtpConfiguration, }); - if (mjmlStatus?.errors.length) { - logger.error("MJML errors"); - logger.error(mjmlStatus?.errors); + if (smtpStatus?.errors.length) { + logger.error("SMTP errors"); + logger.error(smtpStatus?.errors); } } diff --git a/apps/emails-and-messages/src/modules/mjml/configuration/mjml-config-container.ts b/apps/emails-and-messages/src/modules/mjml/configuration/mjml-config-container.ts deleted file mode 100644 index c6d4606..0000000 --- a/apps/emails-and-messages/src/modules/mjml/configuration/mjml-config-container.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { messageEventTypes } from "../../event-handlers/message-event-types"; -import { MjmlConfig as MjmlConfigurationRoot, MjmlConfiguration } from "./mjml-config"; -import { defaultMjmlTemplates, defaultMjmlSubjectTemplates } from "../default-templates"; -import { generateRandomId } from "../../../lib/generate-random-id"; -import { isAvailableInChannel } from "../../../lib/is-available-in-channel"; - -export const getDefaultEventsConfiguration = (): MjmlConfiguration["events"] => - messageEventTypes.map((eventType) => ({ - active: true, - eventType: eventType, - template: defaultMjmlTemplates[eventType], - subject: defaultMjmlSubjectTemplates[eventType], - })); - -export const getDefaultEmptyConfiguration = (): MjmlConfiguration => { - const defaultConfig: MjmlConfiguration = { - id: "", - active: true, - configurationName: "", - senderName: "", - senderEmail: "", - smtpHost: "", - smtpPort: "", - smtpUser: "", - smtpPassword: "", - encryption: "NONE", - events: getDefaultEventsConfiguration(), - channels: { - excludedFrom: [], - restrictedTo: [], - }, - }; - - return defaultConfig; -}; - -interface GetConfigurationArgs { - id: string; -} - -const getConfiguration = - (mjmlConfigRoot: MjmlConfigurationRoot | null | undefined) => - ({ id }: GetConfigurationArgs) => { - if (!mjmlConfigRoot || !mjmlConfigRoot.configurations) { - return; - } - - return mjmlConfigRoot.configurations.find((c) => c.id === id); - }; - -export interface FilterConfigurationsArgs { - ids?: string[]; - availableInChannel?: string; - active?: boolean; -} - -const getConfigurations = - (mjmlConfigRoot: MjmlConfigurationRoot | null | undefined) => - (filter: FilterConfigurationsArgs | undefined): MjmlConfiguration[] => { - if (!mjmlConfigRoot || !mjmlConfigRoot.configurations) { - return []; - } - - let filtered = mjmlConfigRoot.configurations; - - if (!filter) { - return filtered; - } - - if (filter.ids?.length) { - filtered = filtered.filter((c) => filter?.ids?.includes(c.id)); - } - - if (filter.active !== undefined) { - filtered = filtered.filter((c) => c.active === filter.active); - } - - if (filter.availableInChannel?.length) { - filtered = filtered.filter((c) => - isAvailableInChannel({ - channel: filter.availableInChannel!, - restrictedToChannels: c.channels.restrictedTo, - excludedChannels: c.channels.excludedFrom, - }) - ); - } - - return filtered; - }; - -const createConfiguration = - (mjmlConfigRoot: MjmlConfigurationRoot | null | undefined) => - (mjmlConfiguration: Omit) => { - const mjmlConfigNormalized = structuredClone(mjmlConfigRoot) ?? { configurations: [] }; - - // for creating a new configurations, the ID has to be generated - const newConfiguration = { - ...mjmlConfiguration, - id: generateRandomId(), - events: getDefaultEventsConfiguration(), - }; - - mjmlConfigNormalized.configurations.push(newConfiguration); - return mjmlConfigNormalized; - }; - -const updateConfiguration = - (mjmlConfig: MjmlConfigurationRoot | null | undefined) => - (mjmlConfiguration: MjmlConfiguration) => { - const mjmlConfigNormalized = structuredClone(mjmlConfig) ?? { configurations: [] }; - - const configurationIndex = mjmlConfigNormalized.configurations.findIndex( - (configuration) => configuration.id === mjmlConfiguration.id - ); - - mjmlConfigNormalized.configurations[configurationIndex] = mjmlConfiguration; - return mjmlConfigNormalized; - }; - -interface DeleteConfigurationArgs { - id: string; -} - -const deleteConfiguration = - (mjmlConfig: MjmlConfigurationRoot | null | undefined) => - ({ id }: DeleteConfigurationArgs) => { - const mjmlConfigNormalized = structuredClone(mjmlConfig) ?? { configurations: [] }; - - mjmlConfigNormalized.configurations = mjmlConfigNormalized.configurations.filter( - (configuration) => configuration.id !== id - ); - - return mjmlConfigNormalized; - }; - -export const MjmlConfigContainer = { - createConfiguration, - getConfiguration, - updateConfiguration, - deleteConfiguration, - getConfigurations, -}; diff --git a/apps/emails-and-messages/src/modules/mjml/configuration/mjml-config-input-schema.ts b/apps/emails-and-messages/src/modules/mjml/configuration/mjml-config-input-schema.ts deleted file mode 100644 index 19ad5f5..0000000 --- a/apps/emails-and-messages/src/modules/mjml/configuration/mjml-config-input-schema.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { z } from "zod"; -import { messageEventTypes } from "../../event-handlers/message-event-types"; -import { smtpEncryptionTypes } from "./mjml-config"; - -export const mjmlConfigurationEventObjectSchema = z.object({ - active: z.boolean(), - eventType: z.enum(messageEventTypes), - template: z.string().min(1), - subject: z.string().min(1), -}); - -export const mjmlConfigurationBaseObjectSchema = z.object({ - active: z.boolean(), - configurationName: z.string().min(1), - senderName: z.string().min(1), - senderEmail: z.string().email().min(5), - smtpHost: z.string().min(1), - smtpPort: z.string(), - smtpUser: z.string(), - smtpPassword: z.string(), - encryption: z.enum(smtpEncryptionTypes), - channels: z.object({ - excludedFrom: z.array(z.string()), - restrictedTo: z.array(z.string()), - }), -}); - -export const mjmlCreateConfigurationSchema = mjmlConfigurationBaseObjectSchema; -export const mjmlUpdateOrCreateConfigurationSchema = mjmlConfigurationBaseObjectSchema.merge( - z.object({ - id: z.string().optional(), - }) -); -export const mjmlGetConfigurationInputSchema = z.object({ - id: z.string(), -}); -export const mjmlDeleteConfigurationInputSchema = z.object({ - id: z.string(), -}); -export const mjmlGetConfigurationsInputSchema = z - .object({ - ids: z.array(z.string()).optional(), - active: z.boolean().optional(), - }) - .optional(); - -export const mjmlUpdateEventConfigurationInputSchema = z - .object({ - configurationId: z.string(), - }) - .merge(mjmlConfigurationEventObjectSchema); - -export const mjmlGetEventConfigurationInputSchema = z.object({ - configurationId: z.string(), - eventType: z.enum(messageEventTypes), -}); diff --git a/apps/emails-and-messages/src/modules/mjml/configuration/mjml-config.ts b/apps/emails-and-messages/src/modules/mjml/configuration/mjml-config.ts deleted file mode 100644 index b4b59bc..0000000 --- a/apps/emails-and-messages/src/modules/mjml/configuration/mjml-config.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { MessageEventTypes } from "../../event-handlers/message-event-types"; - -export interface MjmlEventConfiguration { - active: boolean; - eventType: MessageEventTypes; - template: string; - subject: string; -} - -export const smtpEncryptionTypes = ["NONE", "TLS", "SSL"] as const; - -export type SmtpEncryptionType = (typeof smtpEncryptionTypes)[number]; - -export interface MjmlConfiguration { - id: string; - active: boolean; - configurationName: string; - channels: { - excludedFrom: string[]; - restrictedTo: string[]; - }; - senderName: string; - senderEmail: string; - smtpHost: string; - smtpPort: string; - smtpUser: string; - smtpPassword: string; - encryption: SmtpEncryptionType; - events: MjmlEventConfiguration[]; -} - -export type MjmlConfig = { - configurations: MjmlConfiguration[]; -}; diff --git a/apps/emails-and-messages/src/modules/mjml/urls.ts b/apps/emails-and-messages/src/modules/mjml/urls.ts deleted file mode 100644 index f0d3182..0000000 --- a/apps/emails-and-messages/src/modules/mjml/urls.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MessageEventTypes } from "../event-handlers/message-event-types"; - -export const mjmlUrls = { - configuration: (id?: string) => (!id ? "/configuration/mjml" : `/configuration/mjml/${id}`), - eventConfiguration: (id: string, event: MessageEventTypes) => - `/configuration/mjml/${id}/event/${event}`, -}; 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 d056506..65c341c 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 { createLogger } from "@saleor/apps-shared"; -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..100b4de 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 @@ -1,33 +1,27 @@ import { generateRandomId } from "../../../lib/generate-random-id"; -import { isAvailableInChannel } from "../../../lib/is-available-in-channel"; +import { isAvailableInChannel } from "../../channels/is-available-in-channel"; 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; }; @@ -77,8 +71,7 @@ const getConfigurations = filtered = filtered.filter((c) => isAvailableInChannel({ channel: filter.availableInChannel!, - restrictedToChannels: c.channels.restrictedTo, - excludedChannels: c.channels.excludedFrom, + channelConfiguration: c.channels, }) ); } 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 b6dd299..abc8054 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,41 +1,26 @@ import { z } from "zod"; import { messageEventTypes } from "../../event-handlers/message-event-types"; +import { + sendgridConfigurationEventSchema, + sendgridConfigurationSchema, +} from "./sendgrid-config-schema"; +import { channelConfigurationSchema } from "../../channels/channel-configuration-schema"; -export const sendgridConfigurationEventObjectSchema = z.object({ - active: z.boolean(), - eventType: z.enum(messageEventTypes), - template: z.string(), +export const sendgridCreateConfigurationInputSchema = sendgridConfigurationSchema.pick({ + name: true, + apiKey: true, }); -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 type SendgridCreateConfigurationInput = z.infer< + typeof sendgridCreateConfigurationInputSchema +>; + +export const sendgridConfigurationIdInputSchema = sendgridConfigurationSchema.pick({ + id: true, }); -export const sendgridCreateConfigurationSchema = sendgridConfigurationBaseObjectSchema.omit({ - senderEmail: true, - senderName: true, -}); -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 type SendgridGetConfigurationIdInput = z.infer; + export const sendgridGetConfigurationsInputSchema = z .object({ ids: z.array(z.string()).optional(), @@ -43,13 +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 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 = channelConfigurationSchema.merge( + sendgridConfigurationSchema.pick({ + id: true, + }) +); + +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..88c4643 --- /dev/null +++ b/apps/emails-and-messages/src/modules/sendgrid/configuration/sendgrid-config-schema.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; +import { messageEventTypes } from "../../event-handlers/message-event-types"; +import { channelConfigurationSchema } from "../../channels/channel-configuration-schema"; + +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 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(), + senderEmail: z.string().email().optional(), + senderName: z.string().optional(), + channels: channelConfigurationSchema, + 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 52cbc43..f8a7332 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,17 +1,22 @@ import { createLogger } from "@saleor/apps-shared"; import { - sendgridCreateConfigurationSchema, - sendgridDeleteConfigurationInputSchema, - sendgridGetConfigurationInputSchema, + sendgridConfigurationIdInputSchema, + sendgridCreateConfigurationInputSchema, sendgridGetConfigurationsInputSchema, sendgridGetEventConfigurationInputSchema, + sendgridUpdateApiConnectionSchema, + sendgridUpdateBasicInformationSchema, 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"; +import { fetchSenders } from "../sendgrid-api"; +import { updateChannelsInputSchema } from "../../channels/channel-configuration-schema"; /* * Allow access only for the dashboard users and attaches the @@ -38,7 +43,7 @@ export const sendgridConfigurationRouter = router({ }), getConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(sendgridGetConfigurationInputSchema) + .input(sendgridConfigurationIdInputSchema) .query(async ({ ctx, input }) => { const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl }); @@ -56,16 +61,21 @@ export const sendgridConfigurationRouter = router({ }), createConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(sendgridCreateConfigurationSchema) + .input(sendgridCreateConfigurationInputSchema) .mutation(async ({ ctx, input }) => { const logger = createLogger({ 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 = createLogger({ saleorApiUrl: ctx.saleorApiUrl }); @@ -81,37 +91,6 @@ 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 = createLogger({ 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; - } - }), getEventConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .input(sendgridGetEventConfigurationInputSchema) @@ -167,6 +146,130 @@ export const sendgridConfigurationRouter = router({ eventType: input.eventType, template: input.template, }; + await ctx.configurationService.updateConfiguration(configuration); + return configuration; + }), + updateBasicInformation: protectedWithConfigurationService + .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) + .input(sendgridUpdateBasicInformationSchema) + .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; + }), + 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", + }); + } + + // Pull fresh sender data from the API + const senders = await fetchSenders({ apiKey: configuration.apiKey })(); + + const chosenSender = senders.find((s) => s.value === input.sender); + + if (!chosenSender) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Sender does not exist", + }); + } + + await ctx.configurationService.updateConfiguration({ + ...configuration, + ...input, + senderEmail: chosenSender.from_email, + senderName: chosenSender.label, + }); + return configuration; + }), + updateChannels: protectedWithConfigurationService + .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) + .input(updateChannelsInputSchema) + .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 6d1bc0e..24647a5 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 { createLogger } from "@saleor/apps-shared"; -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 new file mode 100644 index 0000000..44bd657 --- /dev/null +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/api-connection-section.tsx @@ -0,0 +1,79 @@ +import { SendgridConfiguration } from "../configuration/sendgrid-config-schema"; +import { BoxWithBorder } from "../../../components/box-with-border"; +import { Box, Button, 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 { + SendgridUpdateApiConnection, + sendgridUpdateApiConnectionSchema, +} from "../configuration/sendgrid-config-input-schema"; +import { useForm } from "react-hook-form"; +import { BoxFooter } from "../../../components/box-footer"; +import { SectionWithDescription } from "../../../components/section-with-description"; +import { Input } from "../../../components/react-hook-form-macaw/Input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +interface ApiConnectionSectionProps { + configuration: SendgridConfiguration; +} + +export const ApiConnectionSection = ({ configuration }: ApiConnectionSectionProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + + const { handleSubmit, control, setError, register } = useForm({ + defaultValues: { + id: configuration.id, + apiKey: configuration.apiKey, + sandboxMode: configuration.sandboxMode, + }, + resolver: zodResolver(sendgridUpdateApiConnectionSchema), + }); + + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.sendgridConfiguration.updateApiConnection.useMutation({ + onSuccess: async (data, variables) => { + notifySuccess("Configuration saved"); + trpcContext.sendgridConfiguration.invalidate(); + }, + onError(error) { + setBackendErrors({ + error, + setError, + notifyError, + }); + }, + }); + + return ( + + +
{ + mutate({ + ...data, + }); + })} + > + + + + + + + + +
+
+
+ ); +}; diff --git a/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-basic-information-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-basic-information-section.tsx new file mode 100644 index 0000000..9fb00b1 --- /dev/null +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-basic-information-section.tsx @@ -0,0 +1,88 @@ +import { SendgridConfiguration } from "../configuration/sendgrid-config-schema"; +import { BoxWithBorder } from "../../../components/box-with-border"; +import { Box, Button, 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 { + 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"; +import { Input } from "../../../components/react-hook-form-macaw/Input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +interface SendgridBasicInformationSectionProps { + configuration: SendgridConfiguration; +} + +export const SendgridBasicInformationSection = ({ + configuration, +}: SendgridBasicInformationSectionProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + const { handleSubmit, control, setError, register } = useForm({ + defaultValues: { + id: configuration.id, + name: configuration.name, + active: configuration.active, + }, + resolver: zodResolver(sendgridUpdateBasicInformationSchema), + }); + + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.sendgridConfiguration.updateBasicInformation.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration saved"); + trpcContext.sendgridConfiguration.invalidate(); + }, + onError(error) { + setBackendErrors({ + error, + setError, + notifyError, + }); + }, + }); + + 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. + + } + > + +
{ + mutate({ + ...data, + }); + })} + > + + + + + + + +
+
+
+ ); +}; diff --git a/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-channels-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-channels-section.tsx new file mode 100644 index 0000000..d5d01c1 --- /dev/null +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-channels-section.tsx @@ -0,0 +1,33 @@ +import { SendgridConfiguration } from "../configuration/sendgrid-config-schema"; +import { useDashboardNotification } from "@saleor/apps-shared"; +import { trpcClient } from "../../trpc/trpc-client"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; +import { UniversalChannelsSection } from "../../channels/ui/universal-channels-section"; +import { UpdateChannelsInput } from "../../channels/channel-configuration-schema"; + +interface SendgridChannelsSectionProps { + configuration: SendgridConfiguration; +} + +export const SendgridChannelsSection = ({ configuration }: SendgridChannelsSectionProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.sendgridConfiguration.updateChannels.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration saved"); + trpcContext.sendgridConfiguration.invalidate(); + }, + onError(error) { + setBackendErrors({ error, notifyError }); + }, + }); + + return ( + + ); +}; diff --git a/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-dangrous-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-dangrous-section.tsx new file mode 100644 index 0000000..f785e53 --- /dev/null +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-dangrous-section.tsx @@ -0,0 +1,73 @@ +import { Box, Button, Text } from "@saleor/macaw-ui/next"; +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-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"; +import { appUrls } from "../../app-configuration/urls"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +interface SendgridDangerousSectionProps { + configuration: SendgridConfiguration; +} + +export const SendgridDangerousSection = ({ configuration }: SendgridDangerousSectionProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + const { replace } = useRouter(); + const { handleSubmit, setError } = useForm({ + defaultValues: { + id: configuration.id, + }, + }); + + const { mutate } = trpcClient.sendgridConfiguration.deleteConfiguration.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration removed"); + replace(appUrls.configuration()); + }, + onError(error) { + setBackendErrors({ error, setError, notifyError }); + }, + }); + + return ( + +
{ + 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/sendgrid-events-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-events-section.tsx new file mode 100644 index 0000000..3989983 --- /dev/null +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-events-section.tsx @@ -0,0 +1,119 @@ +import { + SendgridConfiguration, + SendgridEventConfiguration, +} from "../configuration/sendgrid-config-schema"; +import { BoxWithBorder } from "../../../components/box-with-border"; +import { Box, Button, 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, + sendgridUpdateEventSchema, +} from "../configuration/sendgrid-config-input-schema"; +import { 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"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Combobox } from "../../../components/react-hook-form-macaw/Combobox"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +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, + }, + resolver: zodResolver(sendgridUpdateEventSchema), + }); + + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.sendgridConfiguration.updateEvent.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration saved"); + trpcContext.sendgridConfiguration.invalidate(); + }, + onError(error) { + setBackendErrors({ error, setError, notifyError }); + }, + }); + + return ( +
{ + mutate({ + ...data, + }); + })} + > + + + {event.eventType} + {templatesChoices?.length ? ( + ({ + label: sender.label, + value: sender.value, + }))} + /> + ) : ( + + )} + + + + + + + +
+ ); +}; + +interface SendgridEventsSectionProps { + configuration: SendgridConfiguration; +} + +export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionProps) => { + 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/sendgrid-sender-section.tsx b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-sender-section.tsx new file mode 100644 index 0000000..af54897 --- /dev/null +++ b/apps/emails-and-messages/src/modules/sendgrid/ui/sendgrid-sender-section.tsx @@ -0,0 +1,84 @@ +import { SendgridConfiguration } from "../configuration/sendgrid-config-schema"; +import { BoxWithBorder } from "../../../components/box-with-border"; +import { Box, Button } from "@saleor/macaw-ui/next"; +import { defaultPadding } from "../../../components/ui-defaults"; +import { useDashboardNotification } from "@saleor/apps-shared"; +import { trpcClient } from "../../trpc/trpc-client"; +import { + SendgridUpdateSender, + sendgridUpdateSenderSchema, +} from "../configuration/sendgrid-config-input-schema"; +import { 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"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Combobox } from "../../../components/react-hook-form-macaw/Combobox"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +interface SendgridSenderSectionProps { + configuration: SendgridConfiguration; +} + +export const SendgridSenderSection = ({ configuration }: SendgridSenderSectionProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + + const { data: sendersChoices } = useQuery({ + queryKey: ["sendgridSenders"], + queryFn: fetchSenders({ apiKey: configuration.apiKey }), + enabled: !!configuration.apiKey?.length, + }); + + const { handleSubmit, control, setError } = useForm({ + defaultValues: { + id: configuration.id, + sender: configuration.sender, + }, + resolver: zodResolver(sendgridUpdateSenderSchema), + }); + + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.sendgridConfiguration.updateSender.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration saved"); + trpcContext.sendgridConfiguration.invalidate(); + }, + onError(error) { + setBackendErrors({ error, setError, notifyError }); + }, + }); + + return ( + + +
{ + mutate({ + ...data, + }); + })} + > + + {sendersChoices?.length ? ( + ({ + label: sender.label, + value: sender.value, + }))} + /> + ) : ( + + )} + + + + +
+
+
+ ); +}; diff --git a/apps/emails-and-messages/src/modules/sendgrid/urls.ts b/apps/emails-and-messages/src/modules/sendgrid/urls.ts index 81308f4..a80b90b 100644 --- a/apps/emails-and-messages/src/modules/sendgrid/urls.ts +++ b/apps/emails-and-messages/src/modules/sendgrid/urls.ts @@ -1,8 +1,8 @@ import { MessageEventTypes } from "../event-handlers/message-event-types"; export const sendgridUrls = { - configuration: (id?: string) => - !id ? "/configuration/sendgrid" : `/configuration/sendgrid/${id}`, + newConfiguration: () => `/configuration/sendgrid/new`, + configuration: (id: string) => `/configuration/sendgrid/${id}`, eventConfiguration: (id: string, event: MessageEventTypes) => `/configuration/sendgrid/${id}/event/${event}`, }; diff --git a/apps/emails-and-messages/src/modules/mjml/compile-handlebars-template.ts b/apps/emails-and-messages/src/modules/smtp/compile-handlebars-template.ts similarity index 100% rename from apps/emails-and-messages/src/modules/mjml/compile-handlebars-template.ts rename to apps/emails-and-messages/src/modules/smtp/compile-handlebars-template.ts diff --git a/apps/emails-and-messages/src/modules/mjml/compile-mjml.ts b/apps/emails-and-messages/src/modules/smtp/compile-mjml.ts similarity index 100% rename from apps/emails-and-messages/src/modules/mjml/compile-mjml.ts rename to apps/emails-and-messages/src/modules/smtp/compile-mjml.ts diff --git a/apps/emails-and-messages/src/modules/mjml/configuration/get-mjml-configuration.service.ts b/apps/emails-and-messages/src/modules/smtp/configuration/get-smtp-configuration.service.ts similarity index 71% rename from apps/emails-and-messages/src/modules/mjml/configuration/get-mjml-configuration.service.ts rename to apps/emails-and-messages/src/modules/smtp/configuration/get-smtp-configuration.service.ts index 92835b2..4b3b574 100644 --- a/apps/emails-and-messages/src/modules/mjml/configuration/get-mjml-configuration.service.ts +++ b/apps/emails-and-messages/src/modules/smtp/configuration/get-smtp-configuration.service.ts @@ -1,20 +1,20 @@ -import { MjmlConfigurator, PrivateMetadataMjmlConfigurator } from "./mjml-configurator"; +import { SmtpConfigurator, PrivateMetadataSmtpConfigurator } from "./smtp-configurator"; import { Client } from "urql"; import { createLogger } from "@saleor/apps-shared"; -import { MjmlConfig, MjmlConfiguration } from "./mjml-config"; -import { FilterConfigurationsArgs, MjmlConfigContainer } from "./mjml-config-container"; +import { FilterConfigurationsArgs, SmtpConfigContainer } from "./smtp-config-container"; import { createSettingsManager } from "../../../lib/metadata-manager"; +import { SmtpConfig, SmtpConfiguration } from "./smtp-config-schema"; const logger = createLogger({ - service: "MjmlConfigurationService", + service: "SmtpConfigurationService", }); -export class MjmlConfigurationService { - private configurationData?: MjmlConfig; - private metadataConfigurator: MjmlConfigurator; +export class SmtpConfigurationService { + private configurationData?: SmtpConfig; + private metadataConfigurator: SmtpConfigurator; - constructor(args: { apiClient: Client; saleorApiUrl: string; initialData?: MjmlConfig }) { - this.metadataConfigurator = new PrivateMetadataMjmlConfigurator( + constructor(args: { apiClient: Client; saleorApiUrl: string; initialData?: SmtpConfig }) { + this.metadataConfigurator = new PrivateMetadataSmtpConfigurator( createSettingsManager(args.apiClient), args.saleorApiUrl ); @@ -61,7 +61,7 @@ export class MjmlConfigurationService { } // Saves configuration to Saleor API and cache it - async setConfigurationRoot(config: MjmlConfig) { + async setConfigurationRoot(config: SmtpConfig) { logger.debug("Set configuration root"); this.configurationData = config; @@ -70,17 +70,17 @@ export class MjmlConfigurationService { async getConfiguration({ id }: { id: string }) { logger.debug("Get configuration"); - return MjmlConfigContainer.getConfiguration(await this.getConfigurationRoot())({ id }); + return SmtpConfigContainer.getConfiguration(await this.getConfigurationRoot())({ id }); } async getConfigurations(filter?: FilterConfigurationsArgs) { logger.debug("Get configuration"); - return MjmlConfigContainer.getConfigurations(await this.getConfigurationRoot())(filter); + return SmtpConfigContainer.getConfigurations(await this.getConfigurationRoot())(filter); } - async createConfiguration(config: Omit) { + async createConfiguration(config: Omit) { logger.debug("Create configuration"); - const updatedConfigurationRoot = MjmlConfigContainer.createConfiguration( + const updatedConfigurationRoot = SmtpConfigContainer.createConfiguration( await this.getConfigurationRoot() )(config); @@ -91,9 +91,9 @@ export class MjmlConfigurationService { ]; } - async updateConfiguration(config: MjmlConfiguration) { + async updateConfiguration(config: SmtpConfiguration) { logger.debug("Update configuration"); - const updatedConfigurationRoot = MjmlConfigContainer.updateConfiguration( + const updatedConfigurationRoot = SmtpConfigContainer.updateConfiguration( await this.getConfigurationRoot() )(config); @@ -102,7 +102,7 @@ export class MjmlConfigurationService { async deleteConfiguration({ id }: { id: string }) { logger.debug("Delete configuration"); - const updatedConfigurationRoot = MjmlConfigContainer.deleteConfiguration( + const updatedConfigurationRoot = SmtpConfigContainer.deleteConfiguration( await this.getConfigurationRoot() )({ id }); diff --git a/apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-container.ts b/apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-container.ts new file mode 100644 index 0000000..e5259b3 --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-container.ts @@ -0,0 +1,142 @@ +import { messageEventTypes } from "../../event-handlers/message-event-types"; + +import { defaultMjmlTemplates, defaultMjmlSubjectTemplates } from "../default-templates"; +import { generateRandomId } from "../../../lib/generate-random-id"; +import { + SmtpConfiguration, + smtpConfigurationSchema, + smtpConfigurationEventSchema, + SmtpConfig, +} from "./smtp-config-schema"; +import { isAvailableInChannel } from "../../channels/is-available-in-channel"; + +export const getDefaultEventsConfiguration = (): SmtpConfiguration["events"] => + messageEventTypes.map((eventType) => + smtpConfigurationEventSchema.parse({ + eventType: eventType, + template: defaultMjmlTemplates[eventType], + subject: defaultMjmlSubjectTemplates[eventType], + }) + ); + +export const getDefaultEmptyConfiguration = (): SmtpConfiguration => { + const defaultConfig: SmtpConfiguration = smtpConfigurationSchema.parse({ + id: "id", + name: "name", + active: true, + smtpHost: "host", + smtpPort: "1024", + channels: { + excludedFrom: [], + restrictedTo: [], + }, + events: getDefaultEventsConfiguration(), + }); + + return defaultConfig; +}; + +interface GetConfigurationArgs { + id: string; +} + +const getConfiguration = + (smtpConfigRoot: SmtpConfig | null | undefined) => + ({ id }: GetConfigurationArgs) => { + if (!smtpConfigRoot || !smtpConfigRoot.configurations) { + return; + } + + return smtpConfigRoot.configurations.find((c) => c.id === id); + }; + +export interface FilterConfigurationsArgs { + ids?: string[]; + availableInChannel?: string; + active?: boolean; +} + +const getConfigurations = + (smtpConfigRoot: SmtpConfig | null | undefined) => + (filter: FilterConfigurationsArgs | undefined): SmtpConfiguration[] => { + if (!smtpConfigRoot || !smtpConfigRoot.configurations) { + return []; + } + + let filtered = smtpConfigRoot.configurations; + + if (!filter) { + return filtered; + } + + if (filter.ids?.length) { + filtered = filtered.filter((c) => filter?.ids?.includes(c.id)); + } + + if (filter.active !== undefined) { + filtered = filtered.filter((c) => c.active === filter.active); + } + + if (filter.availableInChannel?.length) { + filtered = filtered.filter((c) => + isAvailableInChannel({ + channel: filter.availableInChannel!, + channelConfiguration: c.channels, + }) + ); + } + + return filtered; + }; + +const createConfiguration = + (smtpConfigRoot: SmtpConfig | null | undefined) => + (smtpConfiguration: Omit) => { + const configNormalized = structuredClone(smtpConfigRoot) ?? { configurations: [] }; + + // for creating a new configurations, the ID has to be generated + const newConfiguration = { + ...smtpConfiguration, + id: generateRandomId(), + events: getDefaultEventsConfiguration(), + }; + + configNormalized.configurations.push(newConfiguration); + return configNormalized; + }; + +const updateConfiguration = + (smtpConfig: SmtpConfig | null | undefined) => (smtpConfiguration: SmtpConfiguration) => { + const configNormalized = structuredClone(smtpConfig) ?? { configurations: [] }; + + const configurationIndex = configNormalized.configurations.findIndex( + (configuration) => configuration.id === smtpConfiguration.id + ); + + configNormalized.configurations[configurationIndex] = smtpConfiguration; + return configNormalized; + }; + +interface DeleteConfigurationArgs { + id: string; +} + +const deleteConfiguration = + (smtpConfig: SmtpConfig | null | undefined) => + ({ id }: DeleteConfigurationArgs) => { + const configNormalized = structuredClone(smtpConfig) ?? { configurations: [] }; + + configNormalized.configurations = configNormalized.configurations.filter( + (configuration) => configuration.id !== id + ); + + return configNormalized; + }; + +export const SmtpConfigContainer = { + createConfiguration, + getConfiguration, + updateConfiguration, + deleteConfiguration, + getConfigurations, +}; diff --git a/apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-input-schema.ts b/apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-input-schema.ts new file mode 100644 index 0000000..88d99e0 --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-input-schema.ts @@ -0,0 +1,89 @@ +import { z } from "zod"; +import { messageEventTypes } from "../../event-handlers/message-event-types"; +import { smtpConfigurationEventSchema, smtpConfigurationSchema } from "./smtp-config-schema"; +import { channelConfigurationSchema } from "../../channels/channel-configuration-schema"; + +export const smtpCreateConfigurationInputSchema = smtpConfigurationSchema.pick({ + name: true, + smtpHost: true, + smtpPort: true, + smtpUser: true, + smtpPassword: true, + encryption: true, +}); + +export type SmtpCreateConfigurationInput = z.infer; + +export const smtpConfigurationIdInputSchema = smtpConfigurationSchema.pick({ + id: true, +}); + +export type SmtpGetConfigurationIdInput = z.infer; + +export const smtpGetConfigurationsInputSchema = z + .object({ + ids: z.array(z.string()).optional(), + active: z.boolean().optional(), + }) + .optional(); + +export type SmtpGetConfigurationsInput = z.infer; + +export const smtpUpdateBasicInformationSchema = smtpConfigurationSchema.pick({ + id: true, + name: true, + active: true, +}); + +export type SmtpUpdateBasicInformation = z.infer; + +export const smtpUpdateSmtpSchema = smtpConfigurationSchema.pick({ + id: true, + smtpHost: true, + smtpPort: true, + smtpPassword: true, + smtpUser: true, + encryption: true, +}); + +export type SmtpUpdateSmtp = z.infer; + +export const smtpUpdateSenderSchema = smtpConfigurationSchema.pick({ + id: true, + senderEmail: true, + senderName: true, +}); + +export type SmtpUpdateSender = z.infer; + +export const smtpUpdateChannelsSchema = channelConfigurationSchema.merge( + smtpConfigurationSchema.pick({ + id: true, + }) +); + +export type SmtpUpdateChannels = z.infer; + +export const smtpUpdateEventSchema = smtpConfigurationEventSchema.merge( + smtpConfigurationSchema.pick({ + id: true, + }) +); + +export type SmtpUpdateEvent = z.infer; + +export const smtpGetEventConfigurationInputSchema = smtpConfigurationIdInputSchema.merge( + z.object({ + eventType: z.enum(messageEventTypes), + }) +); + +export type SmtpGetEventConfigurationInput = z.infer; + +export const smtpUpdateEventConfigurationInputSchema = smtpConfigurationIdInputSchema.merge( + smtpConfigurationEventSchema +); + +export type SmtpUpdateEventConfigurationInput = z.infer< + typeof smtpUpdateEventConfigurationInputSchema +>; diff --git a/apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-schema.ts b/apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-schema.ts new file mode 100644 index 0000000..2cec6cc --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/configuration/smtp-config-schema.ts @@ -0,0 +1,37 @@ +import { z } from "zod"; +import { messageEventTypes } from "../../event-handlers/message-event-types"; +import { channelConfigurationSchema } from "../../channels/channel-configuration-schema"; + +export const smtpEncryptionTypes = ["NONE", "TLS", "SSL"] as const; + +export const smtpConfigurationEventSchema = z.object({ + active: z.boolean().default(false), + eventType: z.enum(messageEventTypes), + template: z.string(), + subject: z.string(), +}); + +export type SmtpEventConfiguration = z.infer; + +export const smtpConfigurationSchema = z.object({ + id: z.string().min(1), + active: z.boolean().default(true), + name: z.string().min(1), + senderName: z.string().optional(), + senderEmail: z.string().email().min(5).optional(), + smtpHost: z.string().min(1), + smtpPort: z.string().min(1), + smtpUser: z.string().optional(), + smtpPassword: z.string().optional(), + encryption: z.enum(smtpEncryptionTypes).default("NONE"), + channels: channelConfigurationSchema, + events: z.array(smtpConfigurationEventSchema), +}); + +export type SmtpConfiguration = z.infer; + +export const smtpConfigSchema = z.object({ + configurations: z.array(smtpConfigurationSchema), +}); + +export type SmtpConfig = z.infer; diff --git a/apps/emails-and-messages/src/modules/mjml/configuration/mjml-configuration.router.ts b/apps/emails-and-messages/src/modules/smtp/configuration/smtp-configuration.router.ts similarity index 53% rename from apps/emails-and-messages/src/modules/mjml/configuration/mjml-configuration.router.ts rename to apps/emails-and-messages/src/modules/smtp/configuration/smtp-configuration.router.ts index c26ef88..71e84b5 100644 --- a/apps/emails-and-messages/src/modules/mjml/configuration/mjml-configuration.router.ts +++ b/apps/emails-and-messages/src/modules/smtp/configuration/smtp-configuration.router.ts @@ -1,20 +1,24 @@ import { createLogger } from "@saleor/apps-shared"; -import { - mjmlCreateConfigurationSchema, - mjmlDeleteConfigurationInputSchema, - mjmlGetConfigurationInputSchema, - mjmlGetConfigurationsInputSchema, - mjmlGetEventConfigurationInputSchema, - mjmlUpdateEventConfigurationInputSchema, - mjmlUpdateOrCreateConfigurationSchema, -} from "./mjml-config-input-schema"; -import { MjmlConfigurationService } from "./get-mjml-configuration.service"; +import { SmtpConfigurationService } from "./get-smtp-configuration.service"; import { router } from "../../trpc/trpc-server"; import { protectedClientProcedure } from "../../trpc/protected-client-procedure"; import { z } from "zod"; import { compileMjml } from "../compile-mjml"; import Handlebars from "handlebars"; import { TRPCError } from "@trpc/server"; +import { getDefaultEmptyConfiguration } from "./smtp-config-container"; +import { + smtpConfigurationIdInputSchema, + smtpCreateConfigurationInputSchema, + smtpGetConfigurationsInputSchema, + smtpGetEventConfigurationInputSchema, + smtpUpdateBasicInformationSchema, + smtpUpdateEventConfigurationInputSchema, + smtpUpdateEventSchema, + smtpUpdateSenderSchema, + smtpUpdateSmtpSchema, +} from "./smtp-config-input-schema"; +import { updateChannelsInputSchema } from "../../channels/channel-configuration-schema"; /* * Allow access only for the dashboard users and attaches the @@ -24,7 +28,7 @@ const protectedWithConfigurationService = protectedClientProcedure.use(({ next, next({ ctx: { ...ctx, - configurationService: new MjmlConfigurationService({ + configurationService: new SmtpConfigurationService({ apiClient: ctx.apiClient, saleorApiUrl: ctx.saleorApiUrl, }), @@ -32,47 +36,53 @@ const protectedWithConfigurationService = protectedClientProcedure.use(({ next, }) ); -export const mjmlConfigurationRouter = router({ +export const smtpConfigurationRouter = router({ fetch: protectedWithConfigurationService.query(async ({ ctx }) => { const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl }); - logger.debug("mjmlConfigurationRouter.fetch called"); + logger.debug("smtpConfigurationRouter.fetch called"); return ctx.configurationService.getConfigurationRoot(); }), getConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(mjmlGetConfigurationInputSchema) + .input(smtpConfigurationIdInputSchema) .query(async ({ ctx, input }) => { const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl }); - logger.debug(input, "mjmlConfigurationRouter.get called"); + logger.debug(input, "smtpConfigurationRouter.get called"); return ctx.configurationService.getConfiguration(input); }), getConfigurations: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(mjmlGetConfigurationsInputSchema) + .input(smtpGetConfigurationsInputSchema) .query(async ({ ctx, input }) => { const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl }); - logger.debug(input, "mjmlConfigurationRouter.getConfigurations called"); + logger.debug(input, "smtpConfigurationRouter.getConfigurations called"); return ctx.configurationService.getConfigurations(input); }), createConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(mjmlCreateConfigurationSchema) + .input(smtpCreateConfigurationInputSchema) .mutation(async ({ ctx, input }) => { const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl }); - logger.debug(input, "mjmlConfigurationRouter.create called"); - return await ctx.configurationService.createConfiguration(input); + logger.debug(input, "smtpConfigurationRouter.create called"); + const newConfiguration = { + ...getDefaultEmptyConfiguration(), + ...input, + }; + + console.log(newConfiguration, "this is newConfiguration"); + return await ctx.configurationService.createConfiguration(newConfiguration); }), deleteConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(mjmlDeleteConfigurationInputSchema) + .input(smtpConfigurationIdInputSchema) .mutation(async ({ ctx, input }) => { const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl }); - logger.debug(input, "mjmlConfigurationRouter.delete called"); + logger.debug(input, "smtpConfigurationRouter.delete called"); const existingConfiguration = await ctx.configurationService.getConfiguration(input); if (!existingConfiguration) { @@ -84,47 +94,16 @@ export const mjmlConfigurationRouter = router({ await ctx.configurationService.deleteConfiguration(input); return null; }), - updateOrCreateConfiguration: protectedWithConfigurationService - .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(mjmlUpdateOrCreateConfigurationSchema) - .mutation(async ({ ctx, input }) => { - const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl }); - - logger.debug(input, "mjmlConfigurationRouter.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; - } - }), getEventConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(mjmlGetEventConfigurationInputSchema) + .input(smtpGetEventConfigurationInputSchema) .query(async ({ ctx, input }) => { const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl }); - logger.debug(input, "mjmlConfigurationRouter.getEventConfiguration or create called"); + logger.debug(input, "smtpConfigurationRouter.getEventConfiguration or create called"); const configuration = await ctx.configurationService.getConfiguration({ - id: input.configurationId, + id: input.id, }); if (!configuration) { @@ -146,14 +125,14 @@ export const mjmlConfigurationRouter = router({ }), updateEventConfiguration: protectedWithConfigurationService .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) - .input(mjmlUpdateEventConfigurationInputSchema) + .input(smtpUpdateEventConfigurationInputSchema) .mutation(async ({ ctx, input }) => { const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl }); logger.debug(input, "mjmlConfigurationRouter.updateEventConfiguration or create called"); const configuration = await ctx.configurationService.getConfiguration({ - id: input.configurationId, + id: input.id, }); if (!configuration) { @@ -218,4 +197,114 @@ export const mjmlConfigurationRouter = router({ renderedEmailBody: renderedEmail, }; }), + + updateBasicInformation: protectedWithConfigurationService + .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) + .input(smtpUpdateBasicInformationSchema) + .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; + }), + + updateSmtp: protectedWithConfigurationService + .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) + .input(smtpUpdateSmtpSchema) + .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(smtpUpdateSenderSchema) + .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(updateChannelsInputSchema) + .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(smtpUpdateEventSchema) + .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/mjml/configuration/mjml-configurator.ts b/apps/emails-and-messages/src/modules/smtp/configuration/smtp-configurator.ts similarity index 61% rename from apps/emails-and-messages/src/modules/mjml/configuration/mjml-configurator.ts rename to apps/emails-and-messages/src/modules/smtp/configuration/smtp-configurator.ts index 9e6f9a8..8ba0be2 100644 --- a/apps/emails-and-messages/src/modules/mjml/configuration/mjml-configurator.ts +++ b/apps/emails-and-messages/src/modules/smtp/configuration/smtp-configurator.ts @@ -1,17 +1,17 @@ -import { MjmlConfig } from "./mjml-config"; import { SettingsManager } from "@saleor/app-sdk/settings-manager"; +import { SmtpConfig } from "./smtp-config-schema"; -export interface MjmlConfigurator { - setConfig(config: MjmlConfig): Promise; - getConfig(): Promise; +export interface SmtpConfigurator { + setConfig(config: SmtpConfig): Promise; + getConfig(): Promise; } -export class PrivateMetadataMjmlConfigurator implements MjmlConfigurator { - private metadataKey = "mjml-config"; +export class PrivateMetadataSmtpConfigurator implements SmtpConfigurator { + private metadataKey = "smtp-config"; constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {} - getConfig(): Promise { + getConfig(): Promise { return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => { if (!data) { return data; @@ -25,7 +25,7 @@ export class PrivateMetadataMjmlConfigurator implements MjmlConfigurator { }); } - setConfig(config: MjmlConfig): Promise { + setConfig(config: SmtpConfig): Promise { return this.metadataManager.set({ key: this.metadataKey, value: JSON.stringify(config), diff --git a/apps/emails-and-messages/src/modules/mjml/default-templates.ts b/apps/emails-and-messages/src/modules/smtp/default-templates.ts similarity index 100% rename from apps/emails-and-messages/src/modules/mjml/default-templates.ts rename to apps/emails-and-messages/src/modules/smtp/default-templates.ts diff --git a/apps/emails-and-messages/src/modules/mjml/html-to-plaintext.ts b/apps/emails-and-messages/src/modules/smtp/html-to-plaintext.ts similarity index 100% rename from apps/emails-and-messages/src/modules/mjml/html-to-plaintext.ts rename to apps/emails-and-messages/src/modules/smtp/html-to-plaintext.ts diff --git a/apps/emails-and-messages/src/modules/mjml/send-email-with-smtp.ts b/apps/emails-and-messages/src/modules/smtp/send-email-with-smtp.ts similarity index 100% rename from apps/emails-and-messages/src/modules/mjml/send-email-with-smtp.ts rename to apps/emails-and-messages/src/modules/smtp/send-email-with-smtp.ts diff --git a/apps/emails-and-messages/src/modules/mjml/send-mjml.ts b/apps/emails-and-messages/src/modules/smtp/send-smtp.ts similarity index 86% rename from apps/emails-and-messages/src/modules/mjml/send-mjml.ts rename to apps/emails-and-messages/src/modules/smtp/send-smtp.ts index bda34f5..4293b10 100644 --- a/apps/emails-and-messages/src/modules/mjml/send-mjml.ts +++ b/apps/emails-and-messages/src/modules/smtp/send-smtp.ts @@ -4,10 +4,10 @@ import { compileHandlebarsTemplate } from "./compile-handlebars-template"; import { sendEmailWithSmtp, SendMailArgs } from "./send-email-with-smtp"; import { MessageEventTypes } from "../event-handlers/message-event-types"; import { htmlToPlaintext } from "./html-to-plaintext"; -import { MjmlConfiguration } from "./configuration/mjml-config"; +import { SmtpConfiguration } from "./configuration/smtp-config-schema"; -interface SendMjmlArgs { - mjmlConfiguration: MjmlConfiguration; +interface SendSmtpArgs { + smtpConfiguration: SmtpConfiguration; recipientEmail: string; event: MessageEventTypes; payload: any; @@ -20,18 +20,18 @@ export interface EmailServiceResponse { }[]; } -export const sendMjml = async ({ +export const sendSmtp = async ({ payload, recipientEmail, event, - mjmlConfiguration, -}: SendMjmlArgs) => { + smtpConfiguration, +}: SendSmtpArgs) => { const logger = createLogger({ - fn: "sendMjml", + fn: "sendSmtp", event, }); - const eventSettings = mjmlConfiguration.events.find((e) => e.eventType === event); + const eventSettings = smtpConfiguration.events.find((e) => e.eventType === event); if (!eventSettings) { logger.debug("No active settings for this event, skipping"); @@ -133,20 +133,20 @@ export const sendMjml = async ({ mailData: { text: emailBodyPlaintext, html: emailBodyHtml, - from: `${mjmlConfiguration.senderName} <${mjmlConfiguration.senderEmail}>`, + from: `${smtpConfiguration.senderName} <${smtpConfiguration.senderEmail}>`, to: recipientEmail, subject: emailSubject, }, smtpSettings: { - host: mjmlConfiguration.smtpHost, - port: parseInt(mjmlConfiguration.smtpPort, 10), + host: smtpConfiguration.smtpHost, + port: parseInt(smtpConfiguration.smtpPort, 10), }, }; - if (mjmlConfiguration.smtpUser) { + if (smtpConfiguration.smtpUser) { sendEmailSettings.smtpSettings.auth = { - user: mjmlConfiguration.smtpUser, - pass: mjmlConfiguration.smtpPassword, + user: smtpConfiguration.smtpUser, + pass: smtpConfiguration.smtpPassword, }; } diff --git a/apps/emails-and-messages/src/modules/smtp/ui/code-edtor.tsx b/apps/emails-and-messages/src/modules/smtp/ui/code-edtor.tsx new file mode 100644 index 0000000..57620c8 --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/ui/code-edtor.tsx @@ -0,0 +1,42 @@ +import React, { useCallback, useRef } from "react"; + +import Editor from "@monaco-editor/react"; +import { useTheme } from "@saleor/macaw-ui/next"; + +type Props = { + onChange(value: string): void; + initialTemplate: string; + value: string; + language: string; +}; + +export const CodeEditor = ({ initialTemplate, onChange, value, language }: Props) => { + const { theme } = useTheme(); + const editorRef = useRef(null); + + // @ts-ignore + function handleEditorDidMount(editor, monaco) { + editorRef.current = editor; + } + + const handleOnChange = useCallback( + (value?: string) => { + onChange(value ?? ""); + }, + [value] + ); + + return ( + <> + + + ); +}; diff --git a/apps/emails-and-messages/src/modules/smtp/ui/event-form.tsx b/apps/emails-and-messages/src/modules/smtp/ui/event-form.tsx new file mode 100644 index 0000000..2140707 --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/ui/event-form.tsx @@ -0,0 +1,163 @@ +import { Box, Button, Text } from "@saleor/macaw-ui/next"; +import { SmtpConfiguration } from "../configuration/smtp-config-schema"; +import { MessageEventTypes } from "../../event-handlers/message-event-types"; +import { Input } from "../../../components/react-hook-form-macaw/Input"; +import { useDashboardNotification } from "@saleor/apps-shared"; +import { trpcClient } from "../../trpc/trpc-client"; +import { Controller, useForm } from "react-hook-form"; +import { + SmtpUpdateEventConfigurationInput, + smtpUpdateEventConfigurationInputSchema, +} from "../configuration/smtp-config-input-schema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { CodeEditor } from "./code-edtor"; +import { useDebounce } from "usehooks-ts"; +import { useState, useEffect } from "react"; +import { examplePayloads } from "../../event-handlers/default-payloads"; +import { MjmlPreview } from "./mjml-preview"; +import { defaultPadding } from "../../../components/ui-defaults"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; +const PREVIEW_DEBOUNCE_DELAY = 500; + +interface EventFormProps { + configuration: SmtpConfiguration; + eventType: MessageEventTypes; +} + +export const EventForm = ({ configuration, eventType }: EventFormProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + + const eventConfiguration = configuration?.events.find( + (eventConfiguration) => eventConfiguration.eventType === eventType + )!; // Event conf is not optional, so we can use ! here + + const { handleSubmit, control, getValues, setError } = useForm( + { + defaultValues: { + id: configuration.id, + ...eventConfiguration, + }, + resolver: zodResolver(smtpUpdateEventConfigurationInputSchema), + } + ); + + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.smtpConfiguration.updateSender.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration saved"); + trpcContext.smtpConfiguration.invalidate(); + }, + onError(error) { + setBackendErrors({ + error, + setError, + notifyError, + }); + }, + }); + + const { mutate: fetchTemplatePreview, isLoading: isFetchingTemplatePreview } = + trpcClient.smtpConfiguration.renderTemplate.useMutation({ + onSuccess: (data) => { + if (data.renderedEmailBody) { + setLastValidRenderedTemplate(data.renderedEmailBody); + } + if (data.renderedSubject) { + setLastValidRenderedSubject(data.renderedSubject); + } + }, + }); + + const [lastValidRenderedTemplate, setLastValidRenderedTemplate] = useState(""); + + const [lastValidRenderedSubject, setLastValidRenderedSubject] = useState(""); + + const [payload, setPayload] = useState( + JSON.stringify(examplePayloads[eventType], undefined, 2) + ); + + const { template, subject } = getValues(); + const debouncedMutationVariables = useDebounce( + { template, subject, payload }, + PREVIEW_DEBOUNCE_DELAY + ); + + const { + template: debouncedTemplate, + subject: debouncedSubject, + payload: debouncedPayload, + } = debouncedMutationVariables; + + useEffect(() => { + fetchTemplatePreview({ + template: debouncedTemplate, + subject: debouncedSubject, + payload: debouncedPayload, + }); + }, [debouncedPayload, debouncedSubject, debouncedTemplate, fetchTemplatePreview]); + + return ( +
{ + mutate({ + ...data, + }); + })} + > + + + Edit template + + + + + + + + + { + return ( + + ); + }} + /> + + + + + + + Subject: {lastValidRenderedSubject} + + + + + +
+ ); +}; diff --git a/apps/emails-and-messages/src/modules/smtp/ui/mjml-preview.tsx b/apps/emails-and-messages/src/modules/smtp/ui/mjml-preview.tsx new file mode 100644 index 0000000..f9656b4 --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/ui/mjml-preview.tsx @@ -0,0 +1,17 @@ +import React from "react"; + +type Props = { + value?: string; +}; + +export const MjmlPreview = ({ value }: Props) => { + return ( + <> + {value?.length ? ( +
+ ) : ( +

No template preview

+ )} + + ); +}; diff --git a/apps/emails-and-messages/src/modules/smtp/ui/smtp-basic-information-section.tsx b/apps/emails-and-messages/src/modules/smtp/ui/smtp-basic-information-section.tsx new file mode 100644 index 0000000..83eacaa --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/ui/smtp-basic-information-section.tsx @@ -0,0 +1,88 @@ +import { SmtpConfiguration } from "../configuration/smtp-config-schema"; +import { BoxWithBorder } from "../../../components/box-with-border"; +import { Box, Button, 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 { z } from "zod"; +import { useForm } from "react-hook-form"; +import { BoxFooter } from "../../../components/box-footer"; +import { SectionWithDescription } from "../../../components/section-with-description"; +import { + SmtpUpdateBasicInformation, + smtpUpdateBasicInformationSchema, +} from "../configuration/smtp-config-input-schema"; +import { Input } from "../../../components/react-hook-form-macaw/Input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +interface SmtpBasicInformationSectionProps { + configuration: SmtpConfiguration; +} + +export const SmtpBasicInformationSection = ({ + configuration, +}: SmtpBasicInformationSectionProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + const { handleSubmit, control, setError, register } = useForm({ + defaultValues: { + id: configuration.id, + name: configuration.name, + active: configuration.active, + }, + resolver: zodResolver(smtpUpdateBasicInformationSchema), + }); + + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.smtpConfiguration.updateBasicInformation.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration saved"); + trpcContext.smtpConfiguration.invalidate(); + }, + onError(error) { + setBackendErrors({ + error, + setError, + notifyError, + }); + }, + }); + + 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. + + } + > + +
{ + mutate({ + ...data, + }); + })} + > + + + + + + + +
+
+
+ ); +}; diff --git a/apps/emails-and-messages/src/modules/smtp/ui/smtp-channels-section.tsx b/apps/emails-and-messages/src/modules/smtp/ui/smtp-channels-section.tsx new file mode 100644 index 0000000..c5a226f --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/ui/smtp-channels-section.tsx @@ -0,0 +1,36 @@ +import { SmtpConfiguration } from "../configuration/smtp-config-schema"; +import { useDashboardNotification } from "@saleor/apps-shared"; +import { trpcClient } from "../../trpc/trpc-client"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; +import { UniversalChannelsSection } from "../../channels/ui/universal-channels-section"; +import { UpdateChannelsInput } from "../../channels/channel-configuration-schema"; + +interface SmtpChannelsSectionProps { + configuration: SmtpConfiguration; +} + +export const SmtpChannelsSection = ({ configuration }: SmtpChannelsSectionProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.smtpConfiguration.updateChannels.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration saved"); + trpcContext.smtpConfiguration.invalidate(); + }, + onError(error) { + setBackendErrors({ + error, + notifyError, + }); + }, + }); + + return ( + + ); +}; diff --git a/apps/emails-and-messages/src/modules/smtp/ui/smtp-dangrous-section.tsx b/apps/emails-and-messages/src/modules/smtp/ui/smtp-dangrous-section.tsx new file mode 100644 index 0000000..eb0363b --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/ui/smtp-dangrous-section.tsx @@ -0,0 +1,76 @@ +import { Box, Button, Text } from "@saleor/macaw-ui/next"; +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 { trpcClient } from "../../trpc/trpc-client"; +import { useDashboardNotification } from "@saleor/apps-shared"; +import { useForm } from "react-hook-form"; +import { useRouter } from "next/router"; +import { SmtpConfiguration } from "../configuration/smtp-config-schema"; +import { SmtpGetConfigurationIdInput } from "../configuration/smtp-config-input-schema"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +interface SmtpDangerousSectionProps { + configuration: SmtpConfiguration; +} + +export const SmtpDangerousSection = ({ configuration }: SmtpDangerousSectionProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + const { replace } = useRouter(); + const { handleSubmit, setError } = useForm({ + defaultValues: { + id: configuration.id, + }, + }); + + const { mutate } = trpcClient.smtpConfiguration.deleteConfiguration.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration removed"); + replace("/configuration"); + }, + onError(error) { + setBackendErrors({ + error, + setError, + notifyError, + }); + }, + }); + + return ( + +
{ + 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/smtp/ui/smtp-events-section.tsx b/apps/emails-and-messages/src/modules/smtp/ui/smtp-events-section.tsx new file mode 100644 index 0000000..b641b8d --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/ui/smtp-events-section.tsx @@ -0,0 +1,106 @@ +import { SmtpConfiguration, SmtpEventConfiguration } from "../configuration/smtp-config-schema"; +import { BoxWithBorder } from "../../../components/box-with-border"; +import { Box, Button, 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 { useForm } from "react-hook-form"; +import { BoxFooter } from "../../../components/box-footer"; +import { SectionWithDescription } from "../../../components/section-with-description"; +import { SmtpUpdateEvent, smtpUpdateEventSchema } from "../configuration/smtp-config-input-schema"; +import { useRouter } from "next/router"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { smtpUrls } from "../urls"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +interface EventBoxProps { + configuration: SmtpConfiguration; + event: SmtpEventConfiguration; +} + +const EventBox = ({ event, configuration }: EventBoxProps) => { + const router = useRouter(); + const { notifySuccess, notifyError } = useDashboardNotification(); + + const { handleSubmit, control, setError, register } = useForm({ + defaultValues: { + id: configuration.id, + ...event, + }, + resolver: zodResolver(smtpUpdateEventSchema), + }); + + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.smtpConfiguration.updateEvent.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration saved"); + trpcContext.smtpConfiguration.invalidate(); + }, + onError(error) { + setBackendErrors({ + error, + setError, + notifyError, + }); + }, + }); + + return ( +
{ + mutate({ + ...data, + }); + })} + > + + + {event.eventType} + + + + + + + + +
+ ); +}; + +interface SmtpEventsSectionProps { + configuration: SmtpConfiguration; +} + +export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) => { + 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/smtp/ui/smtp-section.tsx b/apps/emails-and-messages/src/modules/smtp/ui/smtp-section.tsx new file mode 100644 index 0000000..eb800c9 --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/ui/smtp-section.tsx @@ -0,0 +1,109 @@ +import { SmtpConfiguration } from "../configuration/smtp-config-schema"; +import { BoxWithBorder } from "../../../components/box-with-border"; +import { Box, Button, 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 { useForm } from "react-hook-form"; +import { BoxFooter } from "../../../components/box-footer"; +import { SectionWithDescription } from "../../../components/section-with-description"; +import { SmtpUpdateSmtp, smtpUpdateSmtpSchema } from "../configuration/smtp-config-input-schema"; +import { Input } from "../../../components/react-hook-form-macaw/Input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +interface SmtpSectionProps { + configuration: SmtpConfiguration; +} + +export const SmtpSection = ({ configuration }: SmtpSectionProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + const { handleSubmit, control, setError, register } = useForm({ + defaultValues: { + id: configuration.id, + smtpHost: configuration.smtpHost, + smtpPort: configuration.smtpPort, + smtpUser: configuration.smtpUser, + smtpPassword: configuration.smtpPassword, + encryption: configuration.encryption, + }, + resolver: zodResolver(smtpUpdateSmtpSchema), + }); + + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.smtpConfiguration.updateSmtp.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration saved"); + trpcContext.smtpConfiguration.invalidate(); + }, + onError(error) { + setBackendErrors({ + error, + setError, + notifyError, + }); + }, + }); + + return ( + Provide SMTP server configuration} + > + +
{ + mutate({ + ...data, + }); + })} + > + + + + + + + + + + + + + + + +
+
+
+ ); +}; diff --git a/apps/emails-and-messages/src/modules/smtp/ui/smtp-sender-section.tsx b/apps/emails-and-messages/src/modules/smtp/ui/smtp-sender-section.tsx new file mode 100644 index 0000000..6a7e9e9 --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/ui/smtp-sender-section.tsx @@ -0,0 +1,79 @@ +import { SmtpConfiguration } from "../configuration/smtp-config-schema"; +import { BoxWithBorder } from "../../../components/box-with-border"; +import { Box, Button } from "@saleor/macaw-ui/next"; +import { defaultPadding } from "../../../components/ui-defaults"; +import { useDashboardNotification } from "@saleor/apps-shared"; +import { trpcClient } from "../../trpc/trpc-client"; +import { useForm } from "react-hook-form"; +import { BoxFooter } from "../../../components/box-footer"; +import { SectionWithDescription } from "../../../components/section-with-description"; +import { + SmtpUpdateSender, + smtpUpdateSenderSchema, +} from "../configuration/smtp-config-input-schema"; +import { Input } from "../../../components/react-hook-form-macaw/Input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +interface SmtpSenderSectionProps { + configuration: SmtpConfiguration; +} + +export const SmtpSenderSection = ({ configuration }: SmtpSenderSectionProps) => { + const { notifySuccess, notifyError } = useDashboardNotification(); + const { handleSubmit, control, setError, register } = useForm({ + defaultValues: { + id: configuration.id, + senderName: configuration.senderName, + senderEmail: configuration.senderEmail, + }, + resolver: zodResolver(smtpUpdateSenderSchema), + }); + + const trpcContext = trpcClient.useContext(); + const { mutate } = trpcClient.smtpConfiguration.updateSender.useMutation({ + onSuccess: async () => { + notifySuccess("Configuration saved"); + trpcContext.smtpConfiguration.invalidate(); + }, + onError(error) { + setBackendErrors({ + error, + setError, + notifyError, + }); + }, + }); + + return ( + + +
{ + mutate({ + ...data, + }); + })} + > + + + + + + + +
+
+
+ ); +}; diff --git a/apps/emails-and-messages/src/modules/smtp/urls.ts b/apps/emails-and-messages/src/modules/smtp/urls.ts new file mode 100644 index 0000000..9157e70 --- /dev/null +++ b/apps/emails-and-messages/src/modules/smtp/urls.ts @@ -0,0 +1,8 @@ +import { MessageEventTypes } from "../event-handlers/message-event-types"; + +export const smtpUrls = { + newConfiguration: () => `/configuration/smtp/new`, + configuration: (id: string) => `/configuration/smtp/${id}`, + eventConfiguration: (id: string, event: MessageEventTypes) => + `/configuration/smtp/${id}/event/${event}`, +}; diff --git a/apps/emails-and-messages/src/modules/trpc/trpc-app-router.ts b/apps/emails-and-messages/src/modules/trpc/trpc-app-router.ts index 67d020f..856350b 100644 --- a/apps/emails-and-messages/src/modules/trpc/trpc-app-router.ts +++ b/apps/emails-and-messages/src/modules/trpc/trpc-app-router.ts @@ -1,11 +1,11 @@ import { channelsRouter } from "../channels/channels.router"; import { router } from "./trpc-server"; -import { mjmlConfigurationRouter } from "../mjml/configuration/mjml-configuration.router"; +import { smtpConfigurationRouter } from "../smtp/configuration/smtp-configuration.router"; import { sendgridConfigurationRouter } from "../sendgrid/configuration/sendgrid-configuration.router"; export const appRouter = router({ channels: channelsRouter, - mjmlConfiguration: mjmlConfigurationRouter, + smtpConfiguration: smtpConfigurationRouter, sendgridConfiguration: sendgridConfigurationRouter, }); diff --git a/apps/emails-and-messages/src/pages/_app.tsx b/apps/emails-and-messages/src/pages/_app.tsx index 125083e..ea838bd 100644 --- a/apps/emails-and-messages/src/pages/_app.tsx +++ b/apps/emails-and-messages/src/pages/_app.tsx @@ -1,65 +1,15 @@ +import "@saleor/macaw-ui/next/style"; import "../styles/globals.css"; -import { Theme } from "@material-ui/core/styles"; import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge"; import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next"; -import { - dark, - light, - SaleorThemeColors, - ThemeProvider as MacawUIThemeProvider, -} from "@saleor/macaw-ui"; -import React, { PropsWithChildren, useEffect } from "react"; +import React from "react"; import { AppProps } from "next/app"; +import { ThemeProvider } from "@saleor/macaw-ui/next"; import { ThemeSynchronizer } from "../lib/theme-synchronizer"; import { NoSSRWrapper } from "../lib/no-ssr-wrapper"; import { trpcClient } from "../modules/trpc/trpc-client"; -import { createGenerateClassName, StylesProvider } from "@material-ui/core"; - -type PalettesOverride = Record<"light" | "dark", SaleorThemeColors>; - -/** - * Temporary override of colors, to match new dashboard palette. - * Long term this will be replaced with Macaw UI 2.x with up to date design tokens - */ -const palettes: PalettesOverride = { - light: { - ...light, - background: { - default: "#fff", - paper: "#fff", - }, - }, - dark: { - ...dark, - background: { - default: "hsla(211, 42%, 14%, 1)", - paper: "hsla(211, 42%, 14%, 1)", - }, - }, -}; - -const themeOverrides: Partial = { - overrides: { - MuiTableCell: { - body: { - paddingBottom: 8, - paddingTop: 8, - }, - root: { - height: 56, - paddingBottom: 4, - paddingTop: 4, - }, - }, - }, -}; - -const generateClassName = createGenerateClassName({ - productionPrefix: "c", - disableGlobal: true, -}); /** * Ensure instance is a singleton. @@ -67,34 +17,15 @@ const generateClassName = createGenerateClassName({ */ export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined; -/** - * That's a hack required by Macaw-UI incompatibility with React@18 - */ -const ThemeProvider = MacawUIThemeProvider as React.FC< - PropsWithChildren<{ overrides?: Partial; ssr: boolean; palettes: PalettesOverride }> ->; - function NextApp({ Component, pageProps }: AppProps) { - /** - * Configure JSS (used by MacawUI) for SSR. If Macaw is not used, can be removed. - */ - useEffect(() => { - const jssStyles = document.querySelector("#jss-server-side"); - if (jssStyles) { - jssStyles?.parentElement?.removeChild(jssStyles); - } - }, []); - return ( - - - - - - - + + + + + ); diff --git a/apps/emails-and-messages/src/pages/api/register.ts b/apps/emails-and-messages/src/pages/api/register.ts index 93d8cd9..744fb1d 100644 --- a/apps/emails-and-messages/src/pages/api/register.ts +++ b/apps/emails-and-messages/src/pages/api/register.ts @@ -28,7 +28,6 @@ export default createAppRegisterHandler({ onAuthAplSaved: async (request, ctx) => { // Subscribe to Notify using the mutation since it does not use subscriptions and can't be subscribed via manifest logger.debug("onAuthAplSaved executing"); - const baseUrl = getBaseUrl(request.headers); const client = createClient(ctx.authData.saleorApiUrl, async () => Promise.resolve({ token: ctx.authData.token }) diff --git a/apps/emails-and-messages/src/pages/configuration/choose-provider.tsx b/apps/emails-and-messages/src/pages/configuration/choose-provider.tsx new file mode 100644 index 0000000..729ccd6 --- /dev/null +++ b/apps/emails-and-messages/src/pages/configuration/choose-provider.tsx @@ -0,0 +1,46 @@ +import { Box, Text } from "@saleor/macaw-ui/next"; +import { NextPage } from "next"; +import { Breadcrumbs } from "../../components/breadcrumbs"; +import { SectionWithDescription } from "../../components/section-with-description"; +import { ProviderSelectionBox } from "../../modules/app-configuration/ui/provider-selection-box"; +import { useRouter } from "next/router"; +import { sendgridUrls } from "../../modules/sendgrid/urls"; +import { smtpUrls } from "../../modules/smtp/urls"; +import { appUrls } from "../../modules/app-configuration/urls"; +import { BasicLayout } from "../../components/basic-layout"; + +const ChooseProviderPage: NextPage = () => { + const { push } = useRouter(); + + return ( + + + + Select and configure providers to connect Saleor with selected services. + + + + + push(sendgridUrls.newConfiguration())} + /> + + push(smtpUrls.newConfiguration())} + /> + + + + ); +}; + +export default ChooseProviderPage; diff --git a/apps/emails-and-messages/src/pages/configuration/index.tsx b/apps/emails-and-messages/src/pages/configuration/index.tsx new file mode 100644 index 0000000..20beefb --- /dev/null +++ b/apps/emails-and-messages/src/pages/configuration/index.tsx @@ -0,0 +1,61 @@ +import { Box, Text } from "@saleor/macaw-ui/next"; +import { NextPage } from "next"; +import { Breadcrumbs } from "../../components/breadcrumbs"; +import { SectionWithDescription } from "../../components/section-with-description"; +import { + ConfigurationListItem, + MessagingProvidersBox, +} from "../../modules/app-configuration/ui/messaging-providers-box"; +import { trpcClient } from "../../modules/trpc/trpc-client"; +import { appUrls } from "../../modules/app-configuration/urls"; +import { BasicLayout } from "../../components/basic-layout"; + +const ConfigurationPage: NextPage = () => { + const { data: dataSendgrid, isLoading: isLoadingSendgrid } = + trpcClient.sendgridConfiguration.getConfigurations.useQuery(); + + const { data: dataSmtp, isLoading: isLoadingSmtp } = + trpcClient.smtpConfiguration.getConfigurations.useQuery(); + + const data: ConfigurationListItem[] = [ + ...(dataSendgrid?.map((configuration) => ({ + name: configuration.name, + provider: "sendgrid" as const, + id: configuration.id, + active: configuration.active, + })) || []), + ...(dataSmtp?.map((configuration) => ({ + name: configuration.name, + provider: "smtp" as const, + id: configuration.id, + active: configuration.active, + })) || []), + ]; + + const isLoading = isLoadingSendgrid || isLoadingSmtp; + + return ( + + + + + Configure Emails & Messages app to deliver Saleor Events webhooks to various messaging + providers + + + + + Manage providers configuration to connect Saleor events with 3rd party services. + + } + > + + + + ); +}; + +export default ConfigurationPage; diff --git a/apps/emails-and-messages/src/pages/configuration/sendgrid/[configurationId].tsx b/apps/emails-and-messages/src/pages/configuration/sendgrid/[configurationId].tsx new file mode 100644 index 0000000..f5016ff --- /dev/null +++ b/apps/emails-and-messages/src/pages/configuration/sendgrid/[configurationId].tsx @@ -0,0 +1,100 @@ +import { Box, Text } from "@saleor/macaw-ui/next"; +import { NextPage } from "next"; +import { trpcClient } from "../../../modules/trpc/trpc-client"; +import { useRouter } from "next/router"; +import { SendgridBasicInformationSection } from "../../../modules/sendgrid/ui/sendgrid-basic-information-section"; +import { SendgridDangerousSection } from "../../../modules/sendgrid/ui/sendgrid-dangrous-section"; +import { ApiConnectionSection } from "../../../modules/sendgrid/ui/api-connection-section"; +import { SendgridSenderSection } from "../../../modules/sendgrid/ui/sendgrid-sender-section"; +import { SendgridEventsSection } from "../../../modules/sendgrid/ui/sendgrid-events-section"; +import { useDashboardNotification } from "@saleor/apps-shared"; +import { BasicLayout } from "../../../components/basic-layout"; +import { SendgridChannelsSection } from "../../../modules/sendgrid/ui/sendgrid-channels-section"; +import { appUrls } from "../../../modules/app-configuration/urls"; + +const LoadingView = () => { + return ( + + Loading... + + ); +}; + +const NotFoundView = () => { + return ( + + Could not find the requested configuration. + + ); +}; + +const EditSendgridConfigurationPage: NextPage = () => { + const { notifyError } = useDashboardNotification(); + const router = useRouter(); + const configurationId = router.query.configurationId + ? (router.query.configurationId as string) + : undefined; + const { data: configuration, isLoading } = + trpcClient.sendgridConfiguration.getConfiguration.useQuery( + { + id: configurationId!, + }, + { + enabled: !!configurationId, + 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"); + } + }, + } + ); + + if (isLoading) { + return ; + } + + if (!configuration) { + return ; + } + + return ( + + + + Connect Sendgrid with Saleor. + + + + + + + + + + ); +}; + +export default EditSendgridConfigurationPage; diff --git a/apps/emails-and-messages/src/pages/configuration/sendgrid/new.tsx b/apps/emails-and-messages/src/pages/configuration/sendgrid/new.tsx new file mode 100644 index 0000000..158b507 --- /dev/null +++ b/apps/emails-and-messages/src/pages/configuration/sendgrid/new.tsx @@ -0,0 +1,120 @@ +import { Box, Button, Input, Text } from "@saleor/macaw-ui/next"; +import { NextPage } from "next"; +import { SectionWithDescription } from "../../../components/section-with-description"; +import { BoxWithBorder } from "../../../components/box-with-border"; +import { defaultPadding } from "../../../components/ui-defaults"; +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 { SendgridCreateConfigurationInput } from "../../../modules/sendgrid/configuration/sendgrid-config-input-schema"; +import { BasicLayout } from "../../../components/basic-layout"; +import { useRouter } from "next/router"; +import { sendgridUrls } from "../../../modules/sendgrid/urls"; +import { appUrls } from "../../../modules/app-configuration/urls"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +const NewSendgridConfigurationPage: NextPage = () => { + const router = useRouter(); + const { notifySuccess, notifyError } = useDashboardNotification(); + + const { handleSubmit, control, setError } = useForm(); + + const { mutate: createConfiguration } = + trpcClient.sendgridConfiguration.createConfiguration.useMutation({ + onSuccess: async (data, variables) => { + notifySuccess("Configuration saved"); + router.push(sendgridUrls.configuration(data.id)); + }, + onError(error) { + setBackendErrors({ + error, + setError, + notifyError, + }); + }, + }); + + return ( + + + + Connect Sendgrid with Saleor. + + + + 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. + + } + > + +
{ + createConfiguration({ + ...data, + }); + })} + > + + ( + + )} + /> + ( + // TODO: add validation + + )} + /> + + + + +
+
+
+
+ ); +}; + +export default NewSendgridConfigurationPage; diff --git a/apps/emails-and-messages/src/pages/configuration/smtp/[configurationId].tsx b/apps/emails-and-messages/src/pages/configuration/smtp/[configurationId].tsx new file mode 100644 index 0000000..b4b83a0 --- /dev/null +++ b/apps/emails-and-messages/src/pages/configuration/smtp/[configurationId].tsx @@ -0,0 +1,97 @@ +import { Box, Text } from "@saleor/macaw-ui/next"; +import { NextPage } from "next"; +import { trpcClient } from "../../../modules/trpc/trpc-client"; +import { useRouter } from "next/router"; +import { useDashboardNotification } from "@saleor/apps-shared"; +import { BasicLayout } from "../../../components/basic-layout"; +import { SmtpBasicInformationSection } from "../../../modules/smtp/ui/smtp-basic-information-section"; +import { SmtpSection } from "../../../modules/smtp/ui/smtp-section"; +import { SmtpSenderSection } from "../../../modules/smtp/ui/smtp-sender-section"; +import { SmtpDangerousSection } from "../../../modules/smtp/ui/smtp-dangrous-section"; +import { SmtpChannelsSection } from "../../../modules/smtp/ui/smtp-channels-section"; +import { SmtpEventsSection } from "../../../modules/smtp/ui/smtp-events-section"; +import { appUrls } from "../../../modules/app-configuration/urls"; + +const LoadingView = () => { + return ( + + Loading... + + ); +}; + +const NotFoundView = () => { + return ( + + Could not find the requested configuration. + + ); +}; + +const EditSmtpConfigurationPage: NextPage = () => { + const { notifyError } = useDashboardNotification(); + const router = useRouter(); + const configurationId = router.query.configurationId + ? (router.query.configurationId as string) + : undefined; + const { data: configuration, isLoading } = trpcClient.smtpConfiguration.getConfiguration.useQuery( + { + id: configurationId!, + }, + { + enabled: !!configurationId, + 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"); + } + }, + } + ); + + if (isLoading) { + return ; + } + + if (!configuration) { + return ; + } + + return ( + + + + Connect SMTP with Saleor. + + + + + + + + + + ); +}; + +export default EditSmtpConfigurationPage; diff --git a/apps/emails-and-messages/src/pages/configuration/smtp/[configurationId]/event/[eventType].tsx b/apps/emails-and-messages/src/pages/configuration/smtp/[configurationId]/event/[eventType].tsx new file mode 100644 index 0000000..5212789 --- /dev/null +++ b/apps/emails-and-messages/src/pages/configuration/smtp/[configurationId]/event/[eventType].tsx @@ -0,0 +1,98 @@ +import { Box, Text } from "@saleor/macaw-ui/next"; +import { NextPage } from "next"; +import { trpcClient } from "../../../../../modules/trpc/trpc-client"; +import { useRouter } from "next/router"; +import { useDashboardNotification } from "@saleor/apps-shared"; +import { BasicLayout } from "../../../../../components/basic-layout"; +import { parseMessageEventType } from "../../../../../modules/event-handlers/parse-message-event-type"; +import { appUrls } from "../../../../../modules/app-configuration/urls"; +import { EventForm } from "../../../../../modules/smtp/ui/event-form"; +import { smtpUrls } from "../../../../../modules/smtp/urls"; +import { TextLink } from "../../../../../components/text-link"; + +const LoadingView = () => { + return ( + + Loading... + + ); +}; + +const NotFoundView = () => { + return ( + + Could not find the requested configuration. + + ); +}; + +const EditSmtpEventPage: NextPage = () => { + const { notifyError } = useDashboardNotification(); + const router = useRouter(); + + const configurationId = router.query.configurationId as string; + const eventTypeFromQuery = router.query.eventType as string | undefined; + const eventType = parseMessageEventType(eventTypeFromQuery); + + const { data: configuration, isLoading } = trpcClient.smtpConfiguration.getConfiguration.useQuery( + { + id: configurationId, + }, + { + enabled: !!configurationId && !!eventType, + onSettled(data, error) { + if (error) { + console.error("Error during fetching the configuration: ", error); + } + if (error?.data?.code === "NOT_FOUND" || !data) { + notifyError("The requested configuration does not exist."); + router.replace(appUrls.configuration()); + } + }, + } + ); + + // TODO: better error messages + if (!eventType || !configurationId) { + return <>Error: no event type or configuration id; + } + + if (isLoading) { + return ; + } + + if (!configuration) { + return ; + } + + return ( + + + + Edit template for {eventType} event. You can learn more about MJML{" "} + + here + + . + + + + + ); +}; + +export default EditSmtpEventPage; diff --git a/apps/emails-and-messages/src/pages/configuration/smtp/new.tsx b/apps/emails-and-messages/src/pages/configuration/smtp/new.tsx new file mode 100644 index 0000000..cc440d8 --- /dev/null +++ b/apps/emails-and-messages/src/pages/configuration/smtp/new.tsx @@ -0,0 +1,134 @@ +import { Box, Button, Divider, Text } from "@saleor/macaw-ui/next"; +import { NextPage } from "next"; +import { SectionWithDescription } from "../../../components/section-with-description"; +import { BoxWithBorder } from "../../../components/box-with-border"; +import { defaultPadding } from "../../../components/ui-defaults"; +import { BoxFooter } from "../../../components/box-footer"; +import { trpcClient } from "../../../modules/trpc/trpc-client"; +import { useDashboardNotification } from "@saleor/apps-shared/src/use-dashboard-notification"; +import { useForm } from "react-hook-form"; +import { + SmtpCreateConfigurationInput, + smtpCreateConfigurationInputSchema, +} from "../../../modules/smtp/configuration/smtp-config-input-schema"; +import { BasicLayout } from "../../../components/basic-layout"; +import { useRouter } from "next/router"; +import { Input } from "../../../components/react-hook-form-macaw/Input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { appUrls } from "../../../modules/app-configuration/urls"; +import { smtpUrls } from "../../../modules/smtp/urls"; +import { setBackendErrors } from "../../../lib/set-backend-errors"; + +const NewSmtpConfigurationPage: NextPage = () => { + const router = useRouter(); + const { notifySuccess, notifyError } = useDashboardNotification(); + + const { handleSubmit, control, setError, register } = useForm({ + defaultValues: { encryption: "NONE" }, + resolver: zodResolver(smtpCreateConfigurationInputSchema), + }); + + const { mutate } = trpcClient.smtpConfiguration.createConfiguration.useMutation({ + onSuccess: async (data, variables) => { + notifySuccess("Configuration saved"); + router.push(smtpUrls.configuration(data.id)); + }, + onError(error) { + setBackendErrors({ + error, + setError, + notifyError, + }); + }, + }); + + return ( + + + + Connect SMTP with Saleor. + + + + 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. + + } + > + +
{ + mutate({ + ...data, + }); + })} + > + + + + SMTP server connection + + + + + + + + + + + + + + +
+
+
+
+ ); +}; + +export default NewSmtpConfigurationPage; diff --git a/apps/emails-and-messages/src/pages/index.tsx b/apps/emails-and-messages/src/pages/index.tsx index be3b801..ae41e8c 100644 --- a/apps/emails-and-messages/src/pages/index.tsx +++ b/apps/emails-and-messages/src/pages/index.tsx @@ -3,9 +3,9 @@ import { useAppBridge } from "@saleor/app-sdk/app-bridge"; import { useEffect } from "react"; import { useIsMounted } from "usehooks-ts"; import { useRouter } from "next/router"; -import { LinearProgress } from "@material-ui/core"; import { isInIframe } from "../lib/is-in-iframe"; import { appName } from "../const"; +import { appUrls } from "../modules/app-configuration/urls"; const IndexPage: NextPage = () => { const { appBridgeState } = useAppBridge(); @@ -14,12 +14,12 @@ const IndexPage: NextPage = () => { useEffect(() => { if (isMounted() && appBridgeState?.ready) { - replace("/configuration/channels"); + replace(appUrls.configuration()); } }, [isMounted, appBridgeState?.ready, replace]); if (isInIframe()) { - return ; + return

Loading

; } return ( diff --git a/apps/emails-and-messages/src/public/breadcrumb-separator.svg b/apps/emails-and-messages/src/public/breadcrumb-separator.svg new file mode 100644 index 0000000..3fd4463 --- /dev/null +++ b/apps/emails-and-messages/src/public/breadcrumb-separator.svg @@ -0,0 +1,8 @@ + + + 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/apps/emails-and-messages/src/styles/globals.css b/apps/emails-and-messages/src/styles/globals.css index 3a624b6..94c864c 100644 --- a/apps/emails-and-messages/src/styles/globals.css +++ b/apps/emails-and-messages/src/styles/globals.css @@ -1,21 +1,16 @@ body { - font-family: Inter, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, - "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; - color: #111; + color: var(--mu-colors-foreground-text-neutral-plain); + padding: 0; + margin: 0; + background: var(--mu-colors-background-plain); } code { - background: #f6f8fa; - border: 1px solid #eaeaea; - border-radius: 5px; display: inline-block; - margin-top: 10px; - padding: 0.75rem; - font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, - Bitstream Vera Sans Mono, Courier New, monospace; + letter-spacing: 0.1em; + color: var(--mu-colors-foreground-text-neutral-subdued); } -code::before { - content: "$ "; - opacity: 0.6; +a { + text-decoration: none; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e01e0a5..ac4e91b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -466,6 +466,9 @@ importers: apps/emails-and-messages: dependencies: + '@hookform/resolvers': + specifier: ^2.9.10 + version: 2.9.11(react-hook-form@7.43.1) '@monaco-editor/react': specifier: ^4.4.6 version: 4.4.6(monaco-editor@0.38.0)(react-dom@18.2.0)(react@18.2.0) @@ -476,8 +479,8 @@ importers: specifier: workspace:* version: link:../../packages/shared '@saleor/macaw-ui': - specifier: 0.8.0-pre.72 - version: 0.8.0-pre.72(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0) + specifier: 0.8.0-pre.83 + version: 0.8.0-pre.83(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0) '@sendgrid/client': specifier: ^7.7.0 version: 7.7.0 @@ -506,8 +509,8 @@ importers: specifier: ^1.0.0 version: 1.0.0(graphql@16.6.0) '@vitejs/plugin-react': - specifier: ^3.0.1 - version: 3.1.0(vite@4.3.1) + specifier: ^4.0.0 + version: 4.0.0(vite@4.3.8) clsx: specifier: ^1.2.1 version: 1.2.1 @@ -566,11 +569,11 @@ importers: specifier: ^2.9.1 version: 2.9.1(react-dom@18.2.0)(react@18.2.0) vite: - specifier: ^4.3.1 - version: 4.3.1(@types/node@18.13.0) + specifier: ^4.3.8 + version: 4.3.8(@types/node@18.13.0) vitest: - specifier: ^0.30.1 - version: 0.30.1(jsdom@20.0.3) + specifier: ^0.31.1 + version: 0.31.1(jsdom@20.0.3) zod: specifier: ^3.20.2 version: 3.20.2 @@ -2962,10 +2965,6 @@ packages: dependencies: '@babel/highlight': 7.18.6 - /@babel/compat-data@7.20.14: - resolution: {integrity: sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw==} - engines: {node: '>=6.9.0'} - /@babel/compat-data@7.21.9: resolution: {integrity: sha512-FUGed8kfhyWvbYug/Un/VPJD41rDIgoVVcR+FuzhzOYyRz5uED+Gd3SLZml0Uw2l2aHFb7ZgdW5mGA3G2cCCnQ==} engines: {node: '>=6.9.0'} @@ -2975,15 +2974,15 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@ampproject/remapping': 2.2.0 - '@babel/code-frame': 7.18.6 - '@babel/generator': 7.20.14 - '@babel/helper-compilation-targets': 7.20.7(@babel/core@7.20.12) - '@babel/helper-module-transforms': 7.20.11 - '@babel/helpers': 7.20.13 - '@babel/parser': 7.20.15 + '@babel/code-frame': 7.21.4 + '@babel/generator': 7.21.9 + '@babel/helper-compilation-targets': 7.21.5(@babel/core@7.20.12) + '@babel/helper-module-transforms': 7.21.5 + '@babel/helpers': 7.21.5 + '@babel/parser': 7.21.9 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.13 - '@babel/types': 7.20.7 + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 convert-source-map: 1.9.0 debug: 4.3.4 gensync: 1.0.0-beta.2 @@ -3021,6 +3020,7 @@ packages: '@babel/types': 7.20.7 '@jridgewell/gen-mapping': 0.3.2 jsesc: 2.5.2 + dev: true /@babel/generator@7.21.9: resolution: {integrity: sha512-F3fZga2uv09wFdEjEQIJxXALXfz0+JaOb7SabvVMmjHxeVTuGW8wgE8Vp1Hd7O+zMTYtcfEISGRzPkeiaPPsvg==} @@ -3045,15 +3045,15 @@ packages: '@babel/types': 7.21.5 dev: true - /@babel/helper-compilation-targets@7.20.7(@babel/core@7.20.12): - resolution: {integrity: sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==} + /@babel/helper-compilation-targets@7.21.5(@babel/core@7.20.12): + resolution: {integrity: sha512-1RkbFGUKex4lvsB9yhIfWltJM5cZKUftB2eNajaDv3dCMEp49iBG0K14uH8NnX9IPux2+mK7JGEOB0jn48/J6w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 dependencies: - '@babel/compat-data': 7.20.14 + '@babel/compat-data': 7.21.9 '@babel/core': 7.20.12 - '@babel/helper-validator-option': 7.18.6 + '@babel/helper-validator-option': 7.21.0 browserslist: 4.21.5 lru-cache: 5.1.1 semver: 6.3.0 @@ -3138,21 +3138,10 @@ packages: - supports-color dev: true - /@babel/helper-environment-visitor@7.18.9: - resolution: {integrity: sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==} - engines: {node: '>=6.9.0'} - /@babel/helper-environment-visitor@7.21.5: resolution: {integrity: sha512-IYl4gZ3ETsWocUWgsFZLM5i1BYx9SoemminVEXadgLBa9TdeorzgLKm8wWLA6J1N/kT3Kch8XIk1laNzYoHKvQ==} engines: {node: '>=6.9.0'} - /@babel/helper-function-name@7.19.0: - resolution: {integrity: sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.20.7 - '@babel/types': 7.21.5 - /@babel/helper-function-name@7.21.0: resolution: {integrity: sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==} engines: {node: '>=6.9.0'} @@ -3180,33 +3169,12 @@ packages: '@babel/types': 7.21.5 dev: true - /@babel/helper-module-imports@7.18.6: - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - /@babel/helper-module-imports@7.21.4: resolution: {integrity: sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==} engines: {node: '>=6.9.0'} dependencies: '@babel/types': 7.21.5 - /@babel/helper-module-transforms@7.20.11: - resolution: {integrity: sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-module-imports': 7.18.6 - '@babel/helper-simple-access': 7.20.2 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/helper-validator-identifier': 7.19.1 - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.5 - '@babel/types': 7.21.5 - transitivePeerDependencies: - - supports-color - /@babel/helper-module-transforms@7.21.5: resolution: {integrity: sha512-bI2Z9zBGY2q5yMHoBvJ2a9iX3ZOAzJPm7Q8Yz6YeoUjU/Cvhmi2G4QyTNyPBqqXSgTjUxRg3L0xV45HvkNWWBw==} engines: {node: '>=6.9.0'} @@ -3229,14 +3197,9 @@ packages: '@babel/types': 7.21.5 dev: true - /@babel/helper-plugin-utils@7.20.2: - resolution: {integrity: sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==} - engines: {node: '>=6.9.0'} - /@babel/helper-plugin-utils@7.21.5: resolution: {integrity: sha512-0WDaIlXKOX/3KfBK/dwP1oQGiPh6rjMkT7HIRv7i5RR2VUMwrx5ZL0dwBkKx7+SW1zwNdgjHd34IMk5ZjTeHVg==} engines: {node: '>=6.9.0'} - dev: true /@babel/helper-remap-async-to-generator@7.18.9(@babel/core@7.21.8): resolution: {integrity: sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==} @@ -3281,12 +3244,6 @@ packages: - supports-color dev: true - /@babel/helper-simple-access@7.20.2: - resolution: {integrity: sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.21.5 - /@babel/helper-simple-access@7.21.5: resolution: {integrity: sha512-ENPDAMC1wAjR0uaCUwliBdiSl1KBJAVnMTzXqi64c2MG8MPR6ii4qf7bSXDqSFbr4W6W028/rf5ivoHop5/mkg==} engines: {node: '>=6.9.0'} @@ -3318,10 +3275,6 @@ packages: resolution: {integrity: sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==} engines: {node: '>=6.9.0'} - /@babel/helper-validator-option@7.18.6: - resolution: {integrity: sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==} - engines: {node: '>=6.9.0'} - /@babel/helper-validator-option@7.21.0: resolution: {integrity: sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==} engines: {node: '>=6.9.0'} @@ -3338,16 +3291,6 @@ packages: - supports-color dev: true - /@babel/helpers@7.20.13: - resolution: {integrity: sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.20.7 - '@babel/traverse': 7.21.5 - '@babel/types': 7.21.5 - transitivePeerDependencies: - - supports-color - /@babel/helpers@7.21.5: resolution: {integrity: sha512-BSY+JSlHxOmGsPTydUkPf1MdMQ3M81x5xGCOVgWM3G8XH77sJ292Y2oqcp0CbbgxhqBuI46iUz1tT7hqP7EfgA==} engines: {node: '>=6.9.0'} @@ -3371,7 +3314,7 @@ packages: engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.20.7 + '@babel/types': 7.21.5 /@babel/parser@7.21.9: resolution: {integrity: sha512-q5PNg/Bi1OpGgx5jYlvWZwAorZepEudDMCLtj967aeS7WMont7dUZI46M2XwcIQqvUlMxWfdLFu4S/qSxeUu5g==} @@ -3677,7 +3620,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 /@babel/plugin-syntax-jsx@7.21.4(@babel/core@7.21.8): resolution: {integrity: sha512-5hewiLct5OKyh6PLKEYaFclcqtIgCb6bmELouxjF6up5q3Sov7rOayW4RwhbaBL0dit8rA80GNfY+UuDp2mBbQ==} @@ -3770,7 +3713,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 dev: true /@babel/plugin-transform-arrow-functions@7.21.5(@babel/core@7.21.8): @@ -4080,7 +4023,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 /@babel/plugin-transform-react-jsx-self@7.21.0(@babel/core@7.21.8): resolution: {integrity: sha512-f/Eq+79JEu+KUANFks9UZCcvydOOGMgF7jBrcwjHa5jTZD8JivnhCJYvmlhR/WTXBWonDExPoW0eO/CR4QJirA==} @@ -4089,8 +4032,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.20.2 - dev: true + '@babel/helper-plugin-utils': 7.21.5 /@babel/plugin-transform-react-jsx-source@7.19.6(@babel/core@7.20.12): resolution: {integrity: sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==} @@ -4099,7 +4041,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.20.12 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 /@babel/plugin-transform-react-jsx-source@7.19.6(@babel/core@7.21.8): resolution: {integrity: sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==} @@ -4108,8 +4050,7 @@ packages: '@babel/core': ^7.0.0-0 dependencies: '@babel/core': 7.21.8 - '@babel/helper-plugin-utils': 7.20.2 - dev: true + '@babel/helper-plugin-utils': 7.21.5 /@babel/plugin-transform-react-jsx@7.20.13(@babel/core@7.21.8): resolution: {integrity: sha512-MmTZx/bkUrfJhhYAYt3Urjm+h8DQGrPrnKQ94jLo7NLuOU+T89a7IByhKmrb8SKhrIYIQ0FN0CHMbnFRen4qNw==} @@ -4120,7 +4061,7 @@ packages: '@babel/core': 7.21.8 '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-module-imports': 7.21.4 - '@babel/helper-plugin-utils': 7.20.2 + '@babel/helper-plugin-utils': 7.21.5 '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.8) '@babel/types': 7.21.5 dev: true @@ -4393,23 +4334,6 @@ packages: '@babel/parser': 7.20.15 '@babel/types': 7.20.7 - /@babel/traverse@7.20.13: - resolution: {integrity: sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/code-frame': 7.21.4 - '@babel/generator': 7.21.9 - '@babel/helper-environment-visitor': 7.18.9 - '@babel/helper-function-name': 7.19.0 - '@babel/helper-hoist-variables': 7.18.6 - '@babel/helper-split-export-declaration': 7.18.6 - '@babel/parser': 7.21.9 - '@babel/types': 7.21.5 - debug: 4.3.4 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - /@babel/traverse@7.21.5: resolution: {integrity: sha512-AhQoI3YjWi6u/y/ntv7k48mcrCXmus0t79J9qPNlk/lAsFlCiJ047RmbfMOawySTHtywXhbXgpx/8nXMYd+oFw==} engines: {node: '>=6.9.0'} @@ -4676,7 +4600,7 @@ packages: '@babel/core': ^7.0.0 dependencies: '@babel/core': 7.21.8 - '@babel/helper-module-imports': 7.18.6 + '@babel/helper-module-imports': 7.21.4 '@babel/plugin-syntax-jsx': 7.18.6(@babel/core@7.21.8) '@babel/runtime': 7.20.13 '@emotion/hash': 0.9.0 @@ -5994,10 +5918,10 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 dependencies: - '@babel/parser': 7.20.15 + '@babel/parser': 7.21.9 '@babel/plugin-syntax-import-assertions': 7.20.0(@babel/core@7.21.8) - '@babel/traverse': 7.20.13 - '@babel/types': 7.20.7 + '@babel/traverse': 7.21.5 + '@babel/types': 7.21.5 '@graphql-tools/utils': 9.2.1(graphql@16.6.0) graphql: 16.6.0 tslib: 2.5.0 @@ -7841,8 +7765,8 @@ packages: transitivePeerDependencies: - '@types/react' - /@saleor/macaw-ui@0.8.0-pre.72(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-9lcFkzf81q9Mxjqd00rWUUvom26YK3WCu8GCcmpqcEFu723/H76hxg2/LUd2cpqARavS1FgO+Vri7jkxkSz7sQ==} + /@saleor/macaw-ui@0.8.0-pre.83(@types/react@18.0.27)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-IMwN8a39ZqjHoO7P3pAQ6kWxqX5U4EhuOgunpR8QU0v6Tm5DfIeUmIr/3jwm5jQbYzZrNNgaboBJy7oCbJ/cUQ==} engines: {node: '>=16 <19', pnpm: '>=8'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -9848,7 +9772,7 @@ packages: resolution: {integrity: sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==} engines: {node: '>=12'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 '@babel/runtime': 7.20.13 '@types/aria-query': 5.0.1 aria-query: 5.1.3 @@ -10108,11 +10032,14 @@ packages: /@types/chai-subset@1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: - '@types/chai': 4.3.4 + '@types/chai': 4.3.5 /@types/chai@4.3.4: resolution: {integrity: sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==} + /@types/chai@4.3.5: + resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} + /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: @@ -10880,7 +10807,7 @@ packages: lodash: 4.17.21 mlly: 1.2.0 outdent: 0.8.0 - vite: 4.3.6(@types/node@18.13.0) + vite: 4.3.8(@types/node@18.13.0) vite-node: 0.28.5 transitivePeerDependencies: - '@types/node' @@ -10963,6 +10890,21 @@ packages: - supports-color dev: true + /@vitejs/plugin-react@4.0.0(vite@4.3.8): + resolution: {integrity: sha512-HX0XzMjL3hhOYm+0s95pb0Z7F8O81G7joUHgfDd/9J/ZZf5k4xX6QAMFkKsHFxaHlf6X7GD7+XuaZ66ULiJuhQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 + dependencies: + '@babel/core': 7.21.8 + '@babel/plugin-transform-react-jsx-self': 7.21.0(@babel/core@7.21.8) + '@babel/plugin-transform-react-jsx-source': 7.19.6(@babel/core@7.21.8) + react-refresh: 0.14.0 + vite: 4.3.8(@types/node@18.13.0) + transitivePeerDependencies: + - supports-color + dev: false + /@vitest/coverage-c8@0.28.4(jsdom@20.0.3): resolution: {integrity: sha512-btelLBxaWhHnywXRQxDlrvPhGdnuIaD3XulsxcZRIcnpLPbFu39dNTT0IYu2QWP2ZZrV0AmNtdLIfD4c77zMAg==} dependencies: @@ -10999,6 +10941,14 @@ packages: '@vitest/utils': 0.30.1 chai: 4.3.7 + /@vitest/expect@0.31.1: + resolution: {integrity: sha512-BV1LyNvhnX+eNYzJxlHIGPWZpwJFZaCcOIzp2CNG0P+bbetenTupk6EO0LANm4QFt0TTit+yqx7Rxd1qxi/SQA==} + dependencies: + '@vitest/spy': 0.31.1 + '@vitest/utils': 0.31.1 + chai: 4.3.7 + dev: false + /@vitest/runner@0.28.4: resolution: {integrity: sha512-Q8UV6GjDvBSTfUoq0QXVCNpNOUrWu4P2qvRq7ssJWzn0+S0ojbVOxEjMt+8a32X6SdkhF8ak+2nkppsqV0JyNQ==} dependencies: @@ -11015,6 +10965,15 @@ packages: p-limit: 4.0.0 pathe: 1.1.0 + /@vitest/runner@0.31.1: + resolution: {integrity: sha512-imWuc82ngOtxdCUpXwtEzZIuc1KMr+VlQ3Ondph45VhWoQWit5yvG/fFcldbnCi8DUuFi+NmNx5ehMUw/cGLUw==} + dependencies: + '@vitest/utils': 0.31.1 + concordance: 5.0.4 + p-limit: 4.0.0 + pathe: 1.1.0 + dev: false + /@vitest/snapshot@0.30.1: resolution: {integrity: sha512-fJZqKrE99zo27uoZA/azgWyWbFvM1rw2APS05yB0JaLwUIg9aUtvvnBf4q7JWhEcAHmSwbrxKFgyBUga6tq9Tw==} dependencies: @@ -11022,6 +10981,14 @@ packages: pathe: 1.1.0 pretty-format: 27.5.1 + /@vitest/snapshot@0.31.1: + resolution: {integrity: sha512-L3w5uU9bMe6asrNzJ8WZzN+jUTX4KSgCinEJPXyny0o90fG4FPQMV0OWsq7vrCWfQlAilMjDnOF9nP8lidsJ+g==} + dependencies: + magic-string: 0.30.0 + pathe: 1.1.0 + pretty-format: 27.5.1 + dev: false + /@vitest/spy@0.28.4: resolution: {integrity: sha512-8WuhfXLlvCXpNXEGJW6Gc+IKWI32435fQJLh43u70HnZ1otJOa2Cmg2Wy2Aym47ZnNCP4NolF+8cUPwd0MigKQ==} dependencies: @@ -11033,6 +11000,12 @@ packages: dependencies: tinyspy: 2.1.0 + /@vitest/spy@0.31.1: + resolution: {integrity: sha512-1cTpt2m9mdo3hRLDyCG2hDQvRrePTDgEJBFQQNz1ydHHZy03EiA6EpFxY+7ODaY7vMRCie+WlFZBZ0/dQWyssQ==} + dependencies: + tinyspy: 2.1.0 + dev: false + /@vitest/utils@0.28.4: resolution: {integrity: sha512-l2QztOLdc2LkR+w/lP52RGh8hW+Ul4KESmCAgVE8q737I7e7bQoAfkARKpkPJ4JQtGpwW4deqlj1732VZD7TFw==} dependencies: @@ -11050,6 +11023,14 @@ packages: loupe: 2.3.6 pretty-format: 27.5.1 + /@vitest/utils@0.31.1: + resolution: {integrity: sha512-yFyRD5ilwojsZfo3E0BnH72pSVSuLg2356cN1tCEe/0RtDzxTPYwOomIC+eQbot7m6DRy4tPZw+09mB7NkbMmA==} + dependencies: + concordance: 5.0.4 + loupe: 2.3.6 + pretty-format: 27.5.1 + dev: false + /@web-std/blob@3.0.4: resolution: {integrity: sha512-+dibyiw+uHYK4dX5cJ7HA+gtDAaUUe6JsOryp2ZpAC7h4ICsh49E34JwHoEKPlPvP0llCrNzz45vvD+xX5QDBg==} dependencies: @@ -18834,7 +18815,7 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} dependencies: - '@babel/code-frame': 7.18.6 + '@babel/code-frame': 7.21.4 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -19100,7 +19081,7 @@ packages: resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.4 + nanoid: 3.3.6 picocolors: 1.0.0 source-map-js: 1.0.2 @@ -21256,6 +21237,10 @@ packages: /tinybench@2.4.0: resolution: {integrity: sha512-iyziEiyFxX4kyxSp+MtY1oCH/lvjH3PxFN8PGCDeqcZWAJ/i+9y+nL85w99PxVzrIvew/GSkSbDYtiGVa85Afg==} + /tinybench@2.5.0: + resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} + dev: false + /tinypool@0.3.1: resolution: {integrity: sha512-zLA1ZXlstbU2rlpA4CIeVaqvWq41MTWqLY3FfsAXgC8+f7Pk7zroaJQxDgxn1xNudKW6Kmj4808rPFShUlIRmQ==} engines: {node: '>=14.0.0'} @@ -21265,6 +21250,11 @@ packages: resolution: {integrity: sha512-2ksntHOKf893wSAH4z/+JbPpi92esw8Gn9N2deXX+B0EO92hexAVI9GIZZPx7P5aYo5KULfeOSt3kMOmSOy6uA==} engines: {node: '>=14.0.0'} + /tinypool@0.5.0: + resolution: {integrity: sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==} + engines: {node: '>=14.0.0'} + dev: false + /tinyspy@1.1.1: resolution: {integrity: sha512-UVq5AXt/gQlti7oxoIg5oi/9r0WpF7DGEVwXgqWSMmyN16+e3tl5lIvTaOpJ3TAtu5xFzWccFRM4R5NaWHF+4g==} engines: {node: '>=14.0.0'} @@ -22156,7 +22146,7 @@ packages: picocolors: 1.0.0 source-map: 0.6.1 source-map-support: 0.5.21 - vite: 4.3.6(@types/node@18.13.0) + vite: 4.3.8(@types/node@18.13.0) transitivePeerDependencies: - '@types/node' - less @@ -22179,7 +22169,7 @@ packages: picocolors: 1.0.0 source-map: 0.6.1 source-map-support: 0.5.21 - vite: 4.3.6(@types/node@18.13.0) + vite: 4.3.8(@types/node@18.13.0) transitivePeerDependencies: - '@types/node' - less @@ -22200,7 +22190,7 @@ packages: mlly: 1.2.0 pathe: 1.1.0 picocolors: 1.0.0 - vite: 4.3.6(@types/node@18.13.0) + vite: 4.3.8(@types/node@18.13.0) transitivePeerDependencies: - '@types/node' - less @@ -22210,6 +22200,27 @@ packages: - supports-color - terser + /vite-node@0.31.1(@types/node@18.13.0): + resolution: {integrity: sha512-BajE/IsNQ6JyizPzu9zRgHrBwczkAs0erQf/JRpgTIESpKvNj9/Gd0vxX905klLkb0I0SJVCKbdrl5c6FnqYKA==} + engines: {node: '>=v14.18.0'} + hasBin: true + dependencies: + cac: 6.7.14 + debug: 4.3.4 + mlly: 1.2.0 + pathe: 1.1.0 + picocolors: 1.0.0 + vite: 4.3.8(@types/node@18.13.0) + transitivePeerDependencies: + - '@types/node' + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: false + /vite@4.3.1(@types/node@18.0.1): resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==} engines: {node: ^14.18.0 || >=16.0.0} @@ -22237,8 +22248,8 @@ packages: dependencies: '@types/node': 18.0.1 esbuild: 0.17.17 - postcss: 8.4.23 - rollup: 3.23.0 + postcss: 8.4.21 + rollup: 3.20.5 optionalDependencies: fsevents: 2.3.2 @@ -22306,6 +22317,38 @@ packages: optionalDependencies: fsevents: 2.3.2 + /vite@4.3.8(@types/node@18.13.0): + resolution: {integrity: sha512-uYB8PwN7hbMrf4j1xzGDk/lqjsZvCDbt/JC5dyfxc19Pg8kRm14LinK/uq+HSLNswZEoKmweGdtpbnxRtrAXiQ==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 18.13.0 + esbuild: 0.17.17 + postcss: 8.4.23 + rollup: 3.23.0 + optionalDependencies: + fsevents: 2.3.2 + /vitest@0.28.4(jsdom@20.0.3): resolution: {integrity: sha512-sfWIy0AdlbyGRhunm+TLQEJrFH9XuRPdApfubsyLcDbCRrUX717BRQKInTgzEfyl2Ipi1HWoHB84Nqtcwxogcg==} engines: {node: '>=v14.16.0'} @@ -22350,7 +22393,7 @@ packages: tinybench: 2.4.0 tinypool: 0.3.1 tinyspy: 1.1.1 - vite: 4.3.6(@types/node@18.13.0) + vite: 4.3.8(@types/node@18.13.0) vite-node: 0.28.4(@types/node@18.13.0) why-is-node-running: 2.2.2 transitivePeerDependencies: @@ -22428,6 +22471,72 @@ packages: - supports-color - terser + /vitest@0.31.1(jsdom@20.0.3): + resolution: {integrity: sha512-/dOoOgzoFk/5pTvg1E65WVaobknWREN15+HF+0ucudo3dDG/vCZoXTQrjIfEaWvQXmqScwkRodrTbM/ScMpRcQ==} + engines: {node: '>=v14.18.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + playwright: '*' + safaridriver: '*' + webdriverio: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + dependencies: + '@types/chai': 4.3.5 + '@types/chai-subset': 1.3.3 + '@types/node': 18.13.0 + '@vitest/expect': 0.31.1 + '@vitest/runner': 0.31.1 + '@vitest/snapshot': 0.31.1 + '@vitest/spy': 0.31.1 + '@vitest/utils': 0.31.1 + acorn: 8.8.2 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.7 + concordance: 5.0.4 + debug: 4.3.4 + jsdom: 20.0.3 + local-pkg: 0.4.3 + magic-string: 0.30.0 + pathe: 1.1.0 + picocolors: 1.0.0 + std-env: 3.3.2 + strip-literal: 1.0.1 + tinybench: 2.5.0 + tinypool: 0.5.0 + vite: 4.3.8(@types/node@18.13.0) + vite-node: 0.31.1(@types/node@18.13.0) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - sass + - stylus + - sugarss + - supports-color + - terser + dev: false + /void-elements@3.1.0: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'}