Update E&M Provider UI (#459)
* update to macaw next * update ui to new design
This commit is contained in:
parent
e04ee04b3d
commit
44741aa74e
83 changed files with 3651 additions and 769 deletions
|
@ -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": {
|
||||
|
|
17
apps/emails-and-messages/src/components/basic-layout.tsx
Normal file
17
apps/emails-and-messages/src/components/basic-layout.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Box } from "@saleor/macaw-ui/next";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
|
||||
interface BasicLayoutProps {
|
||||
children: React.ReactNode;
|
||||
isLoading?: boolean;
|
||||
breadcrumbs?: { name: string; href?: string }[];
|
||||
}
|
||||
|
||||
export const BasicLayout = ({ children, breadcrumbs, isLoading = false }: BasicLayoutProps) => {
|
||||
return (
|
||||
<Box padding={10} display="grid" gap={13}>
|
||||
{breadcrumbs?.length && <Breadcrumbs items={breadcrumbs} />}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
19
apps/emails-and-messages/src/components/box-footer.tsx
Normal file
19
apps/emails-and-messages/src/components/box-footer.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Box, BoxProps } from "@saleor/macaw-ui/next";
|
||||
import { defaultPadding } from "./ui-defaults";
|
||||
|
||||
export const BoxFooter = (props: BoxProps) => {
|
||||
return (
|
||||
<Box
|
||||
borderTopStyle="solid"
|
||||
borderWidth={1}
|
||||
borderColor="neutralDefault"
|
||||
padding={defaultPadding}
|
||||
display="flex"
|
||||
flexDirection="row"
|
||||
justifyContent="flex-end"
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
);
|
||||
};
|
15
apps/emails-and-messages/src/components/box-with-border.tsx
Normal file
15
apps/emails-and-messages/src/components/box-with-border.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Box, BoxProps } from "@saleor/macaw-ui/next";
|
||||
|
||||
export const BoxWithBorder = (props: BoxProps) => {
|
||||
return (
|
||||
<Box
|
||||
borderWidth={1}
|
||||
borderStyle="solid"
|
||||
borderColor="neutralDefault"
|
||||
borderRadius={5}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
);
|
||||
};
|
55
apps/emails-and-messages/src/components/breadcrumbs.tsx
Normal file
55
apps/emails-and-messages/src/components/breadcrumbs.tsx
Normal file
|
@ -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 (
|
||||
<TextLink href={href} {...textProps}>
|
||||
{name}
|
||||
</TextLink>
|
||||
);
|
||||
}
|
||||
return <Text {...textProps}>{name}</Text>;
|
||||
};
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
items: Array<BreadcrumbItem>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<Box display="flex" gap={6}>
|
||||
{items.map((item) => (
|
||||
<Box key={item.name} display={{ mobile: "none", desktop: "flex" }} gap={6}>
|
||||
<Breadcrumb {...item} key={item.name} />
|
||||
<Image alt="Separator icon" src={icon} height={32} width={32} />
|
||||
</Box>
|
||||
))}
|
||||
<Breadcrumb isLast={true} {...lastItem} />
|
||||
</Box>
|
||||
);
|
||||
};
|
30
apps/emails-and-messages/src/components/chip-text.tsx
Normal file
30
apps/emails-and-messages/src/components/chip-text.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Text, Chip, ChipProps } from "@saleor/macaw-ui/next";
|
||||
|
||||
const colorPropsMapping: Record<ChipProps["variant"], ChipProps["borderColor"]> = {
|
||||
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 (
|
||||
<Chip {...commonProps} borderColor={colorPropsMapping[variant]}>
|
||||
<Text color="textNeutralDefault" size="small" variant="caption">
|
||||
{content}
|
||||
</Text>
|
||||
</Chip>
|
||||
);
|
||||
};
|
|
@ -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<T extends FieldValues = FieldValues> = Omit<$ComboboxProps, "name"> & {
|
||||
name: FieldPath<T>;
|
||||
control: Control<T>;
|
||||
};
|
||||
|
||||
export function Combobox<TFieldValues extends FieldValues = FieldValues>({
|
||||
type,
|
||||
required,
|
||||
name,
|
||||
control,
|
||||
options,
|
||||
...rest
|
||||
}: ComboboxProps<TFieldValues>): JSX.Element {
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field: { value, ...field }, fieldState: { error } }) => (
|
||||
<$Combobox
|
||||
{...rest}
|
||||
{...field}
|
||||
options={options}
|
||||
value={value || ""}
|
||||
name={name}
|
||||
required={required}
|
||||
type={type}
|
||||
error={!!error}
|
||||
helperText={rest.helperText}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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<T extends FieldValues = FieldValues> = Omit<
|
||||
$InputProps,
|
||||
"name"
|
||||
> & {
|
||||
name: FieldPath<T>;
|
||||
control: Control<T>;
|
||||
};
|
||||
|
||||
export function Input<TFieldValues extends FieldValues = FieldValues>({
|
||||
type,
|
||||
required,
|
||||
name,
|
||||
control,
|
||||
...rest
|
||||
}: TextFieldElementProps<TFieldValues>): JSX.Element {
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<$Input
|
||||
{...rest}
|
||||
{...field}
|
||||
name={name}
|
||||
required={required}
|
||||
type={type}
|
||||
error={!!error}
|
||||
helperText={rest.helperText}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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<T extends FieldValues = FieldValues> = Omit<
|
||||
$MultiselectProps,
|
||||
"name"
|
||||
> & {
|
||||
name: FieldPath<T>;
|
||||
control: Control<T>;
|
||||
};
|
||||
|
||||
export function Multiselect<TFieldValues extends FieldValues = FieldValues>({
|
||||
type,
|
||||
required,
|
||||
name,
|
||||
control,
|
||||
options,
|
||||
...rest
|
||||
}: MultiselectProps<TFieldValues>): JSX.Element {
|
||||
return (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field: { value, ...field }, fieldState: { error } }) => (
|
||||
<$Multiselect
|
||||
{...rest}
|
||||
{...field}
|
||||
options={options}
|
||||
value={value || []}
|
||||
name={name}
|
||||
required={required}
|
||||
type={type}
|
||||
error={!!error}
|
||||
helperText={rest.helperText}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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.
|
|
@ -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 (
|
||||
<Box display="grid" gridTemplateColumns={{ desktop: 3, mobile: 1 }} gap={6}>
|
||||
<Box>
|
||||
<Text variant="heading" display="block">
|
||||
{props.title}
|
||||
</Text>
|
||||
{props.description}
|
||||
</Box>
|
||||
{!!props.children && (
|
||||
<Box
|
||||
gridColumnStart={{ desktop: "2", mobile: "1" }}
|
||||
gridColumnEnd={{ desktop: "4", mobile: "1" }}
|
||||
>
|
||||
{props.children}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
35
apps/emails-and-messages/src/components/text-link.tsx
Normal file
35
apps/emails-and-messages/src/components/text-link.tsx
Normal file
|
@ -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<HTMLAnchorElement, MouseEvent>) => {
|
||||
event.preventDefault();
|
||||
|
||||
appBridge?.dispatch(
|
||||
actions.Redirect({
|
||||
to: href,
|
||||
newContext: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Text textDecoration="underline" variant="bodyStrong" {...props}>
|
||||
{openNewTab ? (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer" onClick={onClick}>
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
<Link href={href}>{children}</Link>
|
||||
)}
|
||||
</Text>
|
||||
);
|
||||
};
|
1
apps/emails-and-messages/src/components/ui-defaults.ts
Normal file
1
apps/emails-and-messages/src/components/ui-defaults.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const defaultPadding = 6;
|
|
@ -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}`;
|
||||
};
|
||||
|
|
38
apps/emails-and-messages/src/lib/set-backend-errors.ts
Normal file
38
apps/emails-and-messages/src/lib/set-backend-errors.ts
Normal file
|
@ -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<T extends FieldValues = FieldValues> = {
|
||||
error: TRPCClientErrorLike<AppRouter>;
|
||||
setError?: UseFormSetError<T>;
|
||||
notifyError: (title: string, text?: string, apiMessage?: string) => void;
|
||||
};
|
||||
|
||||
export function setBackendErrors<T extends FieldValues = FieldValues>({
|
||||
error,
|
||||
setError,
|
||||
notifyError,
|
||||
}: SetBackendErrorsProps<T>) {
|
||||
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<T>, {
|
||||
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
|
||||
);
|
||||
}
|
|
@ -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(<ThemeSynchronizer />);
|
||||
|
||||
return waitFor(() => {
|
||||
expect(mockThemeChange).toHaveBeenCalledWith("dark");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 (
|
||||
<BoxWithBorder padding={10} display="grid" alignItems="center" justifyContent="center">
|
||||
<Text>No providers configured yet</Text>
|
||||
<Button onClick={redirectToProvidersSelection}>Add first provider</Button>
|
||||
</BoxWithBorder>
|
||||
);
|
||||
};
|
||||
|
||||
type ProviderType = "sendgrid" | "smtp";
|
||||
|
||||
const providerLabels: Record<ProviderType, string> = {
|
||||
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 (
|
||||
<BoxWithBorder padding={10} display="grid" alignItems="center" justifyContent="center">
|
||||
<Text>Loading</Text>
|
||||
</BoxWithBorder>
|
||||
);
|
||||
}
|
||||
|
||||
if (configurations.length === 0) {
|
||||
return <NoExistingConfigurations />;
|
||||
}
|
||||
|
||||
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 <Image alt="SMTP logo" src={smtp} height={20} width={20} />;
|
||||
case "sendgrid":
|
||||
return <Image alt="Sendgrid logo" src={sendgrid} height={20} width={20} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BoxWithBorder>
|
||||
<Box padding={defaultPadding} display="grid" gridTemplateColumns={4} gap={defaultPadding}>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
Provider
|
||||
</Text>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
Configuration name
|
||||
</Text>
|
||||
<Text variant="caption" color="textNeutralSubdued">
|
||||
Status
|
||||
</Text>
|
||||
<Box />
|
||||
{configurations.map((configuration) => (
|
||||
<React.Fragment key={configuration.id}>
|
||||
<Box display="flex" gap={defaultPadding}>
|
||||
{getProviderLogo(configuration)}
|
||||
<Text>{providerLabels[configuration.provider]}</Text>
|
||||
</Box>
|
||||
|
||||
<Text>{configuration.name}</Text>
|
||||
<ChipText
|
||||
content={configuration.active ? "Active" : "Inactive"}
|
||||
variant={configuration.active ? "success" : "error"}
|
||||
/>
|
||||
<Box display="flex" justifyContent="flex-end">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
push(getEditLink(configuration));
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button onClick={redirectToProvidersSelection}>Add provider</Button>
|
||||
</BoxFooter>
|
||||
</BoxWithBorder>
|
||||
);
|
||||
};
|
|
@ -11,24 +11,14 @@ interface ProviderSelectionBoxProps {
|
|||
|
||||
export const ProviderSelectionBox = (props: ProviderSelectionBoxProps) => {
|
||||
return (
|
||||
<BoxWithBorder display={"grid"} alignItems={"center"} justifyContent={"center"}>
|
||||
<Box
|
||||
padding={defaultPadding}
|
||||
display={"grid"}
|
||||
alignItems={"center"}
|
||||
justifyContent={"center"}
|
||||
>
|
||||
<BoxWithBorder display="grid" alignItems="center" justifyContent="center">
|
||||
<Box padding={defaultPadding} display="grid" alignItems="center" justifyContent="center">
|
||||
<Text variant="heading">{props.providerName}</Text>
|
||||
</Box>
|
||||
<Box
|
||||
padding={defaultPadding}
|
||||
display={"grid"}
|
||||
alignItems={"center"}
|
||||
justifyContent={"center"}
|
||||
>
|
||||
<Box padding={defaultPadding} display="grid" alignItems="center" justifyContent="center">
|
||||
<Text>{props.providerDescription}</Text>
|
||||
</Box>
|
||||
<BoxFooter justifyContent={"center"} alignItems={"center"}>
|
||||
<BoxFooter justifyContent="center" alignItems="center">
|
||||
<Button onClick={props.onClick}>Choose</Button>
|
||||
</BoxFooter>
|
||||
</BoxWithBorder>
|
||||
|
|
|
@ -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",
|
||||
};
|
||||
|
|
|
@ -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<typeof channelConfigurationSchema>;
|
||||
|
||||
export const updateChannelsInputSchema = channelConfigurationSchema.merge(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
);
|
||||
|
||||
export type UpdateChannelsInput = z.infer<typeof updateChannelsInputSchema>;
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
};
|
|
@ -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 (
|
||||
<Text>
|
||||
Configuration will be used with <Text variant="bodyStrong"> all</Text> channels.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === "exclude") {
|
||||
const leftChannels = availableChannels.filter((channel) => !channels.includes(channel));
|
||||
|
||||
if (!leftChannels.length) {
|
||||
return (
|
||||
<Text variant="bodyStrong">
|
||||
Theres no channel which will be used with this configuration.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text>
|
||||
Configuration will be used with channels:{" "}
|
||||
<Text variant="bodyStrong">{leftChannels.join(", ")}</Text>.
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
if (channels.length === 0) {
|
||||
return (
|
||||
<Text>
|
||||
<Text variant="bodyStrong">No channels assigned. The configuration will not be used!</Text>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Text>
|
||||
Configuration will be used with channels:{" "}
|
||||
<Text variant="bodyStrong">{channels.join(", ")}</Text>.
|
||||
</Text>
|
||||
);
|
||||
};
|
|
@ -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<UpdateChannelsInput>({
|
||||
defaultValues: {
|
||||
id: configurationId,
|
||||
...channelConfiguration,
|
||||
},
|
||||
resolver: zodResolver(updateChannelsInputSchema),
|
||||
});
|
||||
|
||||
const { data: channels } = trpcClient.channels.fetch.useQuery();
|
||||
|
||||
return (
|
||||
<SectionWithDescription
|
||||
title="Channels"
|
||||
description={
|
||||
<>
|
||||
<Text display="block">
|
||||
By default, provider will work for every channel. You can change this behavior with
|
||||
excluding or including strategy.
|
||||
</Text>
|
||||
<Text display="block">
|
||||
<Text variant="bodyStrong">Excluding</Text> - all current channels and new created
|
||||
channels will work, excluding selected
|
||||
</Text>
|
||||
<Text display="block">
|
||||
<Text variant="bodyStrong">Including</Text> - only selected channels will work, new
|
||||
created channels will not work
|
||||
</Text>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
onSubmit(data);
|
||||
})}
|
||||
>
|
||||
<BoxWithBorder>
|
||||
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={defaultPadding}>
|
||||
<Box display="flex" flexDirection="column" gap={defaultPadding}>
|
||||
<Text variant="heading">Current behaviour</Text>
|
||||
<AssignedChannelsMessage
|
||||
availableChannels={channels?.map((channel) => channel.slug) || []}
|
||||
channelConfiguration={channelConfiguration}
|
||||
/>
|
||||
<Text variant="heading">Settings</Text>
|
||||
<label>
|
||||
<input type="checkbox" {...register("override")} />
|
||||
<Text paddingLeft={defaultPadding}>Override channels</Text>
|
||||
</label>
|
||||
|
||||
<Controller
|
||||
name="mode"
|
||||
control={control}
|
||||
render={({ field: { onChange } }) => (
|
||||
<Switch
|
||||
defaultValue={channelConfiguration.mode}
|
||||
__maxWidth="max-content"
|
||||
onValueChange={onChange}
|
||||
>
|
||||
<Switch.Item id="1" value="restrict">
|
||||
<TableEditIcon size="medium" />
|
||||
<Text>Include</Text>
|
||||
</Switch.Item>
|
||||
<Switch.Item id="2" value="exclude">
|
||||
<ProductsIcons size="medium" />
|
||||
<Text>Exclude</Text>
|
||||
</Switch.Item>
|
||||
</Switch>
|
||||
)}
|
||||
/>
|
||||
<Multiselect
|
||||
control={control}
|
||||
label="Channels"
|
||||
size="large"
|
||||
name="channels"
|
||||
options={
|
||||
channels?.map((channel) => ({
|
||||
label: channel.name,
|
||||
value: channel.slug,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button type="submit">Save provider</Button>
|
||||
</BoxFooter>
|
||||
</BoxWithBorder>
|
||||
</form>
|
||||
</SectionWithDescription>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<MjmlConfiguration, "id" | "events">) => {
|
||||
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,
|
||||
};
|
|
@ -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),
|
||||
});
|
|
@ -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[];
|
||||
};
|
|
@ -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}`,
|
||||
};
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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<typeof sendgridConfigurationIdInputSchema>;
|
||||
|
||||
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<typeof sendgridGetConfigurationsInputSchema>;
|
||||
|
||||
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<typeof sendgridUpdateBasicInformationSchema>;
|
||||
|
||||
export const sendgridUpdateApiConnectionSchema = sendgridConfigurationSchema.pick({
|
||||
id: true,
|
||||
apiKey: true,
|
||||
sandboxMode: true,
|
||||
});
|
||||
|
||||
export type SendgridUpdateApiConnection = z.infer<typeof sendgridUpdateApiConnectionSchema>;
|
||||
|
||||
export const sendgridUpdateSenderSchema = sendgridConfigurationSchema.pick({
|
||||
id: true,
|
||||
sender: true,
|
||||
});
|
||||
export type SendgridUpdateSender = z.infer<typeof sendgridUpdateSenderSchema>;
|
||||
|
||||
export const sendgridUpdateChannelsSchema = channelConfigurationSchema.merge(
|
||||
sendgridConfigurationSchema.pick({
|
||||
id: true,
|
||||
})
|
||||
);
|
||||
|
||||
export type SendgridUpdateChannels = z.infer<typeof sendgridUpdateChannelsSchema>;
|
||||
|
||||
export const sendgridUpdateEventSchema = sendgridConfigurationEventSchema.merge(
|
||||
sendgridConfigurationSchema.pick({
|
||||
id: true,
|
||||
})
|
||||
);
|
||||
|
||||
export type SendgridUpdateEvent = z.infer<typeof sendgridUpdateEventSchema>;
|
||||
|
|
|
@ -0,0 +1,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<typeof sendgridConfigurationEventSchema>;
|
||||
|
||||
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<typeof sendgridConfigurationSchema>;
|
||||
|
||||
export const sendgridConfigSchema = z.object({
|
||||
configurations: z.array(sendgridConfigurationSchema),
|
||||
});
|
||||
|
||||
export type SendgridConfig = z.infer<typeof sendgridConfigSchema>;
|
|
@ -1,26 +0,0 @@
|
|||
import { MessageEventTypes } from "../../event-handlers/message-event-types";
|
||||
|
||||
export interface SendgridEventConfiguration {
|
||||
active: boolean;
|
||||
eventType: MessageEventTypes;
|
||||
template: string;
|
||||
}
|
||||
|
||||
export interface SendgridConfiguration {
|
||||
id: string;
|
||||
active: boolean;
|
||||
configurationName: string;
|
||||
sandboxMode: boolean;
|
||||
senderName?: string;
|
||||
senderEmail?: string;
|
||||
apiKey: string;
|
||||
events: SendgridEventConfiguration[];
|
||||
channels: {
|
||||
excludedFrom: string[];
|
||||
restrictedTo: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export type SendgridConfig = {
|
||||
configurations: SendgridConfiguration[];
|
||||
};
|
|
@ -1,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;
|
||||
}),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<SendgridUpdateApiConnection>({
|
||||
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<SendgridUpdateApiConnection>({
|
||||
error,
|
||||
setError,
|
||||
notifyError,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionWithDescription title="API Connection">
|
||||
<BoxWithBorder>
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={10}>
|
||||
<Input
|
||||
label="API Key"
|
||||
name="apiKey"
|
||||
control={control}
|
||||
helperText="Name of the configuration, for example 'Production' or 'Test'"
|
||||
/>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" {...register("sandboxMode")} />
|
||||
<Text paddingLeft={defaultPadding}>Sandbox mode</Text>
|
||||
</label>
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button type="submit">Save provider</Button>
|
||||
</BoxFooter>
|
||||
</form>
|
||||
</BoxWithBorder>
|
||||
</SectionWithDescription>
|
||||
);
|
||||
};
|
|
@ -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<SendgridUpdateBasicInformation>({
|
||||
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<SendgridUpdateBasicInformation>({
|
||||
error,
|
||||
setError,
|
||||
notifyError,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionWithDescription
|
||||
title="Connect Sendgrid"
|
||||
description={
|
||||
<Text>
|
||||
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.
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<BoxWithBorder>
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={10}>
|
||||
<Input
|
||||
label="Configuration name"
|
||||
control={control}
|
||||
name="name"
|
||||
helperText="Name of the configuration, for example 'Production' or 'Test'"
|
||||
/>
|
||||
<label>
|
||||
<input type="checkbox" placeholder="Enabled" {...register("active")} />
|
||||
<Text paddingLeft={defaultPadding}>Active</Text>
|
||||
</label>
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button type="submit">Save provider</Button>
|
||||
</BoxFooter>
|
||||
</form>
|
||||
</BoxWithBorder>
|
||||
</SectionWithDescription>
|
||||
);
|
||||
};
|
|
@ -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<UpdateChannelsInput>({ error, notifyError });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<UniversalChannelsSection
|
||||
configurationId={configuration.id}
|
||||
channelConfiguration={configuration.channels}
|
||||
onSubmit={mutate}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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<SendgridGetConfigurationIdInput>({
|
||||
defaultValues: {
|
||||
id: configuration.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate } = trpcClient.sendgridConfiguration.deleteConfiguration.useMutation({
|
||||
onSuccess: async () => {
|
||||
notifySuccess("Configuration removed");
|
||||
replace(appUrls.configuration());
|
||||
},
|
||||
onError(error) {
|
||||
setBackendErrors<SendgridGetConfigurationIdInput>({ error, setError, notifyError });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionWithDescription title="Danger zone">
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<BoxWithBorder backgroundColor="surfaceCriticalSubdued" borderColor="criticalSubdued">
|
||||
<Box padding={defaultPadding}>
|
||||
<Text variant="heading" display="block">
|
||||
Remove provider
|
||||
</Text>
|
||||
<Text display="block">You can remove provider configuration.</Text>
|
||||
<Text display="block">
|
||||
This operation will remove all settings related to this configuration. Data will be
|
||||
permanently removed from the App.{" "}
|
||||
</Text>
|
||||
<Text display="block">This operation cant be undone.</Text>
|
||||
<Text display="block">You still can create new configuration.</Text>
|
||||
</Box>
|
||||
<BoxFooter borderColor="criticalSubdued">
|
||||
<Button
|
||||
color="textNeutralSubdued"
|
||||
backgroundColor="interactiveCriticalDefault"
|
||||
type="submit"
|
||||
>
|
||||
Remove provider
|
||||
</Button>
|
||||
</BoxFooter>
|
||||
</BoxWithBorder>
|
||||
</form>
|
||||
</SectionWithDescription>
|
||||
);
|
||||
};
|
|
@ -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<SendgridUpdateEvent>({
|
||||
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<SendgridUpdateEvent>({ error, setError, notifyError });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<BoxWithBorder>
|
||||
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={defaultPadding}>
|
||||
<Text variant="heading">{event.eventType}</Text>
|
||||
{templatesChoices?.length ? (
|
||||
<Combobox
|
||||
name="template"
|
||||
control={control}
|
||||
label="Template"
|
||||
options={templatesChoices.map((sender) => ({
|
||||
label: sender.label,
|
||||
value: sender.value,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<Combobox name="template" control={control} label="Template" options={[]} />
|
||||
)}
|
||||
|
||||
<label>
|
||||
<input type="checkbox" placeholder="Enabled" {...register("active")} />
|
||||
<Text paddingLeft={defaultPadding}>Active</Text>
|
||||
</label>
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button type="submit">Save event</Button>
|
||||
</BoxFooter>
|
||||
</BoxWithBorder>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
interface SendgridEventsSectionProps {
|
||||
configuration: SendgridConfiguration;
|
||||
}
|
||||
|
||||
export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionProps) => {
|
||||
return (
|
||||
<SectionWithDescription
|
||||
title="Events"
|
||||
description={
|
||||
<>
|
||||
<Text display="block">
|
||||
Provide unique name for your configuration - you can create more than one. For example -
|
||||
production and development.
|
||||
</Text>
|
||||
<Text display="block">Then, pass your API Key. Obtain it here.</Text>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box display="flex" flexDirection="column" gap={defaultPadding}>
|
||||
{configuration.events.map((event) => (
|
||||
<EventBox key={event.eventType} configuration={configuration} event={event} />
|
||||
))}
|
||||
</Box>
|
||||
</SectionWithDescription>
|
||||
);
|
||||
};
|
|
@ -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<SendgridUpdateSender>({
|
||||
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<SendgridUpdateSender>({ error, setError, notifyError });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionWithDescription title="Sender">
|
||||
<BoxWithBorder>
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={10}>
|
||||
{sendersChoices?.length ? (
|
||||
<Combobox
|
||||
name="sender"
|
||||
control={control}
|
||||
label="Sender"
|
||||
options={sendersChoices.map((sender) => ({
|
||||
label: sender.label,
|
||||
value: sender.value,
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<Combobox name="sender" control={control} label="Sender" options={[]} />
|
||||
)}
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button type="submit">Save provider</Button>
|
||||
</BoxFooter>
|
||||
</form>
|
||||
</BoxWithBorder>
|
||||
</SectionWithDescription>
|
||||
);
|
||||
};
|
|
@ -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}`,
|
||||
};
|
||||
|
|
|
@ -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<MjmlConfiguration, "id" | "events">) {
|
||||
async createConfiguration(config: Omit<SmtpConfiguration, "id" | "events">) {
|
||||
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 });
|
||||
|
|
@ -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<SmtpConfiguration, "id" | "events">) => {
|
||||
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,
|
||||
};
|
|
@ -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<typeof smtpCreateConfigurationInputSchema>;
|
||||
|
||||
export const smtpConfigurationIdInputSchema = smtpConfigurationSchema.pick({
|
||||
id: true,
|
||||
});
|
||||
|
||||
export type SmtpGetConfigurationIdInput = z.infer<typeof smtpConfigurationIdInputSchema>;
|
||||
|
||||
export const smtpGetConfigurationsInputSchema = z
|
||||
.object({
|
||||
ids: z.array(z.string()).optional(),
|
||||
active: z.boolean().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
export type SmtpGetConfigurationsInput = z.infer<typeof smtpGetConfigurationsInputSchema>;
|
||||
|
||||
export const smtpUpdateBasicInformationSchema = smtpConfigurationSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
active: true,
|
||||
});
|
||||
|
||||
export type SmtpUpdateBasicInformation = z.infer<typeof smtpUpdateBasicInformationSchema>;
|
||||
|
||||
export const smtpUpdateSmtpSchema = smtpConfigurationSchema.pick({
|
||||
id: true,
|
||||
smtpHost: true,
|
||||
smtpPort: true,
|
||||
smtpPassword: true,
|
||||
smtpUser: true,
|
||||
encryption: true,
|
||||
});
|
||||
|
||||
export type SmtpUpdateSmtp = z.infer<typeof smtpUpdateSmtpSchema>;
|
||||
|
||||
export const smtpUpdateSenderSchema = smtpConfigurationSchema.pick({
|
||||
id: true,
|
||||
senderEmail: true,
|
||||
senderName: true,
|
||||
});
|
||||
|
||||
export type SmtpUpdateSender = z.infer<typeof smtpUpdateSenderSchema>;
|
||||
|
||||
export const smtpUpdateChannelsSchema = channelConfigurationSchema.merge(
|
||||
smtpConfigurationSchema.pick({
|
||||
id: true,
|
||||
})
|
||||
);
|
||||
|
||||
export type SmtpUpdateChannels = z.infer<typeof smtpUpdateChannelsSchema>;
|
||||
|
||||
export const smtpUpdateEventSchema = smtpConfigurationEventSchema.merge(
|
||||
smtpConfigurationSchema.pick({
|
||||
id: true,
|
||||
})
|
||||
);
|
||||
|
||||
export type SmtpUpdateEvent = z.infer<typeof smtpUpdateEventSchema>;
|
||||
|
||||
export const smtpGetEventConfigurationInputSchema = smtpConfigurationIdInputSchema.merge(
|
||||
z.object({
|
||||
eventType: z.enum(messageEventTypes),
|
||||
})
|
||||
);
|
||||
|
||||
export type SmtpGetEventConfigurationInput = z.infer<typeof smtpGetEventConfigurationInputSchema>;
|
||||
|
||||
export const smtpUpdateEventConfigurationInputSchema = smtpConfigurationIdInputSchema.merge(
|
||||
smtpConfigurationEventSchema
|
||||
);
|
||||
|
||||
export type SmtpUpdateEventConfigurationInput = z.infer<
|
||||
typeof smtpUpdateEventConfigurationInputSchema
|
||||
>;
|
|
@ -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<typeof smtpConfigurationEventSchema>;
|
||||
|
||||
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<typeof smtpConfigurationSchema>;
|
||||
|
||||
export const smtpConfigSchema = z.object({
|
||||
configurations: z.array(smtpConfigurationSchema),
|
||||
});
|
||||
|
||||
export type SmtpConfig = z.infer<typeof smtpConfigSchema>;
|
|
@ -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;
|
||||
}),
|
||||
});
|
|
@ -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<void>;
|
||||
getConfig(): Promise<MjmlConfig | undefined>;
|
||||
export interface SmtpConfigurator {
|
||||
setConfig(config: SmtpConfig): Promise<void>;
|
||||
getConfig(): Promise<SmtpConfig | undefined>;
|
||||
}
|
||||
|
||||
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<MjmlConfig | undefined> {
|
||||
getConfig(): Promise<SmtpConfig | undefined> {
|
||||
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<void> {
|
||||
setConfig(config: SmtpConfig): Promise<void> {
|
||||
return this.metadataManager.set({
|
||||
key: this.metadataKey,
|
||||
value: JSON.stringify(config),
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
42
apps/emails-and-messages/src/modules/smtp/ui/code-edtor.tsx
Normal file
42
apps/emails-and-messages/src/modules/smtp/ui/code-edtor.tsx
Normal file
|
@ -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 (
|
||||
<>
|
||||
<Editor
|
||||
height="600px"
|
||||
value={value}
|
||||
theme={theme === "defaultDark" ? "vs-dark" : "vs-light"}
|
||||
defaultLanguage={language}
|
||||
defaultValue={initialTemplate}
|
||||
onMount={handleEditorDidMount}
|
||||
onChange={handleOnChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
163
apps/emails-and-messages/src/modules/smtp/ui/event-form.tsx
Normal file
163
apps/emails-and-messages/src/modules/smtp/ui/event-form.tsx
Normal file
|
@ -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<SmtpUpdateEventConfigurationInput>(
|
||||
{
|
||||
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<SmtpUpdateEventConfigurationInput>({
|
||||
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<string>(
|
||||
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 (
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Box display="flex" flexDirection="column" gap={defaultPadding}>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text variant="hero">Edit template</Text>
|
||||
<Button type="submit">Save</Button>
|
||||
</Box>
|
||||
<Box display="grid" gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
||||
<Input control={control} name="subject" label="Subject" />
|
||||
</Box>
|
||||
|
||||
<Box display="grid" gridTemplateColumns={{ desktop: 5, mobile: 1 }} gap={defaultPadding}>
|
||||
<Box
|
||||
gridColumnStart={{ desktop: "1", mobile: "1" }}
|
||||
gridColumnEnd={{ desktop: "3", mobile: "6" }}
|
||||
>
|
||||
<Controller
|
||||
control={control}
|
||||
name="template"
|
||||
render={({ field: { value, onChange } }) => {
|
||||
return (
|
||||
<CodeEditor
|
||||
initialTemplate={value}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
language="xml"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
gridColumnStart={{ desktop: "3", mobile: "1" }}
|
||||
gridColumnEnd={{ desktop: "5", mobile: "6" }}
|
||||
>
|
||||
<CodeEditor
|
||||
initialTemplate={payload}
|
||||
value={payload}
|
||||
onChange={setPayload}
|
||||
language="json"
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
gridColumnStart={{ desktop: "5", mobile: "1" }}
|
||||
gridColumnEnd={{ desktop: "6", mobile: "6" }}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
gap={defaultPadding}
|
||||
>
|
||||
<Text variant="heading" as="p">
|
||||
Subject: {lastValidRenderedSubject}
|
||||
</Text>
|
||||
<MjmlPreview value={lastValidRenderedTemplate} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
value?: string;
|
||||
};
|
||||
|
||||
export const MjmlPreview = ({ value }: Props) => {
|
||||
return (
|
||||
<>
|
||||
{value?.length ? (
|
||||
<div dangerouslySetInnerHTML={{ __html: value }} />
|
||||
) : (
|
||||
<p>No template preview</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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<SmtpUpdateBasicInformation>({
|
||||
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<SmtpUpdateBasicInformation>({
|
||||
error,
|
||||
setError,
|
||||
notifyError,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionWithDescription
|
||||
title="Connect SMTP"
|
||||
description={
|
||||
<Text>
|
||||
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.
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<BoxWithBorder>
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={10}>
|
||||
<Input
|
||||
name="name"
|
||||
label="Configuration name"
|
||||
control={control}
|
||||
helperText="Name of the configuration, for example 'Production' or 'Test'"
|
||||
/>
|
||||
<label>
|
||||
<input type="checkbox" placeholder="Enabled" {...register("active")} />
|
||||
<Text paddingLeft={defaultPadding}>Active</Text>
|
||||
</label>
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button type="submit">Save provider</Button>
|
||||
</BoxFooter>
|
||||
</form>
|
||||
</BoxWithBorder>
|
||||
</SectionWithDescription>
|
||||
);
|
||||
};
|
|
@ -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<UpdateChannelsInput>({
|
||||
error,
|
||||
notifyError,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<UniversalChannelsSection
|
||||
configurationId={configuration.id}
|
||||
channelConfiguration={configuration.channels}
|
||||
onSubmit={mutate}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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<SmtpGetConfigurationIdInput>({
|
||||
defaultValues: {
|
||||
id: configuration.id,
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate } = trpcClient.smtpConfiguration.deleteConfiguration.useMutation({
|
||||
onSuccess: async () => {
|
||||
notifySuccess("Configuration removed");
|
||||
replace("/configuration");
|
||||
},
|
||||
onError(error) {
|
||||
setBackendErrors<SmtpGetConfigurationIdInput>({
|
||||
error,
|
||||
setError,
|
||||
notifyError,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionWithDescription title="Danger zone">
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<BoxWithBorder backgroundColor="surfaceCriticalSubdued" borderColor="criticalSubdued">
|
||||
<Box padding={defaultPadding}>
|
||||
<Text variant="heading" as="h1">
|
||||
Remove provider
|
||||
</Text>
|
||||
<Text as="p">You can remove provider configuration.</Text>
|
||||
<Text as="p">
|
||||
This operation will remove all settings related to this configuration. Data will be
|
||||
permanently removed from the App.
|
||||
</Text>
|
||||
<Text as="p">This operation cant be undone.</Text>
|
||||
<Text as="p">You still can create new configuration.</Text>
|
||||
</Box>
|
||||
<BoxFooter borderColor="criticalSubdued">
|
||||
<Button
|
||||
color="textNeutralSubdued"
|
||||
backgroundColor="interactiveCriticalDefault"
|
||||
type="submit"
|
||||
>
|
||||
Remove provider
|
||||
</Button>
|
||||
</BoxFooter>
|
||||
</BoxWithBorder>
|
||||
</form>
|
||||
</SectionWithDescription>
|
||||
);
|
||||
};
|
|
@ -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<SmtpUpdateEvent>({
|
||||
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<SmtpUpdateEvent>({
|
||||
error,
|
||||
setError,
|
||||
notifyError,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<BoxWithBorder>
|
||||
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={defaultPadding}>
|
||||
<Text variant="heading">{event.eventType}</Text>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
router.push(smtpUrls.eventConfiguration(configuration.id, event.eventType));
|
||||
}}
|
||||
>
|
||||
Edit template
|
||||
</Button>
|
||||
|
||||
<label>
|
||||
<input type="checkbox" placeholder="Enabled" {...register("active")} />
|
||||
<Text paddingLeft={defaultPadding}>Active</Text>
|
||||
</label>
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button type="submit">Save event</Button>
|
||||
</BoxFooter>
|
||||
</BoxWithBorder>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
interface SmtpEventsSectionProps {
|
||||
configuration: SmtpConfiguration;
|
||||
}
|
||||
|
||||
export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) => {
|
||||
return (
|
||||
<SectionWithDescription
|
||||
title="Events"
|
||||
description={
|
||||
<>
|
||||
<Text display="block">
|
||||
Provide unique name for your configuration - you can create more than one. For example -
|
||||
production and development.
|
||||
</Text>
|
||||
<Text display="block">Then, pass your API Key. Obtain it here.</Text>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box display="flex" flexDirection="column" gap={defaultPadding}>
|
||||
{configuration.events.map((event) => (
|
||||
<EventBox key={event.eventType} configuration={configuration} event={event} />
|
||||
))}
|
||||
</Box>
|
||||
</SectionWithDescription>
|
||||
);
|
||||
};
|
109
apps/emails-and-messages/src/modules/smtp/ui/smtp-section.tsx
Normal file
109
apps/emails-and-messages/src/modules/smtp/ui/smtp-section.tsx
Normal file
|
@ -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<SmtpUpdateSmtp>({
|
||||
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<SmtpUpdateSmtp>({
|
||||
error,
|
||||
setError,
|
||||
notifyError,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionWithDescription
|
||||
title="Connect SMTP server"
|
||||
description={<Text>Provide SMTP server configuration</Text>}
|
||||
>
|
||||
<BoxWithBorder>
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={10}>
|
||||
<Input
|
||||
label="Host"
|
||||
helperText="Server host that will be connected."
|
||||
control={control}
|
||||
name="smtpHost"
|
||||
/>
|
||||
<Input
|
||||
label="Port"
|
||||
name="smtpPort"
|
||||
control={control}
|
||||
helperText="Port that will be used"
|
||||
/>
|
||||
<Input
|
||||
label="User"
|
||||
name="smtpUser"
|
||||
control={control}
|
||||
helperText="User for the SMTP server connection"
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
name="smtpPassword"
|
||||
control={control}
|
||||
helperText="Password for the SMTP server connection"
|
||||
/>
|
||||
|
||||
<Box display="flex" gap={defaultPadding}>
|
||||
<label>
|
||||
<input {...register("encryption")} type="radio" value="NONE" />
|
||||
<Text paddingLeft={defaultPadding}>No encryption</Text>
|
||||
</label>
|
||||
<label>
|
||||
<input {...register("encryption")} type="radio" value="SSL" />
|
||||
<Text paddingLeft={defaultPadding}>SSL</Text>
|
||||
</label>
|
||||
<label>
|
||||
<input {...register("encryption")} type="radio" value="TLS" />
|
||||
<Text paddingLeft={defaultPadding}>TLS</Text>
|
||||
</label>
|
||||
</Box>
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button type="submit">Save provider</Button>
|
||||
</BoxFooter>
|
||||
</form>
|
||||
</BoxWithBorder>
|
||||
</SectionWithDescription>
|
||||
);
|
||||
};
|
|
@ -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<SmtpUpdateSender>({
|
||||
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<SmtpUpdateSender>({
|
||||
error,
|
||||
setError,
|
||||
notifyError,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SectionWithDescription title="Sender">
|
||||
<BoxWithBorder>
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={10}>
|
||||
<Input
|
||||
label="Email"
|
||||
name="senderEmail"
|
||||
control={control}
|
||||
helperText="Email address that will be used as sender"
|
||||
/>
|
||||
<Input
|
||||
label="Name"
|
||||
name="senderName"
|
||||
control={control}
|
||||
helperText="Name that will be used as sender"
|
||||
/>
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button type="submit">Save provider</Button>
|
||||
</BoxFooter>
|
||||
</form>
|
||||
</BoxWithBorder>
|
||||
</SectionWithDescription>
|
||||
);
|
||||
};
|
8
apps/emails-and-messages/src/modules/smtp/urls.ts
Normal file
8
apps/emails-and-messages/src/modules/smtp/urls.ts
Normal file
|
@ -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}`,
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -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<Theme> = {
|
||||
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<Theme>; 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 (
|
||||
<NoSSRWrapper>
|
||||
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
||||
<StylesProvider generateClassName={generateClassName}>
|
||||
<ThemeProvider palettes={palettes} overrides={themeOverrides} ssr>
|
||||
<ThemeSynchronizer />
|
||||
<RoutePropagator />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</StylesProvider>
|
||||
<ThemeProvider defaultTheme="defaultLight">
|
||||
<ThemeSynchronizer />
|
||||
<RoutePropagator />
|
||||
<Component {...pageProps} />
|
||||
</ThemeProvider>
|
||||
</AppBridgeProvider>
|
||||
</NoSSRWrapper>
|
||||
);
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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 (
|
||||
<BasicLayout
|
||||
breadcrumbs={[
|
||||
{ name: "Configuration", href: appUrls.configuration() },
|
||||
{ name: "Add provider" },
|
||||
]}
|
||||
>
|
||||
<Box display="grid" gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
||||
<Box>
|
||||
<Text>Select and configure providers to connect Saleor with selected services.</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<SectionWithDescription title="Choose provider">
|
||||
<Box display="grid" gridTemplateColumns={2} gap={6}>
|
||||
<ProviderSelectionBox
|
||||
providerName="Sendgrid"
|
||||
providerDescription="Use dynamic templates created in Sendgrid dashboard to send messages. Event data will be forwarded to Sendgrid."
|
||||
onClick={() => push(sendgridUrls.newConfiguration())}
|
||||
/>
|
||||
|
||||
<ProviderSelectionBox
|
||||
providerName="SMTP"
|
||||
providerDescription="Provide your own SMTP credentials and map Saleor event to custom MJML templates."
|
||||
onClick={() => push(smtpUrls.newConfiguration())}
|
||||
/>
|
||||
</Box>
|
||||
</SectionWithDescription>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChooseProviderPage;
|
61
apps/emails-and-messages/src/pages/configuration/index.tsx
Normal file
61
apps/emails-and-messages/src/pages/configuration/index.tsx
Normal file
|
@ -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 (
|
||||
<BasicLayout breadcrumbs={[{ name: "Configuration", href: appUrls.configuration() }]}>
|
||||
<Box display="grid" gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
||||
<Box>
|
||||
<Text>
|
||||
Configure Emails & Messages app to deliver Saleor Events webhooks to various messaging
|
||||
providers
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<SectionWithDescription
|
||||
title="Messaging providers"
|
||||
description={
|
||||
<Text>
|
||||
Manage providers configuration to connect Saleor events with 3rd party services.
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<MessagingProvidersBox configurations={data || []} isLoading={isLoading} />
|
||||
</SectionWithDescription>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigurationPage;
|
|
@ -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 (
|
||||
<BasicLayout
|
||||
breadcrumbs={[
|
||||
{ name: "Configuration", href: appUrls.configuration() },
|
||||
{ name: "Sendgrid provider" },
|
||||
{ name: "..." },
|
||||
]}
|
||||
>
|
||||
<Text variant="hero">Loading...</Text>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const NotFoundView = () => {
|
||||
return (
|
||||
<BasicLayout
|
||||
breadcrumbs={[
|
||||
{ name: "Configuration", href: appUrls.configuration() },
|
||||
{ name: "Sendgrid provider" },
|
||||
{ name: "Not found" },
|
||||
]}
|
||||
>
|
||||
<Text variant="hero">Could not find the requested configuration.</Text>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
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 <LoadingView />;
|
||||
}
|
||||
|
||||
if (!configuration) {
|
||||
return <NotFoundView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BasicLayout
|
||||
breadcrumbs={[
|
||||
{ name: "Configuration", href: appUrls.configuration() },
|
||||
{
|
||||
name: `Sendgrid: ${configuration.name}`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Box display="grid" gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
||||
<Box>
|
||||
<Text>Connect Sendgrid with Saleor.</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<SendgridBasicInformationSection configuration={configuration} />
|
||||
<ApiConnectionSection configuration={configuration} />
|
||||
<SendgridSenderSection configuration={configuration} />
|
||||
<SendgridEventsSection configuration={configuration} />
|
||||
<SendgridChannelsSection configuration={configuration} />
|
||||
<SendgridDangerousSection configuration={configuration} />
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditSendgridConfigurationPage;
|
|
@ -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<SendgridCreateConfigurationInput>();
|
||||
|
||||
const { mutate: createConfiguration } =
|
||||
trpcClient.sendgridConfiguration.createConfiguration.useMutation({
|
||||
onSuccess: async (data, variables) => {
|
||||
notifySuccess("Configuration saved");
|
||||
router.push(sendgridUrls.configuration(data.id));
|
||||
},
|
||||
onError(error) {
|
||||
setBackendErrors<SendgridCreateConfigurationInput>({
|
||||
error,
|
||||
setError,
|
||||
notifyError,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<BasicLayout
|
||||
breadcrumbs={[
|
||||
{ name: "Configuration", href: appUrls.configuration() },
|
||||
{ name: "Add provider" },
|
||||
{ name: "Sendgrid" },
|
||||
]}
|
||||
>
|
||||
<Box display="grid" gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
||||
<Box>
|
||||
<Text>Connect Sendgrid with Saleor.</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<SectionWithDescription
|
||||
title="Connect Sendgrid"
|
||||
description={
|
||||
<Text>
|
||||
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.
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<BoxWithBorder>
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
createConfiguration({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={10}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
render={({
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
formState: { errors },
|
||||
}) => (
|
||||
<Input
|
||||
label="Configuration name"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={!!error}
|
||||
helperText={
|
||||
error?.message ||
|
||||
"Name of the configuration, for example 'Production' or 'Test'"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="apiKey"
|
||||
control={control}
|
||||
render={({
|
||||
field: { onChange, value },
|
||||
fieldState: { error },
|
||||
formState: { errors },
|
||||
}) => (
|
||||
// TODO: add validation
|
||||
<Input
|
||||
label="API key"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={!!error}
|
||||
helperText={
|
||||
error?.message || "Your API key, ensure it has permission XYZ enabled"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button type="submit">Save provider</Button>
|
||||
</BoxFooter>
|
||||
</form>
|
||||
</BoxWithBorder>
|
||||
</SectionWithDescription>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewSendgridConfigurationPage;
|
|
@ -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 (
|
||||
<BasicLayout
|
||||
breadcrumbs={[
|
||||
{ name: "Configuration", href: appUrls.configuration() },
|
||||
{ name: "SMTP provider" },
|
||||
{ name: "..." },
|
||||
]}
|
||||
>
|
||||
<Text variant="hero">Loading...</Text>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const NotFoundView = () => {
|
||||
return (
|
||||
<BasicLayout
|
||||
breadcrumbs={[
|
||||
{ name: "Configuration", href: appUrls.configuration() },
|
||||
{ name: "SMTP provider" },
|
||||
{ name: "Not found" },
|
||||
]}
|
||||
>
|
||||
<Text variant="hero">Could not find the requested configuration.</Text>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
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 <LoadingView />;
|
||||
}
|
||||
|
||||
if (!configuration) {
|
||||
return <NotFoundView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BasicLayout
|
||||
breadcrumbs={[
|
||||
{ name: "Configuration", href: appUrls.configuration() },
|
||||
{ name: `SMTP: ${configuration.name}` },
|
||||
]}
|
||||
>
|
||||
<Box display="grid" gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
||||
<Box>
|
||||
<Text>Connect SMTP with Saleor.</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<SmtpBasicInformationSection configuration={configuration} />
|
||||
<SmtpSection configuration={configuration} />
|
||||
<SmtpSenderSection configuration={configuration} />
|
||||
<SmtpEventsSection configuration={configuration} />
|
||||
<SmtpChannelsSection configuration={configuration} />
|
||||
<SmtpDangerousSection configuration={configuration} />
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditSmtpConfigurationPage;
|
|
@ -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 (
|
||||
<BasicLayout
|
||||
breadcrumbs={[{ name: "Configuration", href: appUrls.configuration() }, { name: "..." }]}
|
||||
>
|
||||
<Text variant="hero">Loading...</Text>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const NotFoundView = () => {
|
||||
return (
|
||||
<BasicLayout
|
||||
breadcrumbs={[
|
||||
{ name: "Configuration", href: appUrls.configuration() },
|
||||
{ name: "SMTP provider" },
|
||||
{ name: "Not found" },
|
||||
]}
|
||||
>
|
||||
<Text variant="hero">Could not find the requested configuration.</Text>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
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 <LoadingView />;
|
||||
}
|
||||
|
||||
if (!configuration) {
|
||||
return <NotFoundView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<BasicLayout
|
||||
breadcrumbs={[
|
||||
{ name: "Configuration", href: appUrls.configuration() },
|
||||
{ name: `SMTP: ${configuration.name}`, href: smtpUrls.configuration(configurationId) },
|
||||
{ name: eventType },
|
||||
]}
|
||||
>
|
||||
<Box display="flex" flexDirection="column" gap={13}>
|
||||
<Text as="p">
|
||||
Edit template for {eventType} event. You can learn more about MJML{" "}
|
||||
<TextLink href="https://mjml.io/" openNewTab={true}>
|
||||
here
|
||||
</TextLink>
|
||||
.
|
||||
</Text>
|
||||
<EventForm configuration={configuration} eventType={eventType} />
|
||||
</Box>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditSmtpEventPage;
|
134
apps/emails-and-messages/src/pages/configuration/smtp/new.tsx
Normal file
134
apps/emails-and-messages/src/pages/configuration/smtp/new.tsx
Normal file
|
@ -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<SmtpCreateConfigurationInput>({
|
||||
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<SmtpCreateConfigurationInput>({
|
||||
error,
|
||||
setError,
|
||||
notifyError,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<BasicLayout
|
||||
breadcrumbs={[
|
||||
{ name: "Configuration", href: appUrls.configuration() },
|
||||
{ name: "Add provider" },
|
||||
{ name: "SMTP" },
|
||||
]}
|
||||
>
|
||||
<Box display="grid" gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
||||
<Box>
|
||||
<Text>Connect SMTP with Saleor.</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<SectionWithDescription
|
||||
title="Connect SMTP"
|
||||
description={
|
||||
<Text>
|
||||
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.
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<BoxWithBorder>
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
mutate({
|
||||
...data,
|
||||
});
|
||||
})}
|
||||
>
|
||||
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={10}>
|
||||
<Input
|
||||
name="name"
|
||||
label="Configuration name"
|
||||
control={control}
|
||||
helperText="Name of the configuration, for example 'Production' or 'Test'"
|
||||
/>
|
||||
<Divider />
|
||||
<Text variant="heading">SMTP server connection</Text>
|
||||
<Input
|
||||
label="Host"
|
||||
helperText="Server host that will be connected."
|
||||
control={control}
|
||||
name="smtpHost"
|
||||
/>
|
||||
<Input
|
||||
label="Port"
|
||||
name="smtpPort"
|
||||
control={control}
|
||||
helperText="Port that will be used"
|
||||
/>
|
||||
<Input
|
||||
label="User"
|
||||
name="smtpUser"
|
||||
control={control}
|
||||
helperText="User for the SMTP server connection"
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
name="smtpPassword"
|
||||
control={control}
|
||||
helperText="Password for the SMTP server connection"
|
||||
/>
|
||||
|
||||
<Box display="flex" gap={defaultPadding}>
|
||||
<label>
|
||||
<input {...register("encryption")} type="radio" value="NONE" />
|
||||
<Text paddingLeft={defaultPadding}>No encryption</Text>
|
||||
</label>
|
||||
<label>
|
||||
<input {...register("encryption")} type="radio" value="SSL" />
|
||||
<Text paddingLeft={defaultPadding}>SSL</Text>
|
||||
</label>
|
||||
<label>
|
||||
<input {...register("encryption")} type="radio" value="TLS" />
|
||||
<Text paddingLeft={defaultPadding}>TLS</Text>
|
||||
</label>
|
||||
</Box>
|
||||
</Box>
|
||||
<BoxFooter>
|
||||
<Button type="submit">Save provider</Button>
|
||||
</BoxFooter>
|
||||
</form>
|
||||
</BoxWithBorder>
|
||||
</SectionWithDescription>
|
||||
</BasicLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewSmtpConfigurationPage;
|
|
@ -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 <LinearProgress />;
|
||||
return <p>Loading</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.29289 6.29289C8.90237 6.68342 8.90237 7.31658 9.29289 7.70711L13.5858 12L9.29289 16.2929C8.90237 16.6834 8.90237 17.3166 9.29289 17.7071C9.68342 18.0976 10.3166 18.0976 10.7071 17.7071L15.7071 12.7071C16.0976 12.3166 16.0976 11.6834 15.7071 11.2929L10.7071 6.29289C10.3166 5.90237 9.68342 5.90237 9.29289 6.29289Z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
After Width: | Height: | Size: 492 B |
BIN
apps/emails-and-messages/src/public/sendgrid.png
Normal file
BIN
apps/emails-and-messages/src/public/sendgrid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 332 B |
9
apps/emails-and-messages/src/public/smtp.svg
Normal file
9
apps/emails-and-messages/src/public/smtp.svg
Normal file
|
@ -0,0 +1,9 @@
|
|||
<svg width="20" height="19" viewBox="0 0 20 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 4.625C4.55228 4.625 5 4.17728 5 3.625C5 3.07272 4.55228 2.625 4 2.625C3.44772 2.625 3 3.07272 3 3.625C3 4.17728 3.44772 4.625 4 4.625Z" fill="#4C5C6D"/>
|
||||
<path d="M9 3.625C9 4.17728 8.55229 4.625 8 4.625C7.44772 4.625 7 4.17728 7 3.625C7 3.07272 7.44772 2.625 8 2.625C8.55229 2.625 9 3.07272 9 3.625Z" fill="#4C5C6D"/>
|
||||
<path d="M12 4.625C12.5523 4.625 13 4.17728 13 3.625C13 3.07272 12.5523 2.625 12 2.625C11.4477 2.625 11 3.07272 11 3.625C11 4.17728 11.4477 4.625 12 4.625Z" fill="#4C5C6D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 0C1.10254 0 0.375 0.727538 0.375 1.625V5.625C0.375 6.52246 1.10254 7.25 2 7.25H18C18.8975 7.25 19.625 6.52246 19.625 5.625V1.625C19.625 0.727537 18.8975 0 18 0H2ZM1.625 1.625C1.625 1.41789 1.79289 1.25 2 1.25H18C18.2071 1.25 18.375 1.41789 18.375 1.625V5.625C18.375 5.83211 18.2071 6 18 6H2C1.79289 6 1.625 5.83211 1.625 5.625V1.625Z" fill="#4C5C6D"/>
|
||||
<path d="M5.5 9C5.84518 9 6.125 9.27982 6.125 9.625C6.125 9.97018 5.84518 10.25 5.5 10.25H2.00001C1.7929 10.25 1.62501 10.4179 1.62501 10.625V14.625C1.62501 14.8321 1.7929 15 2.00001 15H5.5C5.84518 15 6.125 15.2798 6.125 15.625C6.125 15.9702 5.84518 16.25 5.5 16.25H2.00001C1.10254 16.25 0.375008 15.5225 0.375008 14.625V10.625C0.375008 9.72754 1.10254 9 2.00001 9H5.5Z" fill="#4C5C6D"/>
|
||||
<path d="M5 12.625C5 13.1773 4.55228 13.625 4 13.625C3.44772 13.625 3 13.1773 3 12.625C3 12.0727 3.44772 11.625 4 11.625C4.55228 11.625 5 12.0727 5 12.625Z" fill="#4C5C6D"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.375 10.625C7.375 9.72754 8.10254 9 9 9H18C18.8975 9 19.625 9.72754 19.625 10.625V11.6053C19.6254 11.6183 19.6254 11.6313 19.625 11.6443V16.625C19.625 17.5225 18.8975 18.25 18 18.25H9C8.10254 18.25 7.375 17.5225 7.375 16.625V11.6251C7.375 11.6249 7.375 11.6253 7.375 11.6251V10.625ZM8.625 12.5957V16.625C8.625 16.8321 8.79289 17 9 17H18C18.2071 17 18.375 16.8321 18.375 16.625V12.5958L14.1726 14.506C13.7453 14.7002 13.255 14.7002 12.8277 14.506L8.625 12.5957ZM18.375 11.2228L13.6553 13.3681C13.5567 13.4129 13.4436 13.4129 13.345 13.3681L8.625 11.2226V10.625C8.625 10.4179 8.79289 10.25 9 10.25H18C18.2071 10.25 18.375 10.4179 18.375 10.625V11.2228Z" fill="#4C5C6D"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -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;
|
||||
}
|
||||
|
|
359
pnpm-lock.yaml
359
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'}
|
||||
|
|
Loading…
Reference in a new issue