CMS UI and webhook updates (#319)
* CMS UI updates * Fix/improve verification against existing product variant in cms providers * Mock urql client in tests * Mark optional text with asterisk
This commit is contained in:
parent
8f2d5e8960
commit
84df6acf9c
15 changed files with 137 additions and 37 deletions
|
@ -0,0 +1,9 @@
|
||||||
|
query FetchProductVariantMetadata($id: ID!) {
|
||||||
|
productVariant(id: $id) {
|
||||||
|
id
|
||||||
|
metadata {
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||||
|
import { type Client } from "urql";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { CMSSchemaChannels, CMSSchemaProviderInstances } from "../config";
|
import { CMSSchemaChannels, CMSSchemaProviderInstances } from "../config";
|
||||||
import { CmsClientOperations } from "../types";
|
import { CmsClientOperations } from "../types";
|
||||||
|
@ -18,6 +19,8 @@ const mockedContext: Pick<WebhookContext, "authData"> = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createMockedClient = () => ({} as Client);
|
||||||
|
|
||||||
vi.mock("../../metadata", () => ({
|
vi.mock("../../metadata", () => ({
|
||||||
createSettingsManager: () => ({}),
|
createSettingsManager: () => ({}),
|
||||||
}));
|
}));
|
||||||
|
@ -37,6 +40,7 @@ describe("CMS Clients Operations", () => {
|
||||||
|
|
||||||
const cmsOperations = await createCmsOperations({
|
const cmsOperations = await createCmsOperations({
|
||||||
context: mockedContext,
|
context: mockedContext,
|
||||||
|
client: createMockedClient(),
|
||||||
productVariantChannels: [],
|
productVariantChannels: [],
|
||||||
productVariantCmsKeys: [],
|
productVariantCmsKeys: [],
|
||||||
});
|
});
|
||||||
|
@ -99,6 +103,7 @@ describe("CMS Clients Operations", () => {
|
||||||
|
|
||||||
const cmsOperations = await createCmsOperations({
|
const cmsOperations = await createCmsOperations({
|
||||||
context: mockedContext,
|
context: mockedContext,
|
||||||
|
client: createMockedClient(),
|
||||||
productVariantChannels: ["default-channel"],
|
productVariantChannels: ["default-channel"],
|
||||||
productVariantCmsKeys: [],
|
productVariantCmsKeys: [],
|
||||||
});
|
});
|
||||||
|
@ -171,6 +176,7 @@ describe("CMS Clients Operations", () => {
|
||||||
|
|
||||||
const cmsOperations = await createCmsOperations({
|
const cmsOperations = await createCmsOperations({
|
||||||
context: mockedContext,
|
context: mockedContext,
|
||||||
|
client: createMockedClient(),
|
||||||
productVariantChannels: ["default-channel"],
|
productVariantChannels: ["default-channel"],
|
||||||
productVariantCmsKeys: [createCmsKeyForSaleorItem("first-provider")],
|
productVariantCmsKeys: [createCmsKeyForSaleorItem("first-provider")],
|
||||||
});
|
});
|
||||||
|
@ -243,6 +249,7 @@ describe("CMS Clients Operations", () => {
|
||||||
|
|
||||||
const cmsOperations = await createCmsOperations({
|
const cmsOperations = await createCmsOperations({
|
||||||
context: mockedContext,
|
context: mockedContext,
|
||||||
|
client: createMockedClient(),
|
||||||
productVariantChannels: [],
|
productVariantChannels: [],
|
||||||
productVariantCmsKeys: [createCmsKeyForSaleorItem("first-provider")],
|
productVariantCmsKeys: [createCmsKeyForSaleorItem("first-provider")],
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,15 +12,18 @@ import cmsProviders, { CMSProvider } 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";
|
||||||
|
import { type Client } from "urql";
|
||||||
|
|
||||||
type WebhookContext = Parameters<NextWebhookApiHandler>["2"];
|
type WebhookContext = Parameters<NextWebhookApiHandler>["2"];
|
||||||
|
|
||||||
export const createCmsOperations = async ({
|
export const createCmsOperations = async ({
|
||||||
context,
|
context,
|
||||||
|
client,
|
||||||
productVariantChannels,
|
productVariantChannels,
|
||||||
productVariantCmsKeys,
|
productVariantCmsKeys,
|
||||||
}: {
|
}: {
|
||||||
context: Pick<WebhookContext, "authData">;
|
context: Pick<WebhookContext, "authData">;
|
||||||
|
client: Client;
|
||||||
productVariantChannels: string[];
|
productVariantChannels: string[];
|
||||||
productVariantCmsKeys: string[];
|
productVariantCmsKeys: string[];
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -29,13 +32,6 @@ export const createCmsOperations = async ({
|
||||||
productVariantCmsKeys,
|
productVariantCmsKeys,
|
||||||
});
|
});
|
||||||
|
|
||||||
const saleorApiUrl = context.authData.saleorApiUrl;
|
|
||||||
const token = context.authData.token;
|
|
||||||
|
|
||||||
const client = createClient(saleorApiUrl, async () => ({
|
|
||||||
token: token,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const settingsManager = createSettingsManager(client);
|
const settingsManager = createSettingsManager(client);
|
||||||
|
|
||||||
const channelsSettingsParsed = await getChannelsSettings(settingsManager);
|
const channelsSettingsParsed = await getChannelsSettings(settingsManager);
|
||||||
|
|
|
@ -7,6 +7,7 @@ type ProviderToken = {
|
||||||
label: string;
|
label: string;
|
||||||
helpText: string;
|
helpText: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
secret?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProviderConfig = {
|
type ProviderConfig = {
|
||||||
|
@ -26,6 +27,7 @@ export const providersConfig = {
|
||||||
tokens: [
|
tokens: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
|
secret: true,
|
||||||
name: "token",
|
name: "token",
|
||||||
label: "Token",
|
label: "Token",
|
||||||
helpText:
|
helpText:
|
||||||
|
@ -63,7 +65,7 @@ export const providersConfig = {
|
||||||
name: "baseUrl",
|
name: "baseUrl",
|
||||||
label: "Base URL",
|
label: "Base URL",
|
||||||
helpText:
|
helpText:
|
||||||
"Optional content management API URL of your Contentful project. If you leave this blank, default https://api.contentful.com will be used.",
|
"Content management API URL of your Contentful project. If you leave this blank, default https://api.contentful.com will be used.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -80,6 +82,7 @@ export const providersConfig = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
|
secret: true,
|
||||||
name: "token",
|
name: "token",
|
||||||
label: "API Token (with full access)",
|
label: "API Token (with full access)",
|
||||||
helpText:
|
helpText:
|
||||||
|
@ -88,7 +91,7 @@ export const providersConfig = {
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
name: "contentTypeId",
|
name: "contentTypeId",
|
||||||
label: "Content Type ID",
|
label: "Content Type ID (plural)",
|
||||||
helpText:
|
helpText:
|
||||||
'You can find this in your Strapi project, go to Content-Type Builder > select content type > click Edit > use API ID (Plural). More instructions at [Strapi "Editing content types" documentation](https://docs.strapi.io/user-docs/content-type-builder/managing-content-types#editing-content-types).',
|
'You can find this in your Strapi project, go to Content-Type Builder > select content type > click Edit > use API ID (Plural). More instructions at [Strapi "Editing content types" documentation](https://docs.strapi.io/user-docs/content-type-builder/managing-content-types#editing-content-types).',
|
||||||
},
|
},
|
||||||
|
@ -101,6 +104,7 @@ export const providersConfig = {
|
||||||
tokens: [
|
tokens: [
|
||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
|
secret: true,
|
||||||
name: "token",
|
name: "token",
|
||||||
label: "API Token (with access to Content Management API)",
|
label: "API Token (with access to Content Management API)",
|
||||||
helpText:
|
helpText:
|
||||||
|
@ -117,13 +121,13 @@ export const providersConfig = {
|
||||||
name: "baseUrl",
|
name: "baseUrl",
|
||||||
label: "Base URL",
|
label: "Base URL",
|
||||||
helpText:
|
helpText:
|
||||||
"Optional URL to your DatoCMS project. If you leave this blank, this URL will be inferred from your API Token.",
|
"URL to your DatoCMS project. If you leave this blank, this URL will be inferred from your API Token.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "environment",
|
name: "environment",
|
||||||
label: "Environment",
|
label: "Environment",
|
||||||
helpText:
|
helpText:
|
||||||
"Optional environment name. If you leave this blank, default environment will be used. You can find this in your DatoCMS project settings.",
|
"Environment name. If you leave this blank, default environment will be used. You can find this in your DatoCMS project settings.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,20 +4,27 @@ import { Client } from "urql";
|
||||||
import {
|
import {
|
||||||
FetchAppDetailsDocument,
|
FetchAppDetailsDocument,
|
||||||
FetchAppDetailsQuery,
|
FetchAppDetailsQuery,
|
||||||
|
FetchProductVariantMetadataDocument,
|
||||||
|
FetchProductVariantMetadataQuery,
|
||||||
UpdateAppMetadataDocument,
|
UpdateAppMetadataDocument,
|
||||||
} from "../../generated/graphql";
|
} from "../../generated/graphql";
|
||||||
|
import { logger as pinoLogger } from "../lib/logger";
|
||||||
|
|
||||||
// Function is using urql graphql client to fetch all available metadata.
|
// Function is using urql graphql client to fetch all available metadata.
|
||||||
// Before returning query result, we are transforming response to list of objects with key and value fields
|
// Before returning query result, we are transforming response to list of objects with key and value fields
|
||||||
// which can be used by the manager.
|
// which can be used by the manager.
|
||||||
// Result of this query is cached by the manager.
|
// Result of this query is cached by the manager.
|
||||||
export async function fetchAllMetadata(client: Client): Promise<MetadataEntry[]> {
|
export async function fetchAllMetadata(client: Client): Promise<MetadataEntry[]> {
|
||||||
|
const logger = pinoLogger.child({
|
||||||
|
function: "fetchAllMetadata",
|
||||||
|
});
|
||||||
|
|
||||||
const { error, data } = await client
|
const { error, data } = await client
|
||||||
.query<FetchAppDetailsQuery>(FetchAppDetailsDocument, {})
|
.query<FetchAppDetailsQuery>(FetchAppDetailsDocument, {})
|
||||||
.toPromise();
|
.toPromise();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.debug("Error during fetching the metadata: ", error);
|
logger.debug("Error during fetching the metadata", error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,13 +35,17 @@ export async function fetchAllMetadata(client: Client): Promise<MetadataEntry[]>
|
||||||
// Before data are send, additional query for required App ID is made.
|
// Before data are send, additional query for required App ID is made.
|
||||||
// The manager will use updated entries returned by this mutation to update it's cache.
|
// The manager will use updated entries returned by this mutation to update it's cache.
|
||||||
export async function mutateMetadata(client: Client, metadata: MetadataEntry[]) {
|
export async function mutateMetadata(client: Client, metadata: MetadataEntry[]) {
|
||||||
|
const logger = pinoLogger.child({
|
||||||
|
function: "mutateMetadata",
|
||||||
|
});
|
||||||
|
|
||||||
// to update the metadata, ID is required
|
// to update the metadata, ID is required
|
||||||
const { error: idQueryError, data: idQueryData } = await client
|
const { error: idQueryError, data: idQueryData } = await client
|
||||||
.query(FetchAppDetailsDocument, {})
|
.query(FetchAppDetailsDocument, {})
|
||||||
.toPromise();
|
.toPromise();
|
||||||
|
|
||||||
if (idQueryError) {
|
if (idQueryError) {
|
||||||
console.debug("Could not fetch the app id: ", idQueryError);
|
logger.debug("Could not fetch the app id", idQueryError);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Could not fetch the app id. Please check if auth data for the client are valid."
|
"Could not fetch the app id. Please check if auth data for the client are valid."
|
||||||
);
|
);
|
||||||
|
@ -43,7 +54,7 @@ export async function mutateMetadata(client: Client, metadata: MetadataEntry[])
|
||||||
const appId = idQueryData?.app?.id;
|
const appId = idQueryData?.app?.id;
|
||||||
|
|
||||||
if (!appId) {
|
if (!appId) {
|
||||||
console.debug("Missing app id");
|
logger.debug("Missing app id");
|
||||||
throw new Error("Could not fetch the app ID");
|
throw new Error("Could not fetch the app ID");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +66,7 @@ export async function mutateMetadata(client: Client, metadata: MetadataEntry[])
|
||||||
.toPromise();
|
.toPromise();
|
||||||
|
|
||||||
if (mutationError) {
|
if (mutationError) {
|
||||||
console.debug("Mutation error: ", mutationError);
|
logger.debug("Mutation error", mutationError);
|
||||||
throw new Error(`Mutation error: ${mutationError.message}`);
|
throw new Error(`Mutation error: ${mutationError.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,3 +89,26 @@ export const createSettingsManager = (client: Client) => {
|
||||||
mutateMetadata: (metadata) => mutateMetadata(client, metadata),
|
mutateMetadata: (metadata) => mutateMetadata(client, metadata),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export async function fetchProductVariantMetadata(
|
||||||
|
client: Client,
|
||||||
|
productId: string
|
||||||
|
): Promise<MetadataEntry[]> {
|
||||||
|
const logger = pinoLogger.child({
|
||||||
|
function: "fetchProductVariantMetadata",
|
||||||
|
productId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { error, data } = await client
|
||||||
|
.query<FetchProductVariantMetadataQuery>(FetchProductVariantMetadataDocument, {
|
||||||
|
id: productId,
|
||||||
|
})
|
||||||
|
.toPromise();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.debug("Error during fetching product metadata", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return data?.productVariant?.metadata.map((md) => ({ key: md.key, value: md.value })) || [];
|
||||||
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ const ChannelConfigurationForm = ({
|
||||||
<List gridTemplate={["1fr", "checkbox"]}>
|
<List gridTemplate={["1fr", "checkbox"]}>
|
||||||
<ListHeader>
|
<ListHeader>
|
||||||
<ListItem className={styles.item}>
|
<ListItem className={styles.item}>
|
||||||
<ListItemCell>CMS provider instance</ListItemCell>
|
<ListItemCell>CMS provider configuration</ListItemCell>
|
||||||
<ListItemCell>Active</ListItemCell>
|
<ListItemCell>Active</ListItemCell>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
|
|
|
@ -87,8 +87,7 @@ const ChannelConfiguration = ({
|
||||||
return (
|
return (
|
||||||
<AppPaper>
|
<AppPaper>
|
||||||
<Typography variant="body1" className={styles.textCenter}>
|
<Typography variant="body1" className={styles.textCenter}>
|
||||||
Please create at least one provider instance before you manage its configuration in
|
Please create at least one provider configuration before you manage its setup in channels.
|
||||||
channels.
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
Go to the <AppTabNavButton to="providers">Providers</AppTabNavButton> tab.
|
Go to the <AppTabNavButton to="providers">Providers</AppTabNavButton> tab.
|
||||||
|
|
|
@ -80,9 +80,11 @@ const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderSchema>(
|
||||||
};
|
};
|
||||||
|
|
||||||
const fields = providersConfig[provider.name as TProvider].tokens;
|
const fields = providersConfig[provider.name as TProvider].tokens;
|
||||||
|
|
||||||
const errors = formState.errors;
|
const errors = formState.errors;
|
||||||
|
|
||||||
|
const getOptionalText = (token: Record<string, unknown>) =>
|
||||||
|
"required" in token && token.required ? "" : "*Optional. ";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(submitHandler)}>
|
<form onSubmit={handleSubmit(submitHandler)}>
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
|
@ -104,7 +106,7 @@ const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderSchema>(
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<TextField
|
<TextField
|
||||||
{...register("name" as Path<ProvidersSchema[TProvider]>)}
|
{...register("name" as Path<ProvidersSchema[TProvider]>)}
|
||||||
label="Custom instance name *"
|
label="Configuration name"
|
||||||
type="text"
|
type="text"
|
||||||
name="name"
|
name="name"
|
||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
|
@ -112,7 +114,12 @@ const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderSchema>(
|
||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
error={!!errors.name}
|
error={!!errors.name}
|
||||||
helperText={<>{errors.name?.message}</>}
|
helperText={
|
||||||
|
<>
|
||||||
|
{errors.name?.message ||
|
||||||
|
"Used to differentiate configuration instance. You may create multiple instances of provider configuration, e.g. Contentful Prod, Contentful Test, etc."}
|
||||||
|
</>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
{fields.map((token) => (
|
{fields.map((token) => (
|
||||||
|
@ -122,8 +129,8 @@ const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderSchema>(
|
||||||
required: "required" in token && token.required,
|
required: "required" in token && token.required,
|
||||||
})}
|
})}
|
||||||
// required={"required" in token && token.required}
|
// required={"required" in token && token.required}
|
||||||
label={token.label + ("required" in token && token.required ? " *" : "")}
|
label={token.label}
|
||||||
type="password"
|
type={token.secret ? "password" : "text"}
|
||||||
name={token.name}
|
name={token.name}
|
||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
shrink: !!watch(token.name as Path<ProvidersSchema[TProvider]>),
|
shrink: !!watch(token.name as Path<ProvidersSchema[TProvider]>),
|
||||||
|
@ -134,7 +141,11 @@ const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderSchema>(
|
||||||
helperText={
|
helperText={
|
||||||
<>
|
<>
|
||||||
{errors[token.name as Path<ProvidersSchema[TProvider]>]?.message ||
|
{errors[token.name as Path<ProvidersSchema[TProvider]>]?.message ||
|
||||||
("helpText" in token && <AppMarkdownText>{token.helpText}</AppMarkdownText>)}
|
("helpText" in token && (
|
||||||
|
<AppMarkdownText>{`${getOptionalText(token)}${
|
||||||
|
token.helpText
|
||||||
|
}`}</AppMarkdownText>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -118,7 +118,7 @@ const ProviderInstanceConfiguration = ({
|
||||||
return (
|
return (
|
||||||
<AppPaper>
|
<AppPaper>
|
||||||
<Typography variant="body1" className={styles.textCenter}>
|
<Typography variant="body1" className={styles.textCenter}>
|
||||||
Please select a provider instance or add new one.
|
Please select a provider configuration or add new one.
|
||||||
</Typography>
|
</Typography>
|
||||||
</AppPaper>
|
</AppPaper>
|
||||||
);
|
);
|
||||||
|
@ -133,12 +133,12 @@ const ProviderInstanceConfiguration = ({
|
||||||
)}
|
)}
|
||||||
{errors.saving && (
|
{errors.saving && (
|
||||||
<Typography variant="body1" color="error">
|
<Typography variant="body1" color="error">
|
||||||
Error saving provider instance configuration
|
Error saving provider configuration
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{!!newProviderInstance && (
|
{!!newProviderInstance && (
|
||||||
<Typography variant="h3" className={styles.textHeader}>
|
<Typography variant="h3" className={styles.textHeader}>
|
||||||
Add new instance
|
Add new configuration
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<RadioGroup value={selectedProvider?.name ?? ""} onChange={handleProviderChange}>
|
<RadioGroup value={selectedProvider?.name ?? ""} onChange={handleProviderChange}>
|
||||||
|
|
|
@ -70,7 +70,7 @@ const ProviderInstancesListItems = ({
|
||||||
<OffsettedList gridTemplate={["1fr", "1fr"]}>
|
<OffsettedList gridTemplate={["1fr", "1fr"]}>
|
||||||
<OffsettedListHeader>
|
<OffsettedListHeader>
|
||||||
<OffsettedListItem className={styles.headerItem}>
|
<OffsettedListItem className={styles.headerItem}>
|
||||||
<OffsettedListItemCell>CMS provider instance</OffsettedListItemCell>
|
<OffsettedListItemCell>CMS provider configuration</OffsettedListItemCell>
|
||||||
</OffsettedListItem>
|
</OffsettedListItem>
|
||||||
</OffsettedListHeader>
|
</OffsettedListHeader>
|
||||||
<OffsettedListBody>
|
<OffsettedListBody>
|
||||||
|
|
|
@ -85,7 +85,7 @@ const ProviderInstancesList = ({
|
||||||
fullWidth
|
fullWidth
|
||||||
onClick={requestAddProviderInstance}
|
onClick={requestAddProviderInstance}
|
||||||
>
|
>
|
||||||
Add provider
|
Add configuration
|
||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
@ -29,16 +29,29 @@ export const Instructions = () => {
|
||||||
<ol className={styles.list}>
|
<ol className={styles.list}>
|
||||||
<li>
|
<li>
|
||||||
In the CMS App, go to the <AppTabNavButton to="providers">Providers</AppTabNavButton>{" "}
|
In the CMS App, go to the <AppTabNavButton to="providers">Providers</AppTabNavButton>{" "}
|
||||||
tab to add an instance of your provider. Click <q>Add provider</q>, and select the cms
|
tab to add a configuration of your provider. Click <q>Add configuration</q>, and
|
||||||
provider you want to use. Fill in the configuration form and hit <q>Save</q>.
|
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>
|
||||||
<li>
|
<li>
|
||||||
Go to the <AppTabNavButton to="channels">Channels</AppTabNavButton> tab. Select a
|
Go to the <AppTabNavButton to="channels">Channels</AppTabNavButton> tab. Select a
|
||||||
channel. In the <q>Channel cms provider</q> field, select the created instance. Fill
|
channel. Select the CMS configurations you want to sync product variants data against
|
||||||
in the rest of the form, and hit <q>Save</q>.
|
available in this channel and hit <q>Save</q>.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Saleor will now use the channel's configured cms provider for product
|
Saleor will now use the channel's configured CMS provider for product variant
|
||||||
syncronisation once it is created, updated or deleted.
|
syncronisation once it is created, updated or deleted.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
|
|
@ -12,6 +12,9 @@ import {
|
||||||
executeMetadataUpdate,
|
executeMetadataUpdate,
|
||||||
} 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 { fetchProductVariantMetadata } from "../../../lib/metadata";
|
||||||
|
import { getCmsKeysFromSaleorItem } from "../../../lib/cms/client/metadata";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
|
@ -52,6 +55,7 @@ export const handler: NextWebhookApiHandler<ProductVariantCreatedWebhookPayloadF
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
const { productVariant } = context.payload;
|
const { productVariant } = context.payload;
|
||||||
|
const { saleorApiUrl, token } = context.authData;
|
||||||
|
|
||||||
const logger = pinoLogger.child({
|
const logger = pinoLogger.child({
|
||||||
productVariant,
|
productVariant,
|
||||||
|
@ -66,11 +70,18 @@ export const handler: NextWebhookApiHandler<ProductVariantCreatedWebhookPayloadF
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const client = createClient(saleorApiUrl, async () => ({
|
||||||
|
token: token,
|
||||||
|
}));
|
||||||
|
|
||||||
const productVariantChannels = getChannelsSlugsFromSaleorItem(productVariant);
|
const productVariantChannels = getChannelsSlugsFromSaleorItem(productVariant);
|
||||||
|
const productVariantMetadata = await fetchProductVariantMetadata(client, productVariant.id);
|
||||||
|
const productVariantCmsKeys = getCmsKeysFromSaleorItem({ metadata: productVariantMetadata });
|
||||||
const cmsOperations = await createCmsOperations({
|
const cmsOperations = await createCmsOperations({
|
||||||
context,
|
context,
|
||||||
|
client,
|
||||||
productVariantChannels: productVariantChannels,
|
productVariantChannels: productVariantChannels,
|
||||||
productVariantCmsKeys: [],
|
productVariantCmsKeys: productVariantCmsKeys,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -12,6 +12,8 @@ import {
|
||||||
executeMetadataUpdate,
|
executeMetadataUpdate,
|
||||||
} 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 { fetchProductVariantMetadata } from "../../../lib/metadata";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
|
@ -52,6 +54,7 @@ export const handler: NextWebhookApiHandler<ProductVariantDeletedWebhookPayloadF
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
const { productVariant } = context.payload;
|
const { productVariant } = context.payload;
|
||||||
|
const { saleorApiUrl, token } = context.authData;
|
||||||
|
|
||||||
const logger = pinoLogger.child({
|
const logger = pinoLogger.child({
|
||||||
productVariant,
|
productVariant,
|
||||||
|
@ -66,9 +69,14 @@ export const handler: NextWebhookApiHandler<ProductVariantDeletedWebhookPayloadF
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const client = createClient(saleorApiUrl, async () => ({
|
||||||
|
token: token,
|
||||||
|
}));
|
||||||
|
|
||||||
const productVariantCmsKeys = getCmsKeysFromSaleorItem(productVariant);
|
const productVariantCmsKeys = getCmsKeysFromSaleorItem(productVariant);
|
||||||
const cmsOperations = await createCmsOperations({
|
const cmsOperations = await createCmsOperations({
|
||||||
context,
|
context,
|
||||||
|
client,
|
||||||
productVariantChannels: [],
|
productVariantChannels: [],
|
||||||
productVariantCmsKeys: productVariantCmsKeys,
|
productVariantCmsKeys: productVariantCmsKeys,
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
executeMetadataUpdate,
|
executeMetadataUpdate,
|
||||||
} 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 { fetchProductVariantMetadata } from "../../../lib/metadata";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
|
@ -52,8 +54,8 @@ export const handler: NextWebhookApiHandler<ProductVariantUpdatedWebhookPayloadF
|
||||||
res,
|
res,
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
// * product_updated event triggers on product_created as well 🤷
|
|
||||||
const { productVariant } = context.payload;
|
const { productVariant } = context.payload;
|
||||||
|
const { saleorApiUrl, token } = context.authData;
|
||||||
|
|
||||||
const logger = pinoLogger.child({
|
const logger = pinoLogger.child({
|
||||||
productVariant,
|
productVariant,
|
||||||
|
@ -68,10 +70,16 @@ export const handler: NextWebhookApiHandler<ProductVariantUpdatedWebhookPayloadF
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const client = createClient(saleorApiUrl, async () => ({
|
||||||
|
token: token,
|
||||||
|
}));
|
||||||
|
|
||||||
const productVariantChannels = getChannelsSlugsFromSaleorItem(productVariant);
|
const productVariantChannels = getChannelsSlugsFromSaleorItem(productVariant);
|
||||||
const productVariantCmsKeys = getCmsKeysFromSaleorItem(productVariant);
|
const productVariantMetadata = await fetchProductVariantMetadata(client, productVariant.id);
|
||||||
|
const productVariantCmsKeys = getCmsKeysFromSaleorItem({ metadata: productVariantMetadata });
|
||||||
const cmsOperations = await createCmsOperations({
|
const cmsOperations = await createCmsOperations({
|
||||||
context,
|
context,
|
||||||
|
client,
|
||||||
productVariantChannels: productVariantChannels,
|
productVariantChannels: productVariantChannels,
|
||||||
productVariantCmsKeys: productVariantCmsKeys,
|
productVariantCmsKeys: productVariantCmsKeys,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue