Update the sendgrid support (#321)
* Update the sendgrid support * Add changeset
This commit is contained in:
parent
f58043f72b
commit
14ac6144c0
20 changed files with 1401 additions and 619 deletions
5
.changeset/large-seals-study.md
Normal file
5
.changeset/large-seals-study.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"saleor-app-emails-and-messages": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Enable Sendgrid support
|
|
@ -59,18 +59,13 @@ export const ChannelsConfigurationTab = () => {
|
||||||
}, [mjmlConfigurations]);
|
}, [mjmlConfigurations]);
|
||||||
|
|
||||||
const { data: sendgridConfigurations, isLoading: isSendgridQueryLoading } =
|
const { data: sendgridConfigurations, isLoading: isSendgridQueryLoading } =
|
||||||
trpcClient.sendgridConfiguration.fetch.useQuery();
|
trpcClient.sendgridConfiguration.getConfigurations.useQuery({});
|
||||||
|
|
||||||
const sendgridConfigurationsListData = useMemo(() => {
|
const sendgridConfigurationsListData = useMemo(() => {
|
||||||
if (!sendgridConfigurations) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const keys = Object.keys(sendgridConfigurations.availableConfigurations ?? {}) || [];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
keys.map((key) => ({
|
sendgridConfigurations?.map((configuration) => ({
|
||||||
value: key,
|
value: configuration.id,
|
||||||
label: sendgridConfigurations.availableConfigurations[key].configurationName,
|
label: configuration.configurationName,
|
||||||
})) ?? []
|
})) ?? []
|
||||||
);
|
);
|
||||||
}, [sendgridConfigurations]);
|
}, [sendgridConfigurations]);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { logger as pinoLogger } from "../../lib/logger";
|
||||||
import { AppConfigurationService } from "../app-configuration/get-app-configuration.service";
|
import { AppConfigurationService } from "../app-configuration/get-app-configuration.service";
|
||||||
import { MjmlConfigurationService } from "../mjml/configuration/get-mjml-configuration.service";
|
import { MjmlConfigurationService } from "../mjml/configuration/get-mjml-configuration.service";
|
||||||
import { sendMjml } from "../mjml/send-mjml";
|
import { sendMjml } from "../mjml/send-mjml";
|
||||||
|
import { SendgridConfigurationService } from "../sendgrid/configuration/get-sendgrid-configuration.service";
|
||||||
import { sendSendgrid } from "../sendgrid/send-sendgrid";
|
import { sendSendgrid } from "../sendgrid/send-sendgrid";
|
||||||
import { MessageEventTypes } from "./message-event-types";
|
import { MessageEventTypes } from "./message-event-types";
|
||||||
|
|
||||||
|
@ -73,16 +74,30 @@ export const sendEventMessages = async ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const sendgridStatus = await sendSendgrid({
|
|
||||||
authData,
|
|
||||||
channel,
|
|
||||||
event,
|
|
||||||
payload,
|
|
||||||
recipientEmail,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (sendgridStatus?.errors.length) {
|
if (channelAppConfiguration.sendgridConfigurationId) {
|
||||||
logger.error("Sending message with Sendgrid has failed");
|
logger.debug("Channel has assigned Sendgrid configuration");
|
||||||
logger.error(sendgridStatus?.errors);
|
|
||||||
|
const sendgridConfigurationService = new SendgridConfigurationService({
|
||||||
|
apiClient: client,
|
||||||
|
saleorApiUrl: authData.saleorApiUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendgridConfiguration = await sendgridConfigurationService.getConfiguration({
|
||||||
|
id: channelAppConfiguration.sendgridConfigurationId,
|
||||||
|
});
|
||||||
|
if (sendgridConfiguration) {
|
||||||
|
const sendgridStatus = await sendSendgrid({
|
||||||
|
event,
|
||||||
|
payload,
|
||||||
|
recipientEmail,
|
||||||
|
sendgridConfiguration,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (sendgridStatus?.errors.length) {
|
||||||
|
logger.error("Sendgrid errors");
|
||||||
|
logger.error(sendgridStatus?.errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,36 +1,107 @@
|
||||||
import { PrivateMetadataSendgridConfigurator } from "./sendgrid-configurator";
|
import { SendgridConfigurator, PrivateMetadataSendgridConfigurator } from "./sendgrid-configurator";
|
||||||
import { Client } from "urql";
|
import { Client } from "urql";
|
||||||
import { logger as pinoLogger } from "../../../lib/logger";
|
import { logger as pinoLogger } from "../../../lib/logger";
|
||||||
|
import { SendgridConfig, SendgridConfiguration } from "./sendgrid-config";
|
||||||
|
import { FilterConfigurationsArgs, SendgridConfigContainer } from "./sendgrid-config-container";
|
||||||
import { createSettingsManager } from "../../../lib/metadata-manager";
|
import { createSettingsManager } from "../../../lib/metadata-manager";
|
||||||
|
|
||||||
// todo test
|
const logger = pinoLogger.child({
|
||||||
export class GetSendgridConfigurationService {
|
service: "SendgridConfigurationService",
|
||||||
constructor(
|
});
|
||||||
private settings: {
|
|
||||||
apiClient: Client;
|
|
||||||
saleorApiUrl: string;
|
|
||||||
}
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async getConfiguration() {
|
export class SendgridConfigurationService {
|
||||||
const logger = pinoLogger.child({
|
private configurationData?: SendgridConfig;
|
||||||
service: "GetSendgridConfigurationService",
|
private metadataConfigurator: SendgridConfigurator;
|
||||||
saleorApiUrl: this.settings.saleorApiUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { saleorApiUrl, apiClient } = this.settings;
|
constructor(args: { apiClient: Client; saleorApiUrl: string; initialData?: SendgridConfig }) {
|
||||||
|
this.metadataConfigurator = new PrivateMetadataSendgridConfigurator(
|
||||||
const sendgridConfigurator = new PrivateMetadataSendgridConfigurator(
|
createSettingsManager(args.apiClient),
|
||||||
createSettingsManager(apiClient),
|
args.saleorApiUrl
|
||||||
saleorApiUrl
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const savedSendgridConfig = (await sendgridConfigurator.getConfig()) ?? null;
|
if (args.initialData) {
|
||||||
|
this.configurationData = args.initialData;
|
||||||
logger.debug(savedSendgridConfig, "Retrieved sendgrid config from Metadata. Will return it");
|
|
||||||
|
|
||||||
if (savedSendgridConfig) {
|
|
||||||
return savedSendgridConfig;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch configuration from Saleor API and cache it
|
||||||
|
private async pullConfiguration() {
|
||||||
|
logger.debug("Fetch configuration from Saleor API");
|
||||||
|
|
||||||
|
const config = await this.metadataConfigurator.getConfig();
|
||||||
|
this.configurationData = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push configuration to Saleor API
|
||||||
|
private async pushConfiguration() {
|
||||||
|
logger.debug("Push configuration to Saleor API");
|
||||||
|
|
||||||
|
await this.metadataConfigurator.setConfig(this.configurationData!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns configuration from cache or fetches it from Saleor API
|
||||||
|
async getConfigurationRoot() {
|
||||||
|
logger.debug("Get configuration root");
|
||||||
|
|
||||||
|
if (this.configurationData) {
|
||||||
|
logger.debug("Using cached configuration");
|
||||||
|
return this.configurationData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No cached data, fetch it from Saleor API
|
||||||
|
await this.pullConfiguration();
|
||||||
|
|
||||||
|
if (!this.configurationData) {
|
||||||
|
logger.warn("No configuration found in Saleor API");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.configurationData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Saves configuration to Saleor API and cache it
|
||||||
|
async setConfigurationRoot(config: SendgridConfig) {
|
||||||
|
logger.debug("Set configuration root");
|
||||||
|
|
||||||
|
this.configurationData = config;
|
||||||
|
await this.pushConfiguration();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfiguration({ id }: { id: string }) {
|
||||||
|
logger.debug("Get configuration");
|
||||||
|
return SendgridConfigContainer.getConfiguration(await this.getConfigurationRoot())({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfigurations(filter?: FilterConfigurationsArgs) {
|
||||||
|
logger.debug("Get configuration");
|
||||||
|
return SendgridConfigContainer.getConfigurations(await this.getConfigurationRoot())(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createConfiguration(config: Omit<SendgridConfiguration, "id" | "events">) {
|
||||||
|
logger.debug("Create configuration");
|
||||||
|
const updatedConfigurationRoot = SendgridConfigContainer.createConfiguration(
|
||||||
|
await this.getConfigurationRoot()
|
||||||
|
)(config);
|
||||||
|
await this.setConfigurationRoot(updatedConfigurationRoot);
|
||||||
|
|
||||||
|
return updatedConfigurationRoot.configurations[
|
||||||
|
updatedConfigurationRoot.configurations.length - 1
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateConfiguration(config: SendgridConfiguration) {
|
||||||
|
logger.debug("Update configuration");
|
||||||
|
const updatedConfigurationRoot = SendgridConfigContainer.updateConfiguration(
|
||||||
|
await this.getConfigurationRoot()
|
||||||
|
)(config);
|
||||||
|
this.setConfigurationRoot(updatedConfigurationRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteConfiguration({ id }: { id: string }) {
|
||||||
|
logger.debug("Delete configuration");
|
||||||
|
const updatedConfigurationRoot = SendgridConfigContainer.deleteConfiguration(
|
||||||
|
await this.getConfigurationRoot()
|
||||||
|
)({ id });
|
||||||
|
this.setConfigurationRoot(updatedConfigurationRoot);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,60 +1,120 @@
|
||||||
import { SendgridConfig as SendgridConfig, SendgridConfiguration } from "./sendgrid-config";
|
import { messageEventTypes } from "../../event-handlers/message-event-types";
|
||||||
|
import {
|
||||||
|
SendgridConfig as SendgridConfigurationRoot,
|
||||||
|
SendgridConfiguration,
|
||||||
|
} from "./sendgrid-config";
|
||||||
|
|
||||||
export const getDefaultEmptySendgridConfiguration = (): SendgridConfiguration => {
|
export const generateSendgridConfigurationId = () => Date.now().toString();
|
||||||
const defaultConfig = {
|
|
||||||
active: false,
|
export const getDefaultEventsConfiguration = (): SendgridConfiguration["events"] =>
|
||||||
|
messageEventTypes.map((eventType) => ({
|
||||||
|
active: true,
|
||||||
|
eventType: eventType,
|
||||||
|
template: "",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const getDefaultEmptyConfiguration = (): SendgridConfiguration => {
|
||||||
|
const defaultConfig: SendgridConfiguration = {
|
||||||
|
id: "",
|
||||||
|
active: true,
|
||||||
configurationName: "",
|
configurationName: "",
|
||||||
sandboxMode: false,
|
senderName: undefined,
|
||||||
senderName: "",
|
senderEmail: undefined,
|
||||||
senderEmail: "",
|
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
templateInvoiceSentSubject: "Invoice sent",
|
sandboxMode: false,
|
||||||
templateInvoiceSentTemplate: "",
|
events: getDefaultEventsConfiguration(),
|
||||||
templateOrderCancelledSubject: "Order Cancelled",
|
|
||||||
templateOrderCancelledTemplate: "",
|
|
||||||
templateOrderConfirmedSubject: "Order Confirmed",
|
|
||||||
templateOrderConfirmedTemplate: "",
|
|
||||||
templateOrderFullyPaidSubject: "Order Fully Paid",
|
|
||||||
templateOrderFullyPaidTemplate: "",
|
|
||||||
templateOrderCreatedSubject: "Order created",
|
|
||||||
templateOrderCreatedTemplate: "",
|
|
||||||
templateOrderFulfilledSubject: "Order fulfilled",
|
|
||||||
templateOrderFulfilledTemplate: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSendgridConfigurationById =
|
interface GetConfigurationArgs {
|
||||||
(sendgridConfig: SendgridConfig | null | undefined) => (configurationId?: string) => {
|
id: string;
|
||||||
if (!configurationId?.length) {
|
}
|
||||||
return getDefaultEmptySendgridConfiguration();
|
|
||||||
|
const getConfiguration =
|
||||||
|
(sendgridConfigRoot: SendgridConfigurationRoot | null | undefined) =>
|
||||||
|
({ id }: GetConfigurationArgs) => {
|
||||||
|
if (!sendgridConfigRoot || !sendgridConfigRoot.configurations) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const existingConfig = sendgridConfig?.availableConfigurations[configurationId];
|
|
||||||
if (!existingConfig) {
|
return sendgridConfigRoot.configurations.find((c) => c.id === id);
|
||||||
return getDefaultEmptySendgridConfiguration();
|
|
||||||
}
|
|
||||||
return existingConfig;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setSendgridConfigurationById =
|
export interface FilterConfigurationsArgs {
|
||||||
(sendgridConfig: SendgridConfig | null | undefined) =>
|
ids?: string[];
|
||||||
(configurationId: string | undefined) =>
|
active?: boolean;
|
||||||
(sendgridConfiguration: SendgridConfiguration) => {
|
}
|
||||||
const sendgridConfigNormalized = structuredClone(sendgridConfig) ?? {
|
|
||||||
availableConfigurations: {},
|
const getConfigurations =
|
||||||
};
|
(sendgridConfigRoot: SendgridConfigurationRoot | null | undefined) =>
|
||||||
|
(filter: FilterConfigurationsArgs | undefined): SendgridConfiguration[] => {
|
||||||
|
if (!sendgridConfigRoot || !sendgridConfigRoot.configurations) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let filtered = sendgridConfigRoot.configurations;
|
||||||
|
|
||||||
|
if (filter?.ids?.length) {
|
||||||
|
filtered = filtered.filter((c) => filter?.ids?.includes(c.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter?.active !== undefined) {
|
||||||
|
filtered = filtered.filter((c) => c.active === filter.active);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createConfiguration =
|
||||||
|
(sendgridConfigRoot: SendgridConfigurationRoot | null | undefined) =>
|
||||||
|
(sendgridConfiguration: Omit<SendgridConfiguration, "id" | "events">) => {
|
||||||
|
const sendgridConfigNormalized = structuredClone(sendgridConfigRoot) ?? { configurations: [] };
|
||||||
|
|
||||||
// for creating a new configurations, the ID has to be generated
|
// for creating a new configurations, the ID has to be generated
|
||||||
const id = configurationId || Date.now();
|
const newConfiguration = {
|
||||||
sendgridConfigNormalized.availableConfigurations[id] ??= getDefaultEmptySendgridConfiguration();
|
...sendgridConfiguration,
|
||||||
|
id: generateSendgridConfigurationId(),
|
||||||
|
events: getDefaultEventsConfiguration(),
|
||||||
|
};
|
||||||
|
sendgridConfigNormalized.configurations.push(newConfiguration);
|
||||||
|
return sendgridConfigNormalized;
|
||||||
|
};
|
||||||
|
|
||||||
sendgridConfigNormalized.availableConfigurations[id] = sendgridConfiguration;
|
const updateConfiguration =
|
||||||
|
(sendgridConfig: SendgridConfigurationRoot | null | undefined) =>
|
||||||
|
(sendgridConfiguration: SendgridConfiguration) => {
|
||||||
|
const sendgridConfigNormalized = structuredClone(sendgridConfig) ?? { configurations: [] };
|
||||||
|
|
||||||
|
const configurationIndex = sendgridConfigNormalized.configurations.findIndex(
|
||||||
|
(configuration) => configuration.id === sendgridConfiguration.id
|
||||||
|
);
|
||||||
|
|
||||||
|
sendgridConfigNormalized.configurations[configurationIndex] = sendgridConfiguration;
|
||||||
|
return sendgridConfigNormalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DeleteConfigurationArgs {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteConfiguration =
|
||||||
|
(sendgridConfig: SendgridConfigurationRoot | null | undefined) =>
|
||||||
|
({ id }: DeleteConfigurationArgs) => {
|
||||||
|
const sendgridConfigNormalized = structuredClone(sendgridConfig) ?? { configurations: [] };
|
||||||
|
|
||||||
|
sendgridConfigNormalized.configurations = sendgridConfigNormalized.configurations.filter(
|
||||||
|
(configuration) => configuration.id !== id
|
||||||
|
);
|
||||||
|
|
||||||
return sendgridConfigNormalized;
|
return sendgridConfigNormalized;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SendgridConfigContainer = {
|
export const SendgridConfigContainer = {
|
||||||
getSendgridConfigurationById,
|
createConfiguration,
|
||||||
setSendgridConfigurationById,
|
getConfiguration,
|
||||||
|
updateConfiguration,
|
||||||
|
deleteConfiguration,
|
||||||
|
getConfigurations,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,26 +1,51 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { messageEventTypes } from "../../event-handlers/message-event-types";
|
||||||
|
|
||||||
export const sendgridConfigInputSchema = z.object({
|
export const sendgridConfigurationEventObjectSchema = z.object({
|
||||||
availableConfigurations: z.record(
|
active: z.boolean(),
|
||||||
z.object({
|
eventType: z.enum(messageEventTypes),
|
||||||
active: z.boolean(),
|
template: z.string().min(1),
|
||||||
configurationName: z.string().min(1),
|
});
|
||||||
sandboxMode: z.boolean(),
|
|
||||||
senderName: z.string().min(0),
|
export const sendgridConfigurationBaseObjectSchema = z.object({
|
||||||
senderEmail: z.string().email(),
|
active: z.boolean(),
|
||||||
apiKey: z.string().min(0),
|
configurationName: z.string().min(1),
|
||||||
templateInvoiceSentSubject: z.string(),
|
sandboxMode: z.boolean(),
|
||||||
templateInvoiceSentTemplate: z.string(),
|
apiKey: z.string().min(1),
|
||||||
templateOrderCancelledSubject: z.string(),
|
senderName: z.string().min(1).optional(),
|
||||||
templateOrderCancelledTemplate: z.string(),
|
senderEmail: z.string().email().min(5).optional(),
|
||||||
templateOrderConfirmedSubject: z.string(),
|
});
|
||||||
templateOrderConfirmedTemplate: z.string(),
|
|
||||||
templateOrderFullyPaidSubject: z.string(),
|
export const sendgridCreateConfigurationSchema = sendgridConfigurationBaseObjectSchema.omit({
|
||||||
templateOrderFullyPaidTemplate: z.string(),
|
senderEmail: true,
|
||||||
templateOrderCreatedSubject: z.string(),
|
senderName: true,
|
||||||
templateOrderCreatedTemplate: z.string(),
|
});
|
||||||
templateOrderFulfilledSubject: z.string(),
|
export const sendgridUpdateOrCreateConfigurationSchema =
|
||||||
templateOrderFulfilledTemplate: z.string(),
|
sendgridConfigurationBaseObjectSchema.merge(
|
||||||
})
|
z.object({
|
||||||
),
|
id: z.string().optional(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
export const sendgridGetConfigurationInputSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
export const sendgridDeleteConfigurationInputSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
});
|
||||||
|
export const sendgridGetConfigurationsInputSchema = z
|
||||||
|
.object({
|
||||||
|
ids: z.array(z.string()).optional(),
|
||||||
|
active: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
export const sendgridUpdateEventConfigurationInputSchema = z
|
||||||
|
.object({
|
||||||
|
configurationId: z.string(),
|
||||||
|
})
|
||||||
|
.merge(sendgridConfigurationEventObjectSchema);
|
||||||
|
|
||||||
|
export const sendgridGetEventConfigurationInputSchema = z.object({
|
||||||
|
configurationId: z.string(),
|
||||||
|
eventType: z.enum(messageEventTypes),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
|
import { MessageEventTypes } from "../../event-handlers/message-event-types";
|
||||||
|
|
||||||
|
export interface SendgridEventConfiguration {
|
||||||
|
active: boolean;
|
||||||
|
eventType: MessageEventTypes;
|
||||||
|
template: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SendgridConfiguration {
|
export interface SendgridConfiguration {
|
||||||
|
id: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
configurationName: string;
|
configurationName: string;
|
||||||
sandboxMode: boolean;
|
sandboxMode: boolean;
|
||||||
senderName: string;
|
senderName?: string;
|
||||||
senderEmail: string;
|
senderEmail?: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
templateInvoiceSentSubject: string;
|
events: SendgridEventConfiguration[];
|
||||||
templateInvoiceSentTemplate: string;
|
|
||||||
templateOrderCancelledSubject: string;
|
|
||||||
templateOrderCancelledTemplate: string;
|
|
||||||
templateOrderConfirmedSubject: string;
|
|
||||||
templateOrderConfirmedTemplate: string;
|
|
||||||
templateOrderFullyPaidSubject: string;
|
|
||||||
templateOrderFullyPaidTemplate: string;
|
|
||||||
templateOrderCreatedSubject: string;
|
|
||||||
templateOrderCreatedTemplate: string;
|
|
||||||
templateOrderFulfilledSubject: string;
|
|
||||||
templateOrderFulfilledTemplate: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SendgridConfigurationsIdMap = Record<string, SendgridConfiguration>;
|
|
||||||
|
|
||||||
export type SendgridConfig = {
|
export type SendgridConfig = {
|
||||||
availableConfigurations: SendgridConfigurationsIdMap;
|
configurations: SendgridConfiguration[];
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,37 +1,159 @@
|
||||||
import { PrivateMetadataSendgridConfigurator } from "./sendgrid-configurator";
|
|
||||||
import { logger as pinoLogger } from "../../../lib/logger";
|
import { logger as pinoLogger } from "../../../lib/logger";
|
||||||
import { sendgridConfigInputSchema } from "./sendgrid-config-input-schema";
|
import {
|
||||||
import { GetSendgridConfigurationService } from "./get-sendgrid-configuration.service";
|
sendgridCreateConfigurationSchema,
|
||||||
|
sendgridDeleteConfigurationInputSchema,
|
||||||
|
sendgridGetConfigurationInputSchema,
|
||||||
|
sendgridGetConfigurationsInputSchema,
|
||||||
|
sendgridGetEventConfigurationInputSchema,
|
||||||
|
sendgridUpdateEventConfigurationInputSchema,
|
||||||
|
sendgridUpdateOrCreateConfigurationSchema,
|
||||||
|
} from "./sendgrid-config-input-schema";
|
||||||
|
import { SendgridConfigurationService } from "./get-sendgrid-configuration.service";
|
||||||
import { router } from "../../trpc/trpc-server";
|
import { router } from "../../trpc/trpc-server";
|
||||||
import { protectedClientProcedure } from "../../trpc/protected-client-procedure";
|
import { protectedClientProcedure } from "../../trpc/protected-client-procedure";
|
||||||
import { createSettingsManager } from "../../../lib/metadata-manager";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
// Allow access only for the dashboard users and attaches the
|
||||||
|
// configuration service to the context
|
||||||
|
const protectedWithConfigurationService = protectedClientProcedure.use(({ next, ctx }) =>
|
||||||
|
next({
|
||||||
|
ctx: {
|
||||||
|
...ctx,
|
||||||
|
configurationService: new SendgridConfigurationService({
|
||||||
|
apiClient: ctx.apiClient,
|
||||||
|
saleorApiUrl: ctx.saleorApiUrl,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
export const sendgridConfigurationRouter = router({
|
export const sendgridConfigurationRouter = router({
|
||||||
fetch: protectedClientProcedure.query(async ({ ctx, input }) => {
|
fetch: protectedWithConfigurationService.query(async ({ ctx }) => {
|
||||||
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
|
||||||
logger.debug("sendgridConfigurationRouter.fetch called");
|
logger.debug("sendgridConfigurationRouter.fetch called");
|
||||||
|
return ctx.configurationService.getConfigurationRoot();
|
||||||
return new GetSendgridConfigurationService({
|
|
||||||
apiClient: ctx.apiClient,
|
|
||||||
saleorApiUrl: ctx.saleorApiUrl,
|
|
||||||
}).getConfiguration();
|
|
||||||
}),
|
}),
|
||||||
setAndReplace: protectedClientProcedure
|
getConfiguration: protectedWithConfigurationService
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(sendgridConfigInputSchema)
|
.input(sendgridGetConfigurationInputSchema)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
logger.debug(input, "sendgridConfigurationRouter.get called");
|
||||||
|
return ctx.configurationService.getConfiguration(input);
|
||||||
|
}),
|
||||||
|
getConfigurations: protectedWithConfigurationService
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(sendgridGetConfigurationsInputSchema)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
logger.debug(input, "sendgridConfigurationRouter.getConfigurations called");
|
||||||
|
return ctx.configurationService.getConfigurations(input);
|
||||||
|
}),
|
||||||
|
createConfiguration: protectedWithConfigurationService
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(sendgridCreateConfigurationSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
logger.debug(input, "sendgridConfigurationRouter.create called");
|
||||||
|
return await ctx.configurationService.createConfiguration(input);
|
||||||
|
}),
|
||||||
|
deleteConfiguration: protectedWithConfigurationService
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(sendgridDeleteConfigurationInputSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
logger.debug(input, "sendgridConfigurationRouter.delete called");
|
||||||
|
const existingConfiguration = await ctx.configurationService.getConfiguration(input);
|
||||||
|
if (!existingConfiguration) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Configuration not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await ctx.configurationService.deleteConfiguration(input);
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
updateOrCreateConfiguration: protectedWithConfigurationService
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(sendgridUpdateOrCreateConfigurationSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
logger.debug(input, "sendgridConfigurationRouter.update or create called");
|
||||||
|
|
||||||
|
const { id } = input;
|
||||||
|
if (!id) {
|
||||||
|
return await ctx.configurationService.createConfiguration(input);
|
||||||
|
} else {
|
||||||
|
const existingConfiguration = await ctx.configurationService.getConfiguration({ id });
|
||||||
|
if (!existingConfiguration) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Configuration not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const configuration = {
|
||||||
|
id,
|
||||||
|
...input,
|
||||||
|
events: existingConfiguration.events,
|
||||||
|
};
|
||||||
|
await ctx.configurationService.updateConfiguration(configuration);
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getEventConfiguration: protectedWithConfigurationService
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(sendgridGetEventConfigurationInputSchema)
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
|
||||||
|
logger.debug(input, "sendgridConfigurationRouter.getEventConfiguration or create called");
|
||||||
|
|
||||||
|
const configuration = await ctx.configurationService.getConfiguration({
|
||||||
|
id: input.configurationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!configuration) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Configuration not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const event = configuration.events.find((e) => e.eventType === input.eventType);
|
||||||
|
if (!event) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Event configuration not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
}),
|
||||||
|
updateEventConfiguration: protectedWithConfigurationService
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(sendgridUpdateEventConfigurationInputSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
|
||||||
logger.debug(input, "sendgridConfigurationRouter.setAndReplace called with input");
|
logger.debug(input, "sendgridConfigurationRouter.updateEventConfiguration or create called");
|
||||||
|
|
||||||
const sendgridConfigurator = new PrivateMetadataSendgridConfigurator(
|
const configuration = await ctx.configurationService.getConfiguration({
|
||||||
createSettingsManager(ctx.apiClient),
|
id: input.configurationId,
|
||||||
ctx.saleorApiUrl
|
});
|
||||||
);
|
|
||||||
|
|
||||||
await sendgridConfigurator.setConfig(input);
|
if (!configuration) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Configuration not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
const eventIndex = configuration.events.findIndex((e) => e.eventType === input.eventType);
|
||||||
|
configuration.events[eventIndex] = {
|
||||||
|
active: input.active,
|
||||||
|
eventType: input.eventType,
|
||||||
|
template: input.template,
|
||||||
|
};
|
||||||
|
await ctx.configurationService.updateConfiguration(configuration);
|
||||||
|
return configuration;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
interface FetchTemplatesArgs {
|
|
||||||
apiKey?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const fetchTemplates =
|
|
||||||
({ apiKey }: FetchTemplatesArgs) =>
|
|
||||||
async () => {
|
|
||||||
if (!apiKey) {
|
|
||||||
console.warn(
|
|
||||||
"The Sendgrid API key has not been set up yet. Skipping fetching available templates."
|
|
||||||
);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const response = await fetch(
|
|
||||||
"https://api.sendgrid.com/v3/templates?generations=dynamic&page_size=18",
|
|
||||||
{
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${apiKey}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error("Could not fetch available Sendgrid templates");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const resJson = (await response.json()) as {
|
|
||||||
result?: { id: string; name: string }[];
|
|
||||||
};
|
|
||||||
const templates =
|
|
||||||
resJson.result?.map((r) => ({
|
|
||||||
value: r.id,
|
|
||||||
label: r.name,
|
|
||||||
})) || [];
|
|
||||||
return templates;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Could not parse the response from Sendgrid", e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,52 +1,146 @@
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
|
Divider,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormControlLabel,
|
|
||||||
InputLabel,
|
InputLabel,
|
||||||
Switch,
|
MenuItem,
|
||||||
|
Select,
|
||||||
TextField,
|
TextField,
|
||||||
TextFieldProps,
|
TextFieldProps,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@material-ui/core";
|
} from "@material-ui/core";
|
||||||
import { Button, makeStyles } from "@saleor/macaw-ui";
|
import { Button, makeStyles, SwitchSelector, SwitchSelectorButton } from "@saleor/macaw-ui";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { SendgridConfiguration } from "../sendgrid-config";
|
import { SendgridConfiguration } from "../sendgrid-config";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { trpcClient } from "../../../trpc/trpc-client";
|
||||||
import { TemplateSelectionField } from "./template-selection-field";
|
import { useAppBridge, actions } from "@saleor/app-sdk/app-bridge";
|
||||||
import { fetchTemplates } from "./fetch-templates";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { fetchSenders } from "../../sendgrid-api";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles((theme) => ({
|
||||||
field: {
|
field: {
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
form: {
|
editor: {
|
||||||
padding: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
});
|
preview: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
sectionHeader: {
|
||||||
|
marginTop: 20,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSubmit(data: SendgridConfiguration): Promise<void>;
|
onConfigurationSaved: () => void;
|
||||||
initialData: SendgridConfiguration;
|
initialData: SendgridConfiguration;
|
||||||
configurationId?: string;
|
configurationId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SendgridConfigurationForm = (props: Props) => {
|
export const SendgridConfigurationForm = (props: Props) => {
|
||||||
const { handleSubmit, control, reset } = useForm<SendgridConfiguration>({
|
const styles = useStyles();
|
||||||
|
const { appBridge } = useAppBridge();
|
||||||
|
const [senderId, setSenderId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const { handleSubmit, control, reset, setError, setValue } = useForm<SendgridConfiguration>({
|
||||||
defaultValues: props.initialData,
|
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();
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Notification({
|
||||||
|
title: "Configuration saved",
|
||||||
|
status: "success",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Notification({
|
||||||
|
title: "Could not save the configuration",
|
||||||
|
text: isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
||||||
|
apiMessage: formErrorMessage,
|
||||||
|
status: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// when the configuration tab is changed, initialData change and form has to be updated
|
// when the configuration tab is changed, initialData change and form has to be updated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset(props.initialData);
|
reset(props.initialData);
|
||||||
}, [props.initialData, reset]);
|
}, [props.initialData, props.configurationId, reset]);
|
||||||
|
|
||||||
const { data: templateChoices, isLoading: isTemplateChoicesLoading } = useQuery({
|
// fill sender email and name when sender is changed
|
||||||
queryKey: ["sendgridTemplates"],
|
useEffect(() => {
|
||||||
queryFn: fetchTemplates({ apiKey: props.initialData.apiKey }),
|
const sender = sendersChoices?.find((choice) => choice.value === senderId);
|
||||||
enabled: !!props.initialData?.apiKey.length,
|
if (sender) {
|
||||||
});
|
setValue("senderName", sender.nickname);
|
||||||
|
setValue("senderEmail", sender.from_email);
|
||||||
const styles = useStyles();
|
} else {
|
||||||
|
setValue("senderName", undefined);
|
||||||
|
setValue("senderEmail", undefined);
|
||||||
|
}
|
||||||
|
}, [senderId, sendersChoices]);
|
||||||
|
|
||||||
const CommonFieldProps: TextFieldProps = {
|
const CommonFieldProps: TextFieldProps = {
|
||||||
className: styles.field,
|
className: styles.field,
|
||||||
|
@ -58,88 +152,80 @@ export const SendgridConfigurationForm = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit((data, event) => {
|
onSubmit={handleSubmit((data, event) => {
|
||||||
props.onSubmit(data);
|
createOrUpdateConfiguration({
|
||||||
|
...data,
|
||||||
|
});
|
||||||
})}
|
})}
|
||||||
className={styles.form}
|
|
||||||
>
|
>
|
||||||
{isNewConfiguration ? (
|
{isNewConfiguration ? (
|
||||||
<Typography variant="h4" paragraph>
|
<Typography variant="h2" paragraph>
|
||||||
Create a new configuration
|
Create a new configuration
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="h4" paragraph>
|
<Typography variant="h2" paragraph>
|
||||||
Configuration {props.initialData?.configurationName}
|
Configuration
|
||||||
|
<strong>{` ${props.initialData.configurationName} `}</strong>
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="active"
|
|
||||||
render={({ field: { value, onChange } }) => {
|
|
||||||
return (
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch value={value} checked={value} onChange={(event, val) => onChange(val)} />
|
|
||||||
}
|
|
||||||
label="Active"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="sandboxMode"
|
|
||||||
render={({ field: { value, onChange } }) => {
|
|
||||||
return (
|
|
||||||
<FormControlLabel
|
|
||||||
control={
|
|
||||||
<Switch value={value} checked={value} onChange={(event, val) => onChange(val)} />
|
|
||||||
}
|
|
||||||
label="Sandbox mode"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Controller
|
<Controller
|
||||||
name="configurationName"
|
name="configurationName"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value }, fieldState: { error }, formState: { errors } }) => (
|
||||||
<TextField
|
<TextField
|
||||||
label="Configuration name"
|
label="Configuration name"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
error={!!error}
|
||||||
|
helperText={error?.message}
|
||||||
{...CommonFieldProps}
|
{...CommonFieldProps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="senderName"
|
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
name="active"
|
||||||
<TextField label="Sender name" value={value} onChange={onChange} {...CommonFieldProps} />
|
render={({ field: { value, name, onChange } }) => (
|
||||||
)}
|
<div className={styles.field}>
|
||||||
/>
|
{/* TODO: fix types in the MacawUI */}
|
||||||
<Controller
|
{/* @ts-ignore: MacawUI use wrong type for */}
|
||||||
name="senderEmail"
|
<SwitchSelector key={name} className={styles.field}>
|
||||||
control={control}
|
{[
|
||||||
render={({ field: { onChange, value } }) => (
|
{ label: "Active", value: true },
|
||||||
<TextField label="Sender email" value={value} onChange={onChange} {...CommonFieldProps} />
|
{ 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
|
<Controller
|
||||||
name="apiKey"
|
name="apiKey"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
<TextField label="API key" value={value} onChange={onChange} {...CommonFieldProps} />
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
name="templateOrderCreatedSubject"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Order Created Email subject"
|
label="Sendgrid API key"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
helperText={error?.message}
|
||||||
|
error={!!error}
|
||||||
{...CommonFieldProps}
|
{...CommonFieldProps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -147,171 +233,107 @@ export const SendgridConfigurationForm = (props: Props) => {
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="templateOrderCreatedTemplate"
|
name="sandboxMode"
|
||||||
render={({ field: { value, onChange } }) => {
|
render={({ field: { value, name, onChange } }) => (
|
||||||
return (
|
<div className={styles.field}>
|
||||||
<FormControl className={styles.field} disabled={isTemplateChoicesLoading} fullWidth>
|
{/* TODO: fix types in the MacawUI */}
|
||||||
<InputLabel>Template for Order Created</InputLabel>
|
{/* @ts-ignore: MacawUI use wrong type for */}
|
||||||
<TemplateSelectionField
|
<SwitchSelector key={name} className={styles.field}>
|
||||||
value={value}
|
{[
|
||||||
onChange={onChange}
|
{ label: "Live", value: false },
|
||||||
templateChoices={templateChoices}
|
{ label: "Sandbox", value: true },
|
||||||
/>
|
].map((button) => (
|
||||||
</FormControl>
|
// @ts-ignore: MacawUI use wrong type for SwitchSelectorButton
|
||||||
);
|
<SwitchSelectorButton
|
||||||
}}
|
value={button.value.toString()}
|
||||||
/>
|
onClick={() => onChange(button.value)}
|
||||||
|
activeTab={value.toString()}
|
||||||
<Controller
|
key={button.label}
|
||||||
name="templateOrderFulfilledSubject"
|
>
|
||||||
control={control}
|
{button.label}
|
||||||
render={({ field: { onChange, value } }) => (
|
</SwitchSelectorButton>
|
||||||
<TextField
|
))}
|
||||||
label="Order Fulfilled Email subject"
|
</SwitchSelector>
|
||||||
value={value}
|
</div>
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<Divider />
|
||||||
control={control}
|
|
||||||
name="templateOrderFulfilledTemplate"
|
{/* Sender can be chosen after the API key is saved in the configuration */}
|
||||||
render={({ field: { value, onChange } }) => {
|
{!isNewConfiguration && (
|
||||||
return (
|
<>
|
||||||
<FormControl className={styles.field} disabled={isTemplateChoicesLoading} fullWidth>
|
<Typography variant="h3" paragraph className={styles.sectionHeader}>
|
||||||
<InputLabel>Template for Order Fulfilled</InputLabel>
|
Sender details
|
||||||
<TemplateSelectionField
|
</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}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
templateChoices={templateChoices}
|
{...CommonFieldProps}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
)}
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="templateOrderConfirmedSubject"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<TextField
|
|
||||||
label="Order Confirmed Email subject"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
name="senderEmail"
|
||||||
name="templateOrderConfirmedTemplate"
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => {
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
||||||
return (
|
<>
|
||||||
<FormControl className={styles.field} disabled={isTemplateChoicesLoading} fullWidth>
|
<TextField
|
||||||
<InputLabel>Template for Order Confirmed</InputLabel>
|
label="Sender email"
|
||||||
<TemplateSelectionField
|
value={value}
|
||||||
value={value}
|
disabled={true}
|
||||||
onChange={onChange}
|
helperText={error?.message}
|
||||||
templateChoices={templateChoices}
|
error={!!error}
|
||||||
/>
|
onChange={onChange}
|
||||||
</FormControl>
|
{...CommonFieldProps}
|
||||||
);
|
/>
|
||||||
}}
|
</>
|
||||||
/>
|
)}
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="templateOrderCancelledSubject"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<TextField
|
|
||||||
label="Order Cancelled Email subject"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</>
|
||||||
/>
|
)}
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="templateOrderCancelledTemplate"
|
|
||||||
render={({ field: { value, onChange } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl className={styles.field} disabled={isTemplateChoicesLoading} fullWidth>
|
|
||||||
<InputLabel>Template for Order Cancelled</InputLabel>
|
|
||||||
<TemplateSelectionField
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
templateChoices={templateChoices}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="templateOrderFullyPaidSubject"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<TextField
|
|
||||||
label="Order Fully Paid Email subject"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="templateOrderFullyPaidTemplate"
|
|
||||||
render={({ field: { value, onChange } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl className={styles.field} disabled={isTemplateChoicesLoading} fullWidth>
|
|
||||||
<InputLabel>Template for Order Fully Paid</InputLabel>
|
|
||||||
<TemplateSelectionField
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
templateChoices={templateChoices}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
name="templateInvoiceSentSubject"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
|
||||||
<TextField
|
|
||||||
label="Invoice sent Email subject"
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
{...CommonFieldProps}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="templateInvoiceSentTemplate"
|
|
||||||
render={({ field: { value, onChange } }) => {
|
|
||||||
return (
|
|
||||||
<FormControl className={styles.field} disabled={isTemplateChoicesLoading} fullWidth>
|
|
||||||
<InputLabel>Template for Invoice Sent</InputLabel>
|
|
||||||
<TemplateSelectionField
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
templateChoices={templateChoices}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button type="submit" fullWidth variant="primary">
|
<Button type="submit" fullWidth variant="primary">
|
||||||
Save configuration
|
Save configuration
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
import { CircularProgress, Paper } from "@material-ui/core";
|
import React from "react";
|
||||||
import React, { useEffect, useState } from "react";
|
import { IconButton, makeStyles } from "@saleor/macaw-ui";
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import { ConfigurationsList } from "../../../app-configuration/ui/configurations-list";
|
|
||||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
import { AppColumnsLayout } from "../../../ui/app-columns-layout";
|
import { AppColumnsLayout } from "../../../ui/app-columns-layout";
|
||||||
import { trpcClient } from "../../../trpc/trpc-client";
|
import { trpcClient } from "../../../trpc/trpc-client";
|
||||||
import { SendgridConfiguration } from "../sendgrid-config";
|
|
||||||
import {
|
|
||||||
getDefaultEmptySendgridConfiguration,
|
|
||||||
SendgridConfigContainer,
|
|
||||||
} from "../sendgrid-config-container";
|
|
||||||
import { SendgridConfigurationForm } from "./sendgrid-configuration-form";
|
import { SendgridConfigurationForm } from "./sendgrid-configuration-form";
|
||||||
|
import { getDefaultEmptyConfiguration } from "../sendgrid-config-container";
|
||||||
|
import { NextRouter, useRouter } from "next/router";
|
||||||
|
import SideMenu from "../../../app-configuration/ui/side-menu";
|
||||||
|
import { SendgridConfiguration } from "../sendgrid-config";
|
||||||
|
import { LoadingIndicator } from "../../../ui/loading-indicator";
|
||||||
|
import { Add } from "@material-ui/icons";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { sendgridUrls } from "../../urls";
|
||||||
|
import { SendgridTemplatesCard } from "./sendgrid-templates-card";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
const useStyles = makeStyles((theme) => {
|
||||||
return {
|
return {
|
||||||
|
@ -24,101 +26,149 @@ const useStyles = makeStyles((theme) => {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: 20,
|
gap: 20,
|
||||||
},
|
maxWidth: 600,
|
||||||
loaderContainer: {
|
|
||||||
margin: "50px auto",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
type Configurations = {
|
interface SendgridConfigurationTabProps {
|
||||||
name: string;
|
configurationId?: string;
|
||||||
id: 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 = () => {
|
export const SendgridConfigurationTab = ({ configurationId }: SendgridConfigurationTabProps) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { appBridge } = useAppBridge();
|
const { appBridge } = useAppBridge();
|
||||||
const [configurationsListData, setConfigurationsListData] = useState<Configurations[]>([]);
|
const router = useRouter();
|
||||||
const [activeConfigurationId, setActiveConfigurationId] = useState<string>();
|
const queryClient = useQueryClient();
|
||||||
const [initialData, setInitialData] = useState<SendgridConfiguration>();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: configurationData,
|
data: configurations,
|
||||||
refetch: refetchConfig,
|
refetch: refetchConfigurations,
|
||||||
isLoading,
|
isLoading: configurationsIsLoading,
|
||||||
} = trpcClient.sendgridConfiguration.fetch.useQuery(undefined, {
|
isFetching: configurationsIsFetching,
|
||||||
|
isRefetching: configurationsIsRefetching,
|
||||||
|
} = trpcClient.sendgridConfiguration.getConfigurations.useQuery(undefined, {
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
if (!data.availableConfigurations) {
|
if (!configurationId) {
|
||||||
return;
|
console.log("no conf id! navigate to first");
|
||||||
|
navigateToFirstConfiguration(router, data);
|
||||||
}
|
}
|
||||||
const keys = Object.keys(data.availableConfigurations);
|
|
||||||
setConfigurationsListData(
|
|
||||||
keys.map((key) => ({ id: key, name: data.availableConfigurations[key].configurationName }))
|
|
||||||
);
|
|
||||||
setActiveConfigurationId(keys[0]);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate, error: saveError } = trpcClient.sendgridConfiguration.setAndReplace.useMutation({
|
const { mutate: deleteConfiguration } =
|
||||||
onSuccess() {
|
trpcClient.sendgridConfiguration.deleteConfiguration.useMutation({
|
||||||
refetchConfig();
|
onError: (error) => {
|
||||||
appBridge?.dispatch(
|
appBridge?.dispatch(
|
||||||
actions.Notification({
|
actions.Notification({
|
||||||
title: "Success",
|
title: "Could not remove the configuration",
|
||||||
text: "Saved configuration",
|
text: error.message,
|
||||||
status: "success",
|
status: "error",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
onSuccess: async (_data, variables) => {
|
||||||
|
await queryClient.cancelQueries({
|
||||||
|
queryKey: ["sendgridConfiguration", "getConfigurations"],
|
||||||
|
});
|
||||||
|
// remove value from the cache after the success
|
||||||
|
queryClient.setQueryData<Array<SendgridConfiguration>>(
|
||||||
|
["sendgridConfiguration", "getConfigurations"],
|
||||||
|
(old) => {
|
||||||
|
if (old) {
|
||||||
|
const index = old.findIndex((c) => c.id === variables.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
delete old[index];
|
||||||
|
return [...old];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
// if we just deleted the configuration that was selected
|
||||||
setInitialData(
|
// we have to update the URL
|
||||||
activeConfigurationId
|
if (variables.id === configurationId) {
|
||||||
? SendgridConfigContainer.getSendgridConfigurationById(configurationData)(
|
router.replace(sendgridUrls.configuration());
|
||||||
activeConfigurationId
|
}
|
||||||
)
|
|
||||||
: getDefaultEmptySendgridConfiguration()
|
|
||||||
);
|
|
||||||
}, [activeConfigurationId, configurationData]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
refetchConfigurations();
|
||||||
return (
|
appBridge?.dispatch(
|
||||||
<div className={styles.loaderContainer}>
|
actions.Notification({
|
||||||
<CircularProgress color="primary" />
|
title: "Success",
|
||||||
</div>
|
text: "Removed successfully",
|
||||||
);
|
status: "success",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (configurationsIsLoading || configurationsIsFetching) {
|
||||||
|
return <LoadingIndicator />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const configuration = configurations?.find((c) => c.id === configurationId?.toString());
|
||||||
|
|
||||||
|
if (configurationId && !configuration) {
|
||||||
|
return <div>Configuration not found</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppColumnsLayout>
|
<AppColumnsLayout>
|
||||||
<ConfigurationsList
|
<SideMenu
|
||||||
// TODO: FIXME
|
title="Configurations"
|
||||||
listItems={[]}
|
selectedItemId={configurationId}
|
||||||
activeItemId={activeConfigurationId}
|
headerToolbar={
|
||||||
onItemClick={setActiveConfigurationId}
|
<IconButton
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
router.replace(sendgridUrls.configuration());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Add />
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
onClick={(id) => router.replace(sendgridUrls.configuration(id))}
|
||||||
|
onDelete={(id) => {
|
||||||
|
deleteConfiguration({ id });
|
||||||
|
}}
|
||||||
|
items={configurations?.map((c) => ({ label: c.configurationName, id: c.id })) || []}
|
||||||
/>
|
/>
|
||||||
<div className={styles.configurationColumn}>
|
<div className={styles.configurationColumn}>
|
||||||
<Paper elevation={0} className={styles.formContainer}>
|
{configurationsIsLoading || configurationsIsFetching ? (
|
||||||
{!!initialData && (
|
<LoadingIndicator />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<SendgridConfigurationForm
|
<SendgridConfigurationForm
|
||||||
onSubmit={async (data) => {
|
onConfigurationSaved={() => refetchConfigurations()}
|
||||||
const newConfig =
|
initialData={configuration || getDefaultEmptyConfiguration()}
|
||||||
SendgridConfigContainer.setSendgridConfigurationById(configurationData)(
|
configurationId={configurationId}
|
||||||
activeConfigurationId
|
|
||||||
)(data);
|
|
||||||
mutate(newConfig);
|
|
||||||
}}
|
|
||||||
initialData={initialData}
|
|
||||||
configurationId={activeConfigurationId}
|
|
||||||
/>
|
/>
|
||||||
)}
|
{!!configurationId && !!configuration && (
|
||||||
{saveError && <span>{saveError.message}</span>}
|
<SendgridTemplatesCard
|
||||||
</Paper>
|
configurationId={configurationId}
|
||||||
|
configuration={configuration}
|
||||||
|
onEventChanged={() => {
|
||||||
|
refetchConfigurations();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</AppColumnsLayout>
|
</AppColumnsLayout>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,227 @@
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
Grid,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
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 { useAppBridge, actions } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { fetchTemplates } from "../../sendgrid-api";
|
||||||
|
|
||||||
|
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 { appBridge } = useAppBridge();
|
||||||
|
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) => {
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Notification({
|
||||||
|
title: "Configuration saved",
|
||||||
|
status: "success",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Notification({
|
||||||
|
title: "Could not save the configuration",
|
||||||
|
text: isFieldErrorSet ? "Submitted form contain errors" : "Error saving configuration",
|
||||||
|
apiMessage: formErrorMessage,
|
||||||
|
status: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,135 @@
|
||||||
|
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";
|
||||||
|
|
||||||
|
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 { appBridge } = useAppBridge();
|
||||||
|
|
||||||
|
const { mutate: updateEventConfiguration } =
|
||||||
|
trpcClient.sendgridConfiguration.updateEventConfiguration.useMutation({
|
||||||
|
onSuccess(_data, variables) {
|
||||||
|
onEventChanged();
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Notification({
|
||||||
|
title: variables.active ? "Event enabled" : "Event disabled",
|
||||||
|
status: "success",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper elevation={0}>
|
||||||
|
<ListHeader>
|
||||||
|
<ListItem className={classes.tableRow}>
|
||||||
|
<ListItemCell>Supported events and templates</ListItemCell>
|
||||||
|
</ListItem>
|
||||||
|
</ListHeader>
|
||||||
|
<List gridTemplate={["1fr"]}>
|
||||||
|
<Divider />
|
||||||
|
{configuration.events.map((eventConfiguration) => (
|
||||||
|
<React.Fragment key={eventConfiguration.eventType}>
|
||||||
|
<ListItem className={classes.tableRow}>
|
||||||
|
<ListItemCell>
|
||||||
|
<div className={classes.spaceBetween}>
|
||||||
|
<Typography>{messageEventTypesLabels[eventConfiguration.eventType]}</Typography>
|
||||||
|
<div className={classes.rowActions}>
|
||||||
|
{/* TODO: fix types in the MacawUI */}
|
||||||
|
{/* @ts-ignore: MacawUI use wrong type for */}
|
||||||
|
<SwitchSelector key={eventConfiguration.eventType}>
|
||||||
|
{[
|
||||||
|
{ label: "Active", value: true },
|
||||||
|
{ label: "Disabled", value: false },
|
||||||
|
].map((button) => (
|
||||||
|
// @ts-ignore: MacawUI use wrong type for SwitchSelectorButton
|
||||||
|
<SwitchSelectorButton
|
||||||
|
value={button.value.toString()}
|
||||||
|
onClick={() => {
|
||||||
|
updateEventConfiguration({
|
||||||
|
configurationId,
|
||||||
|
...eventConfiguration,
|
||||||
|
active: button.value,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
activeTab={eventConfiguration.active.toString()}
|
||||||
|
key={button.label}
|
||||||
|
>
|
||||||
|
{button.label}
|
||||||
|
</SwitchSelectorButton>
|
||||||
|
))}
|
||||||
|
</SwitchSelector>
|
||||||
|
<IconButton
|
||||||
|
variant="secondary"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
router.push(
|
||||||
|
sendgridUrls.eventConfiguration(
|
||||||
|
configurationId,
|
||||||
|
eventConfiguration.eventType
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ListItemCell>
|
||||||
|
</ListItem>
|
||||||
|
<Divider />
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,54 +0,0 @@
|
||||||
import { AuthData } from "@saleor/app-sdk/APL";
|
|
||||||
import { appRouter } from "../trpc/trpc-app-router";
|
|
||||||
import { logger as pinoLogger } from "../../lib/logger";
|
|
||||||
|
|
||||||
interface GetSendgridSettingsArgs {
|
|
||||||
authData: AuthData;
|
|
||||||
channel: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getSendgridSettings = async ({ authData, channel }: GetSendgridSettingsArgs) => {
|
|
||||||
const logger = pinoLogger.child({
|
|
||||||
fn: "getMjmlSettings",
|
|
||||||
channel,
|
|
||||||
});
|
|
||||||
const caller = appRouter.createCaller({
|
|
||||||
appId: authData.appId,
|
|
||||||
saleorApiUrl: authData.saleorApiUrl,
|
|
||||||
token: authData.token,
|
|
||||||
ssr: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sendgridConfigurations = await caller.sendgridConfiguration.fetch();
|
|
||||||
const appConfigurations = await caller.appConfiguration.fetch();
|
|
||||||
|
|
||||||
const channelAppConfiguration = appConfigurations?.configurationsPerChannel[channel];
|
|
||||||
if (!channelAppConfiguration) {
|
|
||||||
logger.warn("App has no configuration for this channel");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!channelAppConfiguration.active) {
|
|
||||||
logger.warn("App configuration is not active for this channel");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sendgridConfigurationId = channelAppConfiguration.sendgridConfigurationId;
|
|
||||||
if (!sendgridConfigurationId?.length) {
|
|
||||||
logger.warn("Sendgrid configuration has not been chosen for this channel");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const configuration = sendgridConfigurations?.availableConfigurations[sendgridConfigurationId];
|
|
||||||
if (!configuration) {
|
|
||||||
logger.warn(`The Sendgrid configuration with id ${sendgridConfigurationId} does not exist`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!configuration.active) {
|
|
||||||
logger.warn(`The Sendgrid configuration ${configuration.configurationName} is not active`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return configuration;
|
|
||||||
};
|
|
|
@ -1,16 +1,13 @@
|
||||||
import { logger as pinoLogger } from "../../lib/logger";
|
import { logger as pinoLogger } from "../../lib/logger";
|
||||||
import { AuthData } from "@saleor/app-sdk/APL";
|
|
||||||
import { SendgridConfiguration } from "./configuration/sendgrid-config";
|
import { SendgridConfiguration } from "./configuration/sendgrid-config";
|
||||||
import { getSendgridSettings } from "./get-sendgrid-settings";
|
|
||||||
import { MailService } from "@sendgrid/mail";
|
import { MailService } from "@sendgrid/mail";
|
||||||
import { MessageEventTypes } from "../event-handlers/message-event-types";
|
import { MessageEventTypes } from "../event-handlers/message-event-types";
|
||||||
|
|
||||||
interface SendSendgridArgs {
|
interface SendSendgridArgs {
|
||||||
authData: AuthData;
|
|
||||||
channel: string;
|
|
||||||
recipientEmail: string;
|
recipientEmail: string;
|
||||||
event: MessageEventTypes;
|
event: MessageEventTypes;
|
||||||
payload: any;
|
payload: any;
|
||||||
|
sendgridConfiguration: SendgridConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmailServiceResponse {
|
export interface EmailServiceResponse {
|
||||||
|
@ -20,65 +17,55 @@ export interface EmailServiceResponse {
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventMapping = (event: SendSendgridArgs["event"], settings: SendgridConfiguration) => {
|
|
||||||
switch (event) {
|
|
||||||
case "ORDER_CREATED":
|
|
||||||
return {
|
|
||||||
templateId: settings.templateOrderCreatedTemplate,
|
|
||||||
subject: settings.templateOrderCreatedSubject || "Order created",
|
|
||||||
};
|
|
||||||
case "ORDER_FULFILLED":
|
|
||||||
return {
|
|
||||||
templateId: settings.templateOrderFulfilledTemplate,
|
|
||||||
subject: settings.templateOrderFulfilledSubject || "Order fulfilled",
|
|
||||||
};
|
|
||||||
case "ORDER_CONFIRMED":
|
|
||||||
return {
|
|
||||||
template: settings.templateOrderConfirmedTemplate,
|
|
||||||
subject: settings.templateOrderConfirmedSubject || "Order confirmed",
|
|
||||||
};
|
|
||||||
case "ORDER_CANCELLED":
|
|
||||||
return {
|
|
||||||
template: settings.templateOrderCancelledTemplate,
|
|
||||||
subject: settings.templateOrderCancelledSubject || "Order cancelled",
|
|
||||||
};
|
|
||||||
case "ORDER_FULLY_PAID":
|
|
||||||
return {
|
|
||||||
template: settings.templateOrderFullyPaidTemplate,
|
|
||||||
subject: settings.templateOrderFullyPaidSubject || "Order fully paid",
|
|
||||||
};
|
|
||||||
case "INVOICE_SENT":
|
|
||||||
return {
|
|
||||||
template: settings.templateInvoiceSentTemplate,
|
|
||||||
subject: settings.templateInvoiceSentSubject || "Invoice sent",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sendSendgrid = async ({
|
export const sendSendgrid = async ({
|
||||||
authData,
|
|
||||||
channel,
|
|
||||||
payload,
|
payload,
|
||||||
recipientEmail,
|
recipientEmail,
|
||||||
event,
|
event,
|
||||||
|
sendgridConfiguration,
|
||||||
}: SendSendgridArgs) => {
|
}: SendSendgridArgs) => {
|
||||||
const logger = pinoLogger.child({
|
const logger = pinoLogger.child({
|
||||||
fn: "sendSendgrid",
|
fn: "sendSendgrid",
|
||||||
event,
|
event,
|
||||||
});
|
});
|
||||||
|
if (!sendgridConfiguration.senderEmail) {
|
||||||
|
logger.debug("Sender email has not been specified, skipping");
|
||||||
|
return {
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: "Sender email has not been set up",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const settings = await getSendgridSettings({ authData, channel });
|
const eventSettings = sendgridConfiguration.events.find((e) => e.eventType === event);
|
||||||
|
if (!eventSettings) {
|
||||||
|
logger.debug("No active settings for this event, skipping");
|
||||||
|
return {
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: "No active settings for this event",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!settings?.active) {
|
if (!eventSettings.active) {
|
||||||
logger.debug("Sendgrid is not active, skipping");
|
logger.debug("Event settings are not active, skipping");
|
||||||
return;
|
return {
|
||||||
|
errors: [
|
||||||
|
{
|
||||||
|
message: "Event settings are not active",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Sending an email using Sendgrid");
|
logger.debug("Sending an email using Sendgrid");
|
||||||
|
|
||||||
const { templateId, subject } = eventMapping(event, settings);
|
const { template } = eventSettings;
|
||||||
|
|
||||||
if (!templateId) {
|
if (!template) {
|
||||||
logger.error("No template defined in the settings");
|
logger.error("No template defined in the settings");
|
||||||
return {
|
return {
|
||||||
errors: [{ message: `No template specified for the event ${event}` }],
|
errors: [{ message: `No template specified for the event ${event}` }],
|
||||||
|
@ -87,35 +74,21 @@ export const sendSendgrid = async ({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mailService = new MailService();
|
const mailService = new MailService();
|
||||||
mailService.setApiKey(settings.apiKey);
|
mailService.setApiKey(sendgridConfiguration.apiKey);
|
||||||
|
|
||||||
await mailService.send({
|
await mailService.send({
|
||||||
mailSettings: {
|
mailSettings: {
|
||||||
sandboxMode: {
|
sandboxMode: {
|
||||||
enable: settings.sandboxMode,
|
enable: sendgridConfiguration.sandboxMode,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
from: {
|
from: {
|
||||||
email: settings.senderEmail,
|
name: sendgridConfiguration.senderName,
|
||||||
|
email: sendgridConfiguration.senderEmail,
|
||||||
},
|
},
|
||||||
to: {
|
to: recipientEmail,
|
||||||
email: recipientEmail,
|
dynamicTemplateData: payload,
|
||||||
},
|
templateId: template,
|
||||||
personalizations: [
|
|
||||||
{
|
|
||||||
from: {
|
|
||||||
email: settings.senderEmail,
|
|
||||||
},
|
|
||||||
to: [
|
|
||||||
{
|
|
||||||
email: recipientEmail,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
subject,
|
|
||||||
dynamicTemplateData: payload,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
templateId,
|
|
||||||
});
|
});
|
||||||
logger.debug("Email has been send");
|
logger.debug("Email has been send");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
interface FetchTemplatesArgs {
|
||||||
|
apiKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchTemplates =
|
||||||
|
({ apiKey }: FetchTemplatesArgs) =>
|
||||||
|
async () => {
|
||||||
|
if (!apiKey) {
|
||||||
|
console.warn(
|
||||||
|
"The Sendgrid API key has not been set up yet. Skipping fetching available templates."
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const response = await fetch(
|
||||||
|
"https://api.sendgrid.com/v3/templates?generations=dynamic&page_size=18",
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error("Could not fetch available Sendgrid templates");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const resJson = (await response.json()) as {
|
||||||
|
result?: { id: string; name: string }[];
|
||||||
|
};
|
||||||
|
const templates =
|
||||||
|
resJson.result?.map((r) => ({
|
||||||
|
value: r.id,
|
||||||
|
label: r.name,
|
||||||
|
})) || [];
|
||||||
|
return templates;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not parse the response from Sendgrid", e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchSenders =
|
||||||
|
({ apiKey }: FetchTemplatesArgs) =>
|
||||||
|
async () => {
|
||||||
|
if (!apiKey) {
|
||||||
|
console.warn(
|
||||||
|
"The Sendgrid API key has not been set up yet. Skipping fetching available senders ."
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const response = await fetch("https://api.sendgrid.com/v3/verified_senders?page_size=18", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${apiKey}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error("Could not fetch available Sendgrid senders");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const resJson = (await response.json()) as {
|
||||||
|
results?: { id: string; nickname: string; from_email: string }[];
|
||||||
|
};
|
||||||
|
const senders =
|
||||||
|
resJson.results?.map((r) => ({
|
||||||
|
value: r.id,
|
||||||
|
label: `${r.nickname} (${r.from_email})`,
|
||||||
|
nickname: r.nickname,
|
||||||
|
from_email: r.from_email,
|
||||||
|
})) || [];
|
||||||
|
return senders;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Could not parse the response from Sendgrid", e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
8
apps/emails-and-messages/src/modules/sendgrid/urls.ts
Normal file
8
apps/emails-and-messages/src/modules/sendgrid/urls.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { MessageEventTypes } from "../event-handlers/message-event-types";
|
||||||
|
|
||||||
|
export const sendgridUrls = {
|
||||||
|
configuration: (id?: string) =>
|
||||||
|
!id ? "/configuration/sendgrid" : `/configuration/sendgrid/${id}`,
|
||||||
|
eventConfiguration: (id: string, event: MessageEventTypes) =>
|
||||||
|
`/configuration/sendgrid/${id}/event/${event}`,
|
||||||
|
};
|
|
@ -24,9 +24,8 @@ export const ConfigurationPageBaseLayout = ({ children }: Props) => {
|
||||||
{ key: "mjml", label: "MJML", url: "/configuration/mjml" },
|
{ key: "mjml", label: "MJML", url: "/configuration/mjml" },
|
||||||
{
|
{
|
||||||
key: "sendgrid",
|
key: "sendgrid",
|
||||||
label: "Sendgrid (Coming soon!)",
|
label: "Sendgrid",
|
||||||
url: "/configuration/sendgrid",
|
url: "/configuration/sendgrid",
|
||||||
disabled: true,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -42,7 +41,7 @@ export const ConfigurationPageBaseLayout = ({ children }: Props) => {
|
||||||
<div className={styles.appContainer}>
|
<div className={styles.appContainer}>
|
||||||
<PageTabs value={activePath} onChange={navigateToTab}>
|
<PageTabs value={activePath} onChange={navigateToTab}>
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<PageTab key={tab.key} value={tab.key} label={tab.label} disabled={tab.disabled} />
|
<PageTab key={tab.key} value={tab.key} label={tab.label} />
|
||||||
))}
|
))}
|
||||||
</PageTabs>
|
</PageTabs>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
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;
|
|
@ -0,0 +1,77 @@
|
||||||
|
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;
|
Loading…
Reference in a new issue