📧 Improve SMTP event section (#548)

* Make channels section expandable based on override setting

* Revert "Make channels section expandable based on override setting"

This reverts commit e107c5e990b4110156043ed494fb0054bd936654.

* Improve copy in the descriptions

* Handle partial updates i n events

* Add status  component

* Fix typos and types

* Improve SMTP events section

* Add changeset

* Implement event sections as tables with array form

* Update the changelog

* Remove no longer used component

* Add empty option for template choice

* Remove no longer used component

* Update the test
This commit is contained in:
Krzysztof Wolski 2023-06-15 10:52:39 +02:00 committed by GitHub
parent f96563fc2b
commit 8287075e29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 339 additions and 229 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-emails-and-messages": patch
---
Events section UI has been updated. All events are displayed now as single table.

View file

@ -0,0 +1,12 @@
import { Box, BoxProps } from "@saleor/macaw-ui/next";
export const Table = {
Container: (props: BoxProps) => <Box __textAlign={"left"} width="100%" {...props} as="table" />,
Header: (props: BoxProps) => <Box {...props} as="thead" />,
Row: (props: BoxProps) => <Box {...props} as="tr" />,
HeaderCell: (props: BoxProps) => (
<Box fontWeight={"captionSmall"} fontSize={"captionSmall"} {...props} as="th" />
),
Body: (props: BoxProps) => <Box {...props} as="tbody" />,
Cell: (props: BoxProps) => <Box fontSize="bodyMedium" {...props} as="td" />,
};

View file

@ -2,7 +2,8 @@ import { Text } from "@saleor/macaw-ui/next";
export const ConfigurationNameDescriptionText = () => ( export const ConfigurationNameDescriptionText = () => (
<Text as="p"> <Text as="p">
The name for your configuration. You can have more than one if you want to use different settings for each channel. The name for your configuration. You can have more than one if you want to use different
settings for each channel.
<br /> <br />
For example - <code>production</code> and <code>development</code>. For example - <code>production</code> and <code>development</code>.
</Text> </Text>

View file

@ -22,7 +22,7 @@ export const messageEventTypesLabels: Record<MessageEventTypes, string> = {
ORDER_FULLY_PAID: "Order fully paid", ORDER_FULLY_PAID: "Order fully paid",
INVOICE_SENT: "Invoice sent", INVOICE_SENT: "Invoice sent",
ACCOUNT_CONFIRMATION: "Customer account confirmation", ACCOUNT_CONFIRMATION: "Customer account confirmation",
ACCOUNT_PASSWORD_RESET: "Customer account password reset", ACCOUNT_PASSWORD_RESET: "Customer account password reset request",
ACCOUNT_CHANGE_EMAIL_REQUEST: "Customer account change email request", ACCOUNT_CHANGE_EMAIL_REQUEST: "Customer account change email request",
ACCOUNT_CHANGE_EMAIL_CONFIRM: "Customer account change email confirmation", ACCOUNT_CHANGE_EMAIL_CONFIRM: "Customer account change email confirmation",
ACCOUNT_DELETE: "Customer account delete request", ACCOUNT_DELETE: "Customer account delete request",

View file

@ -86,3 +86,10 @@ export const sendgridUpdateEventSchema = sendgridConfigurationEventSchema.merge(
); );
export type SendgridUpdateEvent = z.infer<typeof sendgridUpdateEventSchema>; export type SendgridUpdateEvent = z.infer<typeof sendgridUpdateEventSchema>;
export const sendgridUpdateEventArraySchema = z.object({
configurationId: z.string(),
events: z.array(sendgridConfigurationEventSchema),
});
export type SendgridUpdateEventArray = z.infer<typeof sendgridUpdateEventArraySchema>;

View file

@ -6,7 +6,7 @@ import {
sendgridGetEventConfigurationInputSchema, sendgridGetEventConfigurationInputSchema,
sendgridUpdateApiConnectionSchema, sendgridUpdateApiConnectionSchema,
sendgridUpdateBasicInformationSchema, sendgridUpdateBasicInformationSchema,
sendgridUpdateEventConfigurationInputSchema, sendgridUpdateEventArraySchema,
sendgridUpdateEventSchema, sendgridUpdateEventSchema,
sendgridUpdateSenderSchema, sendgridUpdateSenderSchema,
} from "./sendgrid-config-input-schema"; } from "./sendgrid-config-input-schema";
@ -147,23 +147,6 @@ export const sendgridConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e); throwTrpcErrorFromConfigurationServiceError(e);
} }
}), }),
updateEventConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(sendgridUpdateEventConfigurationInputSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug(input, "sendgridConfigurationRouter.updateEventConfiguration called");
try {
return await ctx.sendgridConfigurationService.updateEventConfiguration({
configurationId: input.configurationId,
eventConfiguration: input,
});
} catch (e) {
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
updateBasicInformation: protectedWithConfigurationService updateBasicInformation: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(sendgridUpdateBasicInformationSchema) .input(sendgridUpdateBasicInformationSchema)
@ -260,13 +243,41 @@ export const sendgridConfigurationRouter = router({
logger.debug(input, "sendgridConfigurationRouter.updateEvent called"); logger.debug(input, "sendgridConfigurationRouter.updateEvent called");
const { id: configurationId, eventType, ...eventConfiguration } = input;
try { try {
return await ctx.sendgridConfigurationService.updateEventConfiguration({ return await ctx.sendgridConfigurationService.updateEventConfiguration({
eventConfiguration: input, configurationId,
configurationId: input.id, eventType,
eventConfiguration,
}); });
} catch (e) { } catch (e) {
throwTrpcErrorFromConfigurationServiceError(e); throwTrpcErrorFromConfigurationServiceError(e);
} }
}), }),
updateEventArray: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(sendgridUpdateEventArraySchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug(input, "sendgridConfigurationRouter.updateEventArray called");
return await Promise.all(
input.events.map(async (event) => {
const { eventType, ...eventConfiguration } = event;
try {
return await ctx.sendgridConfigurationService.updateEventConfiguration({
configurationId: input.configurationId,
eventType,
eventConfiguration,
});
} catch (e) {
throwTrpcErrorFromConfigurationServiceError(e);
}
})
);
}),
}); });

View file

@ -512,6 +512,7 @@ describe("SendgridConfigurationService", function () {
await service.updateEventConfiguration({ await service.updateEventConfiguration({
configurationId: validConfig.configurations[0].id, configurationId: validConfig.configurations[0].id,
eventType: validConfig.configurations[0].events[0].eventType,
eventConfiguration: { eventConfiguration: {
...validConfig.configurations[0].events[0], ...validConfig.configurations[0].events[0],
template: "42", template: "42",
@ -544,6 +545,7 @@ describe("SendgridConfigurationService", function () {
await expect(async () => await expect(async () =>
service.updateEventConfiguration({ service.updateEventConfiguration({
configurationId: "this-id-does-not-exist", configurationId: "this-id-does-not-exist",
eventType: validConfig.configurations[0].events[0].eventType,
eventConfiguration: { eventConfiguration: {
...validConfig.configurations[0].events[0], ...validConfig.configurations[0].events[0],
template: "42", template: "42",

View file

@ -234,19 +234,19 @@ export class SendgridConfigurationService {
*/ */
async updateEventConfiguration({ async updateEventConfiguration({
configurationId, configurationId,
eventType,
eventConfiguration, eventConfiguration,
}: { }: {
configurationId: string; configurationId: string;
eventConfiguration: SendgridEventConfiguration; eventType: SendgridEventConfiguration["eventType"];
eventConfiguration: Partial<Omit<SendgridEventConfiguration, "eventType">>;
}) { }) {
logger.debug("Update event configuration"); logger.debug("Update event configuration");
const configuration = await this.getConfiguration({ const configuration = await this.getConfiguration({
id: configurationId, id: configurationId,
}); });
const eventIndex = configuration.events.findIndex( const eventIndex = configuration.events.findIndex((e) => e.eventType === eventType);
(e) => e.eventType === eventConfiguration.eventType
);
if (eventIndex < 0) { if (eventIndex < 0) {
logger.warn("Event configuration not found, throwing an error"); logger.warn("Event configuration not found, throwing an error");
@ -256,9 +256,14 @@ export class SendgridConfigurationService {
); );
} }
configuration.events[eventIndex] = eventConfiguration; const updatedEventConfiguration = {
...configuration.events[eventIndex],
...eventConfiguration,
};
configuration.events[eventIndex] = updatedEventConfiguration;
await this.updateConfiguration(configuration); await this.updateConfiguration(configuration);
return configuration; return updatedEventConfiguration;
} }
} }

View file

@ -8,8 +8,8 @@ import { defaultPadding } from "../../../components/ui-defaults";
import { useDashboardNotification } from "@saleor/apps-shared"; import { useDashboardNotification } from "@saleor/apps-shared";
import { trpcClient } from "../../trpc/trpc-client"; import { trpcClient } from "../../trpc/trpc-client";
import { import {
SendgridUpdateEvent, SendgridUpdateEventArray,
sendgridUpdateEventSchema, sendgridUpdateEventArraySchema,
} from "../configuration/sendgrid-config-input-schema"; } from "../configuration/sendgrid-config-input-schema";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { BoxFooter } from "../../../components/box-footer"; import { BoxFooter } from "../../../components/box-footer";
@ -18,91 +18,57 @@ import { useQuery } from "@tanstack/react-query";
import { fetchTemplates } from "../sendgrid-api"; import { fetchTemplates } from "../sendgrid-api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { setBackendErrors } from "../../../lib/set-backend-errors"; import { setBackendErrors } from "../../../lib/set-backend-errors";
import { Combobox } from "@saleor/react-hook-form-macaw"; import { Select } from "@saleor/react-hook-form-macaw";
import { TextLink } from "@saleor/apps-ui"; import { TextLink } from "@saleor/apps-ui";
import { messageEventTypesLabels } from "../../event-handlers/message-event-types";
interface EventBoxProps { import { Table } from "../../../components/table";
configuration: SendgridConfiguration;
event: SendgridEventConfiguration;
}
const EventBox = ({ event, configuration }: EventBoxProps) => {
const { notifySuccess, notifyError } = useDashboardNotification();
const { data: templatesChoices } = useQuery({
queryKey: ["sendgridTemplates"],
queryFn: fetchTemplates({ apiKey: configuration.apiKey }),
enabled: !!configuration.apiKey?.length,
});
const { handleSubmit, control, setError, register } = useForm<SendgridUpdateEvent>({
defaultValues: {
id: configuration.id,
...event,
},
resolver: zodResolver(sendgridUpdateEventSchema),
});
const trpcContext = trpcClient.useContext();
const { mutate } = trpcClient.sendgridConfiguration.updateEvent.useMutation({
onSuccess: async () => {
notifySuccess("Configuration saved");
trpcContext.sendgridConfiguration.invalidate();
},
onError(error) {
setBackendErrors<SendgridUpdateEvent>({ error, setError, notifyError });
},
});
return (
<form
onSubmit={handleSubmit((data, event) => {
mutate({
...data,
});
})}
>
<BoxWithBorder>
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={defaultPadding}>
<Text variant="heading">{event.eventType}</Text>
{templatesChoices?.length ? (
<Combobox
name="template"
control={control}
label="Template"
options={templatesChoices.map((sender) => ({
label: sender.label,
value: sender.value,
}))}
/>
) : (
<Combobox name="template" control={control} label="Template" options={[]} />
)}
<label>
<input type="checkbox" placeholder="Enabled" {...register("active")} />
<Text paddingLeft={defaultPadding}>Active</Text>
</label>
</Box>
<BoxFooter>
<Button type="submit">Save event</Button>
</BoxFooter>
</BoxWithBorder>
</form>
);
};
interface SendgridEventsSectionProps { interface SendgridEventsSectionProps {
configuration: SendgridConfiguration; configuration: SendgridConfiguration;
} }
export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionProps) => { export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionProps) => {
const { notifySuccess, notifyError } = useDashboardNotification();
// Sort events by displayed label
const eventsSorted = configuration.events.sort((a, b) =>
messageEventTypesLabels[a.eventType].localeCompare(messageEventTypesLabels[b.eventType])
);
const { control, register, handleSubmit, setError } = useForm<SendgridUpdateEventArray>({
defaultValues: {
configurationId: configuration.id,
events: eventsSorted,
},
resolver: zodResolver(sendgridUpdateEventArraySchema),
});
const trpcContext = trpcClient.useContext();
const { mutate } = trpcClient.sendgridConfiguration.updateEventArray.useMutation({
onSuccess: async () => {
notifySuccess("Configuration saved");
trpcContext.sendgridConfiguration.invalidate();
},
onError(error) {
setBackendErrors<SendgridUpdateEventArray>({ error, setError, notifyError });
},
});
const { data: sendgridTemplates } = useQuery({
queryKey: ["sendgridTemplates"],
queryFn: fetchTemplates({ apiKey: configuration.apiKey }),
enabled: !!configuration.apiKey?.length,
});
const templateChoices = [{ value: "", label: "----" }, ...(sendgridTemplates || [])];
return ( return (
<SectionWithDescription <SectionWithDescription
title="Events" title="Events"
description={ description={
<Text as="p"> <Text as="p">
Choose which Saleor events should send emails via Sendgrid. You can create and modify your templates in the Choose which Saleor events should send emails via Sendgrid. You can create and modify your
templates in the
<TextLink href="https://mc.sendgrid.com/dynamic-templates" newTab={true}> <TextLink href="https://mc.sendgrid.com/dynamic-templates" newTab={true}>
Sendgrid dashboard Sendgrid dashboard
</TextLink> </TextLink>
@ -110,11 +76,47 @@ export const SendgridEventsSection = ({ configuration }: SendgridEventsSectionPr
</Text> </Text>
} }
> >
<Box display="flex" flexDirection="column" gap={defaultPadding}> <form
{configuration.events.map((event) => ( onSubmit={handleSubmit((data) => {
<EventBox key={event.eventType} configuration={configuration} event={event} /> mutate(data);
))} })}
</Box> >
<BoxWithBorder>
<Box padding={defaultPadding}>
<Table.Container>
<Table.Header>
<Table.Row>
<Table.HeaderCell __width={40}>Active</Table.HeaderCell>
<Table.HeaderCell>Event type</Table.HeaderCell>
<Table.HeaderCell>Dynamic template</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{eventsSorted.map((event, index) => (
<Table.Row key={event.eventType}>
<Table.Cell>
<input type="checkbox" {...register(`events.${index}.active`)} />
</Table.Cell>
<Table.Cell>
<Text>{messageEventTypesLabels[event.eventType]}</Text>
</Table.Cell>
<Table.Cell>
<Select
control={control}
name={`events.${index}.template`}
options={templateChoices}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Container>
</Box>
<BoxFooter>
<Button type="submit">Save provider</Button>
</BoxFooter>
</BoxWithBorder>
</form>
</SectionWithDescription> </SectionWithDescription>
); );
}; };

View file

@ -72,6 +72,21 @@ export const smtpUpdateEventSchema = smtpConfigurationEventSchema.merge(
export type SmtpUpdateEvent = z.infer<typeof smtpUpdateEventSchema>; export type SmtpUpdateEvent = z.infer<typeof smtpUpdateEventSchema>;
export const smtpUpdateEventActiveStatusInputSchema = smtpConfigurationEventSchema
.pick({
active: true,
eventType: true,
})
.merge(
smtpConfigurationSchema.pick({
id: true,
})
);
export type SmtpUpdateEventActiveStatusInput = z.infer<
typeof smtpUpdateEventActiveStatusInputSchema
>;
export const smtpGetEventConfigurationInputSchema = smtpConfigurationIdInputSchema.merge( export const smtpGetEventConfigurationInputSchema = smtpConfigurationIdInputSchema.merge(
z.object({ z.object({
eventType: z.enum(messageEventTypes), eventType: z.enum(messageEventTypes),
@ -87,3 +102,10 @@ export const smtpUpdateEventConfigurationInputSchema = smtpConfigurationIdInputS
export type SmtpUpdateEventConfigurationInput = z.infer< export type SmtpUpdateEventConfigurationInput = z.infer<
typeof smtpUpdateEventConfigurationInputSchema typeof smtpUpdateEventConfigurationInputSchema
>; >;
export const smtpUpdateEventArraySchema = z.object({
configurationId: z.string(),
events: z.array(smtpConfigurationEventSchema),
});
export type SmtpUpdateEventArray = z.infer<typeof smtpUpdateEventArraySchema>;

View file

@ -15,7 +15,8 @@ import {
smtpGetConfigurationsInputSchema, smtpGetConfigurationsInputSchema,
smtpGetEventConfigurationInputSchema, smtpGetEventConfigurationInputSchema,
smtpUpdateBasicInformationSchema, smtpUpdateBasicInformationSchema,
smtpUpdateEventConfigurationInputSchema, smtpUpdateEventActiveStatusInputSchema,
smtpUpdateEventArraySchema,
smtpUpdateEventSchema, smtpUpdateEventSchema,
smtpUpdateSenderSchema, smtpUpdateSenderSchema,
smtpUpdateSmtpSchema, smtpUpdateSmtpSchema,
@ -150,23 +151,6 @@ export const smtpConfigurationRouter = router({
throwTrpcErrorFromConfigurationServiceError(e); throwTrpcErrorFromConfigurationServiceError(e);
} }
}), }),
updateEventConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(smtpUpdateEventConfigurationInputSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug(input, "mjmlConfigurationRouter.updateEventConfiguration or create called");
try {
return await ctx.smtpConfigurationService.updateEventConfiguration({
configurationId: input.id,
eventConfiguration: input,
});
} catch (e) {
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
renderTemplate: protectedWithConfigurationService renderTemplate: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
@ -287,13 +271,59 @@ export const smtpConfigurationRouter = router({
logger.debug(input, "smtpConfigurationRouter.updateEvent called"); logger.debug(input, "smtpConfigurationRouter.updateEvent called");
const { id: configurationId, eventType, ...eventConfiguration } = input;
try { try {
return await ctx.smtpConfigurationService.updateEventConfiguration({ return await ctx.smtpConfigurationService.updateEventConfiguration({
eventConfiguration: input, configurationId,
configurationId: input.id, eventType,
eventConfiguration,
}); });
} catch (e) { } catch (e) {
throwTrpcErrorFromConfigurationServiceError(e); throwTrpcErrorFromConfigurationServiceError(e);
} }
}), }),
updateEventActiveStatus: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(smtpUpdateEventActiveStatusInputSchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug(input, "mjmlConfigurationRouter.updateEventActiveStatus called");
try {
return await ctx.smtpConfigurationService.updateEventConfiguration({
configurationId: input.id,
eventType: input.eventType,
eventConfiguration: {
active: input.active,
},
});
} catch (e) {
throwTrpcErrorFromConfigurationServiceError(e);
}
}),
updateEventArray: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(smtpUpdateEventArraySchema)
.mutation(async ({ ctx, input }) => {
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug(input, "smtpConfigurationRouter.updateEventArray called");
return await Promise.all(
input.events.map(async (event) => {
const { eventType, ...eventConfiguration } = event;
try {
return await ctx.smtpConfigurationService.updateEventConfiguration({
configurationId: input.configurationId,
eventType,
eventConfiguration,
});
} catch (e) {
throwTrpcErrorFromConfigurationServiceError(e);
}
})
);
}),
}); });

View file

@ -539,6 +539,7 @@ describe("SmtpConfigurationService", function () {
await service.updateEventConfiguration({ await service.updateEventConfiguration({
configurationId: validConfig.configurations[0].id, configurationId: validConfig.configurations[0].id,
eventType: validConfig.configurations[0].events[0].eventType,
eventConfiguration: { eventConfiguration: {
...validConfig.configurations[0].events[0], ...validConfig.configurations[0].events[0],
subject: "Updated subject", subject: "Updated subject",
@ -571,6 +572,7 @@ describe("SmtpConfigurationService", function () {
await expect(async () => await expect(async () =>
service.updateEventConfiguration({ service.updateEventConfiguration({
configurationId: "this-id-does-not-exist", configurationId: "this-id-does-not-exist",
eventType: validConfig.configurations[0].events[0].eventType,
eventConfiguration: { eventConfiguration: {
...validConfig.configurations[0].events[0], ...validConfig.configurations[0].events[0],
subject: "Updated subject", subject: "Updated subject",

View file

@ -225,18 +225,18 @@ export class SmtpConfigurationService {
async updateEventConfiguration({ async updateEventConfiguration({
configurationId, configurationId,
eventConfiguration, eventConfiguration,
eventType,
}: { }: {
configurationId: string; configurationId: string;
eventConfiguration: SmtpEventConfiguration; eventType: SmtpEventConfiguration["eventType"];
eventConfiguration: Partial<Omit<SmtpEventConfiguration, "eventType">>;
}) { }) {
logger.debug("Update event configuration"); logger.debug("Update event configuration");
const configuration = await this.getConfiguration({ const configuration = await this.getConfiguration({
id: configurationId, id: configurationId,
}); });
const eventIndex = configuration.events.findIndex( const eventIndex = configuration.events.findIndex((e) => e.eventType === eventType);
(e) => e.eventType === eventConfiguration.eventType
);
if (eventIndex < 0) { if (eventIndex < 0) {
logger.warn("Event configuration not found, throwing an error"); logger.warn("Event configuration not found, throwing an error");
@ -246,9 +246,14 @@ export class SmtpConfigurationService {
); );
} }
configuration.events[eventIndex] = eventConfiguration; const updatedEventConfiguration = {
...configuration.events[eventIndex],
...eventConfiguration,
};
configuration.events[eventIndex] = updatedEventConfiguration;
await this.updateConfiguration(configuration); await this.updateConfiguration(configuration);
return configuration; return updatedEventConfiguration;
} }
} }

View file

@ -4,12 +4,8 @@ import { MessageEventTypes } from "../../event-handlers/message-event-types";
import { useDashboardNotification } from "@saleor/apps-shared"; import { useDashboardNotification } from "@saleor/apps-shared";
import { trpcClient } from "../../trpc/trpc-client"; import { trpcClient } from "../../trpc/trpc-client";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import {
SmtpUpdateEventConfigurationInput,
smtpUpdateEventConfigurationInputSchema,
} from "../configuration/smtp-config-input-schema";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { CodeEditor } from "./code-edtor"; import { CodeEditor } from "./code-editor";
import { useDebounce } from "usehooks-ts"; import { useDebounce } from "usehooks-ts";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { examplePayloads } from "../../event-handlers/default-payloads"; import { examplePayloads } from "../../event-handlers/default-payloads";
@ -17,6 +13,7 @@ import { MjmlPreview } from "./mjml-preview";
import { defaultPadding } from "../../../components/ui-defaults"; import { defaultPadding } from "../../../components/ui-defaults";
import { setBackendErrors } from "../../../lib/set-backend-errors"; import { setBackendErrors } from "../../../lib/set-backend-errors";
import { Input } from "@saleor/react-hook-form-macaw"; import { Input } from "@saleor/react-hook-form-macaw";
import { SmtpUpdateEvent, smtpUpdateEventSchema } from "../configuration/smtp-config-input-schema";
const PREVIEW_DEBOUNCE_DELAY = 500; const PREVIEW_DEBOUNCE_DELAY = 500;
interface EventFormProps { interface EventFormProps {
@ -31,15 +28,13 @@ export const EventForm = ({ configuration, eventType }: EventFormProps) => {
(eventConfiguration) => eventConfiguration.eventType === eventType (eventConfiguration) => eventConfiguration.eventType === eventType
)!; // Event conf is not optional, so we can use ! here )!; // Event conf is not optional, so we can use ! here
const { handleSubmit, control, getValues, setError } = useForm<SmtpUpdateEventConfigurationInput>( const { handleSubmit, control, getValues, setError } = useForm<SmtpUpdateEvent>({
{ defaultValues: {
defaultValues: { id: configuration.id,
id: configuration.id, ...eventConfiguration,
...eventConfiguration, },
}, resolver: zodResolver(smtpUpdateEventSchema),
resolver: zodResolver(smtpUpdateEventConfigurationInputSchema), });
}
);
const trpcContext = trpcClient.useContext(); const trpcContext = trpcClient.useContext();
const { mutate } = trpcClient.smtpConfiguration.updateEvent.useMutation({ const { mutate } = trpcClient.smtpConfiguration.updateEvent.useMutation({
@ -48,7 +43,7 @@ export const EventForm = ({ configuration, eventType }: EventFormProps) => {
trpcContext.smtpConfiguration.invalidate(); trpcContext.smtpConfiguration.invalidate();
}, },
onError(error) { onError(error) {
setBackendErrors<SmtpUpdateEventConfigurationInput>({ setBackendErrors<SmtpUpdateEvent>({
error, error,
setError, setError,
notifyError, notifyError,

View file

@ -1,89 +1,57 @@
import { SmtpConfiguration, SmtpEventConfiguration } from "../configuration/smtp-config-schema"; import { SmtpConfiguration } from "../configuration/smtp-config-schema";
import { BoxWithBorder } from "../../../components/box-with-border"; import { BoxWithBorder } from "../../../components/box-with-border";
import { Box, Button, Text } from "@saleor/macaw-ui/next"; import { Box, Button, Text } from "@saleor/macaw-ui/next";
import { defaultPadding } from "../../../components/ui-defaults"; import { defaultPadding } from "../../../components/ui-defaults";
import { SectionWithDescription } from "../../../components/section-with-description";
import { useRouter } from "next/router";
import { smtpUrls } from "../urls";
import { TextLink } from "@saleor/apps-ui";
import React from "react";
import { messageEventTypesLabels } from "../../event-handlers/message-event-types";
import { BoxFooter } from "../../../components/box-footer";
import { Table } from "../../../components/table";
import { useDashboardNotification } from "@saleor/apps-shared"; import { useDashboardNotification } from "@saleor/apps-shared";
import {
SmtpUpdateEventArray,
smtpUpdateEventArraySchema,
} from "../configuration/smtp-config-input-schema";
import { zodResolver } from "@hookform/resolvers/zod";
import { trpcClient } from "../../trpc/trpc-client"; import { trpcClient } from "../../trpc/trpc-client";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { BoxFooter } from "../../../components/box-footer";
import { SectionWithDescription } from "../../../components/section-with-description";
import { SmtpUpdateEvent, smtpUpdateEventSchema } from "../configuration/smtp-config-input-schema";
import { useRouter } from "next/router";
import { zodResolver } from "@hookform/resolvers/zod";
import { smtpUrls } from "../urls";
import { setBackendErrors } from "../../../lib/set-backend-errors"; import { setBackendErrors } from "../../../lib/set-backend-errors";
import { TextLink } from "@saleor/apps-ui";
interface EventBoxProps {
configuration: SmtpConfiguration;
event: SmtpEventConfiguration;
}
const EventBox = ({ event, configuration }: EventBoxProps) => {
const router = useRouter();
const { notifySuccess, notifyError } = useDashboardNotification();
const { handleSubmit, control, setError, register } = useForm<SmtpUpdateEvent>({
defaultValues: {
id: configuration.id,
...event,
},
resolver: zodResolver(smtpUpdateEventSchema),
});
const trpcContext = trpcClient.useContext();
const { mutate } = trpcClient.smtpConfiguration.updateEvent.useMutation({
onSuccess: async () => {
notifySuccess("Configuration saved");
trpcContext.smtpConfiguration.invalidate();
},
onError(error) {
setBackendErrors<SmtpUpdateEvent>({
error,
setError,
notifyError,
});
},
});
return (
<form
onSubmit={handleSubmit((data, event) => {
mutate({
...data,
});
})}
>
<BoxWithBorder>
<Box padding={defaultPadding} display="flex" flexDirection="column" gap={defaultPadding}>
<Text variant="heading">{event.eventType}</Text>
<Button
variant="secondary"
onClick={() => {
router.push(smtpUrls.eventConfiguration(configuration.id, event.eventType));
}}
>
Edit template
</Button>
<label>
<input type="checkbox" placeholder="Enabled" {...register("active")} />
<Text paddingLeft={defaultPadding}>Active</Text>
</label>
</Box>
<BoxFooter>
<Button type="submit">Save event</Button>
</BoxFooter>
</BoxWithBorder>
</form>
);
};
interface SmtpEventsSectionProps { interface SmtpEventsSectionProps {
configuration: SmtpConfiguration; configuration: SmtpConfiguration;
} }
export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) => { export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) => {
const { notifySuccess, notifyError } = useDashboardNotification();
const router = useRouter();
// Sort events by displayed label
const eventsSorted = configuration.events.sort((a, b) =>
messageEventTypesLabels[a.eventType].localeCompare(messageEventTypesLabels[b.eventType])
);
const { register, handleSubmit, setError } = useForm<SmtpUpdateEventArray>({
defaultValues: {
configurationId: configuration.id,
events: eventsSorted,
},
resolver: zodResolver(smtpUpdateEventArraySchema),
});
const trpcContext = trpcClient.useContext();
const { mutate } = trpcClient.smtpConfiguration.updateEventArray.useMutation({
onSuccess: async () => {
notifySuccess("Configuration saved");
trpcContext.smtpConfiguration.invalidate();
},
onError(error) {
setBackendErrors<SmtpUpdateEventArray>({ error, setError, notifyError });
},
});
return ( return (
<SectionWithDescription <SectionWithDescription
title="Events" title="Events"
@ -100,11 +68,53 @@ export const SmtpEventsSection = ({ configuration }: SmtpEventsSectionProps) =>
</Box> </Box>
} }
> >
<Box display="flex" flexDirection="column" gap={defaultPadding}> <form
{configuration.events.map((event) => ( onSubmit={handleSubmit((data) => {
<EventBox key={event.eventType} configuration={configuration} event={event} /> mutate(data);
))} })}
</Box> >
<BoxWithBorder>
<Box padding={defaultPadding}>
<Table.Container>
<Table.Header>
<Table.Row>
<Table.HeaderCell __width={40}>Active</Table.HeaderCell>
<Table.HeaderCell>Event type</Table.HeaderCell>
<Table.HeaderCell __width={110}></Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{eventsSorted.map((event, index) => (
<Table.Row key={event.eventType}>
<Table.Cell>
<input type="checkbox" {...register(`events.${index}.active`)} />
</Table.Cell>
<Table.Cell>
<Text>{messageEventTypesLabels[event.eventType]}</Text>
</Table.Cell>
<Table.Cell>
<Button
variant="tertiary"
size="small"
onClick={() => {
router.push(
smtpUrls.eventConfiguration(configuration.id, event.eventType)
);
}}
>
Edit template
</Button>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Container>
</Box>
<BoxFooter>
<Button type="submit">Save provider</Button>
</BoxFooter>
</BoxWithBorder>
</form>
</SectionWithDescription> </SectionWithDescription>
); );
}; };

View file

@ -9,6 +9,7 @@ import { appUrls } from "../../../../../modules/app-configuration/urls";
import { EventForm } from "../../../../../modules/smtp/ui/event-form"; import { EventForm } from "../../../../../modules/smtp/ui/event-form";
import { smtpUrls } from "../../../../../modules/smtp/urls"; import { smtpUrls } from "../../../../../modules/smtp/urls";
import { TextLink } from "@saleor/apps-ui"; import { TextLink } from "@saleor/apps-ui";
import { messageEventTypesLabels } from "../../../../../modules/event-handlers/message-event-types";
const LoadingView = () => { const LoadingView = () => {
return ( return (
@ -78,12 +79,12 @@ const EditSmtpEventPage: NextPage = () => {
breadcrumbs={[ breadcrumbs={[
{ name: "Configuration", href: appUrls.configuration() }, { name: "Configuration", href: appUrls.configuration() },
{ name: `SMTP: ${configuration.name}`, href: smtpUrls.configuration(configurationId) }, { name: `SMTP: ${configuration.name}`, href: smtpUrls.configuration(configurationId) },
{ name: eventType }, { name: messageEventTypesLabels[eventType] },
]} ]}
> >
<Box display="flex" flexDirection="column" gap={10}> <Box display="flex" flexDirection="column" gap={10}>
<Text as="p"> <Text as="p">
Edit template for {eventType} event. You can learn more about MJML{" "} Edit template for <code>{eventType}</code> event. You can learn more about MJML{" "}
<TextLink href="https://mjml.io/" newTab={true}> <TextLink href="https://mjml.io/" newTab={true}>
here here
</TextLink> </TextLink>