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) => (
+
+
+
+
+ ))}
+
+
+ );
+};
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 ;
+ case "sendgrid":
+ return ;
+ }
+ };
+
+ 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
+
+ >
+ }
+ >
+
+
+ );
+};
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 (
+
+
+
+
+
+ );
+};
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.
+
+ }
+ >
+
+
+
+
+ );
+};
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 (
+
+
+
+ );
+};
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 (
+
+ );
+};
+
+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 (
+
+
+
+
+
+ );
+};
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 (
+
+ );
+};
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.
+
+ }
+ >
+
+
+
+
+ );
+};
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 (
+
+
+
+ );
+};
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 (
+
+ );
+};
+
+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}
+ >
+
+
+
+
+ );
+};
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 (
+
+
+
+
+
+ );
+};
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.
+
+ }
+ >
+
+
+
+
+
+ );
+};
+
+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.
+
+ }
+ >
+
+
+
+
+
+ );
+};
+
+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 0000000..ede13eb
Binary files /dev/null and b/apps/emails-and-messages/src/public/sendgrid.png differ
diff --git a/apps/emails-and-messages/src/public/smtp.svg b/apps/emails-and-messages/src/public/smtp.svg
new file mode 100644
index 0000000..e9199a1
--- /dev/null
+++ b/apps/emails-and-messages/src/public/smtp.svg
@@ -0,0 +1,9 @@
+
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'}