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:
parent
3acc1553a8
commit
1da51639fa
35 changed files with 436 additions and 292 deletions
5
.changeset/rare-crews-guess.md
Normal file
5
.changeset/rare-crews-guess.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
"saleor-app-cms": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Redesigned the app to better match new Dashboard
|
|
@ -14,6 +14,7 @@ module.exports = {
|
||||||
disableServerWebpackPlugin: !isSentryPropertiesInEnvironment,
|
disableServerWebpackPlugin: !isSentryPropertiesInEnvironment,
|
||||||
disableClientWebpackPlugin: !isSentryPropertiesInEnvironment,
|
disableClientWebpackPlugin: !isSentryPropertiesInEnvironment,
|
||||||
},
|
},
|
||||||
|
transpilePackages: ["@saleor/apps-shared"],
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = withSentryConfig(module.exports, { silent: true }, { hideSourcemaps: true });
|
module.exports = withSentryConfig(module.exports, { silent: true }, { hideSourcemaps: true });
|
||||||
|
|
|
@ -38,7 +38,8 @@
|
||||||
"usehooks-ts": "^2.9.1",
|
"usehooks-ts": "^2.9.1",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"vite": "^4.1.4",
|
"vite": "^4.1.4",
|
||||||
"zod": "^3.19.1"
|
"zod": "^3.19.1",
|
||||||
|
"@saleor/apps-shared": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "2.13.3",
|
"@graphql-codegen/cli": "2.13.3",
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||||
import { createClient } from "../../graphql";
|
|
||||||
import { createSettingsManager } from "../../metadata";
|
import { createSettingsManager } from "../../metadata";
|
||||||
import { getOperationType } from "./operations";
|
import { getOperationType } from "./operations";
|
||||||
import {
|
import {
|
||||||
getChannelsSettings,
|
getChannelsSettings,
|
||||||
getProviderInstancesSettings,
|
|
||||||
getProductVariantProviderInstancesToAlter,
|
getProductVariantProviderInstancesToAlter,
|
||||||
|
getProviderInstancesSettings,
|
||||||
} from "./settings";
|
} from "./settings";
|
||||||
import { providersSchemaSet } from "../config";
|
import { providersSchemaSet } from "../config";
|
||||||
import { cmsProviders, CMSProvider } from "../providers";
|
import { CMSProvider, cmsProviders } from "../providers";
|
||||||
import { CmsClientOperations } from "../types";
|
import { CmsClientOperations } from "../types";
|
||||||
import { logger as pinoLogger } from "../../logger";
|
import { logger as pinoLogger } from "../../logger";
|
||||||
import { getCmsIdFromSaleorItemKey } from "./metadata";
|
import { getCmsIdFromSaleorItemKey } from "./metadata";
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import { AuthData } from "@saleor/app-sdk/APL";
|
|
||||||
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||||
import {
|
import {
|
||||||
DeleteMetadataDocument,
|
DeleteMetadataDocument,
|
||||||
ProductVariantUpdatedWebhookPayloadFragment,
|
|
||||||
UpdateMetadataDocument,
|
UpdateMetadataDocument,
|
||||||
WebhookProductVariantFragment,
|
WebhookProductVariantFragment,
|
||||||
} from "../../../../generated/graphql";
|
} from "../../../../generated/graphql";
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { EncryptedMetadataManager } from "@saleor/app-sdk/settings-manager";
|
import { EncryptedMetadataManager } from "@saleor/app-sdk/settings-manager";
|
||||||
import { CMSSchemaChannels, CMSSchemaProviderInstances } from "../config";
|
import { CMSSchemaChannels, CMSSchemaProviderInstances } from "../config";
|
||||||
import { createCmsKeyForSaleorItem, getCmsIdFromSaleorItemKey } from "./metadata";
|
|
||||||
|
|
||||||
export const getChannelsSettings = async (settingsManager: EncryptedMetadataManager) => {
|
export const getChannelsSettings = async (settingsManager: EncryptedMetadataManager) => {
|
||||||
const channelsSettings = await settingsManager.get("channels");
|
const channelsSettings = await settingsManager.get("channels");
|
||||||
|
|
|
@ -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 { Client } from "urql";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
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 {
|
import {
|
||||||
makeStyles,
|
Button,
|
||||||
List,
|
List,
|
||||||
ListBody,
|
ListBody,
|
||||||
|
ListFooter,
|
||||||
ListHeader,
|
ListHeader,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemCell,
|
ListItemCell,
|
||||||
ListFooter,
|
makeStyles,
|
||||||
Button,
|
|
||||||
} from "@saleor/macaw-ui";
|
} from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Controller, useController, useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
import {
|
||||||
channelSchema,
|
channelSchema,
|
||||||
ChannelSchema,
|
ChannelSchema,
|
||||||
|
@ -39,6 +38,10 @@ const useStyles = makeStyles((theme) => {
|
||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
padding: theme.spacing(2, 4),
|
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;
|
const errors = formState.errors;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
|
||||||
{!!Object.entries(errors).length && (
|
{!!Object.entries(errors).length && (
|
||||||
<Typography variant="body1" color="error">
|
<Typography variant="body1" color="error">
|
||||||
Error validating form
|
Error validating form
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { AppPaper } from "../../ui/app-paper";
|
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 { Skeleton } from "@material-ui/lab";
|
||||||
import { ChannelConfigurationForm } from "./channel-configuration-form";
|
import { ChannelConfigurationForm } from "./channel-configuration-form";
|
||||||
import {
|
import {
|
||||||
MergedChannelSchema,
|
MergedChannelSchema,
|
||||||
ProvidersSchema,
|
|
||||||
SingleChannelSchema,
|
SingleChannelSchema,
|
||||||
SingleProviderSchema,
|
SingleProviderSchema,
|
||||||
} from "../../../lib/cms/config";
|
} from "../../../lib/cms/config";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import {
|
import {
|
||||||
|
makeStyles,
|
||||||
OffsettedList,
|
OffsettedList,
|
||||||
OffsettedListBody,
|
OffsettedListBody,
|
||||||
OffsettedListHeader,
|
OffsettedListHeader,
|
||||||
|
@ -7,9 +7,7 @@ import {
|
||||||
OffsettedListItemCell,
|
OffsettedListItemCell,
|
||||||
} from "@saleor/macaw-ui";
|
} from "@saleor/macaw-ui";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ChannelFragment } from "../../../../generated/graphql";
|
import { MergedChannelSchema } from "../../../lib/cms/config";
|
||||||
import { MergedChannelSchema, SingleChannelSchema } from "../../../lib/cms/config";
|
|
||||||
import { ProviderIcon } from "../../provider-instances/ui/provider-icon";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
const useStyles = makeStyles((theme) => {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,25 +1,14 @@
|
||||||
import { Grid } from "@material-ui/core";
|
|
||||||
import { Skeleton } from "@material-ui/lab";
|
import { Skeleton } from "@material-ui/lab";
|
||||||
import { ChannelFragment } from "../../../../generated/graphql";
|
import { MergedChannelSchema } from "../../../lib/cms";
|
||||||
import { MergedChannelSchema, SingleChannelSchema } from "../../../lib/cms";
|
|
||||||
import { AppPaper } from "../../ui/app-paper";
|
import { AppPaper } from "../../ui/app-paper";
|
||||||
import { ChannelsListItems } from "./channels-list-items";
|
|
||||||
import { ChannelsErrors, ChannelsLoading } from "./types";
|
import { ChannelsErrors, ChannelsLoading } from "./types";
|
||||||
|
import { ChannelsSelect } from "./channels-select";
|
||||||
|
|
||||||
const ChannelsListSkeleton = () => {
|
const ChannelsListSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
<AppPaper>
|
<AppPaper>
|
||||||
<Grid container spacing={2}>
|
<Skeleton variant="rect" width={"100%"} height={30} />
|
||||||
<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>
|
</AppPaper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -48,7 +37,7 @@ export const ChannelsList = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChannelsListItems
|
<ChannelsSelect
|
||||||
channels={channels}
|
channels={channels}
|
||||||
activeChannel={activeChannel}
|
activeChannel={activeChannel}
|
||||||
setActiveChannel={setActiveChannel}
|
setActiveChannel={setActiveChannel}
|
||||||
|
|
41
apps/cms/src/modules/channels/ui/channels-select.tsx
Normal file
41
apps/cms/src/modules/channels/ui/channels-select.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,12 +1,22 @@
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { MergedChannelSchema } from "../../../lib/cms/config";
|
import { MergedChannelSchema } from "../../../lib/cms/config";
|
||||||
import { useProviderInstances } from "../../provider-instances/ui/hooks/useProviderInstances";
|
import { useProviderInstances } from "../../provider-instances/ui/hooks/useProviderInstances";
|
||||||
import { Instructions } from "../../ui/instructions";
|
|
||||||
import { ChannelConfiguration } from "./channel-configuration";
|
import { ChannelConfiguration } from "./channel-configuration";
|
||||||
import { ChannelsList } from "./channels-list";
|
import { ChannelsList } from "./channels-list";
|
||||||
import { useChannels } from "./hooks/useChannels";
|
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 = () => {
|
export const Channels = () => {
|
||||||
|
const styles = useStyles();
|
||||||
const { channels, saveChannel, loading, errors } = useChannels();
|
const { channels, saveChannel, loading, errors } = useChannels();
|
||||||
const { providerInstances } = useProviderInstances();
|
const { providerInstances } = useProviderInstances();
|
||||||
|
|
||||||
|
@ -20,23 +30,32 @@ export const Channels = () => {
|
||||||
|
|
||||||
const activeChannel = channels.find((channel) => channel.channelSlug === activeChannelSlug);
|
const activeChannel = channels.find((channel) => channel.channelSlug === activeChannelSlug);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeChannelSlug && channels.length > 0) {
|
||||||
|
setActiveChannelSlug(channels[0].channelSlug);
|
||||||
|
}
|
||||||
|
}, [channels]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ChannelsList
|
<AppTabs activeTab="channels" />
|
||||||
channels={channels}
|
|
||||||
activeChannel={activeChannel}
|
<div className={styles.wrapper}>
|
||||||
setActiveChannel={handleSetActiveChannel}
|
<ChannelsList
|
||||||
loading={loading}
|
channels={channels}
|
||||||
errors={errors}
|
activeChannel={activeChannel}
|
||||||
/>
|
setActiveChannel={handleSetActiveChannel}
|
||||||
<ChannelConfiguration
|
loading={loading}
|
||||||
activeChannel={activeChannel}
|
errors={errors}
|
||||||
providerInstances={providerInstances}
|
/>
|
||||||
saveChannel={saveChannel}
|
<ChannelConfiguration
|
||||||
loading={loading}
|
activeChannel={activeChannel}
|
||||||
errors={errors}
|
providerInstances={providerInstances}
|
||||||
/>
|
saveChannel={saveChannel}
|
||||||
<Instructions />
|
loading={loading}
|
||||||
|
errors={errors}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,8 +3,10 @@ import { MergedChannelSchema, SingleChannelSchema } from "../../../../lib/cms/co
|
||||||
import { ChannelsErrors, ChannelsLoading } from "../types";
|
import { ChannelsErrors, ChannelsLoading } from "../types";
|
||||||
import { useChannelsQuery } from "../../../../../generated/graphql";
|
import { useChannelsQuery } from "../../../../../generated/graphql";
|
||||||
import { useIsMounted } from "usehooks-ts";
|
import { useIsMounted } from "usehooks-ts";
|
||||||
|
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
|
|
||||||
export const useChannels = () => {
|
export const useChannels = () => {
|
||||||
|
const { appBridge } = useAppBridge();
|
||||||
const isMounted = useIsMounted();
|
const isMounted = useIsMounted();
|
||||||
const [channelsQueryData, channelsQueryOpts] = useChannelsQuery({
|
const [channelsQueryData, channelsQueryOpts] = useChannelsQuery({
|
||||||
pause: !isMounted,
|
pause: !isMounted,
|
||||||
|
@ -20,7 +22,15 @@ export const useChannels = () => {
|
||||||
const saveChannel = (channelToSave: SingleChannelSchema) => {
|
const saveChannel = (channelToSave: SingleChannelSchema) => {
|
||||||
console.log("saveChannel", channelToSave);
|
console.log("saveChannel", channelToSave);
|
||||||
|
|
||||||
saveChannelFetch(channelToSave);
|
saveChannelFetch(channelToSave).then(() => {
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Notification({
|
||||||
|
title: "Success",
|
||||||
|
status: "success",
|
||||||
|
text: "Configuration saved",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loading: ChannelsLoading = {
|
const loading: ChannelsLoading = {
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import { CMSSchemaChannels, SingleChannelSchema } from "../../../../lib/cms/config";
|
||||||
CMSSchemaChannels,
|
|
||||||
cmsSchemaChannels,
|
|
||||||
SingleChannelSchema,
|
|
||||||
} from "../../../../lib/cms/config";
|
|
||||||
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
|
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
|
||||||
import { ChannelsApiResponse } from "../../../../pages/api/channels";
|
import { ChannelsApiResponse } from "../../../../pages/api/channels";
|
||||||
|
|
||||||
|
|
|
@ -13,8 +13,6 @@ export const useProviderInstances = () => {
|
||||||
} = useProviderInstancesFetch();
|
} = useProviderInstancesFetch();
|
||||||
|
|
||||||
const saveProviderInstance = async (providerInstanceToSave: SingleProviderSchema) => {
|
const saveProviderInstance = async (providerInstanceToSave: SingleProviderSchema) => {
|
||||||
console.log("saveProviderInstance", providerInstanceToSave);
|
|
||||||
|
|
||||||
return await saveProviderInstanceFetch(providerInstanceToSave);
|
return await saveProviderInstanceFetch(providerInstanceToSave);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,8 @@ export const useProviderInstancesFetch = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveProviderInstance = async (instance: SingleProviderSchema) => {
|
const saveProviderInstance = async (instance: SingleProviderSchema) => {
|
||||||
|
console.log(instance);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
const response = await fetch("/api/provider-instances", {
|
const response = await fetch("/api/provider-instances", {
|
||||||
|
|
|
@ -2,16 +2,13 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Grid, TextField, Typography } from "@material-ui/core";
|
import { Grid, TextField, Typography } from "@material-ui/core";
|
||||||
import { Button, makeStyles } from "@saleor/macaw-ui";
|
import { Button, makeStyles } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
import { Path, useForm } from "react-hook-form";
|
import { Path, useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
|
||||||
import {
|
import {
|
||||||
providersConfig,
|
|
||||||
CMSProviderSchema,
|
CMSProviderSchema,
|
||||||
providersSchemaSet,
|
providersConfig,
|
||||||
ProvidersSchema,
|
ProvidersSchema,
|
||||||
|
providersSchemaSet,
|
||||||
SingleProviderSchema,
|
SingleProviderSchema,
|
||||||
ProviderInstanceSchema,
|
|
||||||
} from "../../../lib/cms/config";
|
} from "../../../lib/cms/config";
|
||||||
import { Provider } from "../../providers/config";
|
import { Provider } from "../../providers/config";
|
||||||
import { AppMarkdownText } from "../../ui/app-markdown-text";
|
import { AppMarkdownText } from "../../ui/app-markdown-text";
|
||||||
|
@ -68,7 +65,6 @@ export const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderS
|
||||||
});
|
});
|
||||||
|
|
||||||
if (providerInstance && providerInstance.providerName === provider.name) {
|
if (providerInstance && providerInstance.providerName === provider.name) {
|
||||||
console.log(providerInstance);
|
|
||||||
reset(providerInstance as ProvidersSchema[TProvider]);
|
reset(providerInstance as ProvidersSchema[TProvider]);
|
||||||
}
|
}
|
||||||
}, [provider, providerInstance]);
|
}, [provider, providerInstance]);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { FormControlLabel, Grid, Radio, RadioGroup, Typography } from "@material-ui/core";
|
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 Image from "next/image";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { CMSProviderSchema, providersConfig, SingleProviderSchema } from "../../../lib/cms/config";
|
import { CMSProviderSchema, providersConfig, SingleProviderSchema } from "../../../lib/cms/config";
|
||||||
|
@ -39,6 +39,18 @@ const useStyles = makeStyles((theme) => ({
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
margin: theme.spacing(1, 0, 3, 0),
|
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 = () => {
|
const ProviderInstanceConfigurationSkeleton = () => {
|
||||||
|
@ -82,6 +94,7 @@ interface ProviderInstanceConfigurationProps {
|
||||||
deleteProviderInstance: (providerInstance: SingleProviderSchema) => any;
|
deleteProviderInstance: (providerInstance: SingleProviderSchema) => any;
|
||||||
loading: ProvidersLoading;
|
loading: ProvidersLoading;
|
||||||
errors: ProvidersErrors;
|
errors: ProvidersErrors;
|
||||||
|
onNewProviderRequest(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProviderInstanceConfiguration = ({
|
export const ProviderInstanceConfiguration = ({
|
||||||
|
@ -90,6 +103,7 @@ export const ProviderInstanceConfiguration = ({
|
||||||
saveProviderInstance,
|
saveProviderInstance,
|
||||||
deleteProviderInstance,
|
deleteProviderInstance,
|
||||||
loading,
|
loading,
|
||||||
|
onNewProviderRequest,
|
||||||
errors,
|
errors,
|
||||||
}: ProviderInstanceConfigurationProps) => {
|
}: ProviderInstanceConfigurationProps) => {
|
||||||
const [selectedProvider, setSelectedProvider] = React.useState<Provider | undefined>(
|
const [selectedProvider, setSelectedProvider] = React.useState<Provider | undefined>(
|
||||||
|
@ -120,70 +134,86 @@ export const ProviderInstanceConfiguration = ({
|
||||||
<Typography variant="body1" className={styles.textCenter}>
|
<Typography variant="body1" className={styles.textCenter}>
|
||||||
Please select a provider configuration or add new one.
|
Please select a provider configuration or add new one.
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<div className={styles.newProviderContainer}>
|
||||||
|
<Button
|
||||||
|
onClick={onNewProviderRequest}
|
||||||
|
variant="primary"
|
||||||
|
className={styles.newProviderButton}
|
||||||
|
>
|
||||||
|
Create a Provider config
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</AppPaper>
|
</AppPaper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppPaper>
|
<AppPaper className={styles.box}>
|
||||||
{errors.fetching && (
|
<div>
|
||||||
<Typography variant="body1" color="error">
|
{errors.fetching && (
|
||||||
Error fetching available providers
|
<Typography variant="body1" color="error">
|
||||||
</Typography>
|
Error fetching available providers
|
||||||
)}
|
|
||||||
{errors.saving && (
|
|
||||||
<Typography variant="body1" color="error">
|
|
||||||
Error saving provider configuration
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{!!newProviderInstance && (
|
|
||||||
<Typography variant="h3" className={styles.textHeader}>
|
|
||||||
Add new configuration
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
<RadioGroup value={selectedProvider?.name ?? ""} onChange={handleProviderChange}>
|
|
||||||
<Grid container justifyContent="center">
|
|
||||||
{Object.entries(providersConfig).map(([name, config]) => (
|
|
||||||
<Grid className={styles.gridItem} item xs={4} key={name}>
|
|
||||||
<FormControlLabel
|
|
||||||
className={
|
|
||||||
selectedProvider?.name === name
|
|
||||||
? `${styles.radioLabelActive} ${styles.radioLabel}`
|
|
||||||
: styles.radioLabel
|
|
||||||
}
|
|
||||||
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} />
|
|
||||||
<Typography variant="body1">{config.label}</Typography>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
labelPlacement="top"
|
|
||||||
aria-label={config.label}
|
|
||||||
/>
|
|
||||||
</Grid>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</RadioGroup>
|
|
||||||
{selectedProvider ? (
|
|
||||||
<>
|
|
||||||
<br />
|
|
||||||
<ProviderInstanceConfigurationForm
|
|
||||||
provider={selectedProvider}
|
|
||||||
providerInstance={activeProviderInstance}
|
|
||||||
loading={loading.saving}
|
|
||||||
onSubmit={saveProviderInstance}
|
|
||||||
onDelete={deleteProviderInstance}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<br />
|
|
||||||
<Typography variant="body1" className={styles.textCenter}>
|
|
||||||
Please select a provider.
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</>
|
)}
|
||||||
)}
|
{errors.saving && (
|
||||||
|
<Typography variant="body1" color="error">
|
||||||
|
Error saving provider configuration
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{!!newProviderInstance && (
|
||||||
|
<Typography variant="h3" className={styles.textHeader}>
|
||||||
|
Add new configuration
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<RadioGroup value={selectedProvider?.name ?? ""} onChange={handleProviderChange}>
|
||||||
|
<Grid container justifyContent="center">
|
||||||
|
{Object.entries(providersConfig).map(([name, config]) => (
|
||||||
|
<Grid className={styles.gridItem} item xs={4} key={name}>
|
||||||
|
<FormControlLabel
|
||||||
|
className={
|
||||||
|
selectedProvider?.name === name
|
||||||
|
? `${styles.radioLabelActive} ${styles.radioLabel}`
|
||||||
|
: styles.radioLabel
|
||||||
|
}
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
<Typography variant="body1">{config.label}</Typography>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
labelPlacement="top"
|
||||||
|
aria-label={config.label}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</RadioGroup>
|
||||||
|
{selectedProvider ? (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<ProviderInstanceConfigurationForm
|
||||||
|
provider={selectedProvider}
|
||||||
|
providerInstance={activeProviderInstance}
|
||||||
|
loading={loading.saving}
|
||||||
|
onSubmit={saveProviderInstance}
|
||||||
|
onDelete={deleteProviderInstance}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<Typography variant="body1" className={styles.textCenter}>
|
||||||
|
Please select a provider.
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</AppPaper>
|
</AppPaper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,11 +6,9 @@ import {
|
||||||
OffsettedListItem,
|
OffsettedListItem,
|
||||||
OffsettedListItemCell,
|
OffsettedListItemCell,
|
||||||
} from "@saleor/macaw-ui";
|
} from "@saleor/macaw-ui";
|
||||||
import Image from "next/image";
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { SingleProviderSchema } from "../../../lib/cms/config";
|
import { SingleProviderSchema } from "../../../lib/cms/config";
|
||||||
import { getProviderByName } from "../../providers/config";
|
|
||||||
import { ProviderIcon } from "./provider-icon";
|
import { ProviderIcon } from "./provider-icon";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
const useStyles = makeStyles((theme) => {
|
||||||
|
|
|
@ -1,20 +1,9 @@
|
||||||
import { Grid } from "@material-ui/core";
|
import { FormControl, Grid, InputLabel, MenuItem, Select } from "@material-ui/core";
|
||||||
import { Add } from "@material-ui/icons";
|
|
||||||
import { Skeleton } from "@material-ui/lab";
|
import { Skeleton } from "@material-ui/lab";
|
||||||
import { Button, makeStyles } from "@saleor/macaw-ui";
|
|
||||||
import { SingleProviderSchema } from "../../../lib/cms/config";
|
import { SingleProviderSchema } from "../../../lib/cms/config";
|
||||||
import { AppPaper } from "../../ui/app-paper";
|
import { AppPaper } from "../../ui/app-paper";
|
||||||
import { ProviderInstancesListItems, ProviderItem } from "./provider-instances-list-items";
|
|
||||||
import { ProvidersErrors, ProvidersLoading } from "./types";
|
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => {
|
import { ProvidersErrors, ProvidersLoading } from "./types";
|
||||||
return {
|
|
||||||
button: {
|
|
||||||
padding: theme.spacing(1, 2),
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const ProviderInstancesListSkeleton = () => {
|
const ProviderInstancesListSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -44,7 +33,7 @@ interface ProviderInstancesListProps {
|
||||||
errors: ProvidersErrors;
|
errors: ProvidersErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProviderInstancesList = ({
|
export const ProviderInstancesSelect = ({
|
||||||
providerInstances,
|
providerInstances,
|
||||||
activeProviderInstance,
|
activeProviderInstance,
|
||||||
newProviderInstance,
|
newProviderInstance,
|
||||||
|
@ -53,8 +42,6 @@ export const ProviderInstancesList = ({
|
||||||
loading,
|
loading,
|
||||||
errors,
|
errors,
|
||||||
}: ProviderInstancesListProps) => {
|
}: ProviderInstancesListProps) => {
|
||||||
const styles = useStyles();
|
|
||||||
|
|
||||||
const handleSetActiveProviderInstance = (providerInstance: SingleProviderSchema) => {
|
const handleSetActiveProviderInstance = (providerInstance: SingleProviderSchema) => {
|
||||||
setActiveProviderInstance(providerInstance);
|
setActiveProviderInstance(providerInstance);
|
||||||
};
|
};
|
||||||
|
@ -67,27 +54,29 @@ export const ProviderInstancesList = ({
|
||||||
return <div>Error loading providers</div>;
|
return <div>Error loading providers</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (providerInstances.length === 0 || !activeProviderInstance) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1}>
|
<FormControl fullWidth>
|
||||||
{!!providerInstances.length && (
|
<InputLabel id="provider-select">Select Provider to configure</InputLabel>
|
||||||
<Grid item xs={12}>
|
|
||||||
<ProviderInstancesListItems
|
<Select
|
||||||
providerInstances={providerInstances}
|
labelId="channel-select"
|
||||||
activeProviderInstance={activeProviderInstance}
|
variant="outlined"
|
||||||
setActiveProviderInstance={handleSetActiveProviderInstance}
|
fullWidth
|
||||||
/>
|
value={activeProviderInstance?.id}
|
||||||
</Grid>
|
onChange={(e, value) => {
|
||||||
)}
|
handleSetActiveProviderInstance(providerInstances.find((p) => p.id === e.target.value)!);
|
||||||
<Grid item xs={12}>
|
}}
|
||||||
<Button
|
>
|
||||||
startIcon={<Add />}
|
{providerInstances.map((p) => (
|
||||||
className={styles.button}
|
<MenuItem key={p.id} value={p.id}>
|
||||||
fullWidth
|
{p.name}
|
||||||
onClick={requestAddProviderInstance}
|
</MenuItem>
|
||||||
>
|
))}
|
||||||
Add configuration
|
</Select>
|
||||||
</Button>
|
</FormControl>
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,19 +1,36 @@
|
||||||
import { ProviderInstancesList } from "./provider-instances-list";
|
|
||||||
import { Instructions } from "../../ui/instructions";
|
|
||||||
import { ProviderInstanceConfiguration } from "./provider-instance-configuration";
|
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 { useEffect, useState } from "react";
|
||||||
import { useProviderInstances } from "./hooks/useProviderInstances";
|
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 = () => {
|
export const ProviderInstances = () => {
|
||||||
|
const styles = useStyles();
|
||||||
|
const { appBridge } = useAppBridge();
|
||||||
const { providerInstances, saveProviderInstance, deleteProviderInstance, loading, errors } =
|
const { providerInstances, saveProviderInstance, deleteProviderInstance, loading, errors } =
|
||||||
useProviderInstances();
|
useProviderInstances();
|
||||||
|
|
||||||
const [activeProviderInstanceId, setActiveProviderInstanceId] = useState<string | null>(
|
const [activeProviderInstanceId, setActiveProviderInstanceId] = useState<string | null>(null);
|
||||||
providerInstances.length ? providerInstances[0].id : null
|
|
||||||
);
|
|
||||||
const [newProviderInstance, setNewProviderInstance] = useState<SingleProviderSchema | null>(null);
|
const [newProviderInstance, setNewProviderInstance] = useState<SingleProviderSchema | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (providerInstances.length) {
|
||||||
|
setActiveProviderInstanceId(providerInstances[0].id);
|
||||||
|
}
|
||||||
|
}, [providerInstances]);
|
||||||
|
|
||||||
const handleSetActiveProviderInstance = (providerInstance: SingleProviderSchema | null) => {
|
const handleSetActiveProviderInstance = (providerInstance: SingleProviderSchema | null) => {
|
||||||
setActiveProviderInstanceId(providerInstance?.id || null);
|
setActiveProviderInstanceId(providerInstance?.id || null);
|
||||||
|
|
||||||
|
@ -31,6 +48,14 @@ export const ProviderInstances = () => {
|
||||||
const handleSaveProviderInstance = async (providerInstance: SingleProviderSchema) => {
|
const handleSaveProviderInstance = async (providerInstance: SingleProviderSchema) => {
|
||||||
const savedProviderInstance = await saveProviderInstance(providerInstance);
|
const savedProviderInstance = await saveProviderInstance(providerInstance);
|
||||||
|
|
||||||
|
appBridge?.dispatch(
|
||||||
|
actions.Notification({
|
||||||
|
title: "Success",
|
||||||
|
status: "success",
|
||||||
|
text: "Configuration saved",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (newProviderInstance) {
|
if (newProviderInstance) {
|
||||||
setNewProviderInstance(null);
|
setNewProviderInstance(null);
|
||||||
}
|
}
|
||||||
|
@ -52,24 +77,42 @@ export const ProviderInstances = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ProviderInstancesList
|
<AppTabs activeTab="providers" />
|
||||||
providerInstances={providerInstances}
|
|
||||||
activeProviderInstance={activeProviderInstance}
|
<div className={styles.wrapper}>
|
||||||
newProviderInstance={newProviderInstance}
|
{!newProviderInstance && (
|
||||||
setActiveProviderInstance={handleSetActiveProviderInstance}
|
<ProviderInstancesSelect
|
||||||
requestAddProviderInstance={handleAddNewProviderInstance}
|
providerInstances={providerInstances}
|
||||||
loading={loading}
|
activeProviderInstance={activeProviderInstance}
|
||||||
errors={errors}
|
newProviderInstance={newProviderInstance}
|
||||||
/>
|
setActiveProviderInstance={handleSetActiveProviderInstance}
|
||||||
<ProviderInstanceConfiguration
|
requestAddProviderInstance={handleAddNewProviderInstance}
|
||||||
activeProviderInstance={activeProviderInstance}
|
loading={loading}
|
||||||
newProviderInstance={newProviderInstance}
|
errors={errors}
|
||||||
saveProviderInstance={handleSaveProviderInstance}
|
/>
|
||||||
deleteProviderInstance={handleDeleteProviderInstance}
|
)}
|
||||||
loading={loading}
|
<ProviderInstanceConfiguration
|
||||||
errors={errors}
|
activeProviderInstance={activeProviderInstance}
|
||||||
/>
|
newProviderInstance={newProviderInstance}
|
||||||
<Instructions />
|
saveProviderInstance={handleSaveProviderInstance}
|
||||||
|
deleteProviderInstance={handleDeleteProviderInstance}
|
||||||
|
loading={loading}
|
||||||
|
errors={errors}
|
||||||
|
onNewProviderRequest={handleAddNewProviderInstance}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{providerInstances.length > 0 && (
|
||||||
|
<Button
|
||||||
|
size="medium"
|
||||||
|
startIcon={<Add />}
|
||||||
|
fullWidth
|
||||||
|
onClick={handleAddNewProviderInstance}
|
||||||
|
>
|
||||||
|
Add configuration
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { z } from "zod";
|
|
||||||
import { ContentfulIcon, DatocmsIcon, StrapiIcon } from "../../assets";
|
import { ContentfulIcon, DatocmsIcon, StrapiIcon } from "../../assets";
|
||||||
|
|
||||||
export const CMS_ID_KEY = "cmsId";
|
export const CMS_ID_KEY = "cmsId";
|
||||||
|
|
|
@ -2,8 +2,7 @@ import { makeStyles } from "@saleor/macaw-ui";
|
||||||
|
|
||||||
export const useStyles = makeStyles({
|
export const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
maxWidth: 1180,
|
margin: "12px auto",
|
||||||
margin: "0 auto",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { PropsWithChildren } from "react";
|
||||||
export const useStyles = makeStyles({
|
export const useStyles = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "280px auto 280px",
|
gridTemplateColumns: "280px minmax(400px, 600px) 280px",
|
||||||
alignItems: "start",
|
alignItems: "start",
|
||||||
gap: 32,
|
gap: 32,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Paper } from "@material-ui/core";
|
import { Paper, PaperProps } from "@material-ui/core";
|
||||||
import { makeStyles } from "@saleor/macaw-ui";
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
root: {
|
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();
|
const styles = useStyles();
|
||||||
return (
|
return (
|
||||||
<Paper elevation={0} className={styles.root}>
|
<Paper elevation={0} className={clsx(styles.root, className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,13 +1,34 @@
|
||||||
import { makeStyles, PageTab, PageTabs } from "@saleor/macaw-ui";
|
import { makeStyles } from "@saleor/macaw-ui";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { ButtonBase, Typography } from "@material-ui/core";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
tabs: {
|
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 = {
|
const tabs = {
|
||||||
|
home: {
|
||||||
|
label: "Home",
|
||||||
|
},
|
||||||
channels: {
|
channels: {
|
||||||
label: "Channels",
|
label: "Channels",
|
||||||
},
|
},
|
||||||
|
@ -32,11 +53,20 @@ export const AppTabs = ({ activeTab }: AppTabsProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.tabs}>
|
<div className={styles.tabs}>
|
||||||
<PageTabs value={activeTab} onChange={handleTabChange}>
|
{Object.entries(tabs).map(([key, config]) => (
|
||||||
{Object.entries(tabs).map(([key, config]) => (
|
<ButtonBase
|
||||||
<PageTab key={key} value={key} label={config.label} />
|
disableRipple
|
||||||
))}
|
className={clsx(styles.button, {
|
||||||
</PageTabs>
|
[styles.active]: activeTab === key,
|
||||||
|
})}
|
||||||
|
key={key}
|
||||||
|
onClick={() => {
|
||||||
|
handleTabChange(key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>{config.label}</Typography>
|
||||||
|
</ButtonBase>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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'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>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -13,7 +13,6 @@ import {
|
||||||
} from "../../../lib/cms/client";
|
} from "../../../lib/cms/client";
|
||||||
import { logger as pinoLogger } from "../../../lib/logger";
|
import { logger as pinoLogger } from "../../../lib/logger";
|
||||||
import { createClient } from "../../../lib/graphql";
|
import { createClient } from "../../../lib/graphql";
|
||||||
import { fetchProductVariantMetadata } from "../../../lib/metadata";
|
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { AppTabs } from "../modules/ui/app-tabs";
|
|
||||||
import { Channels } from "../modules/channels/ui/channels";
|
import { Channels } from "../modules/channels/ui/channels";
|
||||||
import { AppContainer } from "../modules/ui/app-container";
|
|
||||||
import { AppLayout } from "../modules/ui/app-layout";
|
import { AppLayout } from "../modules/ui/app-layout";
|
||||||
import { NextPageWithLayout } from "./_app";
|
import { NextPageWithLayout } from "./_app";
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
@ -10,9 +8,6 @@ const Page: NextPageWithLayout = () => <Channels />;
|
||||||
Page.getLayout = function getLayout(page: ReactElement) {
|
Page.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<AppContainer>
|
|
||||||
<AppTabs activeTab="channels" />
|
|
||||||
</AppContainer>
|
|
||||||
<AppLayout>{page}</AppLayout>
|
<AppLayout>{page}</AppLayout>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
78
apps/cms/src/pages/home.tsx
Normal file
78
apps/cms/src/pages/home.tsx
Normal 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;
|
|
@ -1,9 +1,9 @@
|
||||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { useIsMounted } from "usehooks-ts";
|
import { useIsMounted } from "usehooks-ts";
|
||||||
|
import { isInIframe } from "@saleor/apps-shared";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is page publicly accessible from your app.
|
* This is page publicly accessible from your app.
|
||||||
|
@ -16,9 +16,13 @@ const IndexPage: NextPage = () => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMounted() && appBridgeState?.ready) {
|
if (isMounted() && appBridgeState?.ready) {
|
||||||
replace("/providers");
|
replace("/home");
|
||||||
}
|
}
|
||||||
}, [isMounted, appBridgeState?.ready]);
|
}, [isMounted, appBridgeState?.ready, replace]);
|
||||||
|
|
||||||
|
if (isInIframe()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import { AppContainer } from "../modules/ui/app-container";
|
|
||||||
import { AppLayout } from "../modules/ui/app-layout";
|
import { AppLayout } from "../modules/ui/app-layout";
|
||||||
import { AppTabs } from "../modules/ui/app-tabs";
|
|
||||||
import { ProviderInstances } from "../modules/provider-instances/ui/provider-instances";
|
import { ProviderInstances } from "../modules/provider-instances/ui/provider-instances";
|
||||||
import { NextPageWithLayout } from "./_app";
|
import { NextPageWithLayout } from "./_app";
|
||||||
import { ReactElement } from "react";
|
import { ReactElement } from "react";
|
||||||
|
@ -10,9 +8,6 @@ const Page: NextPageWithLayout = () => <ProviderInstances />;
|
||||||
Page.getLayout = function getLayout(page: ReactElement) {
|
Page.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<AppContainer>
|
|
||||||
<AppTabs activeTab="providers" />
|
|
||||||
</AppContainer>
|
|
||||||
<AppLayout>{page}</AppLayout>
|
<AppLayout>{page}</AppLayout>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
|
@ -10,13 +10,7 @@ code {
|
||||||
border: 1px solid #eaeaea;
|
border: 1px solid #eaeaea;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 10px;
|
padding: 0.2em 0.5em;
|
||||||
padding: 0.75rem;
|
|
||||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
code::before {
|
|
||||||
content: "$ ";
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ importers:
|
||||||
'@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
|
||||||
'@saleor/app-sdk': 0.37.1
|
'@saleor/app-sdk': 0.37.1
|
||||||
|
'@saleor/apps-shared': workspace:*
|
||||||
'@saleor/macaw-ui': ^0.6.7
|
'@saleor/macaw-ui': ^0.6.7
|
||||||
'@sentry/nextjs': ^7.43.0
|
'@sentry/nextjs': ^7.43.0
|
||||||
'@testing-library/react': ^13.4.0
|
'@testing-library/react': ^13.4.0
|
||||||
|
@ -76,6 +77,7 @@ importers:
|
||||||
'@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
|
||||||
'@saleor/app-sdk': 0.37.1_ld2jel3hspngo3u5lti2kgl2sq
|
'@saleor/app-sdk': 0.37.1_ld2jel3hspngo3u5lti2kgl2sq
|
||||||
|
'@saleor/apps-shared': link:../../packages/shared
|
||||||
'@saleor/macaw-ui': 0.6.7_pmlnlm755hlzzzocw2qhf3a34e
|
'@saleor/macaw-ui': 0.6.7_pmlnlm755hlzzzocw2qhf3a34e
|
||||||
'@sentry/nextjs': 7.43.0_next@13.2.4+react@18.2.0
|
'@sentry/nextjs': 7.43.0_next@13.2.4+react@18.2.0
|
||||||
'@urql/exchange-auth': 1.0.0_graphql@16.6.0
|
'@urql/exchange-auth': 1.0.0_graphql@16.6.0
|
||||||
|
|
Loading…
Reference in a new issue