Remove channels config
This commit is contained in:
parent
8e0b08523b
commit
55b998a75f
26 changed files with 144 additions and 1068 deletions
27
apps/emails-and-messages/src/lib/is-available-in-channel.ts
Normal file
27
apps/emails-and-messages/src/lib/is-available-in-channel.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
interface IsAvailableInChannelArgs {
|
||||
channel: string;
|
||||
restrictedToChannels: string[];
|
||||
excludedChannels: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the channel is available for the configuration.
|
||||
*
|
||||
* Is available if:
|
||||
* - it's not in the excluded list
|
||||
* - if assigned list is not empty, it's in the assigned list
|
||||
* - assigned list is empty
|
||||
*/
|
||||
export const isAvailableInChannel = ({
|
||||
channel,
|
||||
restrictedToChannels,
|
||||
excludedChannels,
|
||||
}: IsAvailableInChannelArgs): boolean => {
|
||||
if (channel in excludedChannels) {
|
||||
return false;
|
||||
}
|
||||
if (restrictedToChannels.length > 0 && !(channel in restrictedToChannels)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
|
@ -1,33 +0,0 @@
|
|||
import { AppConfig, AppConfigurationPerChannel } from "./app-config";
|
||||
|
||||
export const getDefaultEmptyAppConfiguration = (): AppConfigurationPerChannel => ({
|
||||
active: false,
|
||||
mjmlConfigurationId: undefined,
|
||||
sendgridConfigurationId: undefined,
|
||||
});
|
||||
|
||||
const getChannelAppConfiguration =
|
||||
(appConfig: AppConfig | null | undefined) => (channelSlug: string) => {
|
||||
try {
|
||||
return appConfig?.configurationsPerChannel[channelSlug] ?? null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const setChannelAppConfiguration =
|
||||
(appConfig: AppConfig | null | undefined) =>
|
||||
(channelSlug: string) =>
|
||||
(appConfiguration: AppConfigurationPerChannel) => {
|
||||
const appConfigNormalized = structuredClone(appConfig) ?? { configurationsPerChannel: {} };
|
||||
|
||||
appConfigNormalized.configurationsPerChannel[channelSlug] ??= getDefaultEmptyAppConfiguration();
|
||||
appConfigNormalized.configurationsPerChannel[channelSlug] = appConfiguration;
|
||||
|
||||
return appConfigNormalized;
|
||||
};
|
||||
|
||||
export const AppConfigContainer = {
|
||||
getChannelAppConfiguration,
|
||||
setChannelAppConfiguration,
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const appConfigInputSchema = z.object({
|
||||
configurationsPerChannel: z.record(
|
||||
z.object({
|
||||
active: z.boolean(),
|
||||
mjmlConfigurationId: z.string().optional(),
|
||||
sendgridConfigurationId: z.string().optional(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export const appChannelConfigurationInputSchema = z.object({
|
||||
channel: z.string(),
|
||||
configuration: z.object({
|
||||
active: z.boolean(),
|
||||
mjmlConfigurationId: z.string().optional(),
|
||||
sendgridConfigurationId: z.string().optional(),
|
||||
}),
|
||||
});
|
|
@ -1,11 +0,0 @@
|
|||
export interface AppConfigurationPerChannel {
|
||||
active: boolean;
|
||||
mjmlConfigurationId?: string;
|
||||
sendgridConfigurationId?: string;
|
||||
}
|
||||
|
||||
export type AppConfigurationsChannelMap = Record<string, AppConfigurationPerChannel>;
|
||||
|
||||
export type AppConfig = {
|
||||
configurationsPerChannel: AppConfigurationsChannelMap;
|
||||
};
|
|
@ -1,70 +0,0 @@
|
|||
import { createLogger } from "@saleor/apps-shared";
|
||||
import {
|
||||
appChannelConfigurationInputSchema,
|
||||
appConfigInputSchema,
|
||||
} from "./app-config-input-schema";
|
||||
import { AppConfigurationService } from "./get-app-configuration.service";
|
||||
import { router } from "../trpc/trpc-server";
|
||||
import { protectedClientProcedure } from "../trpc/protected-client-procedure";
|
||||
import { z } from "zod";
|
||||
|
||||
/*
|
||||
* 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 AppConfigurationService({
|
||||
apiClient: ctx.apiClient,
|
||||
saleorApiUrl: ctx.saleorApiUrl,
|
||||
}),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export const appConfigurationRouter = router({
|
||||
getChannelConfiguration: protectedWithConfigurationService
|
||||
.input(z.object({ channelSlug: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||
|
||||
logger.debug("Get Channel Configuration called");
|
||||
|
||||
return await ctx.configurationService.getChannelConfiguration(input.channelSlug);
|
||||
}),
|
||||
|
||||
setChannelConfiguration: protectedWithConfigurationService
|
||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||
.input(appChannelConfigurationInputSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||
|
||||
logger.debug("Set channel configuration called");
|
||||
|
||||
await ctx.configurationService.setChannelConfiguration(input);
|
||||
}),
|
||||
fetch: protectedWithConfigurationService.query(async ({ ctx, input }) => {
|
||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||
|
||||
logger.debug("appConfigurationRouter.fetch called");
|
||||
|
||||
return new AppConfigurationService({
|
||||
apiClient: ctx.apiClient,
|
||||
saleorApiUrl: ctx.saleorApiUrl,
|
||||
}).getConfiguration();
|
||||
}),
|
||||
setAndReplace: protectedWithConfigurationService
|
||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||
.input(appConfigInputSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||
|
||||
logger.debug(input, "appConfigurationRouter.setAndReplace called with input");
|
||||
|
||||
await ctx.configurationService.setConfigurationRoot(input);
|
||||
|
||||
return null;
|
||||
}),
|
||||
});
|
|
@ -1,35 +0,0 @@
|
|||
import { AppConfig } from "./app-config";
|
||||
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||
|
||||
export interface AppConfigurator {
|
||||
setConfig(config: AppConfig): Promise<void>;
|
||||
getConfig(): Promise<AppConfig | undefined>;
|
||||
}
|
||||
|
||||
export class PrivateMetadataAppConfigurator implements AppConfigurator {
|
||||
private metadataKey = "app-config";
|
||||
|
||||
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
|
||||
|
||||
getConfig(): Promise<AppConfig | undefined> {
|
||||
return this.metadataManager.get(this.metadataKey, this.saleorApiUrl).then((data) => {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid metadata value, cant be parsed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setConfig(config: AppConfig): Promise<void> {
|
||||
return this.metadataManager.set({
|
||||
key: this.metadataKey,
|
||||
value: JSON.stringify(config),
|
||||
domain: this.saleorApiUrl,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
import { AppConfig } from "./app-config";
|
||||
import { AppConfigContainer, getDefaultEmptyAppConfiguration } from "./app-config-container";
|
||||
import { ChannelFragment, ShopInfoFragment } from "../../../generated/graphql";
|
||||
|
||||
/**
|
||||
* TODO Test
|
||||
*/
|
||||
export const FallbackAppConfig = {
|
||||
createFallbackConfigFromExistingShopAndChannels(
|
||||
channels: ChannelFragment[],
|
||||
shopAppConfiguration: ShopInfoFragment | null
|
||||
) {
|
||||
return (channels ?? []).reduce<AppConfig>(
|
||||
(state, channel) => {
|
||||
return AppConfigContainer.setChannelAppConfiguration(state)(channel.slug)(
|
||||
getDefaultEmptyAppConfiguration()
|
||||
);
|
||||
},
|
||||
{ configurationsPerChannel: {} }
|
||||
);
|
||||
},
|
||||
};
|
|
@ -1,95 +0,0 @@
|
|||
import { PrivateMetadataAppConfigurator } from "./app-configurator";
|
||||
import { Client } from "urql";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
import { AppConfig, AppConfigurationPerChannel } from "./app-config";
|
||||
import { getDefaultEmptyAppConfiguration } from "./app-config-container";
|
||||
import { createSettingsManager } from "../../lib/metadata-manager";
|
||||
|
||||
const logger = createLogger({
|
||||
service: "AppConfigurationService",
|
||||
});
|
||||
|
||||
export class AppConfigurationService {
|
||||
private configurationData?: AppConfig;
|
||||
private metadataConfigurator: PrivateMetadataAppConfigurator;
|
||||
|
||||
constructor(args: { apiClient: Client; saleorApiUrl: string; initialData?: AppConfig }) {
|
||||
this.metadataConfigurator = new PrivateMetadataAppConfigurator(
|
||||
createSettingsManager(args.apiClient),
|
||||
args.saleorApiUrl
|
||||
);
|
||||
}
|
||||
|
||||
// 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!);
|
||||
}
|
||||
|
||||
async getConfiguration() {
|
||||
logger.debug("Get configuration");
|
||||
|
||||
if (!this.configurationData) {
|
||||
logger.debug("No configuration found in cache. Will fetch it from Saleor API");
|
||||
await this.pullConfiguration();
|
||||
}
|
||||
|
||||
const savedAppConfig = this.configurationData ?? null;
|
||||
|
||||
logger.debug(savedAppConfig, "Retrieved app config from Metadata. Will return it");
|
||||
|
||||
if (savedAppConfig) {
|
||||
return savedAppConfig;
|
||||
}
|
||||
}
|
||||
|
||||
// Saves configuration to Saleor API and cache it
|
||||
async setConfigurationRoot(config: AppConfig) {
|
||||
logger.debug("Set configuration");
|
||||
|
||||
this.configurationData = config;
|
||||
await this.pushConfiguration();
|
||||
}
|
||||
|
||||
// Returns channel configuration if existing. Otherwise returns default empty one
|
||||
async getChannelConfiguration(channel: string) {
|
||||
logger.debug("Get channel configuration");
|
||||
const configurations = await this.getConfiguration();
|
||||
|
||||
if (!configurations) {
|
||||
return getDefaultEmptyAppConfiguration();
|
||||
}
|
||||
|
||||
const channelConfiguration = configurations.configurationsPerChannel[channel];
|
||||
|
||||
return channelConfiguration || getDefaultEmptyAppConfiguration();
|
||||
}
|
||||
|
||||
async setChannelConfiguration({
|
||||
channel,
|
||||
configuration,
|
||||
}: {
|
||||
channel: string;
|
||||
configuration: AppConfigurationPerChannel;
|
||||
}) {
|
||||
logger.debug("Set channel configuration");
|
||||
let configurations = await this.getConfiguration();
|
||||
|
||||
if (!configurations) {
|
||||
configurations = { configurationsPerChannel: {} };
|
||||
}
|
||||
|
||||
configurations.configurationsPerChannel[channel] = configuration;
|
||||
await this.setConfigurationRoot(configurations);
|
||||
}
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
import { AppConfigurationPerChannel } from "../app-config";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { FormControl, InputLabel, Link, MenuItem, Select, Typography } from "@material-ui/core";
|
||||
import { Button, makeStyles, SwitchSelector, SwitchSelectorButton } from "@saleor/macaw-ui";
|
||||
import React, { useEffect } from "react";
|
||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { useRouter } from "next/router";
|
||||
import { mjmlUrls } from "../../mjml/urls";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
field: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
channelName: {
|
||||
cursor: "pointer",
|
||||
borderBottom: `2px solid ${theme.palette.secondary.main}`,
|
||||
},
|
||||
}));
|
||||
|
||||
type AppConfigurationFormProps = {
|
||||
channelSlug: string;
|
||||
channelName: string;
|
||||
channelID: string;
|
||||
mjmlConfigurationChoices: { label: string; value: string }[];
|
||||
sendgridConfigurationChoices: { label: string; value: string }[];
|
||||
onSubmit(data: AppConfigurationPerChannel): Promise<void>;
|
||||
initialData?: AppConfigurationPerChannel | null;
|
||||
};
|
||||
|
||||
export const AppConfigurationForm = (props: AppConfigurationFormProps) => {
|
||||
const styles = useStyles();
|
||||
const { appBridge } = useAppBridge();
|
||||
const router = useRouter();
|
||||
|
||||
const { handleSubmit, getValues, setValue, control, reset } = useForm<AppConfigurationPerChannel>(
|
||||
{
|
||||
defaultValues: props.initialData ?? undefined,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
reset(props.initialData || undefined);
|
||||
}, [props.initialData, reset]);
|
||||
|
||||
const handleChannelNameClick = () => {
|
||||
appBridge?.dispatch(
|
||||
actions.Redirect({
|
||||
to: `/channels/${props.channelID}`,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const isNoSendgridConfigurations = !props.sendgridConfigurationChoices.length;
|
||||
const isNoMjmlConfigurations = !props.mjmlConfigurationChoices.length;
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit((data, event) => {
|
||||
props.onSubmit(data);
|
||||
})}
|
||||
>
|
||||
<Typography variant="h2" paragraph>
|
||||
Configure
|
||||
<span onClick={handleChannelNameClick} className={styles.channelName}>
|
||||
{` ${props.channelName} `}
|
||||
</span>
|
||||
channel:
|
||||
</Typography>
|
||||
|
||||
<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() || "false"}
|
||||
key={button.label}
|
||||
>
|
||||
{button.label}
|
||||
</SwitchSelectorButton>
|
||||
))}
|
||||
</SwitchSelector>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="mjmlConfigurationId"
|
||||
render={({ field: { value, onChange } }) => {
|
||||
return (
|
||||
<FormControl disabled={isNoMjmlConfigurations} className={styles.field} fullWidth>
|
||||
<InputLabel>MJML Configuration</InputLabel>
|
||||
<Select
|
||||
variant="outlined"
|
||||
value={value}
|
||||
onChange={(event, val) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
>
|
||||
<MenuItem key="none" value={undefined}>
|
||||
No configuration
|
||||
</MenuItem>
|
||||
{props.mjmlConfigurationChoices.map((choice) => (
|
||||
<MenuItem key={choice.value} value={choice.value}>
|
||||
{choice.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{isNoMjmlConfigurations && (
|
||||
<Link
|
||||
href="#"
|
||||
onClick={() => {
|
||||
router.push(mjmlUrls.configuration());
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Currently theres no MJML configuration available. Click here to create one.
|
||||
</Typography>
|
||||
</Link>
|
||||
)}
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
name="sendgridConfigurationId"
|
||||
render={({ field: { value, onChange } }) => {
|
||||
return (
|
||||
<FormControl disabled={isNoSendgridConfigurations} className={styles.field} fullWidth>
|
||||
<InputLabel>Sendgrid Configuration</InputLabel>
|
||||
<Select
|
||||
variant="outlined"
|
||||
value={value}
|
||||
onChange={(event, val) => {
|
||||
onChange(event.target.value);
|
||||
}}
|
||||
>
|
||||
<MenuItem key="none" value={undefined}>
|
||||
No configuration
|
||||
</MenuItem>
|
||||
{props.sendgridConfigurationChoices.map((choice) => (
|
||||
<MenuItem key={choice.value} value={choice.value}>
|
||||
{choice.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{isNoSendgridConfigurations && (
|
||||
<Link
|
||||
href="#"
|
||||
onClick={() => {
|
||||
router.push("");
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
Currently theres no Sendgrid configuration available. Click here to create one.
|
||||
</Typography>
|
||||
</Link>
|
||||
)}
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Button type="submit" fullWidth variant="primary">
|
||||
Save configuration
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
|
@ -1,146 +0,0 @@
|
|||
import React, { useMemo, useState } from "react";
|
||||
import { EditIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
|
||||
import { AppConfigurationForm } from "./app-configuration-form";
|
||||
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { AppColumnsLayout } from "../../ui/app-columns-layout";
|
||||
import { trpcClient } from "../../trpc/trpc-client";
|
||||
import { SideMenu } from "./side-menu";
|
||||
import { LoadingIndicator } from "../../ui/loading-indicator";
|
||||
import { Instructions } from "./instructions";
|
||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||
|
||||
const useStyles = makeStyles((theme) => {
|
||||
return {
|
||||
formContainer: {
|
||||
top: 0,
|
||||
},
|
||||
configurationColumn: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 20,
|
||||
maxWidth: 700,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const ChannelsConfigurationTab = () => {
|
||||
const styles = useStyles();
|
||||
const { appBridge } = useAppBridge();
|
||||
const [activeChannelSlug, setActiveChannelSlug] = useState<string | null>(null);
|
||||
const { notifySuccess } = useDashboardNotification();
|
||||
|
||||
const { data: channelsData, isLoading: isChannelsDataLoading } =
|
||||
trpcClient.channels.fetch.useQuery(undefined, {
|
||||
onSuccess: (data) => {
|
||||
if (data?.length) {
|
||||
setActiveChannelSlug(data[0].slug);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: configurationData,
|
||||
refetch: refetchConfig,
|
||||
isLoading: isConfigurationDataLoading,
|
||||
} = trpcClient.appConfiguration.getChannelConfiguration.useQuery(
|
||||
{
|
||||
channelSlug: activeChannelSlug!,
|
||||
},
|
||||
{ enabled: !!activeChannelSlug }
|
||||
);
|
||||
|
||||
const { data: mjmlConfigurations, isLoading: isMjmlQueryLoading } =
|
||||
trpcClient.mjmlConfiguration.getConfigurations.useQuery({});
|
||||
|
||||
const mjmlConfigurationsListData = useMemo(() => {
|
||||
return (
|
||||
mjmlConfigurations?.map((configuration) => ({
|
||||
value: configuration.id,
|
||||
label: configuration.configurationName,
|
||||
})) ?? []
|
||||
);
|
||||
}, [mjmlConfigurations]);
|
||||
|
||||
const { data: sendgridConfigurations, isLoading: isSendgridQueryLoading } =
|
||||
trpcClient.sendgridConfiguration.getConfigurations.useQuery({});
|
||||
|
||||
const sendgridConfigurationsListData = useMemo(() => {
|
||||
return (
|
||||
sendgridConfigurations?.map((configuration) => ({
|
||||
value: configuration.id,
|
||||
label: configuration.configurationName,
|
||||
})) ?? []
|
||||
);
|
||||
}, [sendgridConfigurations]);
|
||||
|
||||
const { mutate: mutateAppChannelConfiguration, error: saveError } =
|
||||
trpcClient.appConfiguration.setChannelConfiguration.useMutation({
|
||||
onSuccess() {
|
||||
refetchConfig();
|
||||
|
||||
notifySuccess("Success", "Saved app configuration");
|
||||
},
|
||||
});
|
||||
|
||||
const activeChannel = channelsData?.find((c) => c.slug === activeChannelSlug);
|
||||
|
||||
if (isChannelsDataLoading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
if (!channelsData?.length) {
|
||||
return <div>NO CHANNELS</div>;
|
||||
}
|
||||
|
||||
const isFormDataLoading =
|
||||
isConfigurationDataLoading || isMjmlQueryLoading || isSendgridQueryLoading;
|
||||
|
||||
return (
|
||||
<AppColumnsLayout>
|
||||
<SideMenu
|
||||
title="Channels"
|
||||
selectedItemId={activeChannel?.slug}
|
||||
headerToolbar={
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
appBridge?.dispatch(
|
||||
actions.Redirect({
|
||||
to: `/channels/`,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
}
|
||||
onClick={(id) => setActiveChannelSlug(id)}
|
||||
items={channelsData.map((c) => ({ label: c.name, id: c.slug })) || []}
|
||||
/>
|
||||
<div className={styles.configurationColumn}>
|
||||
{!activeChannel || isFormDataLoading ? (
|
||||
<LoadingIndicator />
|
||||
) : (
|
||||
<>
|
||||
<AppConfigurationForm
|
||||
channelID={activeChannel.id}
|
||||
key={activeChannelSlug}
|
||||
channelSlug={activeChannel.slug}
|
||||
mjmlConfigurationChoices={mjmlConfigurationsListData}
|
||||
sendgridConfigurationChoices={sendgridConfigurationsListData}
|
||||
onSubmit={async (data) => {
|
||||
mutateAppChannelConfiguration({
|
||||
channel: activeChannel.slug,
|
||||
configuration: data,
|
||||
});
|
||||
}}
|
||||
initialData={configurationData}
|
||||
channelName={activeChannel?.name ?? activeChannelSlug}
|
||||
/>
|
||||
{saveError && <span>{saveError.message}</span>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Instructions />
|
||||
</AppColumnsLayout>
|
||||
);
|
||||
};
|
|
@ -1,72 +0,0 @@
|
|||
import {
|
||||
makeStyles,
|
||||
OffsettedList,
|
||||
OffsettedListBody,
|
||||
OffsettedListHeader,
|
||||
OffsettedListItem,
|
||||
OffsettedListItemCell,
|
||||
} from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import { Typography } from "@material-ui/core";
|
||||
import React from "react";
|
||||
import { ChannelFragment } from "../../../../generated/graphql";
|
||||
|
||||
const useStyles = makeStyles((theme) => {
|
||||
return {
|
||||
listItem: {
|
||||
cursor: "pointer",
|
||||
height: "auto !important",
|
||||
},
|
||||
listItemActive: {
|
||||
background: "#f4f4f4",
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
},
|
||||
channelSlug: {
|
||||
fontFamily: "monospace",
|
||||
opacity: 0.8,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
channels: ChannelFragment[];
|
||||
activeChannelSlug: string;
|
||||
onChannelClick(channelSlug: string): void;
|
||||
};
|
||||
|
||||
export const ChannelsList = ({ channels, activeChannelSlug, onChannelClick }: Props) => {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<OffsettedList gridTemplate={["1fr"]}>
|
||||
<OffsettedListHeader>
|
||||
<Typography variant="h3" paragraph>
|
||||
Available channels
|
||||
</Typography>
|
||||
</OffsettedListHeader>
|
||||
<OffsettedListBody>
|
||||
{channels.map((c) => {
|
||||
return (
|
||||
<OffsettedListItem
|
||||
className={clsx(styles.listItem, {
|
||||
[styles.listItemActive]: c.slug === activeChannelSlug,
|
||||
})}
|
||||
key={c.slug}
|
||||
onClick={() => {
|
||||
onChannelClick(c.slug);
|
||||
}}
|
||||
>
|
||||
<OffsettedListItemCell>
|
||||
{c.name}
|
||||
<Typography variant="caption" className={styles.channelSlug}>
|
||||
{c.slug}
|
||||
</Typography>
|
||||
</OffsettedListItemCell>
|
||||
</OffsettedListItem>
|
||||
);
|
||||
})}
|
||||
</OffsettedListBody>
|
||||
</OffsettedList>
|
||||
);
|
||||
};
|
|
@ -1,87 +0,0 @@
|
|||
import {
|
||||
DeleteIcon,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
OffsettedList,
|
||||
OffsettedListBody,
|
||||
OffsettedListItem,
|
||||
OffsettedListItemCell,
|
||||
} from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
const useStyles = makeStyles((theme) => {
|
||||
return {
|
||||
listItem: {
|
||||
cursor: "pointer",
|
||||
height: "auto !important",
|
||||
},
|
||||
listItemActive: {
|
||||
background: "#f4f4f4",
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
},
|
||||
channelSlug: {
|
||||
fontFamily: "monospace",
|
||||
opacity: 0.8,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type ListItem = {
|
||||
label: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
listItems: ListItem[];
|
||||
activeItemId?: string;
|
||||
onItemClick(itemId?: string): void;
|
||||
};
|
||||
|
||||
export const ConfigurationsList = ({ listItems, activeItemId, onItemClick }: Props) => {
|
||||
const styles = useStyles();
|
||||
return (
|
||||
<OffsettedList gridTemplate={["1fr"]}>
|
||||
<OffsettedListBody>
|
||||
{listItems.map((c) => {
|
||||
return (
|
||||
<OffsettedListItem
|
||||
className={clsx(styles.listItem, {
|
||||
[styles.listItemActive]: c.id === activeItemId,
|
||||
})}
|
||||
key={c.id}
|
||||
onClick={() => {
|
||||
onItemClick(c.id);
|
||||
}}
|
||||
>
|
||||
<OffsettedListItemCell>
|
||||
{c.label}
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</OffsettedListItemCell>
|
||||
</OffsettedListItem>
|
||||
);
|
||||
})}
|
||||
<OffsettedListItem
|
||||
className={clsx(styles.listItem, {
|
||||
[styles.listItemActive]: activeItemId === undefined,
|
||||
})}
|
||||
key="new"
|
||||
onClick={() => {
|
||||
onItemClick();
|
||||
}}
|
||||
>
|
||||
<OffsettedListItemCell>Create new</OffsettedListItemCell>
|
||||
</OffsettedListItem>
|
||||
</OffsettedListBody>
|
||||
</OffsettedList>
|
||||
);
|
||||
};
|
|
@ -1,109 +0,0 @@
|
|||
import { Card, CardContent, CardHeader, Divider } from "@material-ui/core";
|
||||
("@material-ui/icons");
|
||||
import { DeleteIcon, IconButton, List, ListItem, ListItemCell } from "@saleor/macaw-ui";
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { Skeleton } from "@material-ui/lab";
|
||||
|
||||
export const useStyles = makeStyles((theme) => ({
|
||||
menu: {
|
||||
height: "fit-content",
|
||||
},
|
||||
clickable: {
|
||||
cursor: "pointer",
|
||||
},
|
||||
selected: {
|
||||
"&&&&::before": {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
width: "4px",
|
||||
height: "100%",
|
||||
backgroundColor: theme.palette.saleor.active[1],
|
||||
},
|
||||
},
|
||||
spaceBetween: {
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
},
|
||||
tableRow: {
|
||||
minHeight: "48px",
|
||||
"&::after": {
|
||||
display: "none",
|
||||
},
|
||||
},
|
||||
greyText: {
|
||||
color: theme.palette.text.hint,
|
||||
},
|
||||
link: {
|
||||
all: "inherit",
|
||||
display: "contents",
|
||||
},
|
||||
}));
|
||||
|
||||
interface SideMenuProps {
|
||||
title: string;
|
||||
noItemsText?: string;
|
||||
items: { id: string; label: string }[];
|
||||
selectedItemId?: string;
|
||||
headerToolbar?: React.ReactNode;
|
||||
onDelete?: (itemId: string) => void;
|
||||
onClick: (itemId: string) => void;
|
||||
}
|
||||
|
||||
export const SideMenu: React.FC<SideMenuProps> = ({
|
||||
title,
|
||||
items,
|
||||
headerToolbar,
|
||||
selectedItemId,
|
||||
noItemsText,
|
||||
onDelete,
|
||||
onClick,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
const isNoItems = !items || !items.length;
|
||||
return (
|
||||
<Card className={classes.menu}>
|
||||
<CardHeader title={title} action={headerToolbar} />
|
||||
{isNoItems ? (
|
||||
!!noItemsText && <CardContent className={classes.greyText}>{noItemsText}</CardContent>
|
||||
) : (
|
||||
<List gridTemplate={["1fr"]}>
|
||||
{items.map((item) => (
|
||||
<React.Fragment key={item.id}>
|
||||
<Divider />
|
||||
<ListItem
|
||||
className={clsx(classes.clickable, classes.tableRow, {
|
||||
[classes.selected]: item.id === selectedItemId,
|
||||
})}
|
||||
onClick={() => onClick(item.id)}
|
||||
>
|
||||
<ListItemCell>
|
||||
<div className={classes.spaceBetween}>
|
||||
{item.label}
|
||||
{!!onDelete && (
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
onDelete(item.id);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
</ListItemCell>
|
||||
</ListItem>
|
||||
</React.Fragment>
|
||||
)) ?? <Skeleton />}
|
||||
<Divider />
|
||||
</List>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -1,7 +1,6 @@
|
|||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
import { Client } from "urql";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
import { AppConfigurationService } from "../app-configuration/get-app-configuration.service";
|
||||
import { MjmlConfigurationService } from "../mjml/configuration/get-mjml-configuration.service";
|
||||
import { sendMjml } from "../mjml/send-mjml";
|
||||
import { SendgridConfigurationService } from "../sendgrid/configuration/get-sendgrid-configuration.service";
|
||||
|
@ -31,75 +30,53 @@ export const sendEventMessages = async ({
|
|||
|
||||
logger.debug("Function called");
|
||||
|
||||
const appConfigurationService = new AppConfigurationService({
|
||||
const mjmlConfigurationService = new MjmlConfigurationService({
|
||||
apiClient: client,
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
});
|
||||
|
||||
const channelAppConfiguration = await appConfigurationService.getChannelConfiguration(channel);
|
||||
const availableMjmlConfigurations = await mjmlConfigurationService.getConfigurations({
|
||||
active: true,
|
||||
availableInChannel: channel,
|
||||
});
|
||||
|
||||
if (!channelAppConfiguration) {
|
||||
logger.warn("App has no configuration for this channel");
|
||||
return;
|
||||
}
|
||||
logger.debug("Channel has assigned app configuration");
|
||||
|
||||
if (!channelAppConfiguration.active) {
|
||||
logger.warn("App configuration is not active for this channel");
|
||||
return;
|
||||
}
|
||||
|
||||
if (channelAppConfiguration.mjmlConfigurationId) {
|
||||
logger.debug("Channel has assigned MJML configuration");
|
||||
|
||||
const mjmlConfigurationService = new MjmlConfigurationService({
|
||||
apiClient: client,
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
for (const mjmlConfiguration of availableMjmlConfigurations) {
|
||||
const mjmlStatus = await sendMjml({
|
||||
event,
|
||||
payload,
|
||||
recipientEmail,
|
||||
mjmlConfiguration,
|
||||
});
|
||||
|
||||
const mjmlConfiguration = await mjmlConfigurationService.getConfiguration({
|
||||
id: channelAppConfiguration.mjmlConfigurationId,
|
||||
});
|
||||
|
||||
if (mjmlConfiguration) {
|
||||
const mjmlStatus = await sendMjml({
|
||||
event,
|
||||
payload,
|
||||
recipientEmail,
|
||||
mjmlConfiguration,
|
||||
});
|
||||
|
||||
if (mjmlStatus?.errors.length) {
|
||||
logger.error("MJML errors");
|
||||
logger.error(mjmlStatus?.errors);
|
||||
}
|
||||
if (mjmlStatus?.errors.length) {
|
||||
logger.error("MJML errors");
|
||||
logger.error(mjmlStatus?.errors);
|
||||
}
|
||||
}
|
||||
|
||||
if (channelAppConfiguration.sendgridConfigurationId) {
|
||||
logger.debug("Channel has assigned Sendgrid configuration");
|
||||
logger.debug("Channel has assigned Sendgrid configuration");
|
||||
|
||||
const sendgridConfigurationService = new SendgridConfigurationService({
|
||||
apiClient: client,
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
const sendgridConfigurationService = new SendgridConfigurationService({
|
||||
apiClient: client,
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
});
|
||||
|
||||
const availableSendgridConfigurations = await sendgridConfigurationService.getConfigurations({
|
||||
active: true,
|
||||
availableInChannel: channel,
|
||||
});
|
||||
|
||||
for (const sendgridConfiguration of availableSendgridConfigurations) {
|
||||
const sendgridStatus = await sendSendgrid({
|
||||
event,
|
||||
payload,
|
||||
recipientEmail,
|
||||
sendgridConfiguration,
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
if (sendgridStatus?.errors.length) {
|
||||
logger.error("Sendgrid errors");
|
||||
logger.error(sendgridStatus?.errors);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@ import { messageEventTypes } from "../../event-handlers/message-event-types";
|
|||
import { MjmlConfig as MjmlConfigurationRoot, MjmlConfiguration } from "./mjml-config";
|
||||
import { defaultMjmlTemplates, defaultMjmlSubjectTemplates } from "../default-templates";
|
||||
import { generateRandomId } from "../../../lib/generate-random-id";
|
||||
import { isAvailableInChannel } from "../../../lib/is-available-in-channel";
|
||||
|
||||
export const getDefaultEventsConfiguration = (): MjmlConfiguration["events"] =>
|
||||
messageEventTypes.map((eventType) => ({
|
||||
|
@ -24,6 +25,10 @@ export const getDefaultEmptyConfiguration = (): MjmlConfiguration => {
|
|||
smtpPassword: "",
|
||||
encryption: "NONE",
|
||||
events: getDefaultEventsConfiguration(),
|
||||
channels: {
|
||||
excludedFrom: [],
|
||||
restrictedTo: [],
|
||||
},
|
||||
};
|
||||
|
||||
return defaultConfig;
|
||||
|
@ -45,6 +50,7 @@ const getConfiguration =
|
|||
|
||||
export interface FilterConfigurationsArgs {
|
||||
ids?: string[];
|
||||
availableInChannel?: string;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
|
@ -57,14 +63,28 @@ const getConfigurations =
|
|||
|
||||
let filtered = mjmlConfigRoot.configurations;
|
||||
|
||||
if (filter?.ids?.length) {
|
||||
if (!filter) {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
if (filter.ids?.length) {
|
||||
filtered = filtered.filter((c) => filter?.ids?.includes(c.id));
|
||||
}
|
||||
|
||||
if (filter?.active !== undefined) {
|
||||
if (filter.active !== undefined) {
|
||||
filtered = filtered.filter((c) => c.active === filter.active);
|
||||
}
|
||||
|
||||
if (filter.availableInChannel?.length) {
|
||||
filtered = filtered.filter((c) =>
|
||||
isAvailableInChannel({
|
||||
channel: filter.availableInChannel!,
|
||||
restrictedToChannels: c.channels.restrictedTo,
|
||||
excludedChannels: c.channels.excludedFrom,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
|
@ -79,6 +99,7 @@ const createConfiguration =
|
|||
id: generateRandomId(),
|
||||
events: getDefaultEventsConfiguration(),
|
||||
};
|
||||
|
||||
mjmlConfigNormalized.configurations.push(newConfiguration);
|
||||
return mjmlConfigNormalized;
|
||||
};
|
||||
|
|
|
@ -19,6 +19,10 @@ export const mjmlConfigurationBaseObjectSchema = z.object({
|
|||
smtpUser: z.string(),
|
||||
smtpPassword: z.string(),
|
||||
encryption: z.enum(smtpEncryptionTypes),
|
||||
channels: z.object({
|
||||
excludedFrom: z.array(z.string()),
|
||||
restrictedTo: z.array(z.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
export const mjmlCreateConfigurationSchema = mjmlConfigurationBaseObjectSchema;
|
||||
|
|
|
@ -15,6 +15,10 @@ export interface MjmlConfiguration {
|
|||
id: string;
|
||||
active: boolean;
|
||||
configurationName: string;
|
||||
channels: {
|
||||
excludedFrom: string[];
|
||||
restrictedTo: string[];
|
||||
};
|
||||
senderName: string;
|
||||
senderEmail: string;
|
||||
smtpHost: string;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { IconButton, makeStyles } from "@saleor/macaw-ui";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { AppColumnsLayout } from "../../../ui/app-columns-layout";
|
||||
import { trpcClient } from "../../../trpc/trpc-client";
|
||||
import { MjmlConfigurationForm } from "./mjml-configuration-form";
|
||||
|
@ -7,11 +7,8 @@ import { getDefaultEmptyConfiguration } from "../mjml-config-container";
|
|||
import { NextRouter, useRouter } from "next/router";
|
||||
import { mjmlUrls } from "../../urls";
|
||||
import { MjmlTemplatesCard } from "./mjml-templates-card";
|
||||
import { SideMenu } from "../../../app-configuration/ui/side-menu";
|
||||
import { MjmlConfiguration } from "../mjml-config";
|
||||
import { LoadingIndicator } from "../../../ui/loading-indicator";
|
||||
import { Add } from "@material-ui/icons";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { MjmlInstructions } from "./mjml-instructions";
|
||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||
|
||||
|
@ -42,6 +39,7 @@ const navigateToFirstConfiguration = (router: NextRouter, configurations?: MjmlC
|
|||
return;
|
||||
}
|
||||
const firstConfigurationId = configurations[0]?.id;
|
||||
|
||||
if (firstConfigurationId) {
|
||||
router.replace(mjmlUrls.configuration(firstConfigurationId));
|
||||
return;
|
||||
|
@ -50,16 +48,13 @@ const navigateToFirstConfiguration = (router: NextRouter, configurations?: MjmlC
|
|||
|
||||
export const MjmlConfigurationTab = ({ configurationId }: MjmlConfigurationTabProps) => {
|
||||
const styles = useStyles();
|
||||
const { notifyError, notifySuccess } = useDashboardNotification();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
data: configurations,
|
||||
refetch: refetchConfigurations,
|
||||
isLoading: configurationsIsLoading,
|
||||
isFetching: configurationsIsFetching,
|
||||
isRefetching: configurationsIsRefetching,
|
||||
} = trpcClient.mjmlConfiguration.getConfigurations.useQuery(undefined, {
|
||||
onSuccess(data) {
|
||||
if (!configurationId) {
|
||||
|
@ -69,38 +64,6 @@ export const MjmlConfigurationTab = ({ configurationId }: MjmlConfigurationTabPr
|
|||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteConfiguration } =
|
||||
trpcClient.mjmlConfiguration.deleteConfiguration.useMutation({
|
||||
onError: (error) => {
|
||||
notifyError("Could not remove the configuration", error.message);
|
||||
},
|
||||
onSuccess: async (_data, variables) => {
|
||||
await queryClient.cancelQueries({ queryKey: ["mjmlConfiguration", "getConfigurations"] });
|
||||
// remove value from the cache after the success
|
||||
queryClient.setQueryData<Array<MjmlConfiguration>>(
|
||||
["mjmlConfiguration", "getConfigurations"],
|
||||
(old) => {
|
||||
if (old) {
|
||||
const index = old.findIndex((c) => c.id === variables.id);
|
||||
if (index !== -1) {
|
||||
delete old[index];
|
||||
return [...old];
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// if we just deleted the configuration that was selected
|
||||
// we have to update the URL
|
||||
if (variables.id === configurationId) {
|
||||
router.replace(mjmlUrls.configuration());
|
||||
}
|
||||
|
||||
refetchConfigurations();
|
||||
notifySuccess("Success", "Removed successfully");
|
||||
},
|
||||
});
|
||||
|
||||
if (configurationsIsLoading || configurationsIsFetching) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
@ -113,25 +76,6 @@ export const MjmlConfigurationTab = ({ configurationId }: MjmlConfigurationTabPr
|
|||
|
||||
return (
|
||||
<AppColumnsLayout>
|
||||
<SideMenu
|
||||
title="Configurations"
|
||||
selectedItemId={configurationId}
|
||||
headerToolbar={
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
router.replace(mjmlUrls.configuration());
|
||||
}}
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
}
|
||||
onClick={(id) => router.replace(mjmlUrls.configuration(id))}
|
||||
onDelete={(id) => {
|
||||
deleteConfiguration({ id });
|
||||
}}
|
||||
items={configurations?.map((c) => ({ label: c.configurationName, id: c.id })) || []}
|
||||
/>
|
||||
<div className={styles.configurationColumn}>
|
||||
{configurationsIsLoading || configurationsIsFetching ? (
|
||||
<LoadingIndicator />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { generateRandomId } from "../../../lib/generate-random-id";
|
||||
import { isAvailableInChannel } from "../../../lib/is-available-in-channel";
|
||||
import { messageEventTypes } from "../../event-handlers/message-event-types";
|
||||
import {
|
||||
SendgridConfig as SendgridConfigurationRoot,
|
||||
|
@ -22,6 +23,10 @@ export const getDefaultEmptyConfiguration = (): SendgridConfiguration => {
|
|||
apiKey: "",
|
||||
sandboxMode: false,
|
||||
events: getDefaultEventsConfiguration(),
|
||||
channels: {
|
||||
excludedFrom: [],
|
||||
restrictedTo: [],
|
||||
},
|
||||
};
|
||||
|
||||
return defaultConfig;
|
||||
|
@ -44,6 +49,7 @@ const getConfiguration =
|
|||
export interface FilterConfigurationsArgs {
|
||||
ids?: string[];
|
||||
active?: boolean;
|
||||
availableInChannel?: string;
|
||||
}
|
||||
|
||||
const getConfigurations =
|
||||
|
@ -55,14 +61,28 @@ const getConfigurations =
|
|||
|
||||
let filtered = sendgridConfigRoot.configurations;
|
||||
|
||||
if (filter?.ids?.length) {
|
||||
if (!filter) {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
if (filter.ids?.length) {
|
||||
filtered = filtered.filter((c) => filter?.ids?.includes(c.id));
|
||||
}
|
||||
|
||||
if (filter?.active !== undefined) {
|
||||
if (filter.active !== undefined) {
|
||||
filtered = filtered.filter((c) => c.active === filter.active);
|
||||
}
|
||||
|
||||
if (filter.availableInChannel?.length) {
|
||||
filtered = filtered.filter((c) =>
|
||||
isAvailableInChannel({
|
||||
channel: filter.availableInChannel!,
|
||||
restrictedToChannels: c.channels.restrictedTo,
|
||||
excludedChannels: c.channels.excludedFrom,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
|
@ -77,6 +97,7 @@ const createConfiguration =
|
|||
id: generateRandomId(),
|
||||
events: getDefaultEventsConfiguration(),
|
||||
};
|
||||
|
||||
sendgridConfigNormalized.configurations.push(newConfiguration);
|
||||
return sendgridConfigNormalized;
|
||||
};
|
||||
|
|
|
@ -14,6 +14,10 @@ export const sendgridConfigurationBaseObjectSchema = z.object({
|
|||
apiKey: z.string().min(1),
|
||||
senderName: z.string().min(1).optional(),
|
||||
senderEmail: z.string().email().min(5).optional(),
|
||||
channels: z.object({
|
||||
excludedFrom: z.array(z.string()),
|
||||
restrictedTo: z.array(z.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
export const sendgridCreateConfigurationSchema = sendgridConfigurationBaseObjectSchema.omit({
|
||||
|
|
|
@ -15,6 +15,10 @@ export interface SendgridConfiguration {
|
|||
senderEmail?: string;
|
||||
apiKey: string;
|
||||
events: SendgridEventConfiguration[];
|
||||
channels: {
|
||||
excludedFrom: string[];
|
||||
restrictedTo: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export type SendgridConfig = {
|
||||
|
|
|
@ -1,19 +1,15 @@
|
|||
import React from "react";
|
||||
import { IconButton, makeStyles } from "@saleor/macaw-ui";
|
||||
import { makeStyles } from "@saleor/macaw-ui";
|
||||
import { AppColumnsLayout } from "../../../ui/app-columns-layout";
|
||||
import { trpcClient } from "../../../trpc/trpc-client";
|
||||
import { SendgridConfigurationForm } from "./sendgrid-configuration-form";
|
||||
import { getDefaultEmptyConfiguration } from "../sendgrid-config-container";
|
||||
import { NextRouter, useRouter } from "next/router";
|
||||
import { 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";
|
||||
import { SendgridInstructions } from "./sendgrid-instructions";
|
||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||
|
||||
const useStyles = makeStyles((theme) => {
|
||||
return {
|
||||
|
@ -45,6 +41,7 @@ const navigateToFirstConfiguration = (
|
|||
return;
|
||||
}
|
||||
const firstConfigurationId = configurations[0]?.id;
|
||||
|
||||
if (firstConfigurationId) {
|
||||
router.replace(sendgridUrls.configuration(firstConfigurationId));
|
||||
return;
|
||||
|
@ -54,15 +51,12 @@ const navigateToFirstConfiguration = (
|
|||
export const SendgridConfigurationTab = ({ configurationId }: SendgridConfigurationTabProps) => {
|
||||
const styles = useStyles();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
||||
|
||||
const {
|
||||
data: configurations,
|
||||
refetch: refetchConfigurations,
|
||||
isLoading: configurationsIsLoading,
|
||||
isFetching: configurationsIsFetching,
|
||||
isRefetching: configurationsIsRefetching,
|
||||
} = trpcClient.sendgridConfiguration.getConfigurations.useQuery(undefined, {
|
||||
onSuccess(data) {
|
||||
if (!configurationId) {
|
||||
|
@ -72,41 +66,6 @@ export const SendgridConfigurationTab = ({ configurationId }: SendgridConfigurat
|
|||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteConfiguration } =
|
||||
trpcClient.sendgridConfiguration.deleteConfiguration.useMutation({
|
||||
onError: (error) => {
|
||||
notifyError("Could not remove the configuration", error.message);
|
||||
},
|
||||
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];
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// if we just deleted the configuration that was selected
|
||||
// we have to update the URL
|
||||
if (variables.id === configurationId) {
|
||||
router.replace(sendgridUrls.configuration());
|
||||
}
|
||||
|
||||
refetchConfigurations();
|
||||
|
||||
notifySuccess("Success", "Removed successfully");
|
||||
},
|
||||
});
|
||||
|
||||
if (configurationsIsLoading || configurationsIsFetching) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
@ -119,25 +78,6 @@ export const SendgridConfigurationTab = ({ configurationId }: SendgridConfigurat
|
|||
|
||||
return (
|
||||
<AppColumnsLayout>
|
||||
<SideMenu
|
||||
title="Configurations"
|
||||
selectedItemId={configurationId}
|
||||
headerToolbar={
|
||||
<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}>
|
||||
{configurationsIsLoading || configurationsIsFetching ? (
|
||||
<LoadingIndicator />
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { channelsRouter } from "../channels/channels.router";
|
||||
import { router } from "./trpc-server";
|
||||
import { appConfigurationRouter } from "../app-configuration/app-configuration.router";
|
||||
import { mjmlConfigurationRouter } from "../mjml/configuration/mjml-configuration.router";
|
||||
import { sendgridConfigurationRouter } from "../sendgrid/configuration/sendgrid-configuration.router";
|
||||
|
||||
export const appRouter = router({
|
||||
channels: channelsRouter,
|
||||
appConfiguration: appConfigurationRouter,
|
||||
mjmlConfiguration: mjmlConfigurationRouter,
|
||||
sendgridConfiguration: sendgridConfigurationRouter,
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@ import { PropsWithChildren } from "react";
|
|||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "280px auto 400px",
|
||||
gridTemplateColumns: "auto 400px",
|
||||
alignItems: "start",
|
||||
gap: theme.spacing(3),
|
||||
padding: "20px 0",
|
||||
|
|
|
@ -3,12 +3,14 @@ import React, { useEffect } from "react";
|
|||
import { useRouter } from "next/router";
|
||||
import { trpcClient } from "../../../modules/trpc/trpc-client";
|
||||
import { ConfigurationPageBaseLayout } from "../../../modules/ui/configuration-page-base-layout";
|
||||
import { ChannelsConfigurationTab } from "../../../modules/app-configuration/ui/channels-configuration-tab";
|
||||
|
||||
const ChannelsConfigurationPage: NextPage = () => {
|
||||
const channels = trpcClient.channels.fetch.useQuery();
|
||||
const router = useRouter();
|
||||
|
||||
const sendgridConfigurations = trpcClient.sendgridConfiguration.getConfigurations.useQuery();
|
||||
const mjmlConfigurations = trpcClient.mjmlConfiguration.getConfigurations.useQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (router && channels.isSuccess && channels.data.length === 0) {
|
||||
router.push("/not-ready");
|
||||
|
@ -16,7 +18,18 @@ const ChannelsConfigurationPage: NextPage = () => {
|
|||
}, [channels.data, channels.isSuccess, router]);
|
||||
return (
|
||||
<ConfigurationPageBaseLayout>
|
||||
<ChannelsConfigurationTab />
|
||||
Sendgrid configurations:
|
||||
<ul>
|
||||
{sendgridConfigurations.data?.map((c) => (
|
||||
<li key={c.id}>{c.configurationName}</li>
|
||||
))}
|
||||
</ul>
|
||||
MJML configurations:
|
||||
<ul>
|
||||
{mjmlConfigurations.data?.map((c) => (
|
||||
<li key={c.id}>{c.configurationName}</li>
|
||||
))}
|
||||
</ul>
|
||||
</ConfigurationPageBaseLayout>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
query FetchProductDataForFeed($first:Int!, $after: String, $channel: String!){
|
||||
query FetchProductDataForFeed($first:Int!, $after: String, $channel: String!, $language: String!){
|
||||
productVariants(first:$first, after: $after, channel: $channel){
|
||||
pageInfo{
|
||||
hasNextPage
|
||||
|
|
Loading…
Reference in a new issue