diff --git a/apps/cms/graphql/queries/FetchProductVariantMetadata.graphql b/apps/cms/graphql/queries/FetchProductVariantMetadata.graphql new file mode 100644 index 0000000..f1e5580 --- /dev/null +++ b/apps/cms/graphql/queries/FetchProductVariantMetadata.graphql @@ -0,0 +1,9 @@ +query FetchProductVariantMetadata($id: ID!) { + productVariant(id: $id) { + id + metadata { + key + value + } + } +} diff --git a/apps/cms/src/lib/cms/client/clients-operations.test.ts b/apps/cms/src/lib/cms/client/clients-operations.test.ts index b8fadae..07d9b2e 100644 --- a/apps/cms/src/lib/cms/client/clients-operations.test.ts +++ b/apps/cms/src/lib/cms/client/clients-operations.test.ts @@ -1,4 +1,5 @@ import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; +import { type Client } from "urql"; import { describe, expect, it, vi } from "vitest"; import { CMSSchemaChannels, CMSSchemaProviderInstances } from "../config"; import { CmsClientOperations } from "../types"; @@ -18,6 +19,8 @@ const mockedContext: Pick = { }, }; +const createMockedClient = () => ({} as Client); + vi.mock("../../metadata", () => ({ createSettingsManager: () => ({}), })); @@ -37,6 +40,7 @@ describe("CMS Clients Operations", () => { const cmsOperations = await createCmsOperations({ context: mockedContext, + client: createMockedClient(), productVariantChannels: [], productVariantCmsKeys: [], }); @@ -99,6 +103,7 @@ describe("CMS Clients Operations", () => { const cmsOperations = await createCmsOperations({ context: mockedContext, + client: createMockedClient(), productVariantChannels: ["default-channel"], productVariantCmsKeys: [], }); @@ -171,6 +176,7 @@ describe("CMS Clients Operations", () => { const cmsOperations = await createCmsOperations({ context: mockedContext, + client: createMockedClient(), productVariantChannels: ["default-channel"], productVariantCmsKeys: [createCmsKeyForSaleorItem("first-provider")], }); @@ -243,6 +249,7 @@ describe("CMS Clients Operations", () => { const cmsOperations = await createCmsOperations({ context: mockedContext, + client: createMockedClient(), productVariantChannels: [], productVariantCmsKeys: [createCmsKeyForSaleorItem("first-provider")], }); diff --git a/apps/cms/src/lib/cms/client/clients-operations.ts b/apps/cms/src/lib/cms/client/clients-operations.ts index 532bdbd..46e6697 100644 --- a/apps/cms/src/lib/cms/client/clients-operations.ts +++ b/apps/cms/src/lib/cms/client/clients-operations.ts @@ -12,15 +12,18 @@ import cmsProviders, { CMSProvider } from "../providers"; import { CmsClientOperations } from "../types"; import { logger as pinoLogger } from "../../logger"; import { getCmsIdFromSaleorItemKey } from "./metadata"; +import { type Client } from "urql"; type WebhookContext = Parameters["2"]; export const createCmsOperations = async ({ context, + client, productVariantChannels, productVariantCmsKeys, }: { context: Pick; + client: Client; productVariantChannels: string[]; productVariantCmsKeys: string[]; }) => { @@ -29,13 +32,6 @@ export const createCmsOperations = async ({ productVariantCmsKeys, }); - const saleorApiUrl = context.authData.saleorApiUrl; - const token = context.authData.token; - - const client = createClient(saleorApiUrl, async () => ({ - token: token, - })); - const settingsManager = createSettingsManager(client); const channelsSettingsParsed = await getChannelsSettings(settingsManager); diff --git a/apps/cms/src/lib/cms/config/providers.ts b/apps/cms/src/lib/cms/config/providers.ts index 86ecc20..d097ae1 100644 --- a/apps/cms/src/lib/cms/config/providers.ts +++ b/apps/cms/src/lib/cms/config/providers.ts @@ -7,6 +7,7 @@ type ProviderToken = { label: string; helpText: string; required?: boolean; + secret?: boolean; }; type ProviderConfig = { @@ -26,6 +27,7 @@ export const providersConfig = { tokens: [ { required: true, + secret: true, name: "token", label: "Token", helpText: @@ -63,7 +65,7 @@ export const providersConfig = { name: "baseUrl", label: "Base URL", 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, + secret: true, name: "token", label: "API Token (with full access)", helpText: @@ -88,7 +91,7 @@ export const providersConfig = { { required: true, name: "contentTypeId", - label: "Content Type ID", + label: "Content Type ID (plural)", 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).', }, @@ -101,6 +104,7 @@ export const providersConfig = { tokens: [ { required: true, + secret: true, name: "token", label: "API Token (with access to Content Management API)", helpText: @@ -117,13 +121,13 @@ export const providersConfig = { name: "baseUrl", label: "Base URL", 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", label: "Environment", 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.", }, ], }, diff --git a/apps/cms/src/lib/metadata.ts b/apps/cms/src/lib/metadata.ts index c80b111..b3104cf 100644 --- a/apps/cms/src/lib/metadata.ts +++ b/apps/cms/src/lib/metadata.ts @@ -4,20 +4,27 @@ import { Client } from "urql"; import { FetchAppDetailsDocument, FetchAppDetailsQuery, + FetchProductVariantMetadataDocument, + FetchProductVariantMetadataQuery, UpdateAppMetadataDocument, } from "../../generated/graphql"; +import { logger as pinoLogger } from "../lib/logger"; // 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 // which can be used by the manager. // Result of this query is cached by the manager. export async function fetchAllMetadata(client: Client): Promise { + const logger = pinoLogger.child({ + function: "fetchAllMetadata", + }); + const { error, data } = await client .query(FetchAppDetailsDocument, {}) .toPromise(); if (error) { - console.debug("Error during fetching the metadata: ", error); + logger.debug("Error during fetching the metadata", error); return []; } @@ -28,13 +35,17 @@ export async function fetchAllMetadata(client: Client): Promise // 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. export async function mutateMetadata(client: Client, metadata: MetadataEntry[]) { + const logger = pinoLogger.child({ + function: "mutateMetadata", + }); + // to update the metadata, ID is required const { error: idQueryError, data: idQueryData } = await client .query(FetchAppDetailsDocument, {}) .toPromise(); if (idQueryError) { - console.debug("Could not fetch the app id: ", idQueryError); + logger.debug("Could not fetch the app id", idQueryError); throw new Error( "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; if (!appId) { - console.debug("Missing app id"); + logger.debug("Missing app id"); throw new Error("Could not fetch the app ID"); } @@ -55,7 +66,7 @@ export async function mutateMetadata(client: Client, metadata: MetadataEntry[]) .toPromise(); if (mutationError) { - console.debug("Mutation error: ", mutationError); + logger.debug("Mutation error", mutationError); throw new Error(`Mutation error: ${mutationError.message}`); } @@ -78,3 +89,26 @@ export const createSettingsManager = (client: Client) => { mutateMetadata: (metadata) => mutateMetadata(client, metadata), }); }; + +export async function fetchProductVariantMetadata( + client: Client, + productId: string +): Promise { + const logger = pinoLogger.child({ + function: "fetchProductVariantMetadata", + productId, + }); + + const { error, data } = await client + .query(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 })) || []; +} diff --git a/apps/cms/src/modules/channels/ui/channel-configuration-form.tsx b/apps/cms/src/modules/channels/ui/channel-configuration-form.tsx index 1365d60..d489001 100644 --- a/apps/cms/src/modules/channels/ui/channel-configuration-form.tsx +++ b/apps/cms/src/modules/channels/ui/channel-configuration-form.tsx @@ -97,7 +97,7 @@ const ChannelConfigurationForm = ({ - CMS provider instance + CMS provider configuration Active diff --git a/apps/cms/src/modules/channels/ui/channel-configuration.tsx b/apps/cms/src/modules/channels/ui/channel-configuration.tsx index eb9e501..406f2f7 100644 --- a/apps/cms/src/modules/channels/ui/channel-configuration.tsx +++ b/apps/cms/src/modules/channels/ui/channel-configuration.tsx @@ -87,8 +87,7 @@ const ChannelConfiguration = ({ return ( - Please create at least one provider instance before you manage its configuration in - channels. + Please create at least one provider configuration before you manage its setup in channels.

Go to the Providers tab. diff --git a/apps/cms/src/modules/provider-instances/ui/provider-instance-configuration-form.tsx b/apps/cms/src/modules/provider-instances/ui/provider-instance-configuration-form.tsx index 49cb182..97dcf59 100644 --- a/apps/cms/src/modules/provider-instances/ui/provider-instance-configuration-form.tsx +++ b/apps/cms/src/modules/provider-instances/ui/provider-instance-configuration-form.tsx @@ -80,9 +80,11 @@ const ProviderInstanceConfigurationForm = ( }; const fields = providersConfig[provider.name as TProvider].tokens; - const errors = formState.errors; + const getOptionalText = (token: Record) => + "required" in token && token.required ? "" : "*Optional. "; + return (
@@ -104,7 +106,7 @@ const ProviderInstanceConfigurationForm = ( )} - label="Custom instance name *" + label="Configuration name" type="text" name="name" InputLabelProps={{ @@ -112,7 +114,12 @@ const ProviderInstanceConfigurationForm = ( }} fullWidth 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."} + + } /> {fields.map((token) => ( @@ -122,8 +129,8 @@ const ProviderInstanceConfigurationForm = ( required: "required" in token && token.required, })} // required={"required" in token && token.required} - label={token.label + ("required" in token && token.required ? " *" : "")} - type="password" + label={token.label} + type={token.secret ? "password" : "text"} name={token.name} InputLabelProps={{ shrink: !!watch(token.name as Path), @@ -134,7 +141,11 @@ const ProviderInstanceConfigurationForm = ( helperText={ <> {errors[token.name as Path]?.message || - ("helpText" in token && {token.helpText})} + ("helpText" in token && ( + {`${getOptionalText(token)}${ + token.helpText + }`} + ))} } /> diff --git a/apps/cms/src/modules/provider-instances/ui/provider-instance-configuration.tsx b/apps/cms/src/modules/provider-instances/ui/provider-instance-configuration.tsx index 8116fa4..f08716c 100644 --- a/apps/cms/src/modules/provider-instances/ui/provider-instance-configuration.tsx +++ b/apps/cms/src/modules/provider-instances/ui/provider-instance-configuration.tsx @@ -118,7 +118,7 @@ const ProviderInstanceConfiguration = ({ return ( - Please select a provider instance or add new one. + Please select a provider configuration or add new one. ); @@ -133,12 +133,12 @@ const ProviderInstanceConfiguration = ({ )} {errors.saving && ( - Error saving provider instance configuration + Error saving provider configuration )} {!!newProviderInstance && ( - Add new instance + Add new configuration )} diff --git a/apps/cms/src/modules/provider-instances/ui/provider-instances-list-items.tsx b/apps/cms/src/modules/provider-instances/ui/provider-instances-list-items.tsx index 1845fda..15dccb2 100644 --- a/apps/cms/src/modules/provider-instances/ui/provider-instances-list-items.tsx +++ b/apps/cms/src/modules/provider-instances/ui/provider-instances-list-items.tsx @@ -70,7 +70,7 @@ const ProviderInstancesListItems = ({ - CMS provider instance + CMS provider configuration diff --git a/apps/cms/src/modules/provider-instances/ui/provider-instances-list.tsx b/apps/cms/src/modules/provider-instances/ui/provider-instances-list.tsx index 6dd15a8..bcfeebb 100644 --- a/apps/cms/src/modules/provider-instances/ui/provider-instances-list.tsx +++ b/apps/cms/src/modules/provider-instances/ui/provider-instances-list.tsx @@ -85,7 +85,7 @@ const ProviderInstancesList = ({ fullWidth onClick={requestAddProviderInstance} > - Add provider + Add configuration diff --git a/apps/cms/src/modules/ui/instructions.tsx b/apps/cms/src/modules/ui/instructions.tsx index cec866e..6c7f4b0 100644 --- a/apps/cms/src/modules/ui/instructions.tsx +++ b/apps/cms/src/modules/ui/instructions.tsx @@ -29,16 +29,29 @@ export const Instructions = () => {
  1. In the CMS App, go to the Providers{" "} - tab to add an instance of your provider. Click Add provider, and select the cms - provider you want to use. Fill in the configuration form and hit Save. + tab to add a configuration of your provider. Click Add configuration, and + select the cms provider you want to use. Fill in the configuration form and hit{" "} + Save. +
  2. +
  3. + Go to your CMS website and prepare product variant model shape with: +
      +
    • + string fields: saleor_id, name, product_id,{" "} + product_name, product_slug, +
    • +
    • + JSON fileds: channels. +
    • +
  4. Go to the Channels tab. Select a - channel. In the Channel cms provider field, select the created instance. Fill - in the rest of the form, and hit Save. + channel. Select the CMS configurations you want to sync product variants data against + available in this channel and hit Save.
  5. - 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.
  6. diff --git a/apps/cms/src/pages/api/webhooks/product-variant-created.ts b/apps/cms/src/pages/api/webhooks/product-variant-created.ts index 6fdc11a..8e7bd8d 100644 --- a/apps/cms/src/pages/api/webhooks/product-variant-created.ts +++ b/apps/cms/src/pages/api/webhooks/product-variant-created.ts @@ -12,6 +12,9 @@ import { executeMetadataUpdate, } from "../../../lib/cms/client"; 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 = { api: { @@ -52,6 +55,7 @@ export const handler: NextWebhookApiHandler { const { productVariant } = context.payload; + const { saleorApiUrl, token } = context.authData; const logger = pinoLogger.child({ productVariant, @@ -66,11 +70,18 @@ export const handler: NextWebhookApiHandler ({ + token: token, + })); + const productVariantChannels = getChannelsSlugsFromSaleorItem(productVariant); + const productVariantMetadata = await fetchProductVariantMetadata(client, productVariant.id); + const productVariantCmsKeys = getCmsKeysFromSaleorItem({ metadata: productVariantMetadata }); const cmsOperations = await createCmsOperations({ context, + client, productVariantChannels: productVariantChannels, - productVariantCmsKeys: [], + productVariantCmsKeys: productVariantCmsKeys, }); const { diff --git a/apps/cms/src/pages/api/webhooks/product-variant-deleted.ts b/apps/cms/src/pages/api/webhooks/product-variant-deleted.ts index d8841c3..d35bb55 100644 --- a/apps/cms/src/pages/api/webhooks/product-variant-deleted.ts +++ b/apps/cms/src/pages/api/webhooks/product-variant-deleted.ts @@ -12,6 +12,8 @@ import { executeMetadataUpdate, } 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: { @@ -52,6 +54,7 @@ export const handler: NextWebhookApiHandler { const { productVariant } = context.payload; + const { saleorApiUrl, token } = context.authData; const logger = pinoLogger.child({ productVariant, @@ -66,9 +69,14 @@ export const handler: NextWebhookApiHandler ({ + token: token, + })); + const productVariantCmsKeys = getCmsKeysFromSaleorItem(productVariant); const cmsOperations = await createCmsOperations({ context, + client, productVariantChannels: [], productVariantCmsKeys: productVariantCmsKeys, }); diff --git a/apps/cms/src/pages/api/webhooks/product-variant-updated.ts b/apps/cms/src/pages/api/webhooks/product-variant-updated.ts index 6ce0e1d..bfdf6cd 100644 --- a/apps/cms/src/pages/api/webhooks/product-variant-updated.ts +++ b/apps/cms/src/pages/api/webhooks/product-variant-updated.ts @@ -13,6 +13,8 @@ import { executeMetadataUpdate, } 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: { @@ -52,8 +54,8 @@ export const handler: NextWebhookApiHandler { - // * product_updated event triggers on product_created as well 🤷 const { productVariant } = context.payload; + const { saleorApiUrl, token } = context.authData; const logger = pinoLogger.child({ productVariant, @@ -68,10 +70,16 @@ export const handler: NextWebhookApiHandler ({ + token: token, + })); + const productVariantChannels = getChannelsSlugsFromSaleorItem(productVariant); - const productVariantCmsKeys = getCmsKeysFromSaleorItem(productVariant); + const productVariantMetadata = await fetchProductVariantMetadata(client, productVariant.id); + const productVariantCmsKeys = getCmsKeysFromSaleorItem({ metadata: productVariantMetadata }); const cmsOperations = await createCmsOperations({ context, + client, productVariantChannels: productVariantChannels, productVariantCmsKeys: productVariantCmsKeys, });