diff --git a/.changeset/light-bobcats-prove.md b/.changeset/light-bobcats-prove.md new file mode 100644 index 0000000..c37dbb2 --- /dev/null +++ b/.changeset/light-bobcats-prove.md @@ -0,0 +1,5 @@ +--- +"saleor-app-cms-v2": patch +--- + +Fix styling of modal in the dark mode diff --git a/.changeset/strong-pugs-cover.md b/.changeset/strong-pugs-cover.md new file mode 100644 index 0000000..8309515 --- /dev/null +++ b/.changeset/strong-pugs-cover.md @@ -0,0 +1,5 @@ +--- +"saleor-app-cms-v2": minor +--- + +Added Payload CMS support. diff --git a/apps/cms-v2/package.json b/apps/cms-v2/package.json index 0123ebe..74ae114 100644 --- a/apps/cms-v2/package.json +++ b/apps/cms-v2/package.json @@ -35,6 +35,7 @@ "p-ratelimit": "1.0.1", "pino": "^8.14.1", "pino-pretty": "^10.0.0", + "qs": "6.11.2", "react": "18.2.0", "react-dom": "18.2.0", "react-error-boundary": "4.0.10", @@ -56,6 +57,7 @@ "@graphql-typed-document-node/core": "3.2.0", "@testing-library/react": "^13.4.0", "@testing-library/react-hooks": "^8.0.1", + "@types/qs": "^6.9.7", "@types/react": "18.2.5", "@types/react-dom": "18.2.5", "eslint": "8.46.0", diff --git a/apps/cms-v2/src/modules/configuration/metadata-manager.ts b/apps/cms-v2/src/modules/configuration/metadata-manager.ts index 8a0e4c4..6877d9c 100644 --- a/apps/cms-v2/src/modules/configuration/metadata-manager.ts +++ b/apps/cms-v2/src/modules/configuration/metadata-manager.ts @@ -2,11 +2,11 @@ import { SettingsManager } from "@saleor/app-sdk/settings-manager"; import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared"; import { Client } from "urql"; -const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!); - export const createSettingsManager = ( client: Pick, appId: string, ): SettingsManager => { + const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!); + return metadataManagerFactory.create(client, appId); }; diff --git a/apps/cms-v2/src/modules/configuration/schemas/payloadcms-provider.schema.ts b/apps/cms-v2/src/modules/configuration/schemas/payloadcms-provider.schema.ts new file mode 100644 index 0000000..22197e9 --- /dev/null +++ b/apps/cms-v2/src/modules/configuration/schemas/payloadcms-provider.schema.ts @@ -0,0 +1,27 @@ +import { PayloadCMS } from "@/modules/providers/payloadcms/payloadcms"; +import { z } from "zod"; +import { SaleorProviderFieldsMappingSchema } from "./saleor-provider-fields-mapping.schema"; + +const InputSchema = z.object({ + type: z.literal(PayloadCMS.type), + authToken: z.string(), + configName: z.string().min(1), + collectionName: z.string().min(1), + productVariantFieldsMapping: SaleorProviderFieldsMappingSchema, + payloadApiUrl: z.string().url(), + authenticatedUserSlug: z.string(), +}); + +const FullSchema = InputSchema.extend({ + id: z.string(), +}); + +export namespace PayloadCmsProviderConfig { + export type InputShape = z.infer; + export type FullShape = z.infer; + + export const Schema = { + Input: InputSchema, + Full: FullSchema, + }; +} diff --git a/apps/cms-v2/src/modules/configuration/schemas/root-config.schema.ts b/apps/cms-v2/src/modules/configuration/schemas/root-config.schema.ts index 4ef5ff8..5182976 100644 --- a/apps/cms-v2/src/modules/configuration/schemas/root-config.schema.ts +++ b/apps/cms-v2/src/modules/configuration/schemas/root-config.schema.ts @@ -4,6 +4,7 @@ import { ContentfulProviderConfig } from "./contentful-provider.schema"; import { BuilderIoProviderConfig } from "./builder-provider.schema"; import { StrapiProviderConfig } from "./strapi-provider.schema"; import { DatocmsProviderConfig } from "./datocms-provider.schema"; +import { PayloadCmsProviderConfig } from "./payloadcms-provider.schema"; export namespace ProvidersConfig { const AnyFull = z.union([ @@ -14,6 +15,7 @@ export namespace ProvidersConfig { DatocmsProviderConfig.Schema.Full, StrapiProviderConfig.Schema.Full, BuilderIoProviderConfig.Schema.Full, + PayloadCmsProviderConfig.Schema.Full, ]); export const Schema = { @@ -23,6 +25,7 @@ export namespace ProvidersConfig { DatocmsProviderConfig.Schema.Input, StrapiProviderConfig.Schema.Input, BuilderIoProviderConfig.Schema.Input, + PayloadCmsProviderConfig.Schema.Input, ]), AnyFullList: z.array(AnyFull), }; diff --git a/apps/cms-v2/src/modules/providers/builder.io/builder-io-config-form.tsx b/apps/cms-v2/src/modules/providers/builder.io/builder-io-config-form.tsx index 075eeb8..1374e0f 100644 --- a/apps/cms-v2/src/modules/providers/builder.io/builder-io-config-form.tsx +++ b/apps/cms-v2/src/modules/providers/builder.io/builder-io-config-form.tsx @@ -104,7 +104,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => { padding={2} > Saleor Field - Contentful field + Builder.io field {SaleorProviderFieldsMappingKeys.map((saleorField) => ( // todo extract this table to component @@ -190,7 +190,7 @@ const EditFormVariant = (props: { configId: string }) => { }, { enabled: !!props.configId, - } + }, ); const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({ onSuccess() { diff --git a/apps/cms-v2/src/modules/providers/contentful/contentful-config-form.tsx b/apps/cms-v2/src/modules/providers/contentful/contentful-config-form.tsx index f925578..1fe8551 100644 --- a/apps/cms-v2/src/modules/providers/contentful/contentful-config-form.tsx +++ b/apps/cms-v2/src/modules/providers/contentful/contentful-config-form.tsx @@ -53,7 +53,7 @@ const PureForm = ({ }); notifyError( "Error", - "Could not fetch content types from Contentful. Please check your credentials." + "Could not fetch content types from Contentful. Please check your credentials.", ); }, }); @@ -76,7 +76,7 @@ const PureForm = ({ }); notifyError( "Error", - "Could not fetch environments from Contentful. Please check your credentials." + "Could not fetch environments from Contentful. Please check your credentials.", ); }, }); @@ -197,7 +197,7 @@ const PureForm = ({ newTab size="small" href={`https://app.contentful.com/spaces/${getValues( - "spaceId" + "spaceId", )}/settings/environments`} > here @@ -247,7 +247,7 @@ const PureForm = ({ - Map fields from Saleor to your contentful schema. + Map fields from Saleor to your Contentful schema. All fields should be type of Text. Channels should @@ -357,7 +357,7 @@ const EditVariant = ({ configId }: { configId: string }) => { }, { enabled: !!configId, - } + }, ); const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({ onSuccess() { diff --git a/apps/cms-v2/src/modules/providers/datocms/datocms-config-form.tsx b/apps/cms-v2/src/modules/providers/datocms/datocms-config-form.tsx index 42dea79..81441f7 100644 --- a/apps/cms-v2/src/modules/providers/datocms/datocms-config-form.tsx +++ b/apps/cms-v2/src/modules/providers/datocms/datocms-config-form.tsx @@ -43,7 +43,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => { notifyError( "Error", - "Could not fetch content types from DatoCMS. Please check your credentials." + "Could not fetch content types from DatoCMS. Please check your credentials.", ); }, }); @@ -61,7 +61,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => { notifyError( "Error", - "Could not fetch content types from DatoCMS. Please check your credentials." + "Could not fetch content types from DatoCMS. Please check your credentials.", ); }, }); @@ -161,7 +161,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => { {fieldsData && ( - Map fields from Saleor to your contentful schema. + Map fields from Saleor to your DatoCMS schema. All fields should be type of Text. Channels should @@ -177,7 +177,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => { padding={2} > Saleor Field - Contentful field + DatoCMS field {SaleorProviderFieldsMappingKeys.map((saleorField) => ( // todo extract this table to component @@ -271,7 +271,7 @@ const EditFormVariant = (props: { configId: string }) => { }, { enabled: !!props.configId, - } + }, ); const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({ onSuccess() { diff --git a/apps/cms-v2/src/modules/providers/payloadcms/logo.png b/apps/cms-v2/src/modules/providers/payloadcms/logo.png new file mode 100644 index 0000000..f8af7f5 Binary files /dev/null and b/apps/cms-v2/src/modules/providers/payloadcms/logo.png differ diff --git a/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-bulk-sync-processor.ts b/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-bulk-sync-processor.ts new file mode 100644 index 0000000..9cc9144 --- /dev/null +++ b/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-bulk-sync-processor.ts @@ -0,0 +1,51 @@ +import { BulkImportProductFragment } from "../../../../generated/graphql"; +import { BulkSyncProcessor, BulkSyncProcessorHooks } from "../../bulk-sync/bulk-sync-processor"; + +import { PayloadCmsProviderConfig } from "@/modules/configuration/schemas/payloadcms-provider.schema"; +import { PayloadCMSClient } from "./payloadcms-client"; + +// todo CORS or proxy +export class PayloadCmsBulkSyncProcessor implements BulkSyncProcessor { + constructor(private config: PayloadCmsProviderConfig.FullShape) {} + + async uploadProducts( + products: BulkImportProductFragment[], + hooks: BulkSyncProcessorHooks, + ): Promise { + const client = new PayloadCMSClient(); + + products.flatMap( + (product) => + product.variants?.map((variant) => { + if (hooks.onUploadStart) { + hooks.onUploadStart({ variantId: variant.id }); + } + + return client + .upsertProductVariant({ + configuration: this.config, + variant: { + id: variant.id, + name: variant.name, + channelListings: variant.channelListings, + product: { + id: product.id, + name: product.name, + slug: product.slug, + }, + }, + }) + .then((r) => { + if (hooks.onUploadSuccess) { + hooks.onUploadSuccess({ variantId: variant.id }); + } + }) + .catch((e) => { + if (hooks.onUploadError) { + hooks.onUploadError({ variantId: variant.id, error: e }); + } + }); + }), + ); + } +} diff --git a/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-client.ts b/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-client.ts new file mode 100644 index 0000000..794b1b3 --- /dev/null +++ b/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-client.ts @@ -0,0 +1,172 @@ +import { createLogger } from "@saleor/apps-shared"; +import { WebhookProductVariantFragment } from "../../../../generated/graphql"; + +import { PayloadCmsProviderConfig } from "@/modules/configuration/schemas/payloadcms-provider.schema"; +import { FieldsMapper } from "../fields-mapper"; + +import qs from "qs"; +import { z } from "zod"; + +type Context = { + configuration: PayloadCmsProviderConfig.FullShape; + variant: WebhookProductVariantFragment; +}; + +/** + * Client uses REST API with built-in query language + * https://payloadcms.com/docs/queries/overview#rest-queries + */ +export class PayloadCMSClient { + private logger = createLogger({ name: "PayloadCMSClient" }); + + private mapVariantToPayloadFields({ configuration, variant }: Context) { + const fields = FieldsMapper.mapProductVariantToConfigurationFields({ + variant, + configMapping: configuration.productVariantFieldsMapping, + }); + + return fields; + } + + private constructCollectionUrl(config: PayloadCmsProviderConfig.FullShape) { + return `${config.payloadApiUrl}/${config.collectionName}`; + } + + getItemsBySaleorVariantId(context: Context) { + const queryString = qs.stringify( + { + where: { + [context.configuration.productVariantFieldsMapping.variantId]: { + equals: context.variant.id, + }, + }, + }, + { + addQueryPrefix: true, + }, + ); + + return fetch(`${this.constructCollectionUrl(context.configuration)}${queryString}`, { + headers: this.getHeaders(context), + }).then((r) => r.json()); + } + + async deleteProductVariant(context: Context) { + const queryString = qs.stringify( + { + where: { + [context.configuration.productVariantFieldsMapping.variantId]: { + equals: context.variant.id, + }, + }, + }, + { + addQueryPrefix: true, + }, + ); + + try { + const response = await fetch( + this.constructCollectionUrl(context.configuration) + queryString, + { + method: "DELETE", + headers: this.getHeaders(context), + }, + ); + + if (response.status >= 400) { + throw new Error("Error while deleting product variant"); + } + } catch (e) { + this.logger.error(e); + + throw e; + } + } + + private getHeaders(context: Context) { + const headers = new Headers({ + "Content-Type": "application/json", + }); + + /** + * https://payloadcms.com/docs/authentication/config#api-keys + */ + if ( + context.configuration.authToken.length > 0 && + context.configuration.authenticatedUserSlug.length > 0 + ) { + headers.append( + "Authorization", + `${context.configuration.authenticatedUserSlug} API-Key ${context.configuration.authToken}`, + ); + } + + return headers; + } + + uploadProductVariant(context: Context) { + this.logger.debug("Trying to upload product variant"); + + return fetch(this.constructCollectionUrl(context.configuration), { + method: "POST", + body: JSON.stringify(this.mapVariantToPayloadFields(context)), + headers: this.getHeaders(context), + }) + .then((r) => { + if (r.status >= 400) { + throw new Error(`Error while uploading product variant: ${r.statusText}`); + } + }) + .catch((e) => { + this.logger.error(e); + + throw e; + }); + } + + async updateProductVariant({ configuration, variant }: Context) { + this.logger.debug("Trying to update product variant"); + + const queryString = qs.stringify( + { + where: { + [configuration.productVariantFieldsMapping.variantId]: { + equals: variant.id, + }, + }, + }, + { + addQueryPrefix: true, + }, + ); + + try { + const response = await fetch(this.constructCollectionUrl(configuration) + queryString, { + method: "PATCH", + body: JSON.stringify(this.mapVariantToPayloadFields({ configuration, variant })), + headers: this.getHeaders({ configuration, variant }), + }); + + if (response.status >= 400) { + throw new Error("Error while updating product variant"); + } + } catch (e) { + this.logger.error(e); + + throw e; + } + } + + async upsertProductVariant(context: Context) { + this.logger.debug("Trying to upsert product variant"); + + try { + await this.uploadProductVariant(context); + } catch (e) { + this.logger.debug("Failed to upload, will try to update"); + + await this.updateProductVariant(context); + } + } +} diff --git a/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-config-form.tsx b/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-config-form.tsx new file mode 100644 index 0000000..e3d0743 --- /dev/null +++ b/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-config-form.tsx @@ -0,0 +1,263 @@ +import { SaleorProviderFieldsMappingKeys } from "@/modules/configuration"; +import { PayloadCmsProviderConfig } from "@/modules/configuration/schemas/payloadcms-provider.schema"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useDashboardNotification } from "@saleor/apps-shared"; +import { Box, Button, Text } from "@saleor/macaw-ui/next"; +import { Input, Select } from "@saleor/react-hook-form-macaw"; +import { useRouter } from "next/router"; +import React from "react"; +import { useForm } from "react-hook-form"; +import { printSaleorProductFields } from "../../configuration/print-saleor-product-fields"; +import { trpcClient } from "../../trpc/trpc-client"; +import { ButtonsBox } from "../../ui/buttons-box"; +import { TextLink } from "@saleor/apps-ui"; + +type FormShape = Omit; + +type PureFormProps = { + defaultValues: FormShape; + onSubmit(values: FormShape): void; + onDelete?(): void; +}; + +/* + * todo react on token change, refresh mutation + */ +const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => { + const { notifyError } = useDashboardNotification(); + + const { control, getValues, setValue, watch, handleSubmit, clearErrors, setError } = useForm({ + defaultValues: defaultValues, + resolver: zodResolver(PayloadCmsProviderConfig.Schema.Input.omit({ type: true })), + }); + + return ( + { + onSubmit(vals); + })} + > + + + Provide connection details + + + + + Authorization + + + Payload can be configured to have open operations (not recommended) or to require an API + key. Key can be generated per user. To authenticate, you need to provide both user slug + and the key itself.{" "} + + + Read more in Payload docs + + + If your API is open (e.g. for development purposes) leave both fields empty. + + + + + + + + Configure fields mapping + + + + + Map fields from Saleor to your Payload schema. + + + All fields should be type of Text. Channels should be + type of JSON. + + + Saleor Field + Payload field + + {SaleorProviderFieldsMappingKeys.map((saleorField) => ( + // todo extract this table to component + + + + {printSaleorProductFields(saleorField)} + + + {saleorField === "channels" ? "JSON field" : "Text field"} + + + + + ))} + + + + + {onDelete && ( + + )} + + + + ); +}; + +const AddFormVariant = () => { + const { push } = useRouter(); + const { notifySuccess } = useDashboardNotification(); + + const { mutate } = trpcClient.providersConfigs.addOne.useMutation({ + onSuccess() { + notifySuccess("Success", "Added new configuration"); + push("/configuration"); + }, + }); + + return ( + { + mutate({ + ...values, + type: "payloadcms", + }); + }} + defaultValues={{ + payloadApiUrl: "", + authToken: "", + configName: "", + collectionName: "", + authenticatedUserSlug: "", + productVariantFieldsMapping: { + channels: "", + variantName: "", + productId: "", + productName: "", + productSlug: "", + variantId: "", + }, + }} + /> + ); +}; + +const EditFormVariant = (props: { configId: string }) => { + const { push } = useRouter(); + const { notifySuccess } = useDashboardNotification(); + + const { data } = trpcClient.providersConfigs.getOne.useQuery( + { + id: props.configId, + }, + { + enabled: !!props.configId, + }, + ); + + const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({ + onSuccess() { + notifySuccess("Success", "Updated configuration"); + push("/configuration"); + }, + }); + + const { mutate: deleteProvider } = trpcClient.providersConfigs.deleteOne.useMutation({ + onSuccess() { + notifySuccess("Success", "Removed configuration"); + push("/configuration"); + }, + }); + + if (!data) { + return null; + } + + if (data.type !== "payloadcms") { + throw new Error("Trying to fill Payload CMS form with non Payload CMS data"); + } + + return ( + { + deleteProvider({ + id: props.configId, + }); + }} + onSubmit={(values) => { + mutate({ + ...values, + type: "payloadcms", + id: props.configId, + }); + }} + defaultValues={data} + /> + ); +}; + +export const PayloadCMSConfigForm = { + PureVariant: PureForm, + AddVariant: AddFormVariant, + EditVariant: EditFormVariant, +}; diff --git a/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-webhooks-processor.ts b/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-webhooks-processor.ts new file mode 100644 index 0000000..b6c097e --- /dev/null +++ b/apps/cms-v2/src/modules/providers/payloadcms/payloadcms-webhooks-processor.ts @@ -0,0 +1,69 @@ +import { + WebhookProductFragment, + WebhookProductVariantFragment, +} from "../../../../generated/graphql"; + +import { PayloadCmsProviderConfig } from "@/modules/configuration/schemas/payloadcms-provider.schema"; +import { createLogger } from "@saleor/apps-shared"; +import { ProductWebhooksProcessor } from "../../webhooks-operations/product-webhooks-processor"; +import { PayloadCMSClient } from "./payloadcms-client"; + +/* + * todo error handling + */ +export class PayloadCmsWebhooksProcessor implements ProductWebhooksProcessor { + private client = new PayloadCMSClient(); + + private logger = createLogger({ name: "PayloadCmsWebhooksProcessor" }); + + constructor(private providerConfig: PayloadCmsProviderConfig.FullShape) {} + + async onProductVariantUpdated(productVariant: WebhookProductVariantFragment): Promise { + this.logger.trace("onProductVariantUpdated called"); + + await this.client.upsertProductVariant({ + configuration: this.providerConfig, + variant: productVariant, + }); + } + + async onProductVariantCreated(productVariant: WebhookProductVariantFragment): Promise { + this.logger.trace("onProductVariantCreated called"); + + await this.client.uploadProductVariant({ + configuration: this.providerConfig, + variant: productVariant, + }); + } + async onProductVariantDeleted(productVariant: WebhookProductVariantFragment): Promise { + this.logger.trace("onProductVariantDeleted called"); + + await this.client.deleteProductVariant({ + configuration: this.providerConfig, + variant: productVariant, + }); + } + + async onProductUpdated(product: WebhookProductFragment): Promise { + this.logger.trace("onProductUpdated called"); + + const client = new PayloadCMSClient(); + + await Promise.all( + (product.variants ?? []).map((variant) => { + return client.upsertProductVariant({ + configuration: this.providerConfig, + variant: { + id: variant.id, + name: variant.name, + product: { + id: product.id, + name: product.name, + slug: product.slug, + }, + }, + }); + }), + ); + } +} diff --git a/apps/cms-v2/src/modules/providers/payloadcms/payloadcms.tsx b/apps/cms-v2/src/modules/providers/payloadcms/payloadcms.tsx new file mode 100644 index 0000000..a543c80 --- /dev/null +++ b/apps/cms-v2/src/modules/providers/payloadcms/payloadcms.tsx @@ -0,0 +1,29 @@ +import { Box, Text } from "@saleor/macaw-ui/next"; + +import { CMSProviderMeta } from "../cms-provider-meta"; +import logo from "./logo.png"; +import { TextLink } from "@saleor/apps-ui"; + +export const PayloadCMS = { + formSideInfo: ( + + + Configure the Payload CMS integration by providing required information. + + + Fields are not validated - ensure you enter correct values. + + + Consult{" "} + + docs + {" "} + for more information how to set up Payload CMS. + + + ), + type: "payloadcms" as const, + logoUrl: logo.src, + displayName: "Payload", + description: "Open source, typescript first headless CMS. GraphQL included.", +} satisfies CMSProviderMeta; diff --git a/apps/cms-v2/src/modules/providers/providers-registry.ts b/apps/cms-v2/src/modules/providers/providers-registry.ts index 79e1569..4711cc8 100644 --- a/apps/cms-v2/src/modules/providers/providers-registry.ts +++ b/apps/cms-v2/src/modules/providers/providers-registry.ts @@ -1,12 +1,24 @@ import { BuilderIo } from "./builder.io/builder-io"; import { Contentful } from "./contentful/contentful"; import { Datocms } from "./datocms/datocms"; +import { PayloadCMS } from "./payloadcms/payloadcms"; import { Strapi } from "./strapi/strapi"; -export type CMS = typeof Contentful | typeof Datocms | typeof Strapi | typeof BuilderIo; +export type CMS = + | typeof Contentful + | typeof Datocms + | typeof Strapi + | typeof BuilderIo + | typeof PayloadCMS; export type CMSType = CMS["type"]; -export const cmsTypes = [Contentful.type, Datocms.type, Strapi.type, BuilderIo.type] as const; +export const cmsTypes = [ + Contentful.type, + Datocms.type, + Strapi.type, + BuilderIo.type, + PayloadCMS.type, +] as const; -export const CMSProviders = [Contentful, Datocms, Strapi, BuilderIo] as const; +export const CMSProviders = [Contentful, Datocms, Strapi, BuilderIo, PayloadCMS] as const; diff --git a/apps/cms-v2/src/modules/providers/providers-resolver.tsx b/apps/cms-v2/src/modules/providers/providers-resolver.tsx index 7d46ba7..4477247 100644 --- a/apps/cms-v2/src/modules/providers/providers-resolver.tsx +++ b/apps/cms-v2/src/modules/providers/providers-resolver.tsx @@ -22,6 +22,10 @@ import { DatocmsProviderConfig } from "../configuration/schemas/datocms-provider import { BuilderIo } from "./builder.io/builder-io"; import { BuilderIoWebhooksProcessor } from "./builder.io/builder-io-webhooks-processor"; import { BuilderIoBulkSyncProcessor } from "./builder.io/builder-io-bulk-sync-processor"; +import { PayloadCmsBulkSyncProcessor } from "./payloadcms/payloadcms-bulk-sync-processor"; +import { PayloadCmsProviderConfig } from "../configuration/schemas/payloadcms-provider.schema"; +import { PayloadCMS } from "./payloadcms/payloadcms"; +import { PayloadCmsWebhooksProcessor } from "./payloadcms/payloadcms-webhooks-processor"; /** * Almost-single source of new providers. Every time app will need to resolve a provider, it will use on of these factories. @@ -39,9 +43,9 @@ export const ProvidersResolver = { case "builder.io": { return new BuilderIoBulkSyncProcessor(config); } - - default: - throw new Error(`Unknown provider`); + case "payloadcms": { + return new PayloadCmsBulkSyncProcessor(config); + } } }, getProviderInputSchema(type: CMSType) { @@ -54,9 +58,8 @@ export const ProvidersResolver = { return StrapiProviderConfig.Schema.Input; case "builder.io": return BuilderIoProviderConfig.Schema.Input; - default: { - throw new Error("Failed to build input schema"); - } + case "payloadcms": + return PayloadCmsProviderConfig.Schema.Input; } }, getProviderSchema(type: CMSType) { @@ -69,12 +72,11 @@ export const ProvidersResolver = { return StrapiProviderConfig.Schema.Full; case "builder.io": return BuilderIoProviderConfig.Schema.Full; - default: { - throw new Error("Failed to build provdier schema"); - } + case "payloadcms": + return PayloadCmsProviderConfig.Schema.Full; } }, - createProviderMeta(type: CMSType | string): CMS { + createProviderMeta(type: CMSType): CMS { switch (type) { case "contentful": { return Contentful; @@ -88,8 +90,8 @@ export const ProvidersResolver = { case "builder.io": { return BuilderIo; } - default: { - throw new Error("Unknown provider"); + case "payloadcms": { + return PayloadCMS; } } }, @@ -107,13 +109,13 @@ export const ProvidersResolver = { case "builder.io": { return new BuilderIoWebhooksProcessor(config); } - default: { - throw new Error("Failed to build webhook processor."); + case "payloadcms": { + return new PayloadCmsWebhooksProcessor(config); } } }, getEditProviderFormComponent: ( - type: CMSType + type: CMSType, ): ComponentType<{ configId: string; }> => { @@ -121,33 +123,37 @@ export const ProvidersResolver = { case "contentful": { return dynamic(() => import("./contentful/contentful-config-form").then( - (module) => module.ContentfulConfigForm.EditVariant - ) + (module) => module.ContentfulConfigForm.EditVariant, + ), ); } case "datocms": { return dynamic(() => import("./datocms/datocms-config-form").then( - (module) => module.DatoCMSConfigForm.EditVariant - ) + (module) => module.DatoCMSConfigForm.EditVariant, + ), ); } case "strapi": { return dynamic(() => import("./strapi/strapi-config-form").then( - (module) => module.StrapiConfigForm.EditVariant - ) + (module) => module.StrapiConfigForm.EditVariant, + ), ); } case "builder.io": { return dynamic(() => import("./builder.io/builder-io-config-form").then( - (module) => module.BuilderIoConfigForm.EditVariant - ) + (module) => module.BuilderIoConfigForm.EditVariant, + ), ); } - default: { - throw new Error("Provider form not registered"); + case "payloadcms": { + return dynamic(() => + import("./payloadcms/payloadcms-config-form").then( + (module) => module.PayloadCMSConfigForm.EditVariant, + ), + ); } } }, @@ -156,31 +162,37 @@ export const ProvidersResolver = { case "contentful": { return dynamic(() => import("./contentful/contentful-config-form").then( - (module) => module.ContentfulConfigForm.AddVariant - ) + (module) => module.ContentfulConfigForm.AddVariant, + ), ); } case "datocms": { return dynamic(() => import("./datocms/datocms-config-form").then( - (module) => module.DatoCMSConfigForm.AddVariant - ) + (module) => module.DatoCMSConfigForm.AddVariant, + ), ); } case "strapi": { return dynamic(() => - import("./strapi/strapi-config-form").then((module) => module.StrapiConfigForm.AddVariant) + import("./strapi/strapi-config-form").then( + (module) => module.StrapiConfigForm.AddVariant, + ), ); } case "builder.io": { return dynamic(() => import("./builder.io/builder-io-config-form").then( - (module) => module.BuilderIoConfigForm.AddVariant - ) + (module) => module.BuilderIoConfigForm.AddVariant, + ), ); } - default: { - throw new Error("Provider form not registered"); + case "payloadcms": { + return dynamic(() => + import("./payloadcms/payloadcms-config-form").then( + (module) => module.PayloadCMSConfigForm.AddVariant, + ), + ); } } }, diff --git a/apps/cms-v2/src/modules/providers/strapi/strapi-config-form.tsx b/apps/cms-v2/src/modules/providers/strapi/strapi-config-form.tsx index e444879..52fc192 100644 --- a/apps/cms-v2/src/modules/providers/strapi/strapi-config-form.tsx +++ b/apps/cms-v2/src/modules/providers/strapi/strapi-config-form.tsx @@ -86,7 +86,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => { padding={2} > Saleor Field - Contentful field + Strapi field {SaleorProviderFieldsMappingKeys.map((saleorField) => ( // todo extract this table to component @@ -172,7 +172,7 @@ const EditFormVariant = (props: { configId: string }) => { }, { enabled: !!props.configId, - } + }, ); const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({ onSuccess() { diff --git a/apps/cms-v2/src/modules/theme/styles.css b/apps/cms-v2/src/modules/theme/styles.css index a19ddb8..0327b1e 100644 --- a/apps/cms-v2/src/modules/theme/styles.css +++ b/apps/cms-v2/src/modules/theme/styles.css @@ -9,7 +9,7 @@ dialog { .dialog-overlay { z-index: 1; - background: rgba(255, 255, 255, 0.8); + background: rgba(var(--mu-colors-background-plain), 0.8); backdrop-filter: blur(5px); content: ""; position: fixed; diff --git a/apps/cms-v2/src/modules/ui/modal.tsx b/apps/cms-v2/src/modules/ui/modal.tsx index da438e5..ffe6874 100644 --- a/apps/cms-v2/src/modules/ui/modal.tsx +++ b/apps/cms-v2/src/modules/ui/modal.tsx @@ -1,5 +1,4 @@ import { Box, BoxProps } from "@saleor/macaw-ui/next"; -import { forwardRef } from "react"; export const Modal = ({ onClose, ...rest }: { onClose(): void } & BoxProps) => { return ( @@ -12,6 +11,8 @@ export const Modal = ({ onClose, ...rest }: { onClose(): void } & BoxProps) => { as="dialog" __maxWidth="400px" boxShadow={"modal"} + backgroundColor="surfaceNeutralPlain" + color="textNeutralDefault" open {...rest} /> diff --git a/apps/cms-v2/src/pages/add-provider/[type].tsx b/apps/cms-v2/src/pages/add-provider/[type].tsx index 4141d2d..89e3cfc 100644 --- a/apps/cms-v2/src/pages/add-provider/[type].tsx +++ b/apps/cms-v2/src/pages/add-provider/[type].tsx @@ -1,9 +1,6 @@ -import { ContentfulConfigForm } from "@/modules/providers/contentful/contentful-config-form"; -import { DatoCMSConfigForm } from "@/modules/providers/datocms/datocms-config-form"; import { CMSType } from "@/modules/providers/providers-registry"; import { ProvidersResolver } from "@/modules/providers/providers-resolver"; -import { StrapiConfigForm } from "@/modules/providers/strapi/strapi-config-form"; import { AppHeader } from "@/modules/ui/app-header"; import { AppSection } from "@/modules/ui/app-section"; import { Breadcrumbs } from "@saleor/apps-ui"; @@ -16,7 +13,7 @@ const AddProviderPage: NextPage = () => { const { query } = useRouter(); const provider = useMemo(() => { - return query.type ? ProvidersResolver.createProviderMeta(query.type as string) : null; + return query.type ? ProvidersResolver.createProviderMeta(query.type as CMSType) : null; }, [query.type]); if (!provider) return null; diff --git a/apps/segment/src/modules/configuration/metadata-manager.ts b/apps/segment/src/modules/configuration/metadata-manager.ts index 8a0e4c4..6877d9c 100644 --- a/apps/segment/src/modules/configuration/metadata-manager.ts +++ b/apps/segment/src/modules/configuration/metadata-manager.ts @@ -2,11 +2,11 @@ import { SettingsManager } from "@saleor/app-sdk/settings-manager"; import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared"; import { Client } from "urql"; -const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!); - export const createSettingsManager = ( client: Pick, appId: string, ): SettingsManager => { + const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!); + return metadataManagerFactory.create(client, appId); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d8b053..7b9b960 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,6 +106,9 @@ importers: pino-pretty: specifier: ^10.0.0 version: 10.0.0 + qs: + specifier: 6.11.2 + version: 6.11.2 react: specifier: 18.2.0 version: 18.2.0 @@ -164,6 +167,9 @@ importers: '@testing-library/react-hooks': specifier: ^8.0.1 version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0) + '@types/qs': + specifier: ^6.9.7 + version: 6.9.7 '@types/react': specifier: 18.2.5 version: 18.2.5 @@ -5799,7 +5805,7 @@ packages: /@changesets/apply-release-plan@6.1.4: resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==} dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.22.10 '@changesets/config': 2.3.1 '@changesets/get-version-range-type': 0.3.2 '@changesets/git': 2.0.0 @@ -5816,7 +5822,7 @@ packages: /@changesets/assemble-release-plan@5.2.4: resolution: {integrity: sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==} dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.22.10 '@changesets/errors': 0.1.4 '@changesets/get-dependents-graph': 1.3.6 '@changesets/types': 5.2.1 @@ -5894,7 +5900,7 @@ packages: /@changesets/get-release-plan@3.0.17: resolution: {integrity: sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==} dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.22.10 '@changesets/assemble-release-plan': 5.2.4 '@changesets/config': 2.3.1 '@changesets/pre': 1.0.14 @@ -5908,7 +5914,7 @@ packages: /@changesets/git@2.0.0: resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==} dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.22.10 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -5930,7 +5936,7 @@ packages: /@changesets/pre@1.0.14: resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==} dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.22.10 '@changesets/errors': 0.1.4 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 @@ -5939,7 +5945,7 @@ packages: /@changesets/read@0.5.9: resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==} dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.22.10 '@changesets/git': 2.0.0 '@changesets/logger': 0.0.5 '@changesets/parse': 0.3.16 @@ -5957,7 +5963,7 @@ packages: /@changesets/write@0.2.3: resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==} dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.22.10 '@changesets/types': 5.2.1 fs-extra: 7.0.1 human-id: 1.0.2 @@ -7698,7 +7704,7 @@ packages: /@manypkg/get-packages@1.1.3: resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} dependencies: - '@babel/runtime': 7.22.6 + '@babel/runtime': 7.22.10 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0