Improve CMS app UX & UI (#349)

* Install apps-shared, render null if app is loading in the iframe

Redesign the app

Add notification toasts and imrpve instructions

Change channels list to select

Replace lists with select

* fix build

* Fix build
This commit is contained in:
Lukasz Ostrowski 2023-04-06 12:56:44 +02:00 committed by GitHub
parent 3acc1553a8
commit 1da51639fa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 436 additions and 292 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-cms": minor
---
Redesigned the app to better match new Dashboard

View file

@ -14,6 +14,7 @@ module.exports = {
disableServerWebpackPlugin: !isSentryPropertiesInEnvironment,
disableClientWebpackPlugin: !isSentryPropertiesInEnvironment,
},
transpilePackages: ["@saleor/apps-shared"],
};
module.exports = withSentryConfig(module.exports, { silent: true }, { hideSourcemaps: true });

View file

@ -38,7 +38,8 @@
"usehooks-ts": "^2.9.1",
"uuid": "^9.0.0",
"vite": "^4.1.4",
"zod": "^3.19.1"
"zod": "^3.19.1",
"@saleor/apps-shared": "workspace:*"
},
"devDependencies": {
"@graphql-codegen/cli": "2.13.3",

View file

@ -1,14 +1,13 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { createClient } from "../../graphql";
import { createSettingsManager } from "../../metadata";
import { getOperationType } from "./operations";
import {
getChannelsSettings,
getProviderInstancesSettings,
getProductVariantProviderInstancesToAlter,
getProviderInstancesSettings,
} from "./settings";
import { providersSchemaSet } from "../config";
import { cmsProviders, CMSProvider } from "../providers";
import { CMSProvider, cmsProviders } from "../providers";
import { CmsClientOperations } from "../types";
import { logger as pinoLogger } from "../../logger";
import { getCmsIdFromSaleorItemKey } from "./metadata";

View file

@ -1,8 +1,6 @@
import { AuthData } from "@saleor/app-sdk/APL";
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import {
DeleteMetadataDocument,
ProductVariantUpdatedWebhookPayloadFragment,
UpdateMetadataDocument,
WebhookProductVariantFragment,
} from "../../../../generated/graphql";

View file

@ -1,6 +1,5 @@
import { EncryptedMetadataManager } from "@saleor/app-sdk/settings-manager";
import { CMSSchemaChannels, CMSSchemaProviderInstances } from "../config";
import { createCmsKeyForSaleorItem, getCmsIdFromSaleorItemKey } from "./metadata";
export const getChannelsSettings = async (settingsManager: EncryptedMetadataManager) => {
const channelsSettings = await settingsManager.get("channels");

View file

@ -1,4 +1,4 @@
import { MetadataEntry, EncryptedMetadataManager } from "@saleor/app-sdk/settings-manager";
import { EncryptedMetadataManager, MetadataEntry } from "@saleor/app-sdk/settings-manager";
import { Client } from "urql";
import {

View file

@ -1,18 +1,17 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Checkbox, FormControl, FormControlLabel, Switch, Typography } from "@material-ui/core";
import { Checkbox, FormControl, Typography } from "@material-ui/core";
import {
makeStyles,
Button,
List,
ListBody,
ListFooter,
ListHeader,
ListItem,
ListItemCell,
ListFooter,
Button,
makeStyles,
} from "@saleor/macaw-ui";
import React from "react";
import { Controller, useController, useForm } from "react-hook-form";
import { z } from "zod";
import { useForm } from "react-hook-form";
import {
channelSchema,
ChannelSchema,
@ -39,6 +38,10 @@ const useStyles = makeStyles((theme) => {
justifyContent: "flex-end",
padding: theme.spacing(2, 4),
},
form: {
border: `1px solid hsla(212, 44%, 13%, 0.08)`,
borderRadius: 8,
},
};
});
@ -87,7 +90,7 @@ export const ChannelConfigurationForm = ({
const errors = formState.errors;
return (
<form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
{!!Object.entries(errors).length && (
<Typography variant="body1" color="error">
Error validating form

View file

@ -1,10 +1,9 @@
import { AppPaper } from "../../ui/app-paper";
import { FormControlLabel, Grid, Paper, Radio, RadioGroup, Typography } from "@material-ui/core";
import { Grid, Paper, Typography } from "@material-ui/core";
import { Skeleton } from "@material-ui/lab";
import { ChannelConfigurationForm } from "./channel-configuration-form";
import {
MergedChannelSchema,
ProvidersSchema,
SingleChannelSchema,
SingleProviderSchema,
} from "../../../lib/cms/config";

View file

@ -1,5 +1,5 @@
import { makeStyles } from "@saleor/macaw-ui";
import {
makeStyles,
OffsettedList,
OffsettedListBody,
OffsettedListHeader,
@ -7,9 +7,7 @@ import {
OffsettedListItemCell,
} from "@saleor/macaw-ui";
import clsx from "clsx";
import { ChannelFragment } from "../../../../generated/graphql";
import { MergedChannelSchema, SingleChannelSchema } from "../../../lib/cms/config";
import { ProviderIcon } from "../../provider-instances/ui/provider-icon";
import { MergedChannelSchema } from "../../../lib/cms/config";
const useStyles = makeStyles((theme) => {
return {

View file

@ -1,25 +1,14 @@
import { Grid } from "@material-ui/core";
import { Skeleton } from "@material-ui/lab";
import { ChannelFragment } from "../../../../generated/graphql";
import { MergedChannelSchema, SingleChannelSchema } from "../../../lib/cms";
import { MergedChannelSchema } from "../../../lib/cms";
import { AppPaper } from "../../ui/app-paper";
import { ChannelsListItems } from "./channels-list-items";
import { ChannelsErrors, ChannelsLoading } from "./types";
import { ChannelsSelect } from "./channels-select";
const ChannelsListSkeleton = () => {
return (
<AppPaper>
<Grid container spacing={2}>
<Grid item xs={12}>
<Skeleton variant="rect" width={"45%"} height={10} />
</Grid>
<Grid item xs={12}>
<Skeleton variant="rect" width={"100%"} height={30} />
</Grid>
<Grid item xs={12}>
<Skeleton variant="rect" width={"100%"} height={30} />
</Grid>
</Grid>
</AppPaper>
);
};
@ -48,7 +37,7 @@ export const ChannelsList = ({
}
return (
<ChannelsListItems
<ChannelsSelect
channels={channels}
activeChannel={activeChannel}
setActiveChannel={setActiveChannel}

View file

@ -0,0 +1,41 @@
import { MergedChannelSchema } from "../../../lib/cms/config";
import { FormControl, InputLabel, MenuItem, Select, SelectProps } from "@material-ui/core";
interface ChannelsListItemsProps extends SelectProps {
channels: MergedChannelSchema[];
activeChannel?: MergedChannelSchema | null;
setActiveChannel: (channel: MergedChannelSchema | null) => void;
}
export const ChannelsSelect = ({
channels,
activeChannel,
setActiveChannel,
...props
}: ChannelsListItemsProps) => {
console.log(activeChannel);
return (
<FormControl>
<InputLabel id="channel-select">Select channel to configure</InputLabel>
<Select
labelId="channel-select"
{...props}
variant="outlined"
fullWidth
value={activeChannel?.channel.id}
onChange={(e, value) => {
console.log(e.target.value);
setActiveChannel(channels.find((c) => c.channel.id === e.target.value)!);
}}
>
{channels.map((c) => (
<MenuItem key={c.channel.id} value={c.channel.id}>
{c.channel.name}
</MenuItem>
))}
</Select>
</FormControl>
);
};

View file

@ -1,12 +1,22 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { MergedChannelSchema } from "../../../lib/cms/config";
import { useProviderInstances } from "../../provider-instances/ui/hooks/useProviderInstances";
import { Instructions } from "../../ui/instructions";
import { ChannelConfiguration } from "./channel-configuration";
import { ChannelsList } from "./channels-list";
import { useChannels } from "./hooks/useChannels";
import { AppTabs } from "../../ui/app-tabs";
import { makeStyles } from "@saleor/macaw-ui";
const useStyles = makeStyles({
wrapper: {
display: "flex",
flexDirection: "column",
gap: 16,
},
});
export const Channels = () => {
const styles = useStyles();
const { channels, saveChannel, loading, errors } = useChannels();
const { providerInstances } = useProviderInstances();
@ -20,8 +30,17 @@ export const Channels = () => {
const activeChannel = channels.find((channel) => channel.channelSlug === activeChannelSlug);
useEffect(() => {
if (!activeChannelSlug && channels.length > 0) {
setActiveChannelSlug(channels[0].channelSlug);
}
}, [channels]);
return (
<>
<AppTabs activeTab="channels" />
<div className={styles.wrapper}>
<ChannelsList
channels={channels}
activeChannel={activeChannel}
@ -36,7 +55,7 @@ export const Channels = () => {
loading={loading}
errors={errors}
/>
<Instructions />
</div>
</>
);
};

View file

@ -3,8 +3,10 @@ import { MergedChannelSchema, SingleChannelSchema } from "../../../../lib/cms/co
import { ChannelsErrors, ChannelsLoading } from "../types";
import { useChannelsQuery } from "../../../../../generated/graphql";
import { useIsMounted } from "usehooks-ts";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
export const useChannels = () => {
const { appBridge } = useAppBridge();
const isMounted = useIsMounted();
const [channelsQueryData, channelsQueryOpts] = useChannelsQuery({
pause: !isMounted,
@ -20,7 +22,15 @@ export const useChannels = () => {
const saveChannel = (channelToSave: SingleChannelSchema) => {
console.log("saveChannel", channelToSave);
saveChannelFetch(channelToSave);
saveChannelFetch(channelToSave).then(() => {
appBridge?.dispatch(
actions.Notification({
title: "Success",
status: "success",
text: "Configuration saved",
})
);
});
};
const loading: ChannelsLoading = {

View file

@ -1,10 +1,6 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import React from "react";
import {
CMSSchemaChannels,
cmsSchemaChannels,
SingleChannelSchema,
} from "../../../../lib/cms/config";
import { CMSSchemaChannels, SingleChannelSchema } from "../../../../lib/cms/config";
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
import { ChannelsApiResponse } from "../../../../pages/api/channels";

View file

@ -13,8 +13,6 @@ export const useProviderInstances = () => {
} = useProviderInstancesFetch();
const saveProviderInstance = async (providerInstanceToSave: SingleProviderSchema) => {
console.log("saveProviderInstance", providerInstanceToSave);
return await saveProviderInstanceFetch(providerInstanceToSave);
};

View file

@ -55,6 +55,8 @@ export const useProviderInstancesFetch = () => {
};
const saveProviderInstance = async (instance: SingleProviderSchema) => {
console.log(instance);
try {
setIsSaving(true);
const response = await fetch("/api/provider-instances", {

View file

@ -2,16 +2,13 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { Grid, TextField, Typography } from "@material-ui/core";
import { Button, makeStyles } from "@saleor/macaw-ui";
import React from "react";
import ReactMarkdown from "react-markdown";
import { Path, useForm } from "react-hook-form";
import { z } from "zod";
import {
providersConfig,
CMSProviderSchema,
providersSchemaSet,
providersConfig,
ProvidersSchema,
providersSchemaSet,
SingleProviderSchema,
ProviderInstanceSchema,
} from "../../../lib/cms/config";
import { Provider } from "../../providers/config";
import { AppMarkdownText } from "../../ui/app-markdown-text";
@ -68,7 +65,6 @@ export const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderS
});
if (providerInstance && providerInstance.providerName === provider.name) {
console.log(providerInstance);
reset(providerInstance as ProvidersSchema[TProvider]);
}
}, [provider, providerInstance]);

View file

@ -1,5 +1,5 @@
import { FormControlLabel, Grid, Radio, RadioGroup, Typography } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import { Button, makeStyles } from "@saleor/macaw-ui";
import Image from "next/image";
import React from "react";
import { CMSProviderSchema, providersConfig, SingleProviderSchema } from "../../../lib/cms/config";
@ -39,6 +39,18 @@ const useStyles = makeStyles((theme) => ({
textAlign: "center",
margin: theme.spacing(1, 0, 3, 0),
},
newProviderButton: {
margin: "30px auto",
},
newProviderContainer: {
display: "flex",
justifyContent: "center",
},
box: {
border: `1px solid hsla(212, 44%, 13%, 0.08)`,
borderRadius: 8,
padding: 20,
},
}));
const ProviderInstanceConfigurationSkeleton = () => {
@ -82,6 +94,7 @@ interface ProviderInstanceConfigurationProps {
deleteProviderInstance: (providerInstance: SingleProviderSchema) => any;
loading: ProvidersLoading;
errors: ProvidersErrors;
onNewProviderRequest(): void;
}
export const ProviderInstanceConfiguration = ({
@ -90,6 +103,7 @@ export const ProviderInstanceConfiguration = ({
saveProviderInstance,
deleteProviderInstance,
loading,
onNewProviderRequest,
errors,
}: ProviderInstanceConfigurationProps) => {
const [selectedProvider, setSelectedProvider] = React.useState<Provider | undefined>(
@ -120,12 +134,22 @@ export const ProviderInstanceConfiguration = ({
<Typography variant="body1" className={styles.textCenter}>
Please select a provider configuration or add new one.
</Typography>
<div className={styles.newProviderContainer}>
<Button
onClick={onNewProviderRequest}
variant="primary"
className={styles.newProviderButton}
>
Create a Provider config
</Button>
</div>
</AppPaper>
);
}
return (
<AppPaper>
<AppPaper className={styles.box}>
<div>
{errors.fetching && (
<Typography variant="body1" color="error">
Error fetching available providers
@ -154,7 +178,12 @@ export const ProviderInstanceConfiguration = ({
control={<Radio style={{ display: "none" }} name="provider" value={name} />}
label={
<div className={styles.iconWithLabel}>
<Image src={config.icon} alt={`${config.label} icon`} height={32} width={32} />
<Image
src={config.icon}
alt={`${config.label} icon`}
height={32}
width={32}
/>
<Typography variant="body1">{config.label}</Typography>
</div>
}
@ -184,6 +213,7 @@ export const ProviderInstanceConfiguration = ({
</Typography>
</>
)}
</div>
</AppPaper>
);
};

View file

@ -6,11 +6,9 @@ import {
OffsettedListItem,
OffsettedListItemCell,
} from "@saleor/macaw-ui";
import Image from "next/image";
import clsx from "clsx";
import React from "react";
import { SingleProviderSchema } from "../../../lib/cms/config";
import { getProviderByName } from "../../providers/config";
import { ProviderIcon } from "./provider-icon";
const useStyles = makeStyles((theme) => {

View file

@ -1,20 +1,9 @@
import { Grid } from "@material-ui/core";
import { Add } from "@material-ui/icons";
import { FormControl, Grid, InputLabel, MenuItem, Select } from "@material-ui/core";
import { Skeleton } from "@material-ui/lab";
import { Button, makeStyles } from "@saleor/macaw-ui";
import { SingleProviderSchema } from "../../../lib/cms/config";
import { AppPaper } from "../../ui/app-paper";
import { ProviderInstancesListItems, ProviderItem } from "./provider-instances-list-items";
import { ProvidersErrors, ProvidersLoading } from "./types";
const useStyles = makeStyles((theme) => {
return {
button: {
padding: theme.spacing(1, 2),
justifyContent: "flex-start",
},
};
});
import { ProvidersErrors, ProvidersLoading } from "./types";
const ProviderInstancesListSkeleton = () => {
return (
@ -44,7 +33,7 @@ interface ProviderInstancesListProps {
errors: ProvidersErrors;
}
export const ProviderInstancesList = ({
export const ProviderInstancesSelect = ({
providerInstances,
activeProviderInstance,
newProviderInstance,
@ -53,8 +42,6 @@ export const ProviderInstancesList = ({
loading,
errors,
}: ProviderInstancesListProps) => {
const styles = useStyles();
const handleSetActiveProviderInstance = (providerInstance: SingleProviderSchema) => {
setActiveProviderInstance(providerInstance);
};
@ -67,27 +54,29 @@ export const ProviderInstancesList = ({
return <div>Error loading providers</div>;
}
if (providerInstances.length === 0 || !activeProviderInstance) {
return null;
}
return (
<Grid container spacing={1}>
{!!providerInstances.length && (
<Grid item xs={12}>
<ProviderInstancesListItems
providerInstances={providerInstances}
activeProviderInstance={activeProviderInstance}
setActiveProviderInstance={handleSetActiveProviderInstance}
/>
</Grid>
)}
<Grid item xs={12}>
<Button
startIcon={<Add />}
className={styles.button}
<FormControl fullWidth>
<InputLabel id="provider-select">Select Provider to configure</InputLabel>
<Select
labelId="channel-select"
variant="outlined"
fullWidth
onClick={requestAddProviderInstance}
value={activeProviderInstance?.id}
onChange={(e, value) => {
handleSetActiveProviderInstance(providerInstances.find((p) => p.id === e.target.value)!);
}}
>
Add configuration
</Button>
</Grid>
</Grid>
{providerInstances.map((p) => (
<MenuItem key={p.id} value={p.id}>
{p.name}
</MenuItem>
))}
</Select>
</FormControl>
);
};

View file

@ -1,19 +1,36 @@
import { ProviderInstancesList } from "./provider-instances-list";
import { Instructions } from "../../ui/instructions";
import { ProviderInstanceConfiguration } from "./provider-instance-configuration";
import { providersConfig, ProvidersSchema, SingleProviderSchema } from "../../../lib/cms/config";
import { SingleProviderSchema } from "../../../lib/cms/config";
import { useEffect, useState } from "react";
import { useProviderInstances } from "./hooks/useProviderInstances";
import { AppTabs } from "../../ui/app-tabs";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { Button, makeStyles } from "@saleor/macaw-ui";
import { ProviderInstancesSelect } from "./provider-instances-list";
import { Add } from "@material-ui/icons";
const useStyles = makeStyles({
wrapper: {
display: "flex",
flexDirection: "column",
gap: 16,
},
});
export const ProviderInstances = () => {
const styles = useStyles();
const { appBridge } = useAppBridge();
const { providerInstances, saveProviderInstance, deleteProviderInstance, loading, errors } =
useProviderInstances();
const [activeProviderInstanceId, setActiveProviderInstanceId] = useState<string | null>(
providerInstances.length ? providerInstances[0].id : null
);
const [activeProviderInstanceId, setActiveProviderInstanceId] = useState<string | null>(null);
const [newProviderInstance, setNewProviderInstance] = useState<SingleProviderSchema | null>(null);
useEffect(() => {
if (providerInstances.length) {
setActiveProviderInstanceId(providerInstances[0].id);
}
}, [providerInstances]);
const handleSetActiveProviderInstance = (providerInstance: SingleProviderSchema | null) => {
setActiveProviderInstanceId(providerInstance?.id || null);
@ -31,6 +48,14 @@ export const ProviderInstances = () => {
const handleSaveProviderInstance = async (providerInstance: SingleProviderSchema) => {
const savedProviderInstance = await saveProviderInstance(providerInstance);
appBridge?.dispatch(
actions.Notification({
title: "Success",
status: "success",
text: "Configuration saved",
})
);
if (newProviderInstance) {
setNewProviderInstance(null);
}
@ -52,7 +77,11 @@ export const ProviderInstances = () => {
return (
<>
<ProviderInstancesList
<AppTabs activeTab="providers" />
<div className={styles.wrapper}>
{!newProviderInstance && (
<ProviderInstancesSelect
providerInstances={providerInstances}
activeProviderInstance={activeProviderInstance}
newProviderInstance={newProviderInstance}
@ -61,6 +90,7 @@ export const ProviderInstances = () => {
loading={loading}
errors={errors}
/>
)}
<ProviderInstanceConfiguration
activeProviderInstance={activeProviderInstance}
newProviderInstance={newProviderInstance}
@ -68,8 +98,21 @@ export const ProviderInstances = () => {
deleteProviderInstance={handleDeleteProviderInstance}
loading={loading}
errors={errors}
onNewProviderRequest={handleAddNewProviderInstance}
/>
<Instructions />
</div>
<div>
{providerInstances.length > 0 && (
<Button
size="medium"
startIcon={<Add />}
fullWidth
onClick={handleAddNewProviderInstance}
>
Add configuration
</Button>
)}
</div>
</>
);
};

View file

@ -1,4 +1,3 @@
import { z } from "zod";
import { ContentfulIcon, DatocmsIcon, StrapiIcon } from "../../assets";
export const CMS_ID_KEY = "cmsId";

View file

@ -2,8 +2,7 @@ import { makeStyles } from "@saleor/macaw-ui";
export const useStyles = makeStyles({
root: {
maxWidth: 1180,
margin: "0 auto",
margin: "12px auto",
},
});

View file

@ -4,7 +4,7 @@ import { PropsWithChildren } from "react";
export const useStyles = makeStyles({
root: {
display: "grid",
gridTemplateColumns: "280px auto 280px",
gridTemplateColumns: "280px minmax(400px, 600px) 280px",
alignItems: "start",
gap: 32,
},

View file

@ -1,6 +1,7 @@
import { Paper } from "@material-ui/core";
import { Paper, PaperProps } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import React from "react";
import clsx from "clsx";
const useStyles = makeStyles({
root: {
@ -8,10 +9,10 @@ const useStyles = makeStyles({
},
});
export const AppPaper = ({ children }: { children: React.ReactNode }) => {
export const AppPaper = ({ children, className, ...props }: PaperProps) => {
const styles = useStyles();
return (
<Paper elevation={0} className={styles.root}>
<Paper elevation={0} className={clsx(styles.root, className)} {...props}>
{children}
</Paper>
);

View file

@ -1,13 +1,34 @@
import { makeStyles, PageTab, PageTabs } from "@saleor/macaw-ui";
import { makeStyles } from "@saleor/macaw-ui";
import { useRouter } from "next/router";
import clsx from "clsx";
import { ButtonBase, Typography } from "@material-ui/core";
const useStyles = makeStyles({
tabs: {
margin: "16px 0",
display: "flex",
flexDirection: "column",
},
button: {
background: "#fff",
border: "none",
fontSize: 14,
height: 50,
textAlign: "left",
cursor: "pointer",
borderRadius: 8,
padding: "0 20px",
justifyContent: "flex-start",
},
active: {
border: `1px solid hsla(212, 14%, 77%, 1)`,
},
});
const tabs = {
home: {
label: "Home",
},
channels: {
label: "Channels",
},
@ -32,11 +53,20 @@ export const AppTabs = ({ activeTab }: AppTabsProps) => {
return (
<div className={styles.tabs}>
<PageTabs value={activeTab} onChange={handleTabChange}>
{Object.entries(tabs).map(([key, config]) => (
<PageTab key={key} value={key} label={config.label} />
<ButtonBase
disableRipple
className={clsx(styles.button, {
[styles.active]: activeTab === key,
})}
key={key}
onClick={() => {
handleTabChange(key);
}}
>
<Typography>{config.label}</Typography>
</ButtonBase>
))}
</PageTabs>
</div>
);
};

View file

@ -1,66 +0,0 @@
import { Typography } from "@material-ui/core";
import { makeStyles } from "@saleor/macaw-ui";
import { AppLink } from "./app-link";
import { AppPaper } from "./app-paper";
import { AppTabNavButton } from "./app-tab-nav-button";
const useStyles = makeStyles(() => ({
root: {
display: "flex",
flexDirection: "column",
gap: "16px",
},
list: {
paddingLeft: "16px",
margin: 0,
color: "inherit",
},
}));
export const Instructions = () => {
const styles = useStyles();
return (
<AppPaper>
<div className={styles.root}>
<Typography variant="h4">
Use external service for cms product data syncronization
</Typography>
<Typography variant="body1">
<ol className={styles.list}>
<li>
In the CMS App, go to the <AppTabNavButton to="providers">Providers</AppTabNavButton>{" "}
tab to add a configuration of your provider. Click <q>Add configuration</q>, and
select the cms provider you want to use. Fill in the configuration form and hit{" "}
<q>Save</q>.
</li>
<li>
Go to your CMS website and prepare product variant model shape with:
<ul>
<li>
string fields: <q>saleor_id</q>, <q>name</q>, <q>product_id</q>,{" "}
<q>product_name</q>, <q>product_slug</q>,
</li>
<li>
JSON fileds: <q>channels</q>.
</li>
</ul>
</li>
<li>
Go to the <AppTabNavButton to="channels">Channels</AppTabNavButton> tab. Select a
channel. Select the CMS configurations you want to sync product variants data against
available in this channel and hit <q>Save</q>.
</li>
<li>
Saleor will now use the channel&#39;s configured CMS provider for product variant
syncronisation once it is created, updated or deleted.
</li>
<li>
To see the effect, go to <AppLink href="/products">Products</AppLink>. Add, update or
delete channel listing for any product variant.
</li>
</ol>
</Typography>
</div>
</AppPaper>
);
};

View file

@ -13,7 +13,6 @@ import {
} from "../../../lib/cms/client";
import { logger as pinoLogger } from "../../../lib/logger";
import { createClient } from "../../../lib/graphql";
import { fetchProductVariantMetadata } from "../../../lib/metadata";
export const config = {
api: {

View file

@ -1,6 +1,4 @@
import { AppTabs } from "../modules/ui/app-tabs";
import { Channels } from "../modules/channels/ui/channels";
import { AppContainer } from "../modules/ui/app-container";
import { AppLayout } from "../modules/ui/app-layout";
import { NextPageWithLayout } from "./_app";
import { ReactElement } from "react";
@ -10,9 +8,6 @@ const Page: NextPageWithLayout = () => <Channels />;
Page.getLayout = function getLayout(page: ReactElement) {
return (
<main>
<AppContainer>
<AppTabs activeTab="channels" />
</AppContainer>
<AppLayout>{page}</AppLayout>
</main>
);

View file

@ -0,0 +1,78 @@
import { AppTabs } from "../modules/ui/app-tabs";
import { AppLayout } from "../modules/ui/app-layout";
import { NextPageWithLayout } from "./_app";
import { ReactElement } from "react";
import { AppTabNavButton } from "../modules/ui/app-tab-nav-button";
import { makeStyles } from "@saleor/macaw-ui";
import { Box, Typography } from "@material-ui/core";
const useStyles = makeStyles({
section: {
border: `1px solid hsla(330, 5%, 91%, 1)`, // todo macaw
padding: 20,
borderRadius: 8,
},
dataModelList: {
listStyle: "none",
margin: 0,
padding: 0,
"& li": {
margin: "1em 0",
},
},
});
const HomePage: NextPageWithLayout = () => {
const styles = useStyles();
return (
<>
<AppTabs activeTab="home" />
<div>
<Box>
<Typography variant="h3">Connect CMS</Typography>
<p>
Visit <AppTabNavButton to="providers">providers settings</AppTabNavButton> to configure
one of existing providers. Then, connect provider to each{" "}
<AppTabNavButton to="channels">channel</AppTabNavButton>.
</p>
</Box>
<div className={styles.section}>
<Typography variant="h4">CMS Data Model setup</Typography>
<p>CMS App requires your CMS data model to be configured with following structure:</p>
<ul className={styles.dataModelList}>
<li>
<code>saleor_id</code> - string (Variant ID in Saleor)
</li>
<li>
<code>name</code> - string (Variant name in Saleor)
</li>
<li>
<code>product_id</code> - string (Product ID in Saleor)
</li>
<li>
<code>product_name</code> - string (Product name in Saleor)
</li>
<li>
<code>product_slug</code> - string (Product slug in Saleor)
</li>
<li>
<code>channels</code> - JSON (list of channels product belongs to)
</li>
</ul>
</div>
</div>
</>
);
};
HomePage.getLayout = function getLayout(page: ReactElement) {
return (
<main>
<AppLayout>{page}</AppLayout>
</main>
);
};
export default HomePage;

View file

@ -1,9 +1,9 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { NextPage } from "next";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { useIsMounted } from "usehooks-ts";
import { isInIframe } from "@saleor/apps-shared";
/**
* This is page publicly accessible from your app.
@ -16,9 +16,13 @@ const IndexPage: NextPage = () => {
useEffect(() => {
if (isMounted() && appBridgeState?.ready) {
replace("/providers");
replace("/home");
}
}, [isMounted, appBridgeState?.ready, replace]);
if (isInIframe()) {
return null;
}
}, [isMounted, appBridgeState?.ready]);
return (
<div>

View file

@ -1,6 +1,4 @@
import { AppContainer } from "../modules/ui/app-container";
import { AppLayout } from "../modules/ui/app-layout";
import { AppTabs } from "../modules/ui/app-tabs";
import { ProviderInstances } from "../modules/provider-instances/ui/provider-instances";
import { NextPageWithLayout } from "./_app";
import { ReactElement } from "react";
@ -10,9 +8,6 @@ const Page: NextPageWithLayout = () => <ProviderInstances />;
Page.getLayout = function getLayout(page: ReactElement) {
return (
<main>
<AppContainer>
<AppTabs activeTab="providers" />
</AppContainer>
<AppLayout>{page}</AppLayout>
</main>
);

View file

@ -10,13 +10,7 @@ code {
border: 1px solid #eaeaea;
border-radius: 5px;
display: inline-block;
margin-top: 10px;
padding: 0.75rem;
padding: 0.2em 0.5em;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
code::before {
content: "$ ";
opacity: 0.6;
}

View file

@ -38,6 +38,7 @@ importers:
'@material-ui/icons': ^4.11.3
'@material-ui/lab': 4.0.0-alpha.61
'@saleor/app-sdk': 0.37.1
'@saleor/apps-shared': workspace:*
'@saleor/macaw-ui': ^0.6.7
'@sentry/nextjs': ^7.43.0
'@testing-library/react': ^13.4.0
@ -76,6 +77,7 @@ importers:
'@material-ui/icons': 4.11.3_x54wk6dsnsxe7g7vvfmytp77te
'@material-ui/lab': 4.0.0-alpha.61_x54wk6dsnsxe7g7vvfmytp77te
'@saleor/app-sdk': 0.37.1_ld2jel3hspngo3u5lti2kgl2sq
'@saleor/apps-shared': link:../../packages/shared
'@saleor/macaw-ui': 0.6.7_pmlnlm755hlzzzocw2qhf3a34e
'@sentry/nextjs': 7.43.0_next@13.2.4+react@18.2.0
'@urql/exchange-auth': 1.0.0_graphql@16.6.0