Remove old code

This commit is contained in:
Krzysztof Wolski 2023-04-25 11:49:47 +02:00
parent 135b1649c8
commit a163b04d7a
27 changed files with 157 additions and 2253 deletions

View file

@ -16,13 +16,10 @@
"schemaVersion": "3.11.7"
},
"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",
"@saleor/app-sdk": "0.37.3",
"@saleor/apps-shared": "workspace:*",
"@saleor/macaw-ui": "^0.7.2",
"@saleor/macaw-ui": "0.8.0-pre.72",
"@sendgrid/client": "^7.7.0",
"@sendgrid/mail": "^7.7.0",
"@tanstack/react-query": "^4.24.4",

View file

@ -1,33 +1,24 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useTheme } from "@saleor/macaw-ui";
import { memo, useEffect } from "react";
import { useTheme } from "@saleor/macaw-ui/next";
import { useEffect } from "react";
/**
* 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() {
export function ThemeSynchronizer() {
const { appBridgeState } = useAppBridge();
const { setTheme, themeType } = useTheme();
const { setTheme } = useTheme();
useEffect(() => {
if (!setTheme || !appBridgeState?.theme) {
return;
}
if (themeType !== appBridgeState?.theme) {
setTheme(appBridgeState.theme);
/**
* 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);
if (appBridgeState.theme === "light") {
setTheme("defaultLight");
}
}, [appBridgeState?.theme, setTheme, themeType]);
if (appBridgeState.theme === "dark") {
setTheme("defaultDark");
}
}, [appBridgeState?.theme, setTheme]);
return null;
}
export const ThemeSynchronizer = memo(_ThemeSynchronizer);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,4 @@
import { AlertBase, Button } from "@saleor/macaw-ui";
import React from "react";
import { Typography } from "@material-ui/core";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { appName } from "./const";
@ -10,26 +8,7 @@ export const NotReadyPage = () => {
return (
<div>
<h1>{appName}</h1>
<AlertBase variant="error">
<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>
App can not be used
</div>
);
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -322,13 +322,10 @@ importers:
'@graphql-codegen/typescript-urql': ^3.7.3
'@graphql-codegen/urql-introspection': 2.2.1
'@graphql-typed-document-node/core': ^3.1.2
'@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
'@saleor/app-sdk': 0.37.3
'@saleor/apps-shared': workspace:*
'@saleor/macaw-ui': ^0.7.2
'@saleor/macaw-ui': 0.8.0-pre.72
'@sendgrid/client': ^7.7.0
'@sendgrid/mail': ^7.7.0
'@tanstack/react-query': ^4.24.4
@ -375,13 +372,10 @@ importers:
vitest: ^0.30.1
zod: ^3.20.2
dependencies:
'@material-ui/core': 4.12.4_5ndqzdd6t4rivxsukjv3i3ak2q
'@material-ui/icons': 4.11.3_x54wk6dsnsxe7g7vvfmytp77te
'@material-ui/lab': 4.0.0-alpha.61_x54wk6dsnsxe7g7vvfmytp77te
'@monaco-editor/react': 4.4.6_biqbaboplfbrettd7655fr4n2y
'@saleor/app-sdk': 0.37.3_yucv4tfv7v7nrkw2uguegj6e7e
'@saleor/apps-shared': link:../../packages/shared
'@saleor/macaw-ui': 0.7.2_pmlnlm755hlzzzocw2qhf3a34e
'@saleor/macaw-ui': 0.8.0-pre.72_5ndqzdd6t4rivxsukjv3i3ak2q
'@sendgrid/client': 7.7.0
'@sendgrid/mail': 7.7.0
'@tanstack/react-query': 4.24.4_biqbaboplfbrettd7655fr4n2y
@ -4320,6 +4314,34 @@ packages:
- '@types/react'
dev: false
/@radix-ui/react-popover/1.0.5_5ndqzdd6t4rivxsukjv3i3ak2q:
resolution: {integrity: sha512-GRHZ8yD12MrN2NLobHPE8Rb5uHTxd9x372DE9PPNnBjpczAQHcZ5ne0KXG4xpf+RDdXSzdLv9ym6mYJCDTaUZg==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0_react@18.2.0
'@radix-ui/react-context': 1.0.0_react@18.2.0
'@radix-ui/react-dismissable-layer': 1.0.3_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-focus-guards': 1.0.0_react@18.2.0
'@radix-ui/react-focus-scope': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-id': 1.0.0_react@18.2.0
'@radix-ui/react-popper': 1.1.1_5ndqzdd6t4rivxsukjv3i3ak2q
'@radix-ui/react-portal': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-presence': 1.0.0_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-slot': 1.0.1_react@18.2.0
'@radix-ui/react-use-controllable-state': 1.0.0_react@18.2.0
aria-hidden: 1.2.2_3stiutgnnbnfnf3uowm5cip22i
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-remove-scroll: 2.5.5_3stiutgnnbnfnf3uowm5cip22i
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-popper/1.1.1_5ndqzdd6t4rivxsukjv3i3ak2q:
resolution: {integrity: sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==}
peerDependencies:
@ -4479,6 +4501,31 @@ packages:
react-dom: 18.2.0_react@18.2.0
dev: false
/@radix-ui/react-tooltip/1.0.5_5ndqzdd6t4rivxsukjv3i3ak2q:
resolution: {integrity: sha512-cDKVcfzyO6PpckZekODJZDe5ZxZ2fCZlzKzTmPhe4mX9qTHRfLcKgqb0OKf22xLwDequ2tVleim+ZYx3rabD5w==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.20.13
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0_react@18.2.0
'@radix-ui/react-context': 1.0.0_react@18.2.0
'@radix-ui/react-dismissable-layer': 1.0.3_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-id': 1.0.0_react@18.2.0
'@radix-ui/react-popper': 1.1.1_5ndqzdd6t4rivxsukjv3i3ak2q
'@radix-ui/react-portal': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-presence': 1.0.0_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-primitive': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-slot': 1.0.1_react@18.2.0
'@radix-ui/react-use-controllable-state': 1.0.0_react@18.2.0
'@radix-ui/react-visually-hidden': 1.0.2_biqbaboplfbrettd7655fr4n2y
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-use-callback-ref/1.0.0_react@18.2.0:
resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==}
peerDependencies:
@ -4757,6 +4804,38 @@ packages:
- '@types/react'
dev: false
/@saleor/macaw-ui/0.8.0-pre.72_5ndqzdd6t4rivxsukjv3i3ak2q:
resolution: {integrity: sha512-9lcFkzf81q9Mxjqd00rWUUvom26YK3WCu8GCcmpqcEFu723/H76hxg2/LUd2cpqARavS1FgO+Vri7jkxkSz7sQ==}
engines: {node: '>=16 <19', pnpm: '>=8'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@dessert-box/react': 0.4.0_react@18.2.0
'@floating-ui/react-dom-interactions': 0.5.0_5ndqzdd6t4rivxsukjv3i3ak2q
'@radix-ui/react-accordion': 1.1.1_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-checkbox': 1.0.3_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-dialog': 1.0.3_5ndqzdd6t4rivxsukjv3i3ak2q
'@radix-ui/react-dropdown-menu': 2.0.4_5ndqzdd6t4rivxsukjv3i3ak2q
'@radix-ui/react-popover': 1.0.5_5ndqzdd6t4rivxsukjv3i3ak2q
'@radix-ui/react-portal': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-radio-group': 1.1.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-select': 1.2.1_5ndqzdd6t4rivxsukjv3i3ak2q
'@radix-ui/react-toggle': 1.0.2_biqbaboplfbrettd7655fr4n2y
'@radix-ui/react-tooltip': 1.0.5_5ndqzdd6t4rivxsukjv3i3ak2q
'@vanilla-extract/css-utils': 0.1.3
clsx: 1.2.1
downshift: 6.1.12_react@18.2.0
downshift7: /downshift/7.6.0_react@18.2.0
lodash: 4.17.21
lodash-es: 4.17.21
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-inlinesvg: 3.0.1_react@18.2.0
transitivePeerDependencies:
- '@types/react'
dev: false
/@selderee/plugin-htmlparser2/0.10.0:
resolution: {integrity: sha512-gW69MEamZ4wk1OsOq1nG1jcyhXIQcnrsX5JwixVw/9xaiav8TCyjESAruu1Rz9yyInhgBXxkNwMeygKnN2uxNA==}
dependencies:
@ -6412,6 +6491,10 @@ packages:
graphql: 16.6.0
dev: true
/@vanilla-extract/css-utils/0.1.3:
resolution: {integrity: sha512-PZAcHROlgtCUGI2y0JntdNwvPwCNyeVnkQu6KTYKdmxBbK3w72XJUmLFYapfaFfgami4I9CTLnrJTPdtmS3gpw==}
dev: false
/@vitejs/plugin-react/3.1.0_vite@4.2.1:
resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==}
engines: {node: ^14.18.0 || >=16.0.0}
@ -7741,6 +7824,10 @@ packages:
/compute-scroll-into-view/1.0.20:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
/compute-scroll-into-view/2.0.4:
resolution: {integrity: sha512-y/ZA3BGnxoM/QHHQ2Uy49CLtnWPbt4tTPpEEZiEmmiWBFKjej7nEyH8Ryz54jH0MLXflUYA3Er2zUxPSJu5R+g==}
dev: false
/computed-style/0.1.4:
resolution: {integrity: sha512-WpAmaKbMNmS3OProfHIdJiNleNJdgUrJfbKArXua28QF7+0CoZjlLn0lp6vlc+dl5r2/X9GQiQRQQU4BzSa69w==}
dev: false
@ -8295,6 +8382,19 @@ packages:
react-is: 17.0.2
tslib: 2.5.0
/downshift/7.6.0_react@18.2.0:
resolution: {integrity: sha512-VSoTVynTAsabou/hbZ6HJHUVhtBiVOjQoBsCPcQq5eAROIGP+9XKMp9asAKQ3cEcUP4oe0fFdD2pziUjhFY33Q==}
peerDependencies:
react: '>=16.12.0'
dependencies:
'@babel/runtime': 7.20.13
compute-scroll-into-view: 2.0.4
prop-types: 15.8.1
react: 18.2.0
react-is: 17.0.2
tslib: 2.5.0
dev: false
/dset/3.1.2:
resolution: {integrity: sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==}
engines: {node: '>=4'}