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 { AuthData } from "@saleor/app-sdk/APL";
|
||||||
import { Client } from "urql";
|
import { Client } from "urql";
|
||||||
import { createLogger } from "@saleor/apps-shared";
|
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 { 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 { SendgridConfigurationService } from "../sendgrid/configuration/get-sendgrid-configuration.service";
|
||||||
|
@ -31,75 +30,53 @@ export const sendEventMessages = async ({
|
||||||
|
|
||||||
logger.debug("Function called");
|
logger.debug("Function called");
|
||||||
|
|
||||||
const appConfigurationService = new AppConfigurationService({
|
const mjmlConfigurationService = new MjmlConfigurationService({
|
||||||
apiClient: client,
|
apiClient: client,
|
||||||
saleorApiUrl: authData.saleorApiUrl,
|
saleorApiUrl: authData.saleorApiUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
const channelAppConfiguration = await appConfigurationService.getChannelConfiguration(channel);
|
const availableMjmlConfigurations = await mjmlConfigurationService.getConfigurations({
|
||||||
|
active: true,
|
||||||
|
availableInChannel: channel,
|
||||||
|
});
|
||||||
|
|
||||||
if (!channelAppConfiguration) {
|
for (const mjmlConfiguration of availableMjmlConfigurations) {
|
||||||
logger.warn("App has no configuration for this channel");
|
const mjmlStatus = await sendMjml({
|
||||||
return;
|
event,
|
||||||
}
|
payload,
|
||||||
logger.debug("Channel has assigned app configuration");
|
recipientEmail,
|
||||||
|
mjmlConfiguration,
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mjmlConfiguration = await mjmlConfigurationService.getConfiguration({
|
if (mjmlStatus?.errors.length) {
|
||||||
id: channelAppConfiguration.mjmlConfigurationId,
|
logger.error("MJML errors");
|
||||||
});
|
logger.error(mjmlStatus?.errors);
|
||||||
|
|
||||||
if (mjmlConfiguration) {
|
|
||||||
const mjmlStatus = await sendMjml({
|
|
||||||
event,
|
|
||||||
payload,
|
|
||||||
recipientEmail,
|
|
||||||
mjmlConfiguration,
|
|
||||||
});
|
|
||||||
|
|
||||||
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({
|
const sendgridConfigurationService = new SendgridConfigurationService({
|
||||||
apiClient: client,
|
apiClient: client,
|
||||||
saleorApiUrl: authData.saleorApiUrl,
|
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({
|
if (sendgridStatus?.errors.length) {
|
||||||
id: channelAppConfiguration.sendgridConfigurationId,
|
logger.error("Sendgrid errors");
|
||||||
});
|
logger.error(sendgridStatus?.errors);
|
||||||
|
|
||||||
if (sendgridConfiguration) {
|
|
||||||
const sendgridStatus = await sendSendgrid({
|
|
||||||
event,
|
|
||||||
payload,
|
|
||||||
recipientEmail,
|
|
||||||
sendgridConfiguration,
|
|
||||||
});
|
|
||||||
|
|
||||||
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 { MjmlConfig as MjmlConfigurationRoot, MjmlConfiguration } from "./mjml-config";
|
||||||
import { defaultMjmlTemplates, defaultMjmlSubjectTemplates } from "../default-templates";
|
import { defaultMjmlTemplates, defaultMjmlSubjectTemplates } from "../default-templates";
|
||||||
import { generateRandomId } from "../../../lib/generate-random-id";
|
import { generateRandomId } from "../../../lib/generate-random-id";
|
||||||
|
import { isAvailableInChannel } from "../../../lib/is-available-in-channel";
|
||||||
|
|
||||||
export const getDefaultEventsConfiguration = (): MjmlConfiguration["events"] =>
|
export const getDefaultEventsConfiguration = (): MjmlConfiguration["events"] =>
|
||||||
messageEventTypes.map((eventType) => ({
|
messageEventTypes.map((eventType) => ({
|
||||||
|
@ -24,6 +25,10 @@ export const getDefaultEmptyConfiguration = (): MjmlConfiguration => {
|
||||||
smtpPassword: "",
|
smtpPassword: "",
|
||||||
encryption: "NONE",
|
encryption: "NONE",
|
||||||
events: getDefaultEventsConfiguration(),
|
events: getDefaultEventsConfiguration(),
|
||||||
|
channels: {
|
||||||
|
excludedFrom: [],
|
||||||
|
restrictedTo: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
|
@ -45,6 +50,7 @@ const getConfiguration =
|
||||||
|
|
||||||
export interface FilterConfigurationsArgs {
|
export interface FilterConfigurationsArgs {
|
||||||
ids?: string[];
|
ids?: string[];
|
||||||
|
availableInChannel?: string;
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,14 +63,28 @@ const getConfigurations =
|
||||||
|
|
||||||
let filtered = mjmlConfigRoot.configurations;
|
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));
|
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);
|
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;
|
return filtered;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,6 +99,7 @@ const createConfiguration =
|
||||||
id: generateRandomId(),
|
id: generateRandomId(),
|
||||||
events: getDefaultEventsConfiguration(),
|
events: getDefaultEventsConfiguration(),
|
||||||
};
|
};
|
||||||
|
|
||||||
mjmlConfigNormalized.configurations.push(newConfiguration);
|
mjmlConfigNormalized.configurations.push(newConfiguration);
|
||||||
return mjmlConfigNormalized;
|
return mjmlConfigNormalized;
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,6 +19,10 @@ export const mjmlConfigurationBaseObjectSchema = z.object({
|
||||||
smtpUser: z.string(),
|
smtpUser: z.string(),
|
||||||
smtpPassword: z.string(),
|
smtpPassword: z.string(),
|
||||||
encryption: z.enum(smtpEncryptionTypes),
|
encryption: z.enum(smtpEncryptionTypes),
|
||||||
|
channels: z.object({
|
||||||
|
excludedFrom: z.array(z.string()),
|
||||||
|
restrictedTo: z.array(z.string()),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mjmlCreateConfigurationSchema = mjmlConfigurationBaseObjectSchema;
|
export const mjmlCreateConfigurationSchema = mjmlConfigurationBaseObjectSchema;
|
||||||
|
|
|
@ -15,6 +15,10 @@ export interface MjmlConfiguration {
|
||||||
id: string;
|
id: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
configurationName: string;
|
configurationName: string;
|
||||||
|
channels: {
|
||||||
|
excludedFrom: string[];
|
||||||
|
restrictedTo: string[];
|
||||||
|
};
|
||||||
senderName: string;
|
senderName: string;
|
||||||
senderEmail: string;
|
senderEmail: string;
|
||||||
smtpHost: string;
|
smtpHost: string;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
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 { AppColumnsLayout } from "../../../ui/app-columns-layout";
|
||||||
import { trpcClient } from "../../../trpc/trpc-client";
|
import { trpcClient } from "../../../trpc/trpc-client";
|
||||||
import { MjmlConfigurationForm } from "./mjml-configuration-form";
|
import { MjmlConfigurationForm } from "./mjml-configuration-form";
|
||||||
|
@ -7,11 +7,8 @@ import { getDefaultEmptyConfiguration } from "../mjml-config-container";
|
||||||
import { NextRouter, useRouter } from "next/router";
|
import { NextRouter, useRouter } from "next/router";
|
||||||
import { mjmlUrls } from "../../urls";
|
import { mjmlUrls } from "../../urls";
|
||||||
import { MjmlTemplatesCard } from "./mjml-templates-card";
|
import { MjmlTemplatesCard } from "./mjml-templates-card";
|
||||||
import { SideMenu } from "../../../app-configuration/ui/side-menu";
|
|
||||||
import { MjmlConfiguration } from "../mjml-config";
|
import { MjmlConfiguration } from "../mjml-config";
|
||||||
import { LoadingIndicator } from "../../../ui/loading-indicator";
|
import { LoadingIndicator } from "../../../ui/loading-indicator";
|
||||||
import { Add } from "@material-ui/icons";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { MjmlInstructions } from "./mjml-instructions";
|
import { MjmlInstructions } from "./mjml-instructions";
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
|
|
||||||
|
@ -42,6 +39,7 @@ const navigateToFirstConfiguration = (router: NextRouter, configurations?: MjmlC
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const firstConfigurationId = configurations[0]?.id;
|
const firstConfigurationId = configurations[0]?.id;
|
||||||
|
|
||||||
if (firstConfigurationId) {
|
if (firstConfigurationId) {
|
||||||
router.replace(mjmlUrls.configuration(firstConfigurationId));
|
router.replace(mjmlUrls.configuration(firstConfigurationId));
|
||||||
return;
|
return;
|
||||||
|
@ -50,16 +48,13 @@ const navigateToFirstConfiguration = (router: NextRouter, configurations?: MjmlC
|
||||||
|
|
||||||
export const MjmlConfigurationTab = ({ configurationId }: MjmlConfigurationTabProps) => {
|
export const MjmlConfigurationTab = ({ configurationId }: MjmlConfigurationTabProps) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const { notifyError, notifySuccess } = useDashboardNotification();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: configurations,
|
data: configurations,
|
||||||
refetch: refetchConfigurations,
|
refetch: refetchConfigurations,
|
||||||
isLoading: configurationsIsLoading,
|
isLoading: configurationsIsLoading,
|
||||||
isFetching: configurationsIsFetching,
|
isFetching: configurationsIsFetching,
|
||||||
isRefetching: configurationsIsRefetching,
|
|
||||||
} = trpcClient.mjmlConfiguration.getConfigurations.useQuery(undefined, {
|
} = trpcClient.mjmlConfiguration.getConfigurations.useQuery(undefined, {
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
if (!configurationId) {
|
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) {
|
if (configurationsIsLoading || configurationsIsFetching) {
|
||||||
return <LoadingIndicator />;
|
return <LoadingIndicator />;
|
||||||
}
|
}
|
||||||
|
@ -113,25 +76,6 @@ export const MjmlConfigurationTab = ({ configurationId }: MjmlConfigurationTabPr
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppColumnsLayout>
|
<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}>
|
<div className={styles.configurationColumn}>
|
||||||
{configurationsIsLoading || configurationsIsFetching ? (
|
{configurationsIsLoading || configurationsIsFetching ? (
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { generateRandomId } from "../../../lib/generate-random-id";
|
import { generateRandomId } from "../../../lib/generate-random-id";
|
||||||
|
import { isAvailableInChannel } from "../../../lib/is-available-in-channel";
|
||||||
import { messageEventTypes } from "../../event-handlers/message-event-types";
|
import { messageEventTypes } from "../../event-handlers/message-event-types";
|
||||||
import {
|
import {
|
||||||
SendgridConfig as SendgridConfigurationRoot,
|
SendgridConfig as SendgridConfigurationRoot,
|
||||||
|
@ -22,6 +23,10 @@ export const getDefaultEmptyConfiguration = (): SendgridConfiguration => {
|
||||||
apiKey: "",
|
apiKey: "",
|
||||||
sandboxMode: false,
|
sandboxMode: false,
|
||||||
events: getDefaultEventsConfiguration(),
|
events: getDefaultEventsConfiguration(),
|
||||||
|
channels: {
|
||||||
|
excludedFrom: [],
|
||||||
|
restrictedTo: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
|
@ -44,6 +49,7 @@ const getConfiguration =
|
||||||
export interface FilterConfigurationsArgs {
|
export interface FilterConfigurationsArgs {
|
||||||
ids?: string[];
|
ids?: string[];
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
|
availableInChannel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getConfigurations =
|
const getConfigurations =
|
||||||
|
@ -55,14 +61,28 @@ const getConfigurations =
|
||||||
|
|
||||||
let filtered = sendgridConfigRoot.configurations;
|
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));
|
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);
|
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;
|
return filtered;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -77,6 +97,7 @@ const createConfiguration =
|
||||||
id: generateRandomId(),
|
id: generateRandomId(),
|
||||||
events: getDefaultEventsConfiguration(),
|
events: getDefaultEventsConfiguration(),
|
||||||
};
|
};
|
||||||
|
|
||||||
sendgridConfigNormalized.configurations.push(newConfiguration);
|
sendgridConfigNormalized.configurations.push(newConfiguration);
|
||||||
return sendgridConfigNormalized;
|
return sendgridConfigNormalized;
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,6 +14,10 @@ export const sendgridConfigurationBaseObjectSchema = z.object({
|
||||||
apiKey: z.string().min(1),
|
apiKey: z.string().min(1),
|
||||||
senderName: z.string().min(1).optional(),
|
senderName: z.string().min(1).optional(),
|
||||||
senderEmail: z.string().email().min(5).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({
|
export const sendgridCreateConfigurationSchema = sendgridConfigurationBaseObjectSchema.omit({
|
||||||
|
|
|
@ -15,6 +15,10 @@ export interface SendgridConfiguration {
|
||||||
senderEmail?: string;
|
senderEmail?: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
events: SendgridEventConfiguration[];
|
events: SendgridEventConfiguration[];
|
||||||
|
channels: {
|
||||||
|
excludedFrom: string[];
|
||||||
|
restrictedTo: string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SendgridConfig = {
|
export type SendgridConfig = {
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
import React from "react";
|
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 { AppColumnsLayout } from "../../../ui/app-columns-layout";
|
||||||
import { trpcClient } from "../../../trpc/trpc-client";
|
import { trpcClient } from "../../../trpc/trpc-client";
|
||||||
import { SendgridConfigurationForm } from "./sendgrid-configuration-form";
|
import { SendgridConfigurationForm } from "./sendgrid-configuration-form";
|
||||||
import { getDefaultEmptyConfiguration } from "../sendgrid-config-container";
|
import { getDefaultEmptyConfiguration } from "../sendgrid-config-container";
|
||||||
import { NextRouter, useRouter } from "next/router";
|
import { NextRouter, useRouter } from "next/router";
|
||||||
import { SideMenu } from "../../../app-configuration/ui/side-menu";
|
|
||||||
import { SendgridConfiguration } from "../sendgrid-config";
|
import { SendgridConfiguration } from "../sendgrid-config";
|
||||||
import { LoadingIndicator } from "../../../ui/loading-indicator";
|
import { LoadingIndicator } from "../../../ui/loading-indicator";
|
||||||
import { Add } from "@material-ui/icons";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { sendgridUrls } from "../../urls";
|
import { sendgridUrls } from "../../urls";
|
||||||
import { SendgridTemplatesCard } from "./sendgrid-templates-card";
|
import { SendgridTemplatesCard } from "./sendgrid-templates-card";
|
||||||
import { SendgridInstructions } from "./sendgrid-instructions";
|
import { SendgridInstructions } from "./sendgrid-instructions";
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
const useStyles = makeStyles((theme) => {
|
||||||
return {
|
return {
|
||||||
|
@ -45,6 +41,7 @@ const navigateToFirstConfiguration = (
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const firstConfigurationId = configurations[0]?.id;
|
const firstConfigurationId = configurations[0]?.id;
|
||||||
|
|
||||||
if (firstConfigurationId) {
|
if (firstConfigurationId) {
|
||||||
router.replace(sendgridUrls.configuration(firstConfigurationId));
|
router.replace(sendgridUrls.configuration(firstConfigurationId));
|
||||||
return;
|
return;
|
||||||
|
@ -54,15 +51,12 @@ const navigateToFirstConfiguration = (
|
||||||
export const SendgridConfigurationTab = ({ configurationId }: SendgridConfigurationTabProps) => {
|
export const SendgridConfigurationTab = ({ configurationId }: SendgridConfigurationTabProps) => {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { notifySuccess, notifyError } = useDashboardNotification();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: configurations,
|
data: configurations,
|
||||||
refetch: refetchConfigurations,
|
refetch: refetchConfigurations,
|
||||||
isLoading: configurationsIsLoading,
|
isLoading: configurationsIsLoading,
|
||||||
isFetching: configurationsIsFetching,
|
isFetching: configurationsIsFetching,
|
||||||
isRefetching: configurationsIsRefetching,
|
|
||||||
} = trpcClient.sendgridConfiguration.getConfigurations.useQuery(undefined, {
|
} = trpcClient.sendgridConfiguration.getConfigurations.useQuery(undefined, {
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
if (!configurationId) {
|
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) {
|
if (configurationsIsLoading || configurationsIsFetching) {
|
||||||
return <LoadingIndicator />;
|
return <LoadingIndicator />;
|
||||||
}
|
}
|
||||||
|
@ -119,25 +78,6 @@ export const SendgridConfigurationTab = ({ configurationId }: SendgridConfigurat
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppColumnsLayout>
|
<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}>
|
<div className={styles.configurationColumn}>
|
||||||
{configurationsIsLoading || configurationsIsFetching ? (
|
{configurationsIsLoading || configurationsIsFetching ? (
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { channelsRouter } from "../channels/channels.router";
|
import { channelsRouter } from "../channels/channels.router";
|
||||||
import { router } from "./trpc-server";
|
import { router } from "./trpc-server";
|
||||||
import { appConfigurationRouter } from "../app-configuration/app-configuration.router";
|
|
||||||
import { mjmlConfigurationRouter } from "../mjml/configuration/mjml-configuration.router";
|
import { mjmlConfigurationRouter } from "../mjml/configuration/mjml-configuration.router";
|
||||||
import { sendgridConfigurationRouter } from "../sendgrid/configuration/sendgrid-configuration.router";
|
import { sendgridConfigurationRouter } from "../sendgrid/configuration/sendgrid-configuration.router";
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
channels: channelsRouter,
|
channels: channelsRouter,
|
||||||
appConfiguration: appConfigurationRouter,
|
|
||||||
mjmlConfiguration: mjmlConfigurationRouter,
|
mjmlConfiguration: mjmlConfigurationRouter,
|
||||||
sendgridConfiguration: sendgridConfigurationRouter,
|
sendgridConfiguration: sendgridConfigurationRouter,
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { PropsWithChildren } from "react";
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "280px auto 400px",
|
gridTemplateColumns: "auto 400px",
|
||||||
alignItems: "start",
|
alignItems: "start",
|
||||||
gap: theme.spacing(3),
|
gap: theme.spacing(3),
|
||||||
padding: "20px 0",
|
padding: "20px 0",
|
||||||
|
|
|
@ -3,12 +3,14 @@ import React, { useEffect } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { trpcClient } from "../../../modules/trpc/trpc-client";
|
import { trpcClient } from "../../../modules/trpc/trpc-client";
|
||||||
import { ConfigurationPageBaseLayout } from "../../../modules/ui/configuration-page-base-layout";
|
import { ConfigurationPageBaseLayout } from "../../../modules/ui/configuration-page-base-layout";
|
||||||
import { ChannelsConfigurationTab } from "../../../modules/app-configuration/ui/channels-configuration-tab";
|
|
||||||
|
|
||||||
const ChannelsConfigurationPage: NextPage = () => {
|
const ChannelsConfigurationPage: NextPage = () => {
|
||||||
const channels = trpcClient.channels.fetch.useQuery();
|
const channels = trpcClient.channels.fetch.useQuery();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const sendgridConfigurations = trpcClient.sendgridConfiguration.getConfigurations.useQuery();
|
||||||
|
const mjmlConfigurations = trpcClient.mjmlConfiguration.getConfigurations.useQuery();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (router && channels.isSuccess && channels.data.length === 0) {
|
if (router && channels.isSuccess && channels.data.length === 0) {
|
||||||
router.push("/not-ready");
|
router.push("/not-ready");
|
||||||
|
@ -16,7 +18,18 @@ const ChannelsConfigurationPage: NextPage = () => {
|
||||||
}, [channels.data, channels.isSuccess, router]);
|
}, [channels.data, channels.isSuccess, router]);
|
||||||
return (
|
return (
|
||||||
<ConfigurationPageBaseLayout>
|
<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>
|
</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){
|
productVariants(first:$first, after: $after, channel: $channel){
|
||||||
pageInfo{
|
pageInfo{
|
||||||
hasNextPage
|
hasNextPage
|
||||||
|
|
Loading…
Reference in a new issue