This commit is contained in:
Krzysztof Wolski 2023-04-25 11:50:47 +02:00
parent a163b04d7a
commit b1bc467da6
24 changed files with 1052 additions and 112 deletions

View file

@ -19,7 +19,7 @@
"@monaco-editor/react": "^4.4.6",
"@saleor/app-sdk": "0.37.3",
"@saleor/apps-shared": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.72",
"@saleor/macaw-ui": "0.8.0-pre.76",
"@sendgrid/client": "^7.7.0",
"@sendgrid/mail": "^7.7.0",
"@tanstack/react-query": "^4.24.4",

View 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>
);
};

View 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>
);
};

View 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>
);
};

View file

@ -0,0 +1,3 @@
export const LoadingIndicator = () => {
return <>LOADING</>;
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View 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>
)

View file

@ -0,0 +1 @@
export const defaultPadding = 6;

View file

@ -20,10 +20,15 @@ export const sendgridConfigurationBaseObjectSchema = z.object({
}),
});
export const sendgridCreateConfigurationSchema = sendgridConfigurationBaseObjectSchema.omit({
senderEmail: true,
senderName: true,
export const sendgridCreateConfigurationSchema = sendgridConfigurationBaseObjectSchema.pick({
configurationName: true,
apiKey: true,
});
export type SendgridCreateConfigurationSchemaType = z.infer<
typeof sendgridCreateConfigurationSchema
>;
export const sendgridUpdateOrCreateConfigurationSchema =
sendgridConfigurationBaseObjectSchema.merge(
z.object({
@ -53,3 +58,10 @@ export const sendgridGetEventConfigurationInputSchema = z.object({
configurationId: z.string(),
eventType: z.enum(messageEventTypes),
});
export const sendgridUpdateBasicInformationSchema = sendgridConfigurationBaseObjectSchema
.pick({
configurationName: true,
active: true,
})
.merge(z.object({ id: z.string() }));

View file

@ -5,6 +5,7 @@ import {
sendgridGetConfigurationInputSchema,
sendgridGetConfigurationsInputSchema,
sendgridGetEventConfigurationInputSchema,
sendgridUpdateBasicInformationSchema,
sendgridUpdateEventConfigurationInputSchema,
sendgridUpdateOrCreateConfigurationSchema,
} from "./sendgrid-config-input-schema";
@ -156,4 +157,22 @@ export const sendgridConfigurationRouter = router({
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;
}),
});

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);

View file

@ -27,16 +27,16 @@ 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 })
);
await registerNotifyWebhook({
client: client,
baseUrl: baseUrl,
});
logger.debug("Webhook registered");
// FIXME:
// logger.debug("onAuthAplSaved executing");
// const baseUrl = getBaseUrl(request.headers);
// const client = createClient(ctx.authData.saleorApiUrl, async () =>
// Promise.resolve({ token: ctx.authData.token })
// );
// await registerNotifyWebhook({
// client: client,
// baseUrl: baseUrl,
// });
// logger.debug("Webhook registered");
},
});

View file

@ -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;

View 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;

View file

@ -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;

View file

@ -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;

View file

@ -3,7 +3,6 @@ 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";
@ -14,12 +13,12 @@ const IndexPage: NextPage = () => {
useEffect(() => {
if (isMounted() && appBridgeState?.ready) {
replace("/configuration/channels");
replace("/configuration");
}
}, [isMounted, appBridgeState?.ready, replace]);
if (isInIframe()) {
return <LinearProgress />;
return <p>Loading</p>;
}
return (

View file

@ -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;
}

View file

@ -325,7 +325,7 @@ importers:
'@monaco-editor/react': ^4.4.6
'@saleor/app-sdk': 0.37.3
'@saleor/apps-shared': workspace:*
'@saleor/macaw-ui': 0.8.0-pre.72
'@saleor/macaw-ui': 0.8.0-pre.76
'@sendgrid/client': ^7.7.0
'@sendgrid/mail': ^7.7.0
'@tanstack/react-query': ^4.24.4
@ -375,7 +375,7 @@ importers:
'@monaco-editor/react': 4.4.6_biqbaboplfbrettd7655fr4n2y
'@saleor/app-sdk': 0.37.3_yucv4tfv7v7nrkw2uguegj6e7e
'@saleor/apps-shared': link:../../packages/shared
'@saleor/macaw-ui': 0.8.0-pre.72_5ndqzdd6t4rivxsukjv3i3ak2q
'@saleor/macaw-ui': 0.8.0-pre.76_5ndqzdd6t4rivxsukjv3i3ak2q
'@sendgrid/client': 7.7.0
'@sendgrid/mail': 7.7.0
'@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y
@ -4804,8 +4804,8 @@ packages:
- '@types/react'
dev: false
/@saleor/macaw-ui/0.8.0-pre.72_5ndqzdd6t4rivxsukjv3i3ak2q:
resolution: {integrity: sha512-9lcFkzf81q9Mxjqd00rWUUvom26YK3WCu8GCcmpqcEFu723/H76hxg2/LUd2cpqARavS1FgO+Vri7jkxkSz7sQ==}
/@saleor/macaw-ui/0.8.0-pre.76_5ndqzdd6t4rivxsukjv3i3ak2q:
resolution: {integrity: sha512-z5zlgdiLcJTR4al4FP6Z3JBzcH1VWQQRrUVH/TraqvHfIxC5XCPz1ZSve1/KUyXafbjUaM0ih81B9vqfipbXRA==}
engines: {node: '>=16 <19', pnpm: '>=8'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0