WIP
This commit is contained in:
parent
a163b04d7a
commit
b1bc467da6
24 changed files with 1052 additions and 112 deletions
|
@ -19,7 +19,7 @@
|
||||||
"@monaco-editor/react": "^4.4.6",
|
"@monaco-editor/react": "^4.4.6",
|
||||||
"@saleor/app-sdk": "0.37.3",
|
"@saleor/app-sdk": "0.37.3",
|
||||||
"@saleor/apps-shared": "workspace:*",
|
"@saleor/apps-shared": "workspace:*",
|
||||||
"@saleor/macaw-ui": "0.8.0-pre.72",
|
"@saleor/macaw-ui": "0.8.0-pre.76",
|
||||||
"@sendgrid/client": "^7.7.0",
|
"@sendgrid/client": "^7.7.0",
|
||||||
"@sendgrid/mail": "^7.7.0",
|
"@sendgrid/mail": "^7.7.0",
|
||||||
"@tanstack/react-query": "^4.24.4",
|
"@tanstack/react-query": "^4.24.4",
|
||||||
|
|
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>
|
||||||
|
);
|
||||||
|
};
|
33
apps/emails-and-messages/src/components/breadcrumbs.tsx
Normal file
33
apps/emails-and-messages/src/components/breadcrumbs.tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||||
|
|
||||||
|
interface BreadcrumbsProps {
|
||||||
|
items: Array<{ name: string; href?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Breadcrumbs = (props: BreadcrumbsProps) => {
|
||||||
|
if (props.items.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: do I have to recreate the array here?
|
||||||
|
const i = [...props.items];
|
||||||
|
const lastItem = i.pop()!; // can enforce the type since array is at least one element long
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box display={"flex"} gap={6}>
|
||||||
|
{i.map((item) => (
|
||||||
|
<>
|
||||||
|
<Text variant="hero" display={{ mobile: "none", desktop: "block" }}>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
<Text variant="hero" display={{ mobile: "none", desktop: "block" }}>
|
||||||
|
{">"}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
<Text variant="hero" display="block">
|
||||||
|
{lastItem.name}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,3 @@
|
||||||
|
export const LoadingIndicator = () => {
|
||||||
|
return <>LOADING</>;
|
||||||
|
};
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import { BoxWithBorder } from "./box-with-border";
|
||||||
|
import { BoxFooter } from "./box-footer";
|
||||||
|
import { defaultPadding } from "./ui-defaults";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { SendgridConfiguration } from "../modules/sendgrid/configuration/sendgrid-config";
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MessagingProvidersSectionProps {
|
||||||
|
configurations: SendgridConfiguration[];
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MessagingProvidersBox = ({
|
||||||
|
configurations,
|
||||||
|
isLoading: loading,
|
||||||
|
}: MessagingProvidersSectionProps) => {
|
||||||
|
const { replace } = 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 = () => {
|
||||||
|
replace("/configuration/choose-provider");
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectToEditConfiguration = (configurationId: string) => {
|
||||||
|
replace(`/configuration/sendgrid/edit/${configurationId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoxWithBorder>
|
||||||
|
<Box padding={defaultPadding} display={"grid"} gridTemplateColumns={4}>
|
||||||
|
<Text variant="caption">Provider</Text>
|
||||||
|
<Text variant="caption">Name</Text>
|
||||||
|
<Text variant="caption">Status</Text>
|
||||||
|
<Box />
|
||||||
|
{configurations.map((configuration) => (
|
||||||
|
<>
|
||||||
|
<Text>Sendgrid</Text>
|
||||||
|
<Text>{configuration.configurationName}</Text>
|
||||||
|
<Text>{configuration.active}</Text>
|
||||||
|
<Text onClick={() => redirectToEditConfiguration(configuration.id)}>Edit</Text>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
<BoxFooter>
|
||||||
|
<Button onClick={redirectToProvidersSelection}>Add provider</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</BoxWithBorder>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
160
apps/emails-and-messages/src/components/temp.tsx
Normal file
160
apps/emails-and-messages/src/components/temp.tsx
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
const x = () => (
|
||||||
|
<SectionWithDescription
|
||||||
|
title="Events"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<Text display="block">
|
||||||
|
Provide unique name for your configuration - you can create more than one. For example
|
||||||
|
- production and development.
|
||||||
|
</Text>
|
||||||
|
<Text display="block">Then, pass your API Key. Obtain it here.</Text>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BoxWithBorder backgroundColor={"surfaceCriticalSubdued"} borderColor={"criticalSubdued"}>
|
||||||
|
<Box padding={defaultPadding}>
|
||||||
|
<Text variant="heading" display="block">
|
||||||
|
Remove provider
|
||||||
|
</Text>
|
||||||
|
<Text display="block">You can remove provider configuration.</Text>
|
||||||
|
<Text display="block">
|
||||||
|
This operation will remove all settings related to this configuration. Data will be
|
||||||
|
permanently removed from the App.{" "}
|
||||||
|
</Text>
|
||||||
|
<Text display="block">
|
||||||
|
This operation cant be undone. You still can create new configuration.
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<BoxFooter borderColor={"criticalSubdued"}>
|
||||||
|
<Button color={"textNeutralSubdued"} backgroundColor={"interactiveCriticalDefault"}>
|
||||||
|
Remove provider
|
||||||
|
</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</BoxWithBorder>
|
||||||
|
</SectionWithDescription>
|
||||||
|
<SectionWithDescription
|
||||||
|
title="Events"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<Text display="block">
|
||||||
|
Provide unique name for your configuration - you can create more than one. For example
|
||||||
|
- production and development.
|
||||||
|
</Text>
|
||||||
|
<Text display="block">Then, pass your API Key. Obtain it here.</Text>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BoxWithBorder>
|
||||||
|
<Box
|
||||||
|
padding={defaultPadding}
|
||||||
|
display={"flex"}
|
||||||
|
flexDirection={"column"}
|
||||||
|
gap={defaultPadding}
|
||||||
|
>
|
||||||
|
<Box display="grid" gridTemplateColumns={3} gap={defaultPadding}>
|
||||||
|
<Input value={"Order created"} disabled label="Configuration name" />
|
||||||
|
<Combobox
|
||||||
|
options={[
|
||||||
|
{ label: "Template 1", value: "1" },
|
||||||
|
{ label: "Template 2", value: "2" },
|
||||||
|
]}
|
||||||
|
label="Template"
|
||||||
|
/>
|
||||||
|
<Toggle>
|
||||||
|
<Text variant="body">Enabled</Text>
|
||||||
|
</Toggle>
|
||||||
|
</Box>
|
||||||
|
<Box display="grid" gridTemplateColumns={3} gap={defaultPadding}>
|
||||||
|
<Input value={"Gift card sent"} disabled label="Configuration name" />
|
||||||
|
<Combobox
|
||||||
|
options={[
|
||||||
|
{ label: "Template 1", value: "1" },
|
||||||
|
{ label: "Template 2", value: "2" },
|
||||||
|
]}
|
||||||
|
label="Template"
|
||||||
|
/>
|
||||||
|
<Toggle>
|
||||||
|
<Text variant="body">Enabled</Text>
|
||||||
|
</Toggle>
|
||||||
|
</Box>
|
||||||
|
<Box display="grid" gridTemplateColumns={3} gap={defaultPadding}>
|
||||||
|
<Input value={"Confirm account"} disabled label="Configuration name" />
|
||||||
|
<Combobox
|
||||||
|
options={[
|
||||||
|
{ label: "Template 1", value: "1" },
|
||||||
|
{ label: "Template 2", value: "2" },
|
||||||
|
]}
|
||||||
|
label="Template"
|
||||||
|
/>
|
||||||
|
<Toggle>
|
||||||
|
<Text variant="body">Enabled</Text>
|
||||||
|
</Toggle>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<BoxFooter>
|
||||||
|
<Button>Save provider</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</BoxWithBorder>
|
||||||
|
</SectionWithDescription>
|
||||||
|
<SectionWithDescription
|
||||||
|
title="Connect Sendgrid"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
<Text display="block">
|
||||||
|
Provide unique name for your configuration - you can create more than one. For example
|
||||||
|
- production and development.
|
||||||
|
</Text>
|
||||||
|
<Text display="block">Then, pass your API Key. Obtain it here.</Text>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BoxWithBorder>
|
||||||
|
<Box padding={defaultPadding} display={"flex"} flexDirection={"column"} gap={10}>
|
||||||
|
<Input
|
||||||
|
label="Configuration name"
|
||||||
|
helperText="Used to distinguish between multiple configurations"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="API key"
|
||||||
|
helperText="Your API key, ensure it has permission XYZ enabled"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<BoxFooter>
|
||||||
|
<Button>Save provider</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</BoxWithBorder>
|
||||||
|
</SectionWithDescription>
|
||||||
|
<SectionWithDescription
|
||||||
|
title="Messaging providers"
|
||||||
|
description={
|
||||||
|
<Text>
|
||||||
|
Manage providers configuration to connect Saleor events with 3rd party services.
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<BoxWithBorder
|
||||||
|
padding={10}
|
||||||
|
display={"grid"}
|
||||||
|
alignItems={"center"}
|
||||||
|
justifyContent={"center"}
|
||||||
|
>
|
||||||
|
<Text>No providers configured yet</Text>
|
||||||
|
<Button>Add first provider</Button>
|
||||||
|
</BoxWithBorder>
|
||||||
|
</SectionWithDescription>
|
||||||
|
<SectionWithDescription title="Choose provider">
|
||||||
|
<Box display="grid" gridTemplateColumns={2} gap={6}>
|
||||||
|
<ProviderSelectionBox
|
||||||
|
providerName="Sendgrid"
|
||||||
|
providerDescription="Use dynamic templates created in Sendgrid dashboard to send messages. Event data will be forwarded to Sendgrid."
|
||||||
|
onClick={() => console.log("clicked sendgrid")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProviderSelectionBox
|
||||||
|
providerName="SMTP & MJML"
|
||||||
|
providerDescription="Provide your own SMTP credentials and map Saleor event to custom MJML templates."
|
||||||
|
onClick={() => console.log("clicked mjml")}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</SectionWithDescription>
|
||||||
|
)
|
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;
|
|
@ -20,10 +20,15 @@ export const sendgridConfigurationBaseObjectSchema = z.object({
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const sendgridCreateConfigurationSchema = sendgridConfigurationBaseObjectSchema.omit({
|
export const sendgridCreateConfigurationSchema = sendgridConfigurationBaseObjectSchema.pick({
|
||||||
senderEmail: true,
|
configurationName: true,
|
||||||
senderName: true,
|
apiKey: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type SendgridCreateConfigurationSchemaType = z.infer<
|
||||||
|
typeof sendgridCreateConfigurationSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export const sendgridUpdateOrCreateConfigurationSchema =
|
export const sendgridUpdateOrCreateConfigurationSchema =
|
||||||
sendgridConfigurationBaseObjectSchema.merge(
|
sendgridConfigurationBaseObjectSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -53,3 +58,10 @@ export const sendgridGetEventConfigurationInputSchema = z.object({
|
||||||
configurationId: z.string(),
|
configurationId: z.string(),
|
||||||
eventType: z.enum(messageEventTypes),
|
eventType: z.enum(messageEventTypes),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const sendgridUpdateBasicInformationSchema = sendgridConfigurationBaseObjectSchema
|
||||||
|
.pick({
|
||||||
|
configurationName: true,
|
||||||
|
active: true,
|
||||||
|
})
|
||||||
|
.merge(z.object({ id: z.string() }));
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
sendgridGetConfigurationInputSchema,
|
sendgridGetConfigurationInputSchema,
|
||||||
sendgridGetConfigurationsInputSchema,
|
sendgridGetConfigurationsInputSchema,
|
||||||
sendgridGetEventConfigurationInputSchema,
|
sendgridGetEventConfigurationInputSchema,
|
||||||
|
sendgridUpdateBasicInformationSchema,
|
||||||
sendgridUpdateEventConfigurationInputSchema,
|
sendgridUpdateEventConfigurationInputSchema,
|
||||||
sendgridUpdateOrCreateConfigurationSchema,
|
sendgridUpdateOrCreateConfigurationSchema,
|
||||||
} from "./sendgrid-config-input-schema";
|
} from "./sendgrid-config-input-schema";
|
||||||
|
@ -156,4 +157,22 @@ export const sendgridConfigurationRouter = router({
|
||||||
await ctx.configurationService.updateConfiguration(configuration);
|
await ctx.configurationService.updateConfiguration(configuration);
|
||||||
return 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;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { SendgridConfiguration } from "../configuration/sendgrid-config";
|
||||||
|
import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
|
import { Box, Button, Input, RadioGroup, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
|
import { sendgridUpdateBasicInformationSchema } from "../configuration/sendgrid-config-input-schema";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { BoxFooter } from "../../../components/box-footer";
|
||||||
|
import { SectionWithDescription } from "../../../components/section-with-description";
|
||||||
|
|
||||||
|
interface ApiConnectionSectionProps {
|
||||||
|
configuration?: SendgridConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiConnectionSection = ({ configuration }: ApiConnectionSectionProps) => {
|
||||||
|
const { replace } = useRouter();
|
||||||
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
|
|
||||||
|
const { handleSubmit, control, setError } = useForm<
|
||||||
|
z.infer<typeof sendgridUpdateBasicInformationSchema>
|
||||||
|
>({
|
||||||
|
defaultValues: {
|
||||||
|
configurationName: configuration?.configurationName,
|
||||||
|
active: configuration?.active,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: createConfiguration } =
|
||||||
|
trpcClient.sendgridConfiguration.updateBasicInformation.useMutation({
|
||||||
|
onSuccess: async (data, variables) => {
|
||||||
|
notifySuccess("Configuration saved");
|
||||||
|
// TODO: redirect to configuration details based on id
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
let isFieldErrorSet = false;
|
||||||
|
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
||||||
|
for (const fieldName in fieldErrors) {
|
||||||
|
for (const message of fieldErrors[fieldName] || []) {
|
||||||
|
isFieldErrorSet = true;
|
||||||
|
setError(fieldName as keyof z.infer<typeof sendgridUpdateBasicInformationSchema>, {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!configuration) {
|
||||||
|
return (
|
||||||
|
<BoxWithBorder padding={10} display={"grid"} alignItems={"center"} justifyContent={"center"}>
|
||||||
|
<Text>Loading</Text>
|
||||||
|
</BoxWithBorder>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionWithDescription title="API Connection">
|
||||||
|
<BoxWithBorder>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((data, event) => {
|
||||||
|
createConfiguration({
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Box padding={defaultPadding} display={"flex"} flexDirection={"column"} gap={10}>
|
||||||
|
<Controller
|
||||||
|
name="apiKey"
|
||||||
|
control={control}
|
||||||
|
render={({
|
||||||
|
field: { onChange, value },
|
||||||
|
fieldState: { error },
|
||||||
|
formState: { errors },
|
||||||
|
}) => (
|
||||||
|
<Input
|
||||||
|
label="API Key"
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
error={!!error}
|
||||||
|
helperText={
|
||||||
|
error?.message ||
|
||||||
|
"Name of the configuration, for example 'Production' or 'Test'"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="sandboxMode"
|
||||||
|
control={control}
|
||||||
|
render={({
|
||||||
|
field: { onChange, value },
|
||||||
|
fieldState: { error },
|
||||||
|
formState: { errors },
|
||||||
|
}) => (
|
||||||
|
// TODO: add validation
|
||||||
|
<RadioGroup value={value?.toString()} onChange={(e) => console.log(e)}>
|
||||||
|
<RadioGroup.Item id="default-unchecked" value="live">
|
||||||
|
<Text>Live</Text>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<RadioGroup.Item id="default-checked" value="sandbox">
|
||||||
|
<Text>Sandbox</Text>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<BoxFooter>
|
||||||
|
<Button type="submit">Save provider</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</form>
|
||||||
|
</BoxWithBorder>
|
||||||
|
</SectionWithDescription>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,134 @@
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { SendgridConfiguration } from "../configuration/sendgrid-config";
|
||||||
|
import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
|
import { Box, Button, Input, RadioGroup, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
|
import { sendgridUpdateBasicInformationSchema } from "../configuration/sendgrid-config-input-schema";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { BoxFooter } from "../../../components/box-footer";
|
||||||
|
import { SectionWithDescription } from "../../../components/section-with-description";
|
||||||
|
|
||||||
|
interface BasicInformationSectionProps {
|
||||||
|
configuration?: SendgridConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BasicInformationSection = ({ configuration }: BasicInformationSectionProps) => {
|
||||||
|
const { replace } = useRouter();
|
||||||
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
|
|
||||||
|
const { handleSubmit, control, setError } = useForm<
|
||||||
|
z.infer<typeof sendgridUpdateBasicInformationSchema>
|
||||||
|
>({
|
||||||
|
defaultValues: {
|
||||||
|
configurationName: configuration?.configurationName,
|
||||||
|
active: configuration?.active,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: createConfiguration } =
|
||||||
|
trpcClient.sendgridConfiguration.updateBasicInformation.useMutation({
|
||||||
|
onSuccess: async (data, variables) => {
|
||||||
|
notifySuccess("Configuration saved");
|
||||||
|
// TODO: redirect to configuration details based on id
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
let isFieldErrorSet = false;
|
||||||
|
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
||||||
|
for (const fieldName in fieldErrors) {
|
||||||
|
for (const message of fieldErrors[fieldName] || []) {
|
||||||
|
isFieldErrorSet = true;
|
||||||
|
setError(fieldName as keyof z.infer<typeof sendgridUpdateBasicInformationSchema>, {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!configuration) {
|
||||||
|
return (
|
||||||
|
<BoxWithBorder padding={10} display={"grid"} alignItems={"center"} justifyContent={"center"}>
|
||||||
|
<Text>Loading</Text>
|
||||||
|
</BoxWithBorder>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
createConfiguration({
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Box padding={defaultPadding} display={"flex"} flexDirection={"column"} gap={10}>
|
||||||
|
<Controller
|
||||||
|
name="configurationName"
|
||||||
|
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="active"
|
||||||
|
control={control}
|
||||||
|
render={({
|
||||||
|
field: { onChange, value },
|
||||||
|
fieldState: { error },
|
||||||
|
formState: { errors },
|
||||||
|
}) => (
|
||||||
|
// TODO: add validation
|
||||||
|
<RadioGroup value={value?.toString()} onChange={(e) => console.log(e)}>
|
||||||
|
<RadioGroup.Item id="default-unchecked" value="trueeee">
|
||||||
|
<Text>Active</Text>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
<RadioGroup.Item id="default-checked" value="falseeeee">
|
||||||
|
<Text>Disabled</Text>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<BoxFooter>
|
||||||
|
<Button type="submit">Save provider</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</form>
|
||||||
|
</BoxWithBorder>
|
||||||
|
</SectionWithDescription>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,44 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
interface DangerousSectionProps {
|
||||||
|
configuration?: SendgridConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DangerousSection = ({ configuration }: DangerousSectionProps) => {
|
||||||
|
const onRemoveConfiguration = () => {
|
||||||
|
console.log("remove", configuration?.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionWithDescription title="Danger zone">
|
||||||
|
<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"}
|
||||||
|
onClick={onRemoveConfiguration}
|
||||||
|
>
|
||||||
|
Remove provider
|
||||||
|
</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</BoxWithBorder>
|
||||||
|
</SectionWithDescription>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { SendgridConfiguration } from "../configuration/sendgrid-config";
|
||||||
|
import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
|
import { Box, Button, Combobox, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
|
import { sendgridUpdateBasicInformationSchema } from "../configuration/sendgrid-config-input-schema";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { BoxFooter } from "../../../components/box-footer";
|
||||||
|
import { SectionWithDescription } from "../../../components/section-with-description";
|
||||||
|
|
||||||
|
interface SenderSectionProps {
|
||||||
|
configuration?: SendgridConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SenderSection = ({ configuration }: SenderSectionProps) => {
|
||||||
|
const { replace } = useRouter();
|
||||||
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
|
|
||||||
|
const { handleSubmit, control, setError } = useForm<
|
||||||
|
z.infer<typeof sendgridUpdateBasicInformationSchema>
|
||||||
|
>({
|
||||||
|
defaultValues: {
|
||||||
|
configurationName: configuration?.configurationName,
|
||||||
|
active: configuration?.active,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: createConfiguration } =
|
||||||
|
trpcClient.sendgridConfiguration.updateBasicInformation.useMutation({
|
||||||
|
onSuccess: async (data, variables) => {
|
||||||
|
notifySuccess("Configuration saved");
|
||||||
|
// TODO: redirect to configuration details based on id
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
let isFieldErrorSet = false;
|
||||||
|
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
||||||
|
for (const fieldName in fieldErrors) {
|
||||||
|
for (const message of fieldErrors[fieldName] || []) {
|
||||||
|
isFieldErrorSet = true;
|
||||||
|
setError(fieldName as keyof z.infer<typeof sendgridUpdateBasicInformationSchema>, {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!configuration) {
|
||||||
|
return (
|
||||||
|
<BoxWithBorder padding={10} display={"grid"} alignItems={"center"} justifyContent={"center"}>
|
||||||
|
<Text>Loading</Text>
|
||||||
|
</BoxWithBorder>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionWithDescription title="Sender">
|
||||||
|
<BoxWithBorder>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((data, event) => {
|
||||||
|
createConfiguration({
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Box padding={defaultPadding} display={"flex"} flexDirection={"column"} gap={10}>
|
||||||
|
<Controller
|
||||||
|
name="sender"
|
||||||
|
control={control}
|
||||||
|
render={({
|
||||||
|
field: { onChange, value },
|
||||||
|
fieldState: { error },
|
||||||
|
formState: { errors },
|
||||||
|
}) => (
|
||||||
|
<Combobox
|
||||||
|
label="Sender"
|
||||||
|
value={value}
|
||||||
|
options={[
|
||||||
|
{ label: "test", value: "test" },
|
||||||
|
{ label: "test", value: "test" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<BoxFooter>
|
||||||
|
<Button type="submit">Save provider</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</form>
|
||||||
|
</BoxWithBorder>
|
||||||
|
</SectionWithDescription>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,65 +1,15 @@
|
||||||
|
import "@saleor/macaw-ui/next/style";
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
|
|
||||||
import { Theme } from "@material-ui/core/styles";
|
|
||||||
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
|
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
|
||||||
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
||||||
import {
|
import React from "react";
|
||||||
dark,
|
|
||||||
light,
|
|
||||||
SaleorThemeColors,
|
|
||||||
ThemeProvider as MacawUIThemeProvider,
|
|
||||||
} from "@saleor/macaw-ui";
|
|
||||||
import React, { PropsWithChildren, useEffect } from "react";
|
|
||||||
import { AppProps } from "next/app";
|
import { AppProps } from "next/app";
|
||||||
|
import { ThemeProvider } from "@saleor/macaw-ui/next";
|
||||||
|
|
||||||
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
|
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
|
||||||
import { NoSSRWrapper } from "../lib/no-ssr-wrapper";
|
import { NoSSRWrapper } from "../lib/no-ssr-wrapper";
|
||||||
import { trpcClient } from "../modules/trpc/trpc-client";
|
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.
|
* Ensure instance is a singleton.
|
||||||
|
@ -67,34 +17,15 @@ const generateClassName = createGenerateClassName({
|
||||||
*/
|
*/
|
||||||
export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
|
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) {
|
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 (
|
return (
|
||||||
<NoSSRWrapper>
|
<NoSSRWrapper>
|
||||||
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
||||||
<StylesProvider generateClassName={generateClassName}>
|
<ThemeProvider defaultTheme="defaultLight">
|
||||||
<ThemeProvider palettes={palettes} overrides={themeOverrides} ssr>
|
|
||||||
<ThemeSynchronizer />
|
<ThemeSynchronizer />
|
||||||
<RoutePropagator />
|
<RoutePropagator />
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</StylesProvider>
|
|
||||||
</AppBridgeProvider>
|
</AppBridgeProvider>
|
||||||
</NoSSRWrapper>
|
</NoSSRWrapper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,16 +27,16 @@ export default createAppRegisterHandler({
|
||||||
],
|
],
|
||||||
onAuthAplSaved: async (request, ctx) => {
|
onAuthAplSaved: async (request, ctx) => {
|
||||||
// Subscribe to Notify using the mutation since it does not use subscriptions and can't be subscribed via manifest
|
// Subscribe to Notify using the mutation since it does not use subscriptions and can't be subscribed via manifest
|
||||||
logger.debug("onAuthAplSaved executing");
|
// FIXME:
|
||||||
|
// logger.debug("onAuthAplSaved executing");
|
||||||
const baseUrl = getBaseUrl(request.headers);
|
// const baseUrl = getBaseUrl(request.headers);
|
||||||
const client = createClient(ctx.authData.saleorApiUrl, async () =>
|
// const client = createClient(ctx.authData.saleorApiUrl, async () =>
|
||||||
Promise.resolve({ token: ctx.authData.token })
|
// Promise.resolve({ token: ctx.authData.token })
|
||||||
);
|
// );
|
||||||
await registerNotifyWebhook({
|
// await registerNotifyWebhook({
|
||||||
client: client,
|
// client: client,
|
||||||
baseUrl: baseUrl,
|
// baseUrl: baseUrl,
|
||||||
});
|
// });
|
||||||
logger.debug("Webhook registered");
|
// logger.debug("Webhook registered");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
const ChooseProviderPage: NextPage = () => {
|
||||||
|
const { replace } = useRouter();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box padding={10} display={"grid"} gap={13}>
|
||||||
|
<Breadcrumbs items={[{ name: "Configuration", href: "/" }, { 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={() => replace("/configuration/sendgrid/new")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProviderSelectionBox
|
||||||
|
providerName="SMTP & MJML"
|
||||||
|
providerDescription="Provide your own SMTP credentials and map Saleor event to custom MJML templates."
|
||||||
|
onClick={() => replace("/configuration/mjml/new")}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</SectionWithDescription>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChooseProviderPage;
|
36
apps/emails-and-messages/src/pages/configuration/index.tsx
Normal file
36
apps/emails-and-messages/src/pages/configuration/index.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
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 { MessagingProvidersBox } from "../../components/messaging-providers-box";
|
||||||
|
import { trpcClient } from "../../modules/trpc/trpc-client";
|
||||||
|
|
||||||
|
const ConfigurationPage: NextPage = () => {
|
||||||
|
const { data, isLoading } = trpcClient.sendgridConfiguration.getConfigurations.useQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box padding={10} display={"grid"} gap={13}>
|
||||||
|
<Breadcrumbs items={[{ name: "Configuration", href: "/" }]} />
|
||||||
|
<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>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigurationPage;
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import { Breadcrumbs } from "../../../../components/breadcrumbs";
|
||||||
|
import { trpcClient } from "../../../../modules/trpc/trpc-client";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { BasicInformationSection } from "../../../../modules/sendgrid/ui/basic-information-section";
|
||||||
|
import { DangerousSection } from "../../../../modules/sendgrid/ui/dangrous-section";
|
||||||
|
import { SectionWithDescription } from "../../../../components/section-with-description";
|
||||||
|
import { ApiConnectionSection } from "../../../../modules/sendgrid/ui/api-connection-section";
|
||||||
|
import { SenderSection } from "../../../../modules/sendgrid/ui/sender-section";
|
||||||
|
|
||||||
|
const EditSendgridConfigurationPage: NextPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { id } = router.query;
|
||||||
|
|
||||||
|
const { data: configuration } = trpcClient.sendgridConfiguration.getConfiguration.useQuery({
|
||||||
|
id: id as string,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box padding={10} display={"grid"} gap={13}>
|
||||||
|
<Breadcrumbs
|
||||||
|
items={[{ name: "Configuration", href: "/" }, { name: "Sendgrid" }, { name: "Sendgrid" }]}
|
||||||
|
/>
|
||||||
|
<Box display={"grid"} gridTemplateColumns={{ desktop: 3, mobile: 1 }}>
|
||||||
|
<Box>
|
||||||
|
<Text>Connect Sendgrid with Saleor.</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{!!configuration && <BasicInformationSection configuration={configuration} />}
|
||||||
|
{!!configuration && <ApiConnectionSection configuration={configuration} />}
|
||||||
|
{!!configuration && <SenderSection configuration={configuration} />}
|
||||||
|
{!!configuration && <DangerousSection configuration={configuration} />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditSendgridConfigurationPage;
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { Box, Button, Input, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import { Breadcrumbs } from "../../../components/breadcrumbs";
|
||||||
|
import { SectionWithDescription } from "../../../components/section-with-description";
|
||||||
|
import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
|
import { BoxFooter } from "../../../components/box-footer";
|
||||||
|
import { trpcClient } from "../../../modules/trpc/trpc-client";
|
||||||
|
import { useDashboardNotification } from "@saleor/apps-shared/src/use-dashboard-notification";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { SendgridCreateConfigurationSchemaType } from "../../../modules/sendgrid/configuration/sendgrid-config-input-schema";
|
||||||
|
|
||||||
|
const NewSendgridConfigurationPage: NextPage = () => {
|
||||||
|
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||||
|
|
||||||
|
const { handleSubmit, control, setError } = useForm<SendgridCreateConfigurationSchemaType>();
|
||||||
|
|
||||||
|
const { mutate: createConfiguration } =
|
||||||
|
trpcClient.sendgridConfiguration.createConfiguration.useMutation({
|
||||||
|
onSuccess: async (data, variables) => {
|
||||||
|
notifySuccess("Configuration saved");
|
||||||
|
// TODO: redirect to configuration details based on id
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
let isFieldErrorSet = false;
|
||||||
|
const fieldErrors = error.data?.zodError?.fieldErrors || {};
|
||||||
|
for (const fieldName in fieldErrors) {
|
||||||
|
for (const message of fieldErrors[fieldName] || []) {
|
||||||
|
isFieldErrorSet = true;
|
||||||
|
setError(fieldName as keyof SendgridCreateConfigurationSchemaType, {
|
||||||
|
type: "manual",
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const formErrors = error.data?.zodError?.formErrors || [];
|
||||||
|
const formErrorMessage = formErrors.length ? formErrors.join("\n") : undefined;
|
||||||
|
|
||||||
|
notifyError(
|
||||||
|
"Could not save the configuration",
|
||||||
|
isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
||||||
|
formErrorMessage
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box padding={10} display={"grid"} gap={13}>
|
||||||
|
<Breadcrumbs
|
||||||
|
items={[
|
||||||
|
{ name: "Configuration", href: "/" },
|
||||||
|
{ 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="configurationName"
|
||||||
|
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>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewSendgridConfigurationPage;
|
|
@ -3,7 +3,6 @@ import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useIsMounted } from "usehooks-ts";
|
import { useIsMounted } from "usehooks-ts";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { LinearProgress } from "@material-ui/core";
|
|
||||||
import { isInIframe } from "../lib/is-in-iframe";
|
import { isInIframe } from "../lib/is-in-iframe";
|
||||||
import { appName } from "../const";
|
import { appName } from "../const";
|
||||||
|
|
||||||
|
@ -14,12 +13,12 @@ const IndexPage: NextPage = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMounted() && appBridgeState?.ready) {
|
if (isMounted() && appBridgeState?.ready) {
|
||||||
replace("/configuration/channels");
|
replace("/configuration");
|
||||||
}
|
}
|
||||||
}, [isMounted, appBridgeState?.ready, replace]);
|
}, [isMounted, appBridgeState?.ready, replace]);
|
||||||
|
|
||||||
if (isInIframe()) {
|
if (isInIframe()) {
|
||||||
return <LinearProgress />;
|
return <p>Loading</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,21 +1,16 @@
|
||||||
body {
|
body {
|
||||||
font-family: Inter, -apple-system, "system-ui", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell,
|
color: var(--mu-colors-foreground-text-neutral-plain);
|
||||||
"Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
padding: 0;
|
||||||
color: #111;
|
margin: 0;
|
||||||
|
background: var(--mu-colors-background-plain);
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
background: #f6f8fa;
|
|
||||||
border: 1px solid #eaeaea;
|
|
||||||
border-radius: 5px;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 10px;
|
letter-spacing: 0.1em;
|
||||||
padding: 0.75rem;
|
color: var(--mu-colors-foreground-text-neutral-subdued);
|
||||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
|
||||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code::before {
|
a {
|
||||||
content: "$ ";
|
text-decoration: none;
|
||||||
opacity: 0.6;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -325,7 +325,7 @@ importers:
|
||||||
'@monaco-editor/react': ^4.4.6
|
'@monaco-editor/react': ^4.4.6
|
||||||
'@saleor/app-sdk': 0.37.3
|
'@saleor/app-sdk': 0.37.3
|
||||||
'@saleor/apps-shared': workspace:*
|
'@saleor/apps-shared': workspace:*
|
||||||
'@saleor/macaw-ui': 0.8.0-pre.72
|
'@saleor/macaw-ui': 0.8.0-pre.76
|
||||||
'@sendgrid/client': ^7.7.0
|
'@sendgrid/client': ^7.7.0
|
||||||
'@sendgrid/mail': ^7.7.0
|
'@sendgrid/mail': ^7.7.0
|
||||||
'@tanstack/react-query': ^4.24.4
|
'@tanstack/react-query': ^4.24.4
|
||||||
|
@ -375,7 +375,7 @@ importers:
|
||||||
'@monaco-editor/react': 4.4.6_biqbaboplfbrettd7655fr4n2y
|
'@monaco-editor/react': 4.4.6_biqbaboplfbrettd7655fr4n2y
|
||||||
'@saleor/app-sdk': 0.37.3_yucv4tfv7v7nrkw2uguegj6e7e
|
'@saleor/app-sdk': 0.37.3_yucv4tfv7v7nrkw2uguegj6e7e
|
||||||
'@saleor/apps-shared': link:../../packages/shared
|
'@saleor/apps-shared': link:../../packages/shared
|
||||||
'@saleor/macaw-ui': 0.8.0-pre.72_5ndqzdd6t4rivxsukjv3i3ak2q
|
'@saleor/macaw-ui': 0.8.0-pre.76_5ndqzdd6t4rivxsukjv3i3ak2q
|
||||||
'@sendgrid/client': 7.7.0
|
'@sendgrid/client': 7.7.0
|
||||||
'@sendgrid/mail': 7.7.0
|
'@sendgrid/mail': 7.7.0
|
||||||
'@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y
|
'@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y
|
||||||
|
@ -4804,8 +4804,8 @@ packages:
|
||||||
- '@types/react'
|
- '@types/react'
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@saleor/macaw-ui/0.8.0-pre.72_5ndqzdd6t4rivxsukjv3i3ak2q:
|
/@saleor/macaw-ui/0.8.0-pre.76_5ndqzdd6t4rivxsukjv3i3ak2q:
|
||||||
resolution: {integrity: sha512-9lcFkzf81q9Mxjqd00rWUUvom26YK3WCu8GCcmpqcEFu723/H76hxg2/LUd2cpqARavS1FgO+Vri7jkxkSz7sQ==}
|
resolution: {integrity: sha512-z5zlgdiLcJTR4al4FP6Z3JBzcH1VWQQRrUVH/TraqvHfIxC5XCPz1ZSve1/KUyXafbjUaM0ih81B9vqfipbXRA==}
|
||||||
engines: {node: '>=16 <19', pnpm: '>=8'}
|
engines: {node: '>=16 <19', pnpm: '>=8'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
Loading…
Reference in a new issue