Add Google Category Mapping for the feed (#325)
* Add Google Category Mapping for the feed * Update the link href
This commit is contained in:
parent
5fad97c6d2
commit
f58043f72b
21 changed files with 14600 additions and 17 deletions
5
.changeset/small-parrots-listen.md
Normal file
5
.changeset/small-parrots-listen.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"saleor-app-products-feed": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add Google Products Category mapping
|
|
@ -0,0 +1,11 @@
|
||||||
|
fragment CategoryWithMappingFragment on Category {
|
||||||
|
id
|
||||||
|
parent{
|
||||||
|
name
|
||||||
|
parent{
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name
|
||||||
|
googleCategoryId: metafield(key: "google_category_id")
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ fragment GoogleFeedProductVariant on ProductVariant {
|
||||||
category {
|
category {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
googleCategoryId: metafield(key: "google_category_id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
mutation UpdateCategoryMapping($id: ID!, $googleCategoryId: String!) {
|
||||||
|
updateMetadata(id: $id, input: {key: "google_category_id", value: $googleCategoryId}) {
|
||||||
|
errors {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
query FetchCategoriesWithMapping{
|
||||||
|
categories(first: 10){
|
||||||
|
pageInfo{
|
||||||
|
hasNextPage
|
||||||
|
endCursor
|
||||||
|
}
|
||||||
|
edges{
|
||||||
|
node{
|
||||||
|
...CategoryWithMappingFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,7 @@
|
||||||
"schemaVersion": "3.10"
|
"schemaVersion": "3.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hookform/resolvers": "^2.9.10",
|
||||||
"@material-ui/core": "^4.12.4",
|
"@material-ui/core": "^4.12.4",
|
||||||
"@material-ui/icons": "^4.11.3",
|
"@material-ui/icons": "^4.11.3",
|
||||||
"@material-ui/lab": "4.0.0-alpha.61",
|
"@material-ui/lab": "4.0.0-alpha.61",
|
||||||
|
|
|
@ -39,8 +39,7 @@ export const generateGoogleXmlFeed = ({
|
||||||
description: v.product.seoDescription || v.product.description,
|
description: v.product.seoDescription || v.product.description,
|
||||||
availability: v.quantityAvailable && v.quantityAvailable > 0 ? "in_stock" : "out_of_stock",
|
availability: v.quantityAvailable && v.quantityAvailable > 0 ? "in_stock" : "out_of_stock",
|
||||||
category: v.product.category?.name || "unknown",
|
category: v.product.category?.name || "unknown",
|
||||||
// TODO: prepare category mapping for google categories
|
googleProductCategory: v.product.category?.googleCategoryId || "",
|
||||||
googleProductCategory: "",
|
|
||||||
price: price,
|
price: price,
|
||||||
imageUrl: v.product.thumbnail?.url || "",
|
imageUrl: v.product.thumbnail?.url || "",
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,19 +24,7 @@ export const productToProxy = (p: ProductEntry) => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* Consider implementing categories
|
|
||||||
* https://support.google.com/merchants/answer/6324436?hl=en
|
|
||||||
*
|
|
||||||
* However, this field is optional and google seems to automatically match category
|
|
||||||
*/
|
|
||||||
// {
|
|
||||||
// "g:google_product_category": [
|
|
||||||
// {
|
|
||||||
// "#text": p.googleProductCategory,
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
"g:availability": [
|
"g:availability": [
|
||||||
{
|
{
|
||||||
|
@ -63,6 +51,22 @@ export const productToProxy = (p: ProductEntry) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field is optional and Google automatically match category if not has been provided
|
||||||
|
*
|
||||||
|
* https://support.google.com/merchants/answer/6324436?hl=en
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (p.googleProductCategory?.length) {
|
||||||
|
item.push({
|
||||||
|
"g:google_product_category": [
|
||||||
|
{
|
||||||
|
"#text": p.googleProductCategory,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (p.storefrontUrlTemplate?.length) {
|
if (p.storefrontUrlTemplate?.length) {
|
||||||
item.push({
|
item.push({
|
||||||
link: [
|
link: [
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { AppColumnsLayout } from "../../ui/app-columns-layout";
|
||||||
import { FeedPreviewCard } from "./feed-preview-card";
|
import { FeedPreviewCard } from "./feed-preview-card";
|
||||||
import { Instructions } from "./instructions";
|
import { Instructions } from "./instructions";
|
||||||
import SideMenu from "./side-menu";
|
import SideMenu from "./side-menu";
|
||||||
|
import { CategoryMappingForm } from "../../category-mapping/ui/category-mapping-form";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
const useStyles = makeStyles((theme) => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const SetCategoryMappingInputSchema = z.object({
|
||||||
|
categoryId: z.string().min(0),
|
||||||
|
googleCategoryId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SetCategoryMappingInputType = z.infer<typeof SetCategoryMappingInputSchema>;
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { router } from "../trpc/trpc-server";
|
||||||
|
import { protectedClientProcedure } from "../trpc/protected-client-procedure";
|
||||||
|
import { logger as pinoLogger } from "../../lib/logger";
|
||||||
|
import { SetCategoryMappingInputSchema } from "./category-mapping-input-schema";
|
||||||
|
import {
|
||||||
|
FetchCategoriesWithMappingDocument,
|
||||||
|
UpdateCategoryMappingDocument,
|
||||||
|
} from "../../../generated/graphql";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
export const categoryMappingRouter = router({
|
||||||
|
/**
|
||||||
|
* Get all the category mappings to Google categories from its public metadata
|
||||||
|
*/
|
||||||
|
getCategoryMappings: protectedClientProcedure.query(async ({ ctx, input }) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
|
||||||
|
logger.debug("categoriesRouter.getCategoryMappings called");
|
||||||
|
|
||||||
|
const result = await ctx.apiClient.query(FetchCategoriesWithMappingDocument, {}).toPromise();
|
||||||
|
const categories = result.data?.categories?.edges?.map((edge) => edge.node) || [];
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
logger.error(`Error during the GraphqlAPI call: ${result.error.message}`);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Can't fetch the categories",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
}),
|
||||||
|
/**
|
||||||
|
* Sets the Google category mapping for a given category in its public metadata
|
||||||
|
*/
|
||||||
|
setCategoryMapping: protectedClientProcedure
|
||||||
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
|
.input(SetCategoryMappingInputSchema)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
logger.debug("categoriesRouter.setCategoryMapping called");
|
||||||
|
const { error } = await ctx.apiClient
|
||||||
|
.mutation(UpdateCategoryMappingDocument, {
|
||||||
|
id: input.categoryId,
|
||||||
|
googleCategoryId: input.googleCategoryId || "",
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.error(`Error during the GraphqlAPI call: ${error.message}`);
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
|
message: "Can't save the category",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}),
|
||||||
|
});
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,127 @@
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
Grid,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextFieldProps,
|
||||||
|
Typography,
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { Button, makeStyles } from "@saleor/macaw-ui";
|
||||||
|
import React from "react";
|
||||||
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import {
|
||||||
|
SetCategoryMappingInputSchema,
|
||||||
|
SetCategoryMappingInputType,
|
||||||
|
} from "../category-mapping-input-schema";
|
||||||
|
import { CategoryWithMappingFragmentFragment } from "../../../../generated/graphql";
|
||||||
|
import { GoogleProductCategories } from "../google-product-categories";
|
||||||
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
|
|
||||||
|
const useStyles = makeStyles({
|
||||||
|
field: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
channelName: {
|
||||||
|
fontFamily: "monospace",
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type CategoryMappingFormProps = {
|
||||||
|
category: CategoryWithMappingFragmentFragment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CategoryMappingForm = ({ category }: CategoryMappingFormProps) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { appBridge } = useAppBridge();
|
||||||
|
|
||||||
|
const { control, handleSubmit, formState } = useForm<SetCategoryMappingInputType>({
|
||||||
|
defaultValues: {
|
||||||
|
categoryId: category.id,
|
||||||
|
googleCategoryId: category.googleCategoryId || undefined,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(SetCategoryMappingInputSchema),
|
||||||
|
});
|
||||||
|
const { mutate, isLoading } = trpcClient.categoryMapping.setCategoryMapping.useMutation({
|
||||||
|
onError() {
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Notification({
|
||||||
|
title: "Could not save the category mapping",
|
||||||
|
status: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onSuccess() {
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Notification({
|
||||||
|
title: "Success",
|
||||||
|
status: "success",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const categoryBreadcrumbs = [category.parent?.parent?.name, category.parent?.name, category.name]
|
||||||
|
.filter((segment) => segment)
|
||||||
|
.join(" > ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit((data, event) => {
|
||||||
|
mutate(data);
|
||||||
|
})}
|
||||||
|
className={styles.form}
|
||||||
|
>
|
||||||
|
<Typography variant="h4" paragraph>
|
||||||
|
{categoryBreadcrumbs}
|
||||||
|
</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={10}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="googleCategoryId"
|
||||||
|
render={({ field: { value, onChange } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl className={styles.field} fullWidth>
|
||||||
|
<InputLabel>Google Product Category</InputLabel>
|
||||||
|
<Select
|
||||||
|
variant="outlined"
|
||||||
|
value={value}
|
||||||
|
onChange={(event, val) => {
|
||||||
|
onChange(event.target.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem key="none" value={undefined}>
|
||||||
|
No configuration
|
||||||
|
</MenuItem>
|
||||||
|
{GoogleProductCategories.map((choice) => (
|
||||||
|
<MenuItem key={choice.id} value={choice.id.toString()}>
|
||||||
|
{choice.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={2}>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
fullWidth
|
||||||
|
variant="primary"
|
||||||
|
disabled={isLoading || !formState.isDirty}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { trpcClient } from "../../trpc/trpc-client";
|
||||||
|
import { Paper } from "@material-ui/core";
|
||||||
|
import React from "react";
|
||||||
|
import { CategoryMappingForm } from "./category-mapping-form";
|
||||||
|
|
||||||
|
export const CategoryMapping = () => {
|
||||||
|
const categories = trpcClient.categoryMapping.getCategoryMappings.useQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper elevation={0}>
|
||||||
|
{categories.data?.length
|
||||||
|
? categories.data.map((category) => (
|
||||||
|
<CategoryMappingForm category={category} key={category.id} />
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Link, Paper, Typography } from "@material-ui/core";
|
||||||
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => {
|
||||||
|
return {
|
||||||
|
instructionsContainer: {
|
||||||
|
padding: 15,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Instructions = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const { appBridge } = useAppBridge();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Paper elevation={0} className={styles.instructionsContainer}>
|
||||||
|
<Typography paragraph variant="h4">
|
||||||
|
Using the feed
|
||||||
|
</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
Configure your Google Merchant account to schedule fetches of the feed. Instructions can be
|
||||||
|
found{" "}
|
||||||
|
<Link
|
||||||
|
href="https://support.google.com/merchants/answer/1219255"
|
||||||
|
onClick={() => {
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Redirect({
|
||||||
|
to: "https://support.google.com/merchants/answer/1219255",
|
||||||
|
newContext: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<Typography paragraph variant="h4">
|
||||||
|
URL templates
|
||||||
|
</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
URLs to products in your storefront are generated dynamically, based on the product data.
|
||||||
|
For example, the template
|
||||||
|
</Typography>
|
||||||
|
<code>{"https://example.com/product/{productSlug}"}</code>
|
||||||
|
<Typography paragraph>Will produce</Typography>
|
||||||
|
<code>{"https://example.com/product/red-t-shirt"}</code>
|
||||||
|
<Typography paragraph>Available fields: productId, productSlug, variantId</Typography>
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,10 +1,12 @@
|
||||||
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 { appConfigurationRouter } from "../app-configuration/app-configuration.router";
|
||||||
|
import { categoryMappingRouter } from "../category-mapping/category-mapping.router";
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
channels: channelsRouter,
|
channels: channelsRouter,
|
||||||
appConfiguration: appConfigurationRouter,
|
appConfiguration: appConfigurationRouter,
|
||||||
|
categoryMapping: categoryMappingRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { PropsWithChildren } from "react";
|
||||||
|
import { makeStyles, PageTab, PageTabs } from "@saleor/macaw-ui";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
appContainer: {
|
||||||
|
marginTop: theme.spacing(3),
|
||||||
|
marginLeft: theme.spacing(3),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
type Props = PropsWithChildren<{}>;
|
||||||
|
|
||||||
|
export const ConfigurationPageBaseLayout = ({ children }: Props) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
key: "channels",
|
||||||
|
label: "Channels",
|
||||||
|
url: "/configuration",
|
||||||
|
},
|
||||||
|
{ key: "categories", label: "Category mapping", url: "/categories" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const activePath = tabs.find((tab) => router.pathname === tab.url);
|
||||||
|
|
||||||
|
const navigateToTab = (value: string) => {
|
||||||
|
const redirectionUrl = tabs.find((tab) => tab.key === value)?.url;
|
||||||
|
if (redirectionUrl) {
|
||||||
|
router.push(redirectionUrl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={styles.appContainer}>
|
||||||
|
<PageTabs value={activePath} onChange={navigateToTab}>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<PageTab key={tab.key} value={tab.key} label={tab.label} />
|
||||||
|
))}
|
||||||
|
</PageTabs>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -9,7 +9,7 @@ export default createManifestHandler({
|
||||||
name: "Product Feed",
|
name: "Product Feed",
|
||||||
tokenTargetUrl: `${context.appBaseUrl}/api/register`,
|
tokenTargetUrl: `${context.appBaseUrl}/api/register`,
|
||||||
appUrl: context.appBaseUrl,
|
appUrl: context.appBaseUrl,
|
||||||
permissions: [],
|
permissions: ["MANAGE_PRODUCTS"],
|
||||||
id: "saleor.app.products-feed",
|
id: "saleor.app.products-feed",
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
webhooks: [],
|
webhooks: [],
|
||||||
|
|
25
apps/products-feed/src/pages/categories.tsx
Normal file
25
apps/products-feed/src/pages/categories.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { trpcClient } from "../modules/trpc/trpc-client";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { ConfigurationPageBaseLayout } from "../modules/ui/configuration-page-base-layout";
|
||||||
|
import { CategoryMapping } from "../modules/category-mapping/ui/category-mapping";
|
||||||
|
|
||||||
|
const ConfigurationPage: NextPage = () => {
|
||||||
|
const channels = trpcClient.channels.fetch.useQuery();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (channels.isSuccess && channels.data.length === 0) {
|
||||||
|
router.push("/not-ready");
|
||||||
|
}
|
||||||
|
}, [channels.data, channels.isSuccess]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigurationPageBaseLayout>
|
||||||
|
<CategoryMapping />
|
||||||
|
</ConfigurationPageBaseLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigurationPage;
|
|
@ -3,6 +3,7 @@ import React, { useEffect } from "react";
|
||||||
import { ChannelsConfiguration } from "../modules/app-configuration/ui/channels-configuration";
|
import { ChannelsConfiguration } from "../modules/app-configuration/ui/channels-configuration";
|
||||||
import { trpcClient } from "../modules/trpc/trpc-client";
|
import { trpcClient } from "../modules/trpc/trpc-client";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { ConfigurationPageBaseLayout } from "../modules/ui/configuration-page-base-layout";
|
||||||
|
|
||||||
const ConfigurationPage: NextPage = () => {
|
const ConfigurationPage: NextPage = () => {
|
||||||
const channels = trpcClient.channels.fetch.useQuery();
|
const channels = trpcClient.channels.fetch.useQuery();
|
||||||
|
@ -14,7 +15,11 @@ const ConfigurationPage: NextPage = () => {
|
||||||
}
|
}
|
||||||
}, [channels.data, channels.isSuccess]);
|
}, [channels.data, channels.isSuccess]);
|
||||||
|
|
||||||
return <ChannelsConfiguration />;
|
return (
|
||||||
|
<ConfigurationPageBaseLayout>
|
||||||
|
<ChannelsConfiguration />
|
||||||
|
</ConfigurationPageBaseLayout>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ConfigurationPage;
|
export default ConfigurationPage;
|
||||||
|
|
|
@ -619,6 +619,7 @@ importers:
|
||||||
'@graphql-codegen/typescript-urql': ^3.7.3
|
'@graphql-codegen/typescript-urql': ^3.7.3
|
||||||
'@graphql-codegen/urql-introspection': 2.2.1
|
'@graphql-codegen/urql-introspection': 2.2.1
|
||||||
'@graphql-typed-document-node/core': ^3.1.2
|
'@graphql-typed-document-node/core': ^3.1.2
|
||||||
|
'@hookform/resolvers': ^2.9.10
|
||||||
'@material-ui/core': ^4.12.4
|
'@material-ui/core': ^4.12.4
|
||||||
'@material-ui/icons': ^4.11.3
|
'@material-ui/icons': ^4.11.3
|
||||||
'@material-ui/lab': 4.0.0-alpha.61
|
'@material-ui/lab': 4.0.0-alpha.61
|
||||||
|
@ -662,6 +663,7 @@ importers:
|
||||||
vitest: ^0.27.1
|
vitest: ^0.27.1
|
||||||
zod: ^3.20.2
|
zod: ^3.20.2
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@hookform/resolvers': 2.9.11_react-hook-form@7.43.1
|
||||||
'@material-ui/core': 4.12.4_5ndqzdd6t4rivxsukjv3i3ak2q
|
'@material-ui/core': 4.12.4_5ndqzdd6t4rivxsukjv3i3ak2q
|
||||||
'@material-ui/icons': 4.11.3_x54wk6dsnsxe7g7vvfmytp77te
|
'@material-ui/icons': 4.11.3_x54wk6dsnsxe7g7vvfmytp77te
|
||||||
'@material-ui/lab': 4.0.0-alpha.61_x54wk6dsnsxe7g7vvfmytp77te
|
'@material-ui/lab': 4.0.0-alpha.61_x54wk6dsnsxe7g7vvfmytp77te
|
||||||
|
|
Loading…
Reference in a new issue