Remove old code
This commit is contained in:
parent
55b998a75f
commit
e04ee04b3d
27 changed files with 526 additions and 2509 deletions
|
@ -16,13 +16,10 @@
|
||||||
"schemaVersion": "3.11.7"
|
"schemaVersion": "3.11.7"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material-ui/core": "^4.12.4",
|
|
||||||
"@material-ui/icons": "^4.11.3",
|
|
||||||
"@material-ui/lab": "4.0.0-alpha.61",
|
|
||||||
"@monaco-editor/react": "^4.4.6",
|
"@monaco-editor/react": "^4.4.6",
|
||||||
"@saleor/app-sdk": "0.38.0",
|
"@saleor/app-sdk": "0.38.0",
|
||||||
"@saleor/apps-shared": "workspace:*",
|
"@saleor/apps-shared": "workspace:*",
|
||||||
"@saleor/macaw-ui": "^0.7.2",
|
"@saleor/macaw-ui": "0.8.0-pre.72",
|
||||||
"@sendgrid/client": "^7.7.0",
|
"@sendgrid/client": "^7.7.0",
|
||||||
"@sendgrid/mail": "^7.7.0",
|
"@sendgrid/mail": "^7.7.0",
|
||||||
"@sentry/nextjs": "^7.52.1",
|
"@sentry/nextjs": "^7.52.1",
|
||||||
|
|
|
@ -1,33 +1,24 @@
|
||||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
import { useTheme } from "@saleor/macaw-ui";
|
import { useTheme } from "@saleor/macaw-ui/next";
|
||||||
import { memo, useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
/**
|
export function ThemeSynchronizer() {
|
||||||
* Macaw-ui stores its theme mode in memory and local storage. To synchronize App with Dashboard,
|
|
||||||
* Macaw must be informed about this change from AppBridge.
|
|
||||||
*
|
|
||||||
* If you are not using Macaw, you can remove this.
|
|
||||||
*/
|
|
||||||
function _ThemeSynchronizer() {
|
|
||||||
const { appBridgeState } = useAppBridge();
|
const { appBridgeState } = useAppBridge();
|
||||||
const { setTheme, themeType } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!setTheme || !appBridgeState?.theme) {
|
if (!setTheme || !appBridgeState?.theme) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeType !== appBridgeState?.theme) {
|
if (appBridgeState.theme === "light") {
|
||||||
setTheme(appBridgeState.theme);
|
setTheme("defaultLight");
|
||||||
/**
|
|
||||||
* Hack to fix macaw, which is going into infinite loop on light mode (probably de-sync local storage with react state)
|
|
||||||
* TODO Fix me when Macaw 2.0 is shipped
|
|
||||||
*/
|
|
||||||
window.localStorage.setItem("macaw-ui-theme", appBridgeState.theme);
|
|
||||||
}
|
}
|
||||||
}, [appBridgeState?.theme, setTheme, themeType]);
|
|
||||||
|
if (appBridgeState.theme === "dark") {
|
||||||
|
setTheme("defaultDark");
|
||||||
|
}
|
||||||
|
}, [appBridgeState?.theme, setTheme]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThemeSynchronizer = memo(_ThemeSynchronizer);
|
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { Paper, Typography } from "@material-ui/core";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
|
||||||
return {
|
|
||||||
instructionsContainer: {
|
|
||||||
padding: 15,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const Instructions = () => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper elevation={0} className={styles.instructionsContainer}>
|
|
||||||
<Typography paragraph variant="h4">
|
|
||||||
Welcome to Emails and Messages App!
|
|
||||||
</Typography>
|
|
||||||
<Typography paragraph>
|
|
||||||
The application will allow you to send emails and messages to your customers using different
|
|
||||||
services.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography paragraph variant="h4">
|
|
||||||
How to configure the app
|
|
||||||
</Typography>
|
|
||||||
<Typography paragraph>
|
|
||||||
Start by creating a new configuration for provider of your choice. You can create multiple
|
|
||||||
configurations and then assign them to channels. Navigate to the relevant tab to configure
|
|
||||||
the provider.
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||||
|
import { BoxWithBorder } from "../../../components/box-with-border";
|
||||||
|
import { defaultPadding } from "../../../components/ui-defaults";
|
||||||
|
import { BoxFooter } from "../../../components/box-footer";
|
||||||
|
|
||||||
|
interface ProviderSelectionBoxProps {
|
||||||
|
providerName: string;
|
||||||
|
providerDescription: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProviderSelectionBox = (props: ProviderSelectionBoxProps) => {
|
||||||
|
return (
|
||||||
|
<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"}
|
||||||
|
>
|
||||||
|
<Text>{props.providerDescription}</Text>
|
||||||
|
</Box>
|
||||||
|
<BoxFooter justifyContent={"center"} alignItems={"center"}>
|
||||||
|
<Button onClick={props.onClick}>Choose</Button>
|
||||||
|
</BoxFooter>
|
||||||
|
</BoxWithBorder>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,303 +0,0 @@
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { Divider, TextField, TextFieldProps, Typography } from "@material-ui/core";
|
|
||||||
import { Button, makeStyles, SwitchSelector, SwitchSelectorButton } from "@saleor/macaw-ui";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { MjmlConfiguration, smtpEncryptionTypes } from "../mjml-config";
|
|
||||||
import { trpcClient } from "../../../trpc/trpc-client";
|
|
||||||
import { useAppBridge, actions } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
field: {
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
editor: {
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
preview: {
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
sectionHeader: {
|
|
||||||
marginTop: 20,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onConfigurationSaved: () => void;
|
|
||||||
initialData: MjmlConfiguration;
|
|
||||||
configurationId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MjmlConfigurationForm = (props: Props) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
|
||||||
|
|
||||||
const { handleSubmit, control, reset, setError } = useForm<MjmlConfiguration>({
|
|
||||||
defaultValues: props.initialData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { mutate: createOrUpdateConfiguration } =
|
|
||||||
trpcClient.mjmlConfiguration.updateOrCreateConfiguration.useMutation({
|
|
||||||
onSuccess: async (data, variables) => {
|
|
||||||
await queryClient.cancelQueries({ queryKey: ["mjmlConfiguration", "getConfigurations"] });
|
|
||||||
|
|
||||||
// Optimistically update to the new value
|
|
||||||
queryClient.setQueryData<Array<MjmlConfiguration>>(
|
|
||||||
["mjmlConfiguration", "getConfigurations", undefined],
|
|
||||||
(old) => {
|
|
||||||
if (old) {
|
|
||||||
const index = old.findIndex((c) => c.id === data.id);
|
|
||||||
// If thats an update, replace the old one
|
|
||||||
if (index !== -1) {
|
|
||||||
old[index] = data;
|
|
||||||
return [...old];
|
|
||||||
} else {
|
|
||||||
return [...old, data];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return [data];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Trigger refetch to make sure we have a fresh data
|
|
||||||
props.onConfigurationSaved();
|
|
||||||
notifySuccess("Configuration saved");
|
|
||||||
},
|
|
||||||
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 MjmlConfiguration, {
|
|
||||||
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
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// when the configuration tab is changed, initialData change and form has to be updated
|
|
||||||
useEffect(() => {
|
|
||||||
reset(props.initialData);
|
|
||||||
}, [props.initialData, props.configurationId, reset]);
|
|
||||||
|
|
||||||
const CommonFieldProps: TextFieldProps = {
|
|
||||||
className: styles.field,
|
|
||||||
fullWidth: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const isNewConfiguration = !props.configurationId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit((data, event) => {
|
|
||||||
createOrUpdateConfiguration({
|
|
||||||
...data,
|
|
||||||
});
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{isNewConfiguration ? (
|
|
||||||
<Typography variant="h2" paragraph>
|
|
||||||
Create a new configuration
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<Typography variant="h2" paragraph>
|
|
||||||
Configuration
|
|
||||||
<strong>{` ${props.initialData.configurationName} `}</strong>
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="configurationName"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error }, formState: { errors } }) => (
|
|
||||||
<TextField
|
|
||||||
label="Configuration name"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
error={!!error}
|
|
||||||
helperText={
|
|
||||||
error?.message || "Name of the configuration, for example 'Production' or 'Test'"
|
|
||||||
}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="active"
|
|
||||||
render={({ field: { value, name, onChange } }) => (
|
|
||||||
<div className={styles.field}>
|
|
||||||
{/* TODO: fix types in the MacawUI */}
|
|
||||||
{/* @ts-ignore: MacawUI use wrong type for */}
|
|
||||||
<SwitchSelector key={name} className={styles.field}>
|
|
||||||
{[
|
|
||||||
{ label: "Active", value: true },
|
|
||||||
{ label: "Disabled", value: false },
|
|
||||||
].map((button) => (
|
|
||||||
// @ts-ignore: MacawUI use wrong type for SwitchSelectorButton
|
|
||||||
<SwitchSelectorButton
|
|
||||||
value={button.value.toString()}
|
|
||||||
onClick={() => onChange(button.value)}
|
|
||||||
activeTab={value.toString()}
|
|
||||||
key={button.label}
|
|
||||||
>
|
|
||||||
{button.label}
|
|
||||||
</SwitchSelectorButton>
|
|
||||||
))}
|
|
||||||
</SwitchSelector>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Typography variant="h3" paragraph className={styles.sectionHeader}>
|
|
||||||
Sender details
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="senderName"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<TextField
|
|
||||||
label="Sender name"
|
|
||||||
error={!!error}
|
|
||||||
helperText={error?.message || "Name which will be presented as author of the email"}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="senderEmail"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<>
|
|
||||||
<TextField
|
|
||||||
label="Sender email"
|
|
||||||
value={value}
|
|
||||||
helperText={error?.message || "Email which will be presented as author of the email"}
|
|
||||||
error={!!error}
|
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Typography variant="h3" paragraph className={styles.sectionHeader}>
|
|
||||||
SMTP server configuration
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="smtpHost"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<TextField
|
|
||||||
label="SMTP server host"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
helperText={
|
|
||||||
error?.message ||
|
|
||||||
"Address of the SMTP server, without the protocol. For example 'smtp.example.com'"
|
|
||||||
}
|
|
||||||
error={!!error}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="smtpPort"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<TextField
|
|
||||||
label="SMTP server port"
|
|
||||||
helperText={error?.message}
|
|
||||||
error={!!error}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="smtpUser"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<TextField
|
|
||||||
label="SMTP server user"
|
|
||||||
value={value}
|
|
||||||
helperText={error?.message}
|
|
||||||
error={!!error}
|
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="smtpPassword"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<TextField
|
|
||||||
label="SMTP server password"
|
|
||||||
value={value}
|
|
||||||
helperText={error?.message}
|
|
||||||
error={!!error}
|
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="encryption"
|
|
||||||
render={({ field: { value, name, onChange } }) => (
|
|
||||||
<div className={styles.field}>
|
|
||||||
{/* TODO: fix types in the MacawUI */}
|
|
||||||
{/* @ts-ignore: MacawUI use wrong type for SwitchSelector */}
|
|
||||||
<SwitchSelector key={name}>
|
|
||||||
{smtpEncryptionTypes.map((encryptionType) => (
|
|
||||||
// @ts-ignore: MacawUI use wrong type for SwitchSelectorButton
|
|
||||||
<SwitchSelectorButton
|
|
||||||
value={encryptionType}
|
|
||||||
onClick={() => onChange(encryptionType)}
|
|
||||||
activeTab={value}
|
|
||||||
key={encryptionType}
|
|
||||||
>
|
|
||||||
{encryptionType === "NONE" ? "No encryption" : encryptionType}
|
|
||||||
</SwitchSelectorButton>
|
|
||||||
))}
|
|
||||||
</SwitchSelector>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" fullWidth variant="primary">
|
|
||||||
Save configuration
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,104 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import { AppColumnsLayout } from "../../../ui/app-columns-layout";
|
|
||||||
import { trpcClient } from "../../../trpc/trpc-client";
|
|
||||||
import { MjmlConfigurationForm } from "./mjml-configuration-form";
|
|
||||||
import { getDefaultEmptyConfiguration } from "../mjml-config-container";
|
|
||||||
import { NextRouter, useRouter } from "next/router";
|
|
||||||
import { mjmlUrls } from "../../urls";
|
|
||||||
import { MjmlTemplatesCard } from "./mjml-templates-card";
|
|
||||||
import { MjmlConfiguration } from "../mjml-config";
|
|
||||||
import { LoadingIndicator } from "../../../ui/loading-indicator";
|
|
||||||
import { MjmlInstructions } from "./mjml-instructions";
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
|
||||||
return {
|
|
||||||
formContainer: {
|
|
||||||
top: 0,
|
|
||||||
},
|
|
||||||
instructionsContainer: {
|
|
||||||
padding: 15,
|
|
||||||
},
|
|
||||||
configurationColumn: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: 20,
|
|
||||||
maxWidth: 600,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
interface MjmlConfigurationTabProps {
|
|
||||||
configurationId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigateToFirstConfiguration = (router: NextRouter, configurations?: MjmlConfiguration[]) => {
|
|
||||||
if (!configurations || !configurations?.length) {
|
|
||||||
router.replace(mjmlUrls.configuration());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const firstConfigurationId = configurations[0]?.id;
|
|
||||||
|
|
||||||
if (firstConfigurationId) {
|
|
||||||
router.replace(mjmlUrls.configuration(firstConfigurationId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MjmlConfigurationTab = ({ configurationId }: MjmlConfigurationTabProps) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: configurations,
|
|
||||||
refetch: refetchConfigurations,
|
|
||||||
isLoading: configurationsIsLoading,
|
|
||||||
isFetching: configurationsIsFetching,
|
|
||||||
} = trpcClient.mjmlConfiguration.getConfigurations.useQuery(undefined, {
|
|
||||||
onSuccess(data) {
|
|
||||||
if (!configurationId) {
|
|
||||||
console.log("no conf id! navigate to first");
|
|
||||||
navigateToFirstConfiguration(router, data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (configurationsIsLoading || configurationsIsFetching) {
|
|
||||||
return <LoadingIndicator />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuration = configurations?.find((c) => c.id === configurationId?.toString());
|
|
||||||
|
|
||||||
if (configurationId && !configuration) {
|
|
||||||
return <div>Configuration not found</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppColumnsLayout>
|
|
||||||
<div className={styles.configurationColumn}>
|
|
||||||
{configurationsIsLoading || configurationsIsFetching ? (
|
|
||||||
<LoadingIndicator />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<MjmlConfigurationForm
|
|
||||||
onConfigurationSaved={() => refetchConfigurations()}
|
|
||||||
initialData={configuration || getDefaultEmptyConfiguration()}
|
|
||||||
configurationId={configurationId}
|
|
||||||
/>
|
|
||||||
{!!configurationId && !!configuration && (
|
|
||||||
<MjmlTemplatesCard
|
|
||||||
configurationId={configurationId}
|
|
||||||
configuration={configuration}
|
|
||||||
onEventChanged={() => {
|
|
||||||
refetchConfigurations();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<MjmlInstructions />
|
|
||||||
</AppColumnsLayout>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,271 +0,0 @@
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { CircularProgress, Grid, TextField, TextFieldProps, Typography } from "@material-ui/core";
|
|
||||||
import {
|
|
||||||
BackSmallIcon,
|
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
makeStyles,
|
|
||||||
SwitchSelector,
|
|
||||||
SwitchSelectorButton,
|
|
||||||
} from "@saleor/macaw-ui";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { MjmlEventConfiguration } from "../mjml-config";
|
|
||||||
import { CodeEditor } from "../../../ui/code-editor";
|
|
||||||
import { MjmlPreview } from "./mjml-preview";
|
|
||||||
import {
|
|
||||||
MessageEventTypes,
|
|
||||||
messageEventTypesLabels,
|
|
||||||
} from "../../../event-handlers/message-event-types";
|
|
||||||
import { trpcClient } from "../../../trpc/trpc-client";
|
|
||||||
import { useDebounce } from "usehooks-ts";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { mjmlUrls } from "../../urls";
|
|
||||||
import { useAppBridge, actions } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { examplePayloads } from "../../../event-handlers/default-payloads";
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
|
|
||||||
const PREVIEW_DEBOUNCE_DELAY = 500;
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
viewContainer: {
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
margin: "0 auto",
|
|
||||||
},
|
|
||||||
previewHeader: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
},
|
|
||||||
|
|
||||||
field: {
|
|
||||||
marginBottom: theme.spacing(3),
|
|
||||||
},
|
|
||||||
editor: {
|
|
||||||
marginBottom: theme.spacing(3),
|
|
||||||
},
|
|
||||||
preview: {
|
|
||||||
marginBottom: theme.spacing(3),
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
maxWidth: 800,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
type EventConfigurationFormProps = {
|
|
||||||
initialData: MjmlEventConfiguration;
|
|
||||||
configurationId: string;
|
|
||||||
eventType: MessageEventTypes;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EventConfigurationForm = ({
|
|
||||||
initialData,
|
|
||||||
configurationId,
|
|
||||||
eventType,
|
|
||||||
}: EventConfigurationFormProps) => {
|
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
|
||||||
const router = useRouter();
|
|
||||||
const { appBridge } = useAppBridge();
|
|
||||||
const { handleSubmit, control, getValues, setError } = useForm<MjmlEventConfiguration>({
|
|
||||||
defaultValues: initialData,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 styles = useStyles();
|
|
||||||
|
|
||||||
const CommonFieldProps: TextFieldProps = {
|
|
||||||
className: styles.field,
|
|
||||||
fullWidth: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { mutate: fetchTemplatePreview, isLoading: isFetchingTemplatePreview } =
|
|
||||||
trpcClient.mjmlConfiguration.renderTemplate.useMutation({
|
|
||||||
onSuccess: (data) => {
|
|
||||||
if (data.renderedEmailBody) {
|
|
||||||
setLastValidRenderedTemplate(data.renderedEmailBody);
|
|
||||||
}
|
|
||||||
if (data.renderedSubject) {
|
|
||||||
setLastValidRenderedSubject(data.renderedSubject);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { mutate: updateEventConfiguration } =
|
|
||||||
trpcClient.mjmlConfiguration.updateEventConfiguration.useMutation({
|
|
||||||
onSuccess: (data) => {
|
|
||||||
notifySuccess("Success", "Configuration saved");
|
|
||||||
},
|
|
||||||
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 MjmlEventConfiguration, {
|
|
||||||
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
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
template: debouncedTemplate,
|
|
||||||
subject: debouncedSubject,
|
|
||||||
payload: debouncedPayload,
|
|
||||||
} = debouncedMutationVariables;
|
|
||||||
useEffect(() => {
|
|
||||||
fetchTemplatePreview({
|
|
||||||
template: debouncedTemplate,
|
|
||||||
subject: debouncedSubject,
|
|
||||||
payload: debouncedPayload,
|
|
||||||
});
|
|
||||||
}, [debouncedPayload, debouncedSubject, debouncedTemplate, fetchTemplatePreview]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.viewContainer}>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<IconButton
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
router.push(mjmlUrls.configuration(configurationId));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BackSmallIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Typography variant="h2">
|
|
||||||
{messageEventTypesLabels[eventType]} event configuration
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12} lg={7}>
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit((data, event) => {
|
|
||||||
updateEventConfiguration({ ...data, configurationId });
|
|
||||||
})}
|
|
||||||
className={styles.form}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="active"
|
|
||||||
render={({ field: { value, name, onChange } }) => (
|
|
||||||
<div className={styles.field}>
|
|
||||||
{/* TODO: fix types in the MacawUI */}
|
|
||||||
{/* @ts-ignore: MacawUI use wrong type for */}
|
|
||||||
<SwitchSelector key={name} className={styles.field}>
|
|
||||||
{[
|
|
||||||
{ label: "Active", value: true },
|
|
||||||
{ label: "Disabled", value: false },
|
|
||||||
].map((button) => (
|
|
||||||
// @ts-ignore: MacawUI use wrong type for SwitchSelectorButton
|
|
||||||
<SwitchSelectorButton
|
|
||||||
value={button.value.toString()}
|
|
||||||
onClick={() => onChange(button.value)}
|
|
||||||
activeTab={value.toString()}
|
|
||||||
key={button.label}
|
|
||||||
>
|
|
||||||
{button.label}
|
|
||||||
</SwitchSelectorButton>
|
|
||||||
))}
|
|
||||||
</SwitchSelector>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="subject"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<TextField
|
|
||||||
label="Email subject"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
error={!!error}
|
|
||||||
helperText={
|
|
||||||
!error
|
|
||||||
? "You can use variables like {{ order.number }} or {{ order.userEmail }}"
|
|
||||||
: error.message
|
|
||||||
}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="template"
|
|
||||||
render={({ field: { value, onChange } }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.editor}>
|
|
||||||
<CodeEditor
|
|
||||||
initialTemplate={value}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
language="xml"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button type="submit" fullWidth variant="primary">
|
|
||||||
Save configuration
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={12} lg={5}>
|
|
||||||
<div>
|
|
||||||
<div className={styles.previewHeader}>
|
|
||||||
<Typography variant="h2">Preview</Typography>
|
|
||||||
{isFetchingTemplatePreview && <CircularProgress size="3rem" color="primary" />}
|
|
||||||
</div>
|
|
||||||
<Typography variant="h3" paragraph>
|
|
||||||
Subject: {lastValidRenderedSubject}
|
|
||||||
</Typography>
|
|
||||||
<div className={styles.preview}>
|
|
||||||
<MjmlPreview value={lastValidRenderedTemplate} />
|
|
||||||
</div>
|
|
||||||
<CodeEditor
|
|
||||||
initialTemplate={payload}
|
|
||||||
value={payload}
|
|
||||||
onChange={setPayload}
|
|
||||||
language="json"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,53 +0,0 @@
|
||||||
import { Link, Paper, Typography } from "@material-ui/core";
|
|
||||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
|
||||||
return {
|
|
||||||
instructionsContainer: {
|
|
||||||
padding: 15,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const MjmlInstructions = () => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
const { appBridge } = useAppBridge();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper elevation={0} className={styles.instructionsContainer}>
|
|
||||||
<Typography paragraph variant="h4">
|
|
||||||
MJML Provider
|
|
||||||
</Typography>
|
|
||||||
<Typography paragraph>
|
|
||||||
You can use this provider to send emails using MJML as a template language. The emails are
|
|
||||||
then sent using the SMTP.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography paragraph>
|
|
||||||
<Link
|
|
||||||
href="https://mjml.io/"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
appBridge?.dispatch(
|
|
||||||
actions.Redirect({
|
|
||||||
to: "https://mjml.io/",
|
|
||||||
newContext: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Visit the MJML Homepage
|
|
||||||
</Link>
|
|
||||||
</Typography>
|
|
||||||
<Typography paragraph variant="h4">
|
|
||||||
How to configure
|
|
||||||
</Typography>
|
|
||||||
<Typography paragraph>
|
|
||||||
Create a new configuration and fill in the required fields. After the configuration is
|
|
||||||
saved, you will be able to modify the email templates.
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,18 +0,0 @@
|
||||||
import { Card } from "@material-ui/core";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MjmlPreview = ({ value }: Props) => {
|
|
||||||
return (
|
|
||||||
<Card style={{ padding: "2rem", width: "100%" }}>
|
|
||||||
{value?.length ? (
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: value }} />
|
|
||||||
) : (
|
|
||||||
<p>No template preview</p>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,128 +0,0 @@
|
||||||
import { Divider, Paper, Typography } from "@material-ui/core";
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
EditIcon,
|
|
||||||
IconButton,
|
|
||||||
List,
|
|
||||||
ListHeader,
|
|
||||||
ListItem,
|
|
||||||
ListItemCell,
|
|
||||||
makeStyles,
|
|
||||||
SwitchSelector,
|
|
||||||
SwitchSelectorButton,
|
|
||||||
} from "@saleor/macaw-ui";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { mjmlUrls } from "../../urls";
|
|
||||||
import { messageEventTypesLabels } from "../../../event-handlers/message-event-types";
|
|
||||||
import { MjmlConfiguration } from "../mjml-config";
|
|
||||||
import { trpcClient } from "../../../trpc/trpc-client";
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
|
||||||
return {
|
|
||||||
spaceBetween: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
rowActions: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
tableRow: {
|
|
||||||
minHeight: "48px",
|
|
||||||
"&::after": {
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
interface MjmlTemplatesCardProps {
|
|
||||||
configurationId: string;
|
|
||||||
configuration: MjmlConfiguration;
|
|
||||||
onEventChanged: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MjmlTemplatesCard = ({
|
|
||||||
configurationId,
|
|
||||||
configuration,
|
|
||||||
onEventChanged,
|
|
||||||
}: MjmlTemplatesCardProps) => {
|
|
||||||
const classes = useStyles();
|
|
||||||
const router = useRouter();
|
|
||||||
const { notifySuccess } = useDashboardNotification();
|
|
||||||
|
|
||||||
const { mutate: updateEventConfiguration } =
|
|
||||||
trpcClient.mjmlConfiguration.updateEventConfiguration.useMutation({
|
|
||||||
onSuccess(data, variables) {
|
|
||||||
onEventChanged();
|
|
||||||
|
|
||||||
notifySuccess(variables.active ? "Event enabled" : "Event disabled");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper elevation={0}>
|
|
||||||
<ListHeader>
|
|
||||||
<ListItem className={classes.tableRow}>
|
|
||||||
<ListItemCell>Supported events and templates</ListItemCell>
|
|
||||||
</ListItem>
|
|
||||||
</ListHeader>
|
|
||||||
<List gridTemplate={["1fr"]}>
|
|
||||||
<Divider />
|
|
||||||
{configuration.events.map((eventConfiguration) => (
|
|
||||||
<React.Fragment key={eventConfiguration.eventType}>
|
|
||||||
<ListItem className={classes.tableRow}>
|
|
||||||
<ListItemCell>
|
|
||||||
<div className={classes.spaceBetween}>
|
|
||||||
<Typography>{messageEventTypesLabels[eventConfiguration.eventType]}</Typography>
|
|
||||||
<div className={classes.rowActions}>
|
|
||||||
{/* TODO: fix types in the MacawUI */}
|
|
||||||
{/* @ts-ignore: MacawUI use wrong type for */}
|
|
||||||
<SwitchSelector key={eventConfiguration.eventType}>
|
|
||||||
{[
|
|
||||||
{ label: "Active", value: true },
|
|
||||||
{ label: "Disabled", value: false },
|
|
||||||
].map((button) => (
|
|
||||||
// @ts-ignore: MacawUI use wrong type for SwitchSelectorButton
|
|
||||||
<SwitchSelectorButton
|
|
||||||
value={button.value.toString()}
|
|
||||||
onClick={() => {
|
|
||||||
updateEventConfiguration({
|
|
||||||
configurationId,
|
|
||||||
...eventConfiguration,
|
|
||||||
active: button.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
activeTab={eventConfiguration.active.toString()}
|
|
||||||
key={button.label}
|
|
||||||
>
|
|
||||||
{button.label}
|
|
||||||
</SwitchSelectorButton>
|
|
||||||
))}
|
|
||||||
</SwitchSelector>
|
|
||||||
<IconButton
|
|
||||||
variant="secondary"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
router.push(
|
|
||||||
mjmlUrls.eventConfiguration(configurationId, eventConfiguration.eventType)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditIcon />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ListItemCell>
|
|
||||||
</ListItem>
|
|
||||||
<Divider />
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,342 +0,0 @@
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import {
|
|
||||||
Divider,
|
|
||||||
FormControl,
|
|
||||||
InputLabel,
|
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
TextField,
|
|
||||||
TextFieldProps,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { Button, makeStyles, SwitchSelector, SwitchSelectorButton } from "@saleor/macaw-ui";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { SendgridConfiguration } from "../sendgrid-config";
|
|
||||||
import { trpcClient } from "../../../trpc/trpc-client";
|
|
||||||
import { useAppBridge, actions } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { fetchSenders } from "../../sendgrid-api";
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
field: {
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
editor: {
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
preview: {
|
|
||||||
marginBottom: 20,
|
|
||||||
},
|
|
||||||
sectionHeader: {
|
|
||||||
marginTop: 20,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onConfigurationSaved: () => void;
|
|
||||||
initialData: SendgridConfiguration;
|
|
||||||
configurationId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SendgridConfigurationForm = (props: Props) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const { appBridge } = useAppBridge();
|
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
|
||||||
const [senderId, setSenderId] = useState<string | undefined>(undefined);
|
|
||||||
|
|
||||||
const { handleSubmit, control, reset, setError, setValue } = useForm<SendgridConfiguration>({
|
|
||||||
defaultValues: props.initialData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: sendersChoices, isLoading: isSendersChoicesLoading } = useQuery({
|
|
||||||
queryKey: ["sendgridSenders"],
|
|
||||||
queryFn: fetchSenders({ apiKey: props.initialData.apiKey }),
|
|
||||||
enabled: !!props.initialData.apiKey?.length,
|
|
||||||
onSuccess(data) {
|
|
||||||
// we are not keeping senders ID in the database, so we need to find the ID of the sender
|
|
||||||
// configuration contains nickname and email set up in the Sendgrid account
|
|
||||||
if (data.length) {
|
|
||||||
const sender = data?.find((sender) => sender.from_email === props.initialData.senderEmail);
|
|
||||||
if (sender?.value) {
|
|
||||||
setSenderId(sender?.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { mutate: createOrUpdateConfiguration } =
|
|
||||||
trpcClient.sendgridConfiguration.updateOrCreateConfiguration.useMutation({
|
|
||||||
onSuccess: async (data, variables) => {
|
|
||||||
await queryClient.cancelQueries({
|
|
||||||
queryKey: ["sendgridConfiguration", "getConfigurations"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Optimistically update to the new value
|
|
||||||
queryClient.setQueryData<Array<SendgridConfiguration>>(
|
|
||||||
["sendgridConfiguration", "getConfigurations", undefined],
|
|
||||||
(old) => {
|
|
||||||
if (old) {
|
|
||||||
const index = old.findIndex((c) => c.id === data.id);
|
|
||||||
// If thats an update, replace the old one
|
|
||||||
if (index !== -1) {
|
|
||||||
old[index] = data;
|
|
||||||
return [...old];
|
|
||||||
} else {
|
|
||||||
return [...old, data];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return [data];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Trigger refetch to make sure we have a fresh data
|
|
||||||
props.onConfigurationSaved();
|
|
||||||
notifySuccess("Configuration saved");
|
|
||||||
},
|
|
||||||
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 SendgridConfiguration, {
|
|
||||||
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
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// when the configuration tab is changed, initialData change and form has to be updated
|
|
||||||
useEffect(() => {
|
|
||||||
reset(props.initialData);
|
|
||||||
}, [props.initialData, props.configurationId, reset]);
|
|
||||||
|
|
||||||
// fill sender email and name when sender is changed
|
|
||||||
useEffect(() => {
|
|
||||||
const sender = sendersChoices?.find((choice) => choice.value === senderId);
|
|
||||||
if (sender) {
|
|
||||||
setValue("senderName", sender.nickname);
|
|
||||||
setValue("senderEmail", sender.from_email);
|
|
||||||
} else {
|
|
||||||
setValue("senderName", undefined);
|
|
||||||
setValue("senderEmail", undefined);
|
|
||||||
}
|
|
||||||
}, [senderId, sendersChoices]);
|
|
||||||
|
|
||||||
const CommonFieldProps: TextFieldProps = {
|
|
||||||
className: styles.field,
|
|
||||||
fullWidth: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const isNewConfiguration = !props.configurationId;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit((data, event) => {
|
|
||||||
createOrUpdateConfiguration({
|
|
||||||
...data,
|
|
||||||
});
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{isNewConfiguration ? (
|
|
||||||
<Typography variant="h2" paragraph>
|
|
||||||
Create a new configuration
|
|
||||||
</Typography>
|
|
||||||
) : (
|
|
||||||
<Typography variant="h2" paragraph>
|
|
||||||
Configuration
|
|
||||||
<strong>{` ${props.initialData.configurationName} `}</strong>
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="configurationName"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error }, formState: { errors } }) => (
|
|
||||||
<TextField
|
|
||||||
label="Configuration name"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
error={!!error}
|
|
||||||
helperText={
|
|
||||||
error?.message || "Name of the configuration, for example 'Production' or 'Test'"
|
|
||||||
}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="active"
|
|
||||||
render={({ field: { value, name, onChange } }) => (
|
|
||||||
<div className={styles.field}>
|
|
||||||
{/* TODO: fix types in the MacawUI */}
|
|
||||||
{/* @ts-ignore: MacawUI use wrong type for */}
|
|
||||||
<SwitchSelector key={name} className={styles.field}>
|
|
||||||
{[
|
|
||||||
{ label: "Active", value: true },
|
|
||||||
{ label: "Disabled", value: false },
|
|
||||||
].map((button) => (
|
|
||||||
// @ts-ignore: MacawUI use wrong type for SwitchSelectorButton
|
|
||||||
<SwitchSelectorButton
|
|
||||||
value={button.value.toString()}
|
|
||||||
onClick={() => onChange(button.value)}
|
|
||||||
activeTab={value.toString()}
|
|
||||||
key={button.label}
|
|
||||||
>
|
|
||||||
{button.label}
|
|
||||||
</SwitchSelectorButton>
|
|
||||||
))}
|
|
||||||
</SwitchSelector>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Typography variant="h3" paragraph className={styles.sectionHeader}>
|
|
||||||
API configuration
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="apiKey"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<TextField
|
|
||||||
label="Sendgrid API key"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
helperText={
|
|
||||||
error?.message ||
|
|
||||||
"The API key can be generated at Sendgrid dashboard, in the Settings / API Keys section"
|
|
||||||
}
|
|
||||||
error={!!error}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="sandboxMode"
|
|
||||||
render={({ field: { value, name, onChange } }) => (
|
|
||||||
<div className={styles.field}>
|
|
||||||
{/* TODO: fix types in the MacawUI */}
|
|
||||||
{/* @ts-ignore: MacawUI use wrong type for */}
|
|
||||||
<SwitchSelector key={name} className={styles.field}>
|
|
||||||
{[
|
|
||||||
{ label: "Live", value: false },
|
|
||||||
{ label: "Sandbox", value: true },
|
|
||||||
].map((button) => (
|
|
||||||
// @ts-ignore: MacawUI use wrong type for SwitchSelectorButton
|
|
||||||
<SwitchSelectorButton
|
|
||||||
value={button.value.toString()}
|
|
||||||
onClick={() => onChange(button.value)}
|
|
||||||
activeTab={value.toString()}
|
|
||||||
key={button.label}
|
|
||||||
>
|
|
||||||
{button.label}
|
|
||||||
</SwitchSelectorButton>
|
|
||||||
))}
|
|
||||||
</SwitchSelector>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{/* Sender can be chosen after the API key is saved in the configuration */}
|
|
||||||
{!isNewConfiguration && (
|
|
||||||
<>
|
|
||||||
<Typography variant="h3" paragraph className={styles.sectionHeader}>
|
|
||||||
Sender details
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<FormControl className={styles.field} fullWidth>
|
|
||||||
<InputLabel>Sender</InputLabel>
|
|
||||||
<Select
|
|
||||||
variant="outlined"
|
|
||||||
value={senderId}
|
|
||||||
disabled={isSendersChoicesLoading}
|
|
||||||
onChange={(event, val) => {
|
|
||||||
if (val) {
|
|
||||||
const node = val as React.ReactElement;
|
|
||||||
setSenderId(node.props.value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSenderId(undefined);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem key="none" value={undefined}>
|
|
||||||
No sender
|
|
||||||
</MenuItem>
|
|
||||||
{!!sendersChoices &&
|
|
||||||
sendersChoices.map((choice) => (
|
|
||||||
<MenuItem key={choice.value} value={choice.value}>
|
|
||||||
{choice.label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
{!sendersChoices?.length && (
|
|
||||||
<Typography variant="caption" color="textSecondary">
|
|
||||||
Please set up and verify senders in your Sendgrid dashboard.
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="senderName"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<TextField
|
|
||||||
label="Sender name"
|
|
||||||
disabled={true}
|
|
||||||
error={!!error}
|
|
||||||
helperText={error?.message}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="senderEmail"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
||||||
<>
|
|
||||||
<TextField
|
|
||||||
label="Sender email"
|
|
||||||
value={value}
|
|
||||||
disabled={true}
|
|
||||||
helperText={error?.message}
|
|
||||||
error={!!error}
|
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Button type="submit" fullWidth variant="primary">
|
|
||||||
Save configuration
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,106 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import { AppColumnsLayout } from "../../../ui/app-columns-layout";
|
|
||||||
import { trpcClient } from "../../../trpc/trpc-client";
|
|
||||||
import { SendgridConfigurationForm } from "./sendgrid-configuration-form";
|
|
||||||
import { getDefaultEmptyConfiguration } from "../sendgrid-config-container";
|
|
||||||
import { NextRouter, useRouter } from "next/router";
|
|
||||||
import { SendgridConfiguration } from "../sendgrid-config";
|
|
||||||
import { LoadingIndicator } from "../../../ui/loading-indicator";
|
|
||||||
import { sendgridUrls } from "../../urls";
|
|
||||||
import { SendgridTemplatesCard } from "./sendgrid-templates-card";
|
|
||||||
import { SendgridInstructions } from "./sendgrid-instructions";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
|
||||||
return {
|
|
||||||
formContainer: {
|
|
||||||
top: 0,
|
|
||||||
},
|
|
||||||
instructionsContainer: {
|
|
||||||
padding: 15,
|
|
||||||
},
|
|
||||||
configurationColumn: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: 20,
|
|
||||||
maxWidth: 700,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
interface SendgridConfigurationTabProps {
|
|
||||||
configurationId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const navigateToFirstConfiguration = (
|
|
||||||
router: NextRouter,
|
|
||||||
configurations?: SendgridConfiguration[]
|
|
||||||
) => {
|
|
||||||
if (!configurations || !configurations?.length) {
|
|
||||||
router.replace(sendgridUrls.configuration());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const firstConfigurationId = configurations[0]?.id;
|
|
||||||
|
|
||||||
if (firstConfigurationId) {
|
|
||||||
router.replace(sendgridUrls.configuration(firstConfigurationId));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SendgridConfigurationTab = ({ configurationId }: SendgridConfigurationTabProps) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: configurations,
|
|
||||||
refetch: refetchConfigurations,
|
|
||||||
isLoading: configurationsIsLoading,
|
|
||||||
isFetching: configurationsIsFetching,
|
|
||||||
} = trpcClient.sendgridConfiguration.getConfigurations.useQuery(undefined, {
|
|
||||||
onSuccess(data) {
|
|
||||||
if (!configurationId) {
|
|
||||||
console.log("no conf id! navigate to first");
|
|
||||||
navigateToFirstConfiguration(router, data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (configurationsIsLoading || configurationsIsFetching) {
|
|
||||||
return <LoadingIndicator />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuration = configurations?.find((c) => c.id === configurationId?.toString());
|
|
||||||
|
|
||||||
if (configurationId && !configuration) {
|
|
||||||
return <div>Configuration not found</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppColumnsLayout>
|
|
||||||
<div className={styles.configurationColumn}>
|
|
||||||
{configurationsIsLoading || configurationsIsFetching ? (
|
|
||||||
<LoadingIndicator />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<SendgridConfigurationForm
|
|
||||||
onConfigurationSaved={() => refetchConfigurations()}
|
|
||||||
initialData={configuration || getDefaultEmptyConfiguration()}
|
|
||||||
configurationId={configurationId}
|
|
||||||
/>
|
|
||||||
{!!configurationId && !!configuration && (
|
|
||||||
<SendgridTemplatesCard
|
|
||||||
configurationId={configurationId}
|
|
||||||
configuration={configuration}
|
|
||||||
onEventChanged={() => {
|
|
||||||
refetchConfigurations();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<SendgridInstructions />
|
|
||||||
</AppColumnsLayout>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,219 +0,0 @@
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import {
|
|
||||||
FormControl,
|
|
||||||
Grid,
|
|
||||||
InputLabel,
|
|
||||||
MenuItem,
|
|
||||||
Select,
|
|
||||||
TextFieldProps,
|
|
||||||
Typography,
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import {
|
|
||||||
BackSmallIcon,
|
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
makeStyles,
|
|
||||||
SwitchSelector,
|
|
||||||
SwitchSelectorButton,
|
|
||||||
} from "@saleor/macaw-ui";
|
|
||||||
import React from "react";
|
|
||||||
import { SendgridConfiguration, SendgridEventConfiguration } from "../sendgrid-config";
|
|
||||||
import {
|
|
||||||
MessageEventTypes,
|
|
||||||
messageEventTypesLabels,
|
|
||||||
} from "../../../event-handlers/message-event-types";
|
|
||||||
import { trpcClient } from "../../../trpc/trpc-client";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { sendgridUrls } from "../../urls";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { fetchTemplates } from "../../sendgrid-api";
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
viewContainer: {
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing(2),
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
margin: "0 auto",
|
|
||||||
},
|
|
||||||
previewHeader: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
marginTop: theme.spacing(2),
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
},
|
|
||||||
|
|
||||||
field: {
|
|
||||||
marginBottom: theme.spacing(3),
|
|
||||||
},
|
|
||||||
editor: {
|
|
||||||
marginBottom: theme.spacing(3),
|
|
||||||
},
|
|
||||||
preview: {
|
|
||||||
marginBottom: theme.spacing(3),
|
|
||||||
},
|
|
||||||
form: {
|
|
||||||
maxWidth: 800,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
type EventConfigurationFormProps = {
|
|
||||||
initialData: SendgridEventConfiguration;
|
|
||||||
configurationId: string;
|
|
||||||
eventType: MessageEventTypes;
|
|
||||||
configuration: SendgridConfiguration;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EventConfigurationForm = ({
|
|
||||||
initialData,
|
|
||||||
configurationId,
|
|
||||||
eventType,
|
|
||||||
configuration,
|
|
||||||
}: EventConfigurationFormProps) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
|
||||||
|
|
||||||
const { handleSubmit, control, getValues, setError } = useForm<SendgridEventConfiguration>({
|
|
||||||
defaultValues: initialData,
|
|
||||||
});
|
|
||||||
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
const { data: templateChoices, isLoading: isTemplateChoicesLoading } = useQuery({
|
|
||||||
queryKey: ["sendgridTemplates"],
|
|
||||||
queryFn: fetchTemplates({ apiKey: configuration.apiKey }),
|
|
||||||
enabled: !!configuration.apiKey?.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
const CommonFieldProps: TextFieldProps = {
|
|
||||||
className: styles.field,
|
|
||||||
fullWidth: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { mutate: updateEventConfiguration } =
|
|
||||||
trpcClient.sendgridConfiguration.updateEventConfiguration.useMutation({
|
|
||||||
onSuccess: (data) => {
|
|
||||||
notifySuccess("Configuration saved");
|
|
||||||
},
|
|
||||||
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 SendgridEventConfiguration, {
|
|
||||||
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 (
|
|
||||||
<div className={styles.viewContainer}>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<IconButton
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => {
|
|
||||||
router.push(sendgridUrls.configuration(configurationId));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<BackSmallIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Typography variant="h2">
|
|
||||||
{messageEventTypesLabels[eventType]} event configuration
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
<Grid container spacing={2}>
|
|
||||||
<Grid item xs={12} lg={7}>
|
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit((data, event) => {
|
|
||||||
updateEventConfiguration({ ...data, configurationId });
|
|
||||||
})}
|
|
||||||
className={styles.form}
|
|
||||||
>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="active"
|
|
||||||
render={({ field: { value, name, onChange } }) => (
|
|
||||||
<div className={styles.field}>
|
|
||||||
{/* TODO: fix types in the MacawUI */}
|
|
||||||
{/* @ts-ignore: MacawUI use wrong type for */}
|
|
||||||
<SwitchSelector key={name} className={styles.field}>
|
|
||||||
{[
|
|
||||||
{ label: "Active", value: true },
|
|
||||||
{ label: "Disabled", value: false },
|
|
||||||
].map((button) => (
|
|
||||||
// @ts-ignore: MacawUI use wrong type for SwitchSelectorButton
|
|
||||||
<SwitchSelectorButton
|
|
||||||
value={button.value.toString()}
|
|
||||||
onClick={() => onChange(button.value)}
|
|
||||||
activeTab={value.toString()}
|
|
||||||
key={button.label}
|
|
||||||
>
|
|
||||||
{button.label}
|
|
||||||
</SwitchSelectorButton>
|
|
||||||
))}
|
|
||||||
</SwitchSelector>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="template"
|
|
||||||
render={({ field: { value, onChange } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl className={styles.field} fullWidth>
|
|
||||||
<InputLabel>Template</InputLabel>
|
|
||||||
<Select
|
|
||||||
variant="outlined"
|
|
||||||
value={value}
|
|
||||||
onChange={(event, val) => {
|
|
||||||
onChange(event.target.value);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem key="none" value={undefined}>
|
|
||||||
No template
|
|
||||||
</MenuItem>
|
|
||||||
{!!templateChoices &&
|
|
||||||
templateChoices.map((choice) => (
|
|
||||||
<MenuItem key={choice.value} value={choice.value}>
|
|
||||||
{choice.label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
{!templateChoices?.length && (
|
|
||||||
<Typography variant="caption" color="textSecondary">
|
|
||||||
No templates found in your account. Visit Sendgrid dashboard and create one.
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" fullWidth variant="primary">
|
|
||||||
Save configuration
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,103 +0,0 @@
|
||||||
import { Link, Paper, Typography } from "@material-ui/core";
|
|
||||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
|
||||||
return {
|
|
||||||
instructionsContainer: {
|
|
||||||
padding: 15,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const SendgridInstructions = () => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
const { appBridge } = useAppBridge();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper elevation={0} className={styles.instructionsContainer}>
|
|
||||||
<Typography paragraph variant="h4">
|
|
||||||
Sendgrid Provider
|
|
||||||
</Typography>
|
|
||||||
<Typography paragraph>
|
|
||||||
The integration uses dynamic email templates to send the messages to your customers.
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography paragraph>
|
|
||||||
<Link
|
|
||||||
href="https://sendgrid.com/"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
appBridge?.dispatch(
|
|
||||||
actions.Redirect({
|
|
||||||
to: "https://sendgrid.com/",
|
|
||||||
newContext: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Visit the Sendgrid Homepage
|
|
||||||
</Link>
|
|
||||||
</Typography>
|
|
||||||
<Typography paragraph variant="h4">
|
|
||||||
How to configure
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography paragraph>
|
|
||||||
Before configuring the app, make sure you have a Sendgrid account set up. To proceed you
|
|
||||||
will need:
|
|
||||||
<br />
|
|
||||||
<Link
|
|
||||||
href="https://app.sendgrid.com/settings/api_keys"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
appBridge?.dispatch(
|
|
||||||
actions.Redirect({
|
|
||||||
to: "https://app.sendgrid.com/settings/api_keys",
|
|
||||||
newContext: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
API key which can be generated in the Sendgrid dashboard
|
|
||||||
</Link>
|
|
||||||
<br />
|
|
||||||
<Link
|
|
||||||
href="https://app.sendgrid.com/settings/sender_auth"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
appBridge?.dispatch(
|
|
||||||
actions.Redirect({
|
|
||||||
to: "https://app.sendgrid.com/settings/sender_auth",
|
|
||||||
newContext: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Verified sender account
|
|
||||||
</Link>
|
|
||||||
<br />
|
|
||||||
<Link
|
|
||||||
href="https://mc.sendgrid.com/dynamic-templates"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
appBridge?.dispatch(
|
|
||||||
actions.Redirect({
|
|
||||||
to: "https://mc.sendgrid.com/dynamic-templates",
|
|
||||||
newContext: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Created dynamic email templates
|
|
||||||
</Link>
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography paragraph>
|
|
||||||
Create a new configuration and fill in the required fields. After the configuration is
|
|
||||||
saved, you will be able to assign the email template to each of the events.
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,131 +0,0 @@
|
||||||
import { Divider, Paper, Typography } from "@material-ui/core";
|
|
||||||
import React from "react";
|
|
||||||
import {
|
|
||||||
EditIcon,
|
|
||||||
IconButton,
|
|
||||||
List,
|
|
||||||
ListHeader,
|
|
||||||
ListItem,
|
|
||||||
ListItemCell,
|
|
||||||
makeStyles,
|
|
||||||
SwitchSelector,
|
|
||||||
SwitchSelectorButton,
|
|
||||||
} from "@saleor/macaw-ui";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { messageEventTypesLabels } from "../../../event-handlers/message-event-types";
|
|
||||||
import { trpcClient } from "../../../trpc/trpc-client";
|
|
||||||
import { useAppBridge, actions } from "@saleor/app-sdk/app-bridge";
|
|
||||||
import { SendgridConfiguration } from "../sendgrid-config";
|
|
||||||
import { sendgridUrls } from "../../urls";
|
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
|
||||||
return {
|
|
||||||
spaceBetween: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
rowActions: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
gap: theme.spacing(1),
|
|
||||||
},
|
|
||||||
tableRow: {
|
|
||||||
minHeight: "48px",
|
|
||||||
"&::after": {
|
|
||||||
display: "none",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
interface SendgridTemplatesCardProps {
|
|
||||||
configurationId: string;
|
|
||||||
configuration: SendgridConfiguration;
|
|
||||||
onEventChanged: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SendgridTemplatesCard = ({
|
|
||||||
configurationId,
|
|
||||||
configuration,
|
|
||||||
onEventChanged,
|
|
||||||
}: SendgridTemplatesCardProps) => {
|
|
||||||
const classes = useStyles();
|
|
||||||
const router = useRouter();
|
|
||||||
const { notifySuccess } = useDashboardNotification();
|
|
||||||
|
|
||||||
const { mutate: updateEventConfiguration } =
|
|
||||||
trpcClient.sendgridConfiguration.updateEventConfiguration.useMutation({
|
|
||||||
onSuccess(_data, variables) {
|
|
||||||
onEventChanged();
|
|
||||||
notifySuccess(variables.active ? "Event enabled" : "Event disabled");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper elevation={0}>
|
|
||||||
<ListHeader>
|
|
||||||
<ListItem className={classes.tableRow}>
|
|
||||||
<ListItemCell>Supported events and templates</ListItemCell>
|
|
||||||
</ListItem>
|
|
||||||
</ListHeader>
|
|
||||||
<List gridTemplate={["1fr"]}>
|
|
||||||
<Divider />
|
|
||||||
{configuration.events.map((eventConfiguration) => (
|
|
||||||
<React.Fragment key={eventConfiguration.eventType}>
|
|
||||||
<ListItem className={classes.tableRow}>
|
|
||||||
<ListItemCell>
|
|
||||||
<div className={classes.spaceBetween}>
|
|
||||||
<Typography>{messageEventTypesLabels[eventConfiguration.eventType]}</Typography>
|
|
||||||
<div className={classes.rowActions}>
|
|
||||||
{/* TODO: fix types in the MacawUI */}
|
|
||||||
{/* @ts-ignore: MacawUI use wrong type for */}
|
|
||||||
<SwitchSelector key={eventConfiguration.eventType}>
|
|
||||||
{[
|
|
||||||
{ label: "Active", value: true },
|
|
||||||
{ label: "Disabled", value: false },
|
|
||||||
].map((button) => (
|
|
||||||
// @ts-ignore: MacawUI use wrong type for SwitchSelectorButton
|
|
||||||
<SwitchSelectorButton
|
|
||||||
value={button.value.toString()}
|
|
||||||
onClick={() => {
|
|
||||||
updateEventConfiguration({
|
|
||||||
configurationId,
|
|
||||||
...eventConfiguration,
|
|
||||||
active: button.value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
activeTab={eventConfiguration.active.toString()}
|
|
||||||
key={button.label}
|
|
||||||
>
|
|
||||||
{button.label}
|
|
||||||
</SwitchSelectorButton>
|
|
||||||
))}
|
|
||||||
</SwitchSelector>
|
|
||||||
<IconButton
|
|
||||||
variant="secondary"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
event.preventDefault();
|
|
||||||
router.push(
|
|
||||||
sendgridUrls.eventConfiguration(
|
|
||||||
configurationId,
|
|
||||||
eventConfiguration.eventType
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<EditIcon />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ListItemCell>
|
|
||||||
</ListItem>
|
|
||||||
<Divider />
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,31 +0,0 @@
|
||||||
import { MenuItem, Select } from "@material-ui/core";
|
|
||||||
|
|
||||||
interface TemplateSelectionFieldProps {
|
|
||||||
templateChoices?: { label: string; value: string }[];
|
|
||||||
value?: string;
|
|
||||||
onChange: (value: unknown) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TemplateSelectionField = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
templateChoices,
|
|
||||||
}: TemplateSelectionFieldProps) => {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
variant="outlined"
|
|
||||||
value={value}
|
|
||||||
onChange={(event, val) => onChange(event.target.value)}
|
|
||||||
>
|
|
||||||
{!templateChoices?.length ? (
|
|
||||||
<MenuItem value="">No templates available</MenuItem>
|
|
||||||
) : (
|
|
||||||
templateChoices.map((choice) => (
|
|
||||||
<MenuItem key={choice.value} value={choice.value}>
|
|
||||||
{choice.label}
|
|
||||||
</MenuItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Select>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,20 +0,0 @@
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import { PropsWithChildren } from "react";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
root: {
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "auto 400px",
|
|
||||||
alignItems: "start",
|
|
||||||
gap: theme.spacing(3),
|
|
||||||
padding: "20px 0",
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
type AppColumnsLayoutProps = PropsWithChildren<{}>;
|
|
||||||
|
|
||||||
export const AppColumnsLayout = ({ children }: AppColumnsLayoutProps) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
return <div className={styles.root}>{children}</div>;
|
|
||||||
};
|
|
|
@ -1,42 +0,0 @@
|
||||||
import React, { useCallback, useRef } from "react";
|
|
||||||
|
|
||||||
import Editor from "@monaco-editor/react";
|
|
||||||
import { useTheme } from "@saleor/macaw-ui";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onChange(value: string): void;
|
|
||||||
initialTemplate: string;
|
|
||||||
value: string;
|
|
||||||
language: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CodeEditor = ({ initialTemplate, onChange, value, language }: Props) => {
|
|
||||||
const { themeType } = 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={themeType === "dark" ? "vs-dark" : "vs-light"}
|
|
||||||
defaultLanguage={language}
|
|
||||||
defaultValue={initialTemplate}
|
|
||||||
onMount={handleEditorDidMount}
|
|
||||||
onChange={handleOnChange}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,50 +0,0 @@
|
||||||
import React, { PropsWithChildren } from "react";
|
|
||||||
import { makeStyles, PageTab, PageTabs } from "@saleor/macaw-ui";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
|
||||||
appContainer: {
|
|
||||||
marginTop: theme.spacing(3),
|
|
||||||
marginLeft: theme.spacing(3),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
type Props = PropsWithChildren<{}>;
|
|
||||||
|
|
||||||
export const ConfigurationPageBaseLayout = ({ children }: Props) => {
|
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
key: "channels",
|
|
||||||
label: "Channels",
|
|
||||||
url: "/configuration/channels",
|
|
||||||
},
|
|
||||||
{ key: "mjml", label: "MJML", url: "/configuration/mjml" },
|
|
||||||
{
|
|
||||||
key: "sendgrid",
|
|
||||||
label: "Sendgrid",
|
|
||||||
url: "/configuration/sendgrid",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const activePath = tabs.find((tab) => router.pathname.startsWith(tab.url))?.key;
|
|
||||||
|
|
||||||
const navigateToTab = (value: string) => {
|
|
||||||
const redirectionUrl = tabs.find((tab) => tab.key === value)?.url;
|
|
||||||
if (redirectionUrl) {
|
|
||||||
router.push(redirectionUrl);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className={styles.appContainer}>
|
|
||||||
<PageTabs value={activePath} onChange={navigateToTab}>
|
|
||||||
{tabs.map((tab) => (
|
|
||||||
<PageTab key={tab.key} value={tab.key} label={tab.label} />
|
|
||||||
))}
|
|
||||||
</PageTabs>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,22 +0,0 @@
|
||||||
import { CircularProgress } from "@material-ui/core";
|
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
|
||||||
return {
|
|
||||||
loaderContainer: {
|
|
||||||
margin: "50px auto",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const LoadingIndicator = () => {
|
|
||||||
const styles = useStyles();
|
|
||||||
return (
|
|
||||||
<div className={styles.loaderContainer}>
|
|
||||||
<CircularProgress color="primary" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { AlertBase, Button } from "@saleor/macaw-ui";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Typography } from "@material-ui/core";
|
|
||||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
import { appName } from "./const";
|
import { appName } from "./const";
|
||||||
|
|
||||||
|
@ -10,26 +8,7 @@ export const NotReadyPage = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1>{appName}</h1>
|
<h1>{appName}</h1>
|
||||||
<AlertBase variant="error">
|
App can not be used
|
||||||
<Typography variant="h3" paragraph>
|
|
||||||
App can not be used
|
|
||||||
</Typography>
|
|
||||||
<Typography paragraph>
|
|
||||||
To configure the app you need to create at least 1 channel
|
|
||||||
</Typography>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={() => {
|
|
||||||
appBridge?.dispatch(
|
|
||||||
actions.Redirect({
|
|
||||||
to: `/channels/add`,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Set up channel
|
|
||||||
</Button>
|
|
||||||
</AlertBase>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
import { NextPage } from "next";
|
|
||||||
import React, { useEffect } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { trpcClient } from "../../../modules/trpc/trpc-client";
|
|
||||||
import { ConfigurationPageBaseLayout } from "../../../modules/ui/configuration-page-base-layout";
|
|
||||||
|
|
||||||
const ChannelsConfigurationPage: NextPage = () => {
|
|
||||||
const channels = trpcClient.channels.fetch.useQuery();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const sendgridConfigurations = trpcClient.sendgridConfiguration.getConfigurations.useQuery();
|
|
||||||
const mjmlConfigurations = trpcClient.mjmlConfiguration.getConfigurations.useQuery();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (router && channels.isSuccess && channels.data.length === 0) {
|
|
||||||
router.push("/not-ready");
|
|
||||||
}
|
|
||||||
}, [channels.data, channels.isSuccess, router]);
|
|
||||||
return (
|
|
||||||
<ConfigurationPageBaseLayout>
|
|
||||||
Sendgrid configurations:
|
|
||||||
<ul>
|
|
||||||
{sendgridConfigurations.data?.map((c) => (
|
|
||||||
<li key={c.id}>{c.configurationName}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
MJML configurations:
|
|
||||||
<ul>
|
|
||||||
{mjmlConfigurations.data?.map((c) => (
|
|
||||||
<li key={c.id}>{c.configurationName}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</ConfigurationPageBaseLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChannelsConfigurationPage;
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { NextPage } from "next";
|
|
||||||
import React from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { ConfigurationPageBaseLayout } from "../../../modules/ui/configuration-page-base-layout";
|
|
||||||
import { MjmlConfigurationTab } from "../../../modules/mjml/configuration/ui/mjml-configuration-tab";
|
|
||||||
|
|
||||||
const MjmlConfigurationPage: NextPage = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const configurationId = router.query.configurationId
|
|
||||||
? router.query.configurationId[0] // optional routes are passed as an array
|
|
||||||
: undefined;
|
|
||||||
return (
|
|
||||||
<ConfigurationPageBaseLayout>
|
|
||||||
<MjmlConfigurationTab configurationId={configurationId} />
|
|
||||||
</ConfigurationPageBaseLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MjmlConfigurationPage;
|
|
|
@ -1,69 +0,0 @@
|
||||||
import { NextPage } from "next";
|
|
||||||
import React from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { trpcClient } from "../../../../../modules/trpc/trpc-client";
|
|
||||||
|
|
||||||
import { parseMessageEventType } from "../../../../../modules/event-handlers/parse-message-event-type";
|
|
||||||
import { ConfigurationPageBaseLayout } from "../../../../../modules/ui/configuration-page-base-layout";
|
|
||||||
import { EventConfigurationForm } from "../../../../../modules/mjml/configuration/ui/mjml-event-configuration-form";
|
|
||||||
import { LoadingIndicator } from "../../../../../modules/ui/loading-indicator";
|
|
||||||
|
|
||||||
const EventConfigurationPage: NextPage = () => {
|
|
||||||
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,
|
|
||||||
isError,
|
|
||||||
isFetched,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
} = trpcClient.mjmlConfiguration.getEventConfiguration.useQuery(
|
|
||||||
{
|
|
||||||
configurationId,
|
|
||||||
// if event type is not valid, it calling the query will not be enabled
|
|
||||||
// so we can safely cast it
|
|
||||||
eventType: eventType!,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!configurationId && !!eventType,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: better error messages
|
|
||||||
if (!eventType || !configurationId) {
|
|
||||||
return <>Error: no event type or configuration id</>;
|
|
||||||
}
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<ConfigurationPageBaseLayout>
|
|
||||||
<LoadingIndicator />
|
|
||||||
</ConfigurationPageBaseLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
Error: could not load the config: fetched: {isFetched} is error {isError}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!configuration) {
|
|
||||||
return <>Error: no configuration with given id</>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ConfigurationPageBaseLayout>
|
|
||||||
<EventConfigurationForm
|
|
||||||
initialData={configuration}
|
|
||||||
configurationId={configurationId}
|
|
||||||
eventType={eventType}
|
|
||||||
/>
|
|
||||||
</ConfigurationPageBaseLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EventConfigurationPage;
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { NextPage } from "next";
|
|
||||||
import React from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { ConfigurationPageBaseLayout } from "../../../modules/ui/configuration-page-base-layout";
|
|
||||||
import { SendgridConfigurationTab } from "../../../modules/sendgrid/configuration/ui/sendgrid-configuration-tab";
|
|
||||||
|
|
||||||
const SendgridConfigurationPage: NextPage = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const configurationId = router.query.configurationId
|
|
||||||
? router.query.configurationId[0] // optional routes are passed as an array
|
|
||||||
: undefined;
|
|
||||||
return (
|
|
||||||
<ConfigurationPageBaseLayout>
|
|
||||||
<SendgridConfigurationTab configurationId={configurationId} />
|
|
||||||
</ConfigurationPageBaseLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SendgridConfigurationPage;
|
|
|
@ -1,77 +0,0 @@
|
||||||
import { NextPage } from "next";
|
|
||||||
import React from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { trpcClient } from "../../../../../modules/trpc/trpc-client";
|
|
||||||
|
|
||||||
import { parseMessageEventType } from "../../../../../modules/event-handlers/parse-message-event-type";
|
|
||||||
import { ConfigurationPageBaseLayout } from "../../../../../modules/ui/configuration-page-base-layout";
|
|
||||||
import { LoadingIndicator } from "../../../../../modules/ui/loading-indicator";
|
|
||||||
import { EventConfigurationForm } from "../../../../../modules/sendgrid/configuration/ui/sendgrid-event-configuration-form";
|
|
||||||
|
|
||||||
const EventConfigurationPage: NextPage = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const configurationId = router.query.configurationId as string;
|
|
||||||
const eventTypeFromQuery = router.query.eventType as string | undefined;
|
|
||||||
const eventType = parseMessageEventType(eventTypeFromQuery);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: eventConfiguration,
|
|
||||||
isError,
|
|
||||||
isFetched,
|
|
||||||
isLoading,
|
|
||||||
} = trpcClient.sendgridConfiguration.getEventConfiguration.useQuery(
|
|
||||||
{
|
|
||||||
configurationId,
|
|
||||||
// if event type is not valid, it calling the query will not be enabled
|
|
||||||
// so we can safely cast it
|
|
||||||
eventType: eventType!,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!configurationId && !!eventType,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: configuration } = trpcClient.sendgridConfiguration.getConfiguration.useQuery(
|
|
||||||
{
|
|
||||||
id: configurationId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!configurationId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!eventType || !configurationId) {
|
|
||||||
return <>Error: no event type or configuration id</>;
|
|
||||||
}
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<ConfigurationPageBaseLayout>
|
|
||||||
<LoadingIndicator />
|
|
||||||
</ConfigurationPageBaseLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
Error: could not load the config: fetched: {isFetched} is error {isError}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!eventConfiguration || !configuration) {
|
|
||||||
return <>Error: no configuration with given id</>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<ConfigurationPageBaseLayout>
|
|
||||||
<EventConfigurationForm
|
|
||||||
initialData={eventConfiguration}
|
|
||||||
configurationId={configurationId}
|
|
||||||
configuration={configuration}
|
|
||||||
eventType={eventType}
|
|
||||||
/>
|
|
||||||
</ConfigurationPageBaseLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EventConfigurationPage;
|
|
741
pnpm-lock.yaml
741
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue