Add Payload CMS (#905)

This commit is contained in:
Lukasz Ostrowski 2023-08-25 11:34:28 +02:00 committed by GitHub
parent 45ed9fb444
commit 6f1c5c9436
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 723 additions and 69 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-cms-v2": patch
---
Fix styling of modal in the dark mode

View file

@ -0,0 +1,5 @@
---
"saleor-app-cms-v2": minor
---
Added Payload CMS support.

View file

@ -35,6 +35,7 @@
"p-ratelimit": "1.0.1", "p-ratelimit": "1.0.1",
"pino": "^8.14.1", "pino": "^8.14.1",
"pino-pretty": "^10.0.0", "pino-pretty": "^10.0.0",
"qs": "6.11.2",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-error-boundary": "4.0.10", "react-error-boundary": "4.0.10",
@ -56,6 +57,7 @@
"@graphql-typed-document-node/core": "3.2.0", "@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/react-hooks": "^8.0.1", "@testing-library/react-hooks": "^8.0.1",
"@types/qs": "^6.9.7",
"@types/react": "18.2.5", "@types/react": "18.2.5",
"@types/react-dom": "18.2.5", "@types/react-dom": "18.2.5",
"eslint": "8.46.0", "eslint": "8.46.0",

View file

@ -2,11 +2,11 @@ import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared"; import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared";
import { Client } from "urql"; import { Client } from "urql";
const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!);
export const createSettingsManager = ( export const createSettingsManager = (
client: Pick<Client, "query" | "mutation">, client: Pick<Client, "query" | "mutation">,
appId: string, appId: string,
): SettingsManager => { ): SettingsManager => {
const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!);
return metadataManagerFactory.create(client, appId); return metadataManagerFactory.create(client, appId);
}; };

View file

@ -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<typeof InputSchema>;
export type FullShape = z.infer<typeof FullSchema>;
export const Schema = {
Input: InputSchema,
Full: FullSchema,
};
}

View file

@ -4,6 +4,7 @@ import { ContentfulProviderConfig } from "./contentful-provider.schema";
import { BuilderIoProviderConfig } from "./builder-provider.schema"; import { BuilderIoProviderConfig } from "./builder-provider.schema";
import { StrapiProviderConfig } from "./strapi-provider.schema"; import { StrapiProviderConfig } from "./strapi-provider.schema";
import { DatocmsProviderConfig } from "./datocms-provider.schema"; import { DatocmsProviderConfig } from "./datocms-provider.schema";
import { PayloadCmsProviderConfig } from "./payloadcms-provider.schema";
export namespace ProvidersConfig { export namespace ProvidersConfig {
const AnyFull = z.union([ const AnyFull = z.union([
@ -14,6 +15,7 @@ export namespace ProvidersConfig {
DatocmsProviderConfig.Schema.Full, DatocmsProviderConfig.Schema.Full,
StrapiProviderConfig.Schema.Full, StrapiProviderConfig.Schema.Full,
BuilderIoProviderConfig.Schema.Full, BuilderIoProviderConfig.Schema.Full,
PayloadCmsProviderConfig.Schema.Full,
]); ]);
export const Schema = { export const Schema = {
@ -23,6 +25,7 @@ export namespace ProvidersConfig {
DatocmsProviderConfig.Schema.Input, DatocmsProviderConfig.Schema.Input,
StrapiProviderConfig.Schema.Input, StrapiProviderConfig.Schema.Input,
BuilderIoProviderConfig.Schema.Input, BuilderIoProviderConfig.Schema.Input,
PayloadCmsProviderConfig.Schema.Input,
]), ]),
AnyFullList: z.array(AnyFull), AnyFullList: z.array(AnyFull),
}; };

View file

@ -104,7 +104,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
padding={2} padding={2}
> >
<Text variant="caption">Saleor Field</Text> <Text variant="caption">Saleor Field</Text>
<Text variant="caption">Contentful field</Text> <Text variant="caption">Builder.io field</Text>
</Box> </Box>
{SaleorProviderFieldsMappingKeys.map((saleorField) => ( {SaleorProviderFieldsMappingKeys.map((saleorField) => (
// todo extract this table to component // todo extract this table to component
@ -190,7 +190,7 @@ const EditFormVariant = (props: { configId: string }) => {
}, },
{ {
enabled: !!props.configId, enabled: !!props.configId,
} },
); );
const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({ const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({
onSuccess() { onSuccess() {

View file

@ -53,7 +53,7 @@ const PureForm = ({
}); });
notifyError( notifyError(
"Error", "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( notifyError(
"Error", "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 newTab
size="small" size="small"
href={`https://app.contentful.com/spaces/${getValues( href={`https://app.contentful.com/spaces/${getValues(
"spaceId" "spaceId",
)}/settings/environments`} )}/settings/environments`}
> >
here here
@ -247,7 +247,7 @@ const PureForm = ({
<Box marginTop={4}> <Box marginTop={4}>
<Text as="p" variant="heading" size="small"> <Text as="p" variant="heading" size="small">
Map fields from Saleor to your contentful schema. Map fields from Saleor to your Contentful schema.
</Text> </Text>
<Text as="p" marginTop={2} marginBottom={4}> <Text as="p" marginTop={2} marginBottom={4}>
All fields should be type of <Text variant="bodyStrong">Text</Text>. Channels should All fields should be type of <Text variant="bodyStrong">Text</Text>. Channels should
@ -357,7 +357,7 @@ const EditVariant = ({ configId }: { configId: string }) => {
}, },
{ {
enabled: !!configId, enabled: !!configId,
} },
); );
const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({ const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({
onSuccess() { onSuccess() {

View file

@ -43,7 +43,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
notifyError( notifyError(
"Error", "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( notifyError(
"Error", "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 && ( {fieldsData && (
<React.Fragment> <React.Fragment>
<Text as="p" variant="heading" size="small"> <Text as="p" variant="heading" size="small">
Map fields from Saleor to your contentful schema. Map fields from Saleor to your DatoCMS schema.
</Text> </Text>
<Text as="p" marginTop={2} marginBottom={4}> <Text as="p" marginTop={2} marginBottom={4}>
All fields should be type of <Text variant="bodyStrong">Text</Text>. Channels should All fields should be type of <Text variant="bodyStrong">Text</Text>. Channels should
@ -177,7 +177,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
padding={2} padding={2}
> >
<Text variant="caption">Saleor Field</Text> <Text variant="caption">Saleor Field</Text>
<Text variant="caption">Contentful field</Text> <Text variant="caption">DatoCMS field</Text>
</Box> </Box>
{SaleorProviderFieldsMappingKeys.map((saleorField) => ( {SaleorProviderFieldsMappingKeys.map((saleorField) => (
// todo extract this table to component // todo extract this table to component
@ -271,7 +271,7 @@ const EditFormVariant = (props: { configId: string }) => {
}, },
{ {
enabled: !!props.configId, enabled: !!props.configId,
} },
); );
const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({ const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({
onSuccess() { onSuccess() {

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View file

@ -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<void> {
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 });
}
});
}),
);
}
}

View file

@ -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);
}
}
}

View file

@ -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<PayloadCmsProviderConfig.InputShape, "type">;
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 (
<Box
as="form"
display={"grid"}
gap={4}
onSubmit={handleSubmit((vals) => {
onSubmit(vals);
})}
>
<Input
required
control={control}
name="configName"
label="Configuration name"
helperText="Meaningful name that will help you understand it later. E.g. 'staging' or 'prod' "
/>
<Box display={"grid"} gap={4} marginY={4}>
<Text variant="heading">Provide connection details</Text>
<Input
required
control={control}
name="payloadApiUrl"
type="url"
label="API url"
helperText="URL where Payload API is available. By default ends with /api"
/>
<Box
backgroundColor="surfaceNeutralHighlight"
borderColor="neutralHighlight"
borderWidth={1}
borderStyle={"solid"}
padding={4}
borderRadius={4}
>
<Text variant="heading" as="h1" marginBottom={4}>
Authorization
</Text>
<Text marginBottom={2} as="p">
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.{" "}
</Text>
<TextLink
marginBottom={2}
display="block"
newTab
href="https://payloadcms.com/docs/authentication/config"
>
Read more in Payload docs
</TextLink>
<Text as="p" marginBottom={4}>
If your API is open (e.g. for development purposes) leave both fields empty.
</Text>
<Input
control={control}
name="authenticatedUserSlug"
label="Authenticated user slug"
placeholder="e.g. apps"
/>
<Input control={control} name="authToken" type="password" label="User API Key" />
</Box>
</Box>
<Box display={"grid"} gap={4} marginY={4}>
<Text variant="heading">Configure fields mapping</Text>
<Input
label="Collection Slug"
name="collectionName"
control={control}
helperText="Slug of your collection in Payload, e.g. 'saleorVariants'"
/>
<React.Fragment>
<Text as="p" variant="heading" size="small">
Map fields from Saleor to your Payload schema.
</Text>
<Text as="p" marginTop={2} marginBottom={4}>
All fields should be type of <Text variant="bodyStrong">Text</Text>. Channels should be
type of <Text variant="bodyStrong">JSON</Text>.
</Text>
<Box
marginBottom={4}
display="grid"
__gridTemplateColumns={"50% 50%"}
borderBottomWidth={1}
borderBottomStyle="solid"
borderColor="neutralHighlight"
padding={2}
>
<Text variant="caption">Saleor Field</Text>
<Text variant="caption">Payload field</Text>
</Box>
{SaleorProviderFieldsMappingKeys.map((saleorField) => (
// todo extract this table to component
<Box
display="grid"
__gridTemplateColumns={"50% 50%"}
padding={2}
key={saleorField}
alignItems="center"
>
<Box>
<Text as="p" variant="bodyStrong">
{printSaleorProductFields(saleorField)}
</Text>
<Text variant="caption">
{saleorField === "channels" ? "JSON field" : "Text field"}
</Text>
</Box>
<Input
size="small"
control={control}
name={`productVariantFieldsMapping.${saleorField}`}
label="CMS Field"
/>
</Box>
))}
</React.Fragment>
</Box>
<ButtonsBox>
{onDelete && (
<Button onClick={onDelete} variant="tertiary">
Delete
</Button>
)}
<Button type="submit">Save</Button>
</ButtonsBox>
</Box>
);
};
const AddFormVariant = () => {
const { push } = useRouter();
const { notifySuccess } = useDashboardNotification();
const { mutate } = trpcClient.providersConfigs.addOne.useMutation({
onSuccess() {
notifySuccess("Success", "Added new configuration");
push("/configuration");
},
});
return (
<PureForm
onSubmit={(values) => {
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 (
<PureForm
onDelete={() => {
deleteProvider({
id: props.configId,
});
}}
onSubmit={(values) => {
mutate({
...values,
type: "payloadcms",
id: props.configId,
});
}}
defaultValues={data}
/>
);
};
export const PayloadCMSConfigForm = {
PureVariant: PureForm,
AddVariant: AddFormVariant,
EditVariant: EditFormVariant,
};

View file

@ -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<void> {
this.logger.trace("onProductVariantUpdated called");
await this.client.upsertProductVariant({
configuration: this.providerConfig,
variant: productVariant,
});
}
async onProductVariantCreated(productVariant: WebhookProductVariantFragment): Promise<void> {
this.logger.trace("onProductVariantCreated called");
await this.client.uploadProductVariant({
configuration: this.providerConfig,
variant: productVariant,
});
}
async onProductVariantDeleted(productVariant: WebhookProductVariantFragment): Promise<void> {
this.logger.trace("onProductVariantDeleted called");
await this.client.deleteProductVariant({
configuration: this.providerConfig,
variant: productVariant,
});
}
async onProductUpdated(product: WebhookProductFragment): Promise<void> {
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,
},
},
});
}),
);
}
}

View file

@ -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: (
<Box>
<Text as="p" marginBottom={2}>
Configure the Payload CMS integration by providing required information.
</Text>
<Text as="p" marginBottom={2}>
Fields are not validated - ensure you enter correct values.
</Text>
<Text as="p" marginBottom={2}>
Consult{" "}
<TextLink newTab href="https://docs.saleor.io/docs/3.x/developer/app-store/apps/cms">
docs
</TextLink>{" "}
for more information how to set up Payload CMS.
</Text>
</Box>
),
type: "payloadcms" as const,
logoUrl: logo.src,
displayName: "Payload",
description: "Open source, typescript first headless CMS. GraphQL included.",
} satisfies CMSProviderMeta;

View file

@ -1,12 +1,24 @@
import { BuilderIo } from "./builder.io/builder-io"; import { BuilderIo } from "./builder.io/builder-io";
import { Contentful } from "./contentful/contentful"; import { Contentful } from "./contentful/contentful";
import { Datocms } from "./datocms/datocms"; import { Datocms } from "./datocms/datocms";
import { PayloadCMS } from "./payloadcms/payloadcms";
import { Strapi } from "./strapi/strapi"; 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 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;

View file

@ -22,6 +22,10 @@ import { DatocmsProviderConfig } from "../configuration/schemas/datocms-provider
import { BuilderIo } from "./builder.io/builder-io"; import { BuilderIo } from "./builder.io/builder-io";
import { BuilderIoWebhooksProcessor } from "./builder.io/builder-io-webhooks-processor"; import { BuilderIoWebhooksProcessor } from "./builder.io/builder-io-webhooks-processor";
import { BuilderIoBulkSyncProcessor } from "./builder.io/builder-io-bulk-sync-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. * 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": { case "builder.io": {
return new BuilderIoBulkSyncProcessor(config); return new BuilderIoBulkSyncProcessor(config);
} }
case "payloadcms": {
default: return new PayloadCmsBulkSyncProcessor(config);
throw new Error(`Unknown provider`); }
} }
}, },
getProviderInputSchema(type: CMSType) { getProviderInputSchema(type: CMSType) {
@ -54,9 +58,8 @@ export const ProvidersResolver = {
return StrapiProviderConfig.Schema.Input; return StrapiProviderConfig.Schema.Input;
case "builder.io": case "builder.io":
return BuilderIoProviderConfig.Schema.Input; return BuilderIoProviderConfig.Schema.Input;
default: { case "payloadcms":
throw new Error("Failed to build input schema"); return PayloadCmsProviderConfig.Schema.Input;
}
} }
}, },
getProviderSchema(type: CMSType) { getProviderSchema(type: CMSType) {
@ -69,12 +72,11 @@ export const ProvidersResolver = {
return StrapiProviderConfig.Schema.Full; return StrapiProviderConfig.Schema.Full;
case "builder.io": case "builder.io":
return BuilderIoProviderConfig.Schema.Full; return BuilderIoProviderConfig.Schema.Full;
default: { case "payloadcms":
throw new Error("Failed to build provdier schema"); return PayloadCmsProviderConfig.Schema.Full;
}
} }
}, },
createProviderMeta(type: CMSType | string): CMS { createProviderMeta(type: CMSType): CMS {
switch (type) { switch (type) {
case "contentful": { case "contentful": {
return Contentful; return Contentful;
@ -88,8 +90,8 @@ export const ProvidersResolver = {
case "builder.io": { case "builder.io": {
return BuilderIo; return BuilderIo;
} }
default: { case "payloadcms": {
throw new Error("Unknown provider"); return PayloadCMS;
} }
} }
}, },
@ -107,13 +109,13 @@ export const ProvidersResolver = {
case "builder.io": { case "builder.io": {
return new BuilderIoWebhooksProcessor(config); return new BuilderIoWebhooksProcessor(config);
} }
default: { case "payloadcms": {
throw new Error("Failed to build webhook processor."); return new PayloadCmsWebhooksProcessor(config);
} }
} }
}, },
getEditProviderFormComponent: ( getEditProviderFormComponent: (
type: CMSType type: CMSType,
): ComponentType<{ ): ComponentType<{
configId: string; configId: string;
}> => { }> => {
@ -121,33 +123,37 @@ export const ProvidersResolver = {
case "contentful": { case "contentful": {
return dynamic(() => return dynamic(() =>
import("./contentful/contentful-config-form").then( import("./contentful/contentful-config-form").then(
(module) => module.ContentfulConfigForm.EditVariant (module) => module.ContentfulConfigForm.EditVariant,
) ),
); );
} }
case "datocms": { case "datocms": {
return dynamic(() => return dynamic(() =>
import("./datocms/datocms-config-form").then( import("./datocms/datocms-config-form").then(
(module) => module.DatoCMSConfigForm.EditVariant (module) => module.DatoCMSConfigForm.EditVariant,
) ),
); );
} }
case "strapi": { case "strapi": {
return dynamic(() => return dynamic(() =>
import("./strapi/strapi-config-form").then( import("./strapi/strapi-config-form").then(
(module) => module.StrapiConfigForm.EditVariant (module) => module.StrapiConfigForm.EditVariant,
) ),
); );
} }
case "builder.io": { case "builder.io": {
return dynamic(() => return dynamic(() =>
import("./builder.io/builder-io-config-form").then( import("./builder.io/builder-io-config-form").then(
(module) => module.BuilderIoConfigForm.EditVariant (module) => module.BuilderIoConfigForm.EditVariant,
) ),
); );
} }
default: { case "payloadcms": {
throw new Error("Provider form not registered"); return dynamic(() =>
import("./payloadcms/payloadcms-config-form").then(
(module) => module.PayloadCMSConfigForm.EditVariant,
),
);
} }
} }
}, },
@ -156,31 +162,37 @@ export const ProvidersResolver = {
case "contentful": { case "contentful": {
return dynamic(() => return dynamic(() =>
import("./contentful/contentful-config-form").then( import("./contentful/contentful-config-form").then(
(module) => module.ContentfulConfigForm.AddVariant (module) => module.ContentfulConfigForm.AddVariant,
) ),
); );
} }
case "datocms": { case "datocms": {
return dynamic(() => return dynamic(() =>
import("./datocms/datocms-config-form").then( import("./datocms/datocms-config-form").then(
(module) => module.DatoCMSConfigForm.AddVariant (module) => module.DatoCMSConfigForm.AddVariant,
) ),
); );
} }
case "strapi": { case "strapi": {
return dynamic(() => 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": { case "builder.io": {
return dynamic(() => return dynamic(() =>
import("./builder.io/builder-io-config-form").then( import("./builder.io/builder-io-config-form").then(
(module) => module.BuilderIoConfigForm.AddVariant (module) => module.BuilderIoConfigForm.AddVariant,
) ),
); );
} }
default: { case "payloadcms": {
throw new Error("Provider form not registered"); return dynamic(() =>
import("./payloadcms/payloadcms-config-form").then(
(module) => module.PayloadCMSConfigForm.AddVariant,
),
);
} }
} }
}, },

View file

@ -86,7 +86,7 @@ const PureForm = ({ defaultValues, onSubmit, onDelete }: PureFormProps) => {
padding={2} padding={2}
> >
<Text variant="caption">Saleor Field</Text> <Text variant="caption">Saleor Field</Text>
<Text variant="caption">Contentful field</Text> <Text variant="caption">Strapi field</Text>
</Box> </Box>
{SaleorProviderFieldsMappingKeys.map((saleorField) => ( {SaleorProviderFieldsMappingKeys.map((saleorField) => (
// todo extract this table to component // todo extract this table to component
@ -172,7 +172,7 @@ const EditFormVariant = (props: { configId: string }) => {
}, },
{ {
enabled: !!props.configId, enabled: !!props.configId,
} },
); );
const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({ const { mutate } = trpcClient.providersConfigs.updateOne.useMutation({
onSuccess() { onSuccess() {

View file

@ -9,7 +9,7 @@ dialog {
.dialog-overlay { .dialog-overlay {
z-index: 1; z-index: 1;
background: rgba(255, 255, 255, 0.8); background: rgba(var(--mu-colors-background-plain), 0.8);
backdrop-filter: blur(5px); backdrop-filter: blur(5px);
content: ""; content: "";
position: fixed; position: fixed;

View file

@ -1,5 +1,4 @@
import { Box, BoxProps } from "@saleor/macaw-ui/next"; import { Box, BoxProps } from "@saleor/macaw-ui/next";
import { forwardRef } from "react";
export const Modal = ({ onClose, ...rest }: { onClose(): void } & BoxProps) => { export const Modal = ({ onClose, ...rest }: { onClose(): void } & BoxProps) => {
return ( return (
@ -12,6 +11,8 @@ export const Modal = ({ onClose, ...rest }: { onClose(): void } & BoxProps) => {
as="dialog" as="dialog"
__maxWidth="400px" __maxWidth="400px"
boxShadow={"modal"} boxShadow={"modal"}
backgroundColor="surfaceNeutralPlain"
color="textNeutralDefault"
open open
{...rest} {...rest}
/> />

View file

@ -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 { CMSType } from "@/modules/providers/providers-registry";
import { ProvidersResolver } from "@/modules/providers/providers-resolver"; import { ProvidersResolver } from "@/modules/providers/providers-resolver";
import { StrapiConfigForm } from "@/modules/providers/strapi/strapi-config-form";
import { AppHeader } from "@/modules/ui/app-header"; import { AppHeader } from "@/modules/ui/app-header";
import { AppSection } from "@/modules/ui/app-section"; import { AppSection } from "@/modules/ui/app-section";
import { Breadcrumbs } from "@saleor/apps-ui"; import { Breadcrumbs } from "@saleor/apps-ui";
@ -16,7 +13,7 @@ const AddProviderPage: NextPage = () => {
const { query } = useRouter(); const { query } = useRouter();
const provider = useMemo(() => { 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]); }, [query.type]);
if (!provider) return null; if (!provider) return null;

View file

@ -2,11 +2,11 @@ import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared"; import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared";
import { Client } from "urql"; import { Client } from "urql";
const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!);
export const createSettingsManager = ( export const createSettingsManager = (
client: Pick<Client, "query" | "mutation">, client: Pick<Client, "query" | "mutation">,
appId: string, appId: string,
): SettingsManager => { ): SettingsManager => {
const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!);
return metadataManagerFactory.create(client, appId); return metadataManagerFactory.create(client, appId);
}; };

View file

@ -106,6 +106,9 @@ importers:
pino-pretty: pino-pretty:
specifier: ^10.0.0 specifier: ^10.0.0
version: 10.0.0 version: 10.0.0
qs:
specifier: 6.11.2
version: 6.11.2
react: react:
specifier: 18.2.0 specifier: 18.2.0
version: 18.2.0 version: 18.2.0
@ -164,6 +167,9 @@ importers:
'@testing-library/react-hooks': '@testing-library/react-hooks':
specifier: ^8.0.1 specifier: ^8.0.1
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0) 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': '@types/react':
specifier: 18.2.5 specifier: 18.2.5
version: 18.2.5 version: 18.2.5
@ -5799,7 +5805,7 @@ packages:
/@changesets/apply-release-plan@6.1.4: /@changesets/apply-release-plan@6.1.4:
resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==} resolution: {integrity: sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==}
dependencies: dependencies:
'@babel/runtime': 7.22.6 '@babel/runtime': 7.22.10
'@changesets/config': 2.3.1 '@changesets/config': 2.3.1
'@changesets/get-version-range-type': 0.3.2 '@changesets/get-version-range-type': 0.3.2
'@changesets/git': 2.0.0 '@changesets/git': 2.0.0
@ -5816,7 +5822,7 @@ packages:
/@changesets/assemble-release-plan@5.2.4: /@changesets/assemble-release-plan@5.2.4:
resolution: {integrity: sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==} resolution: {integrity: sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==}
dependencies: dependencies:
'@babel/runtime': 7.22.6 '@babel/runtime': 7.22.10
'@changesets/errors': 0.1.4 '@changesets/errors': 0.1.4
'@changesets/get-dependents-graph': 1.3.6 '@changesets/get-dependents-graph': 1.3.6
'@changesets/types': 5.2.1 '@changesets/types': 5.2.1
@ -5894,7 +5900,7 @@ packages:
/@changesets/get-release-plan@3.0.17: /@changesets/get-release-plan@3.0.17:
resolution: {integrity: sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==} resolution: {integrity: sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==}
dependencies: dependencies:
'@babel/runtime': 7.22.6 '@babel/runtime': 7.22.10
'@changesets/assemble-release-plan': 5.2.4 '@changesets/assemble-release-plan': 5.2.4
'@changesets/config': 2.3.1 '@changesets/config': 2.3.1
'@changesets/pre': 1.0.14 '@changesets/pre': 1.0.14
@ -5908,7 +5914,7 @@ packages:
/@changesets/git@2.0.0: /@changesets/git@2.0.0:
resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==} resolution: {integrity: sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==}
dependencies: dependencies:
'@babel/runtime': 7.22.6 '@babel/runtime': 7.22.10
'@changesets/errors': 0.1.4 '@changesets/errors': 0.1.4
'@changesets/types': 5.2.1 '@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3 '@manypkg/get-packages': 1.1.3
@ -5930,7 +5936,7 @@ packages:
/@changesets/pre@1.0.14: /@changesets/pre@1.0.14:
resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==} resolution: {integrity: sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==}
dependencies: dependencies:
'@babel/runtime': 7.22.6 '@babel/runtime': 7.22.10
'@changesets/errors': 0.1.4 '@changesets/errors': 0.1.4
'@changesets/types': 5.2.1 '@changesets/types': 5.2.1
'@manypkg/get-packages': 1.1.3 '@manypkg/get-packages': 1.1.3
@ -5939,7 +5945,7 @@ packages:
/@changesets/read@0.5.9: /@changesets/read@0.5.9:
resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==} resolution: {integrity: sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==}
dependencies: dependencies:
'@babel/runtime': 7.22.6 '@babel/runtime': 7.22.10
'@changesets/git': 2.0.0 '@changesets/git': 2.0.0
'@changesets/logger': 0.0.5 '@changesets/logger': 0.0.5
'@changesets/parse': 0.3.16 '@changesets/parse': 0.3.16
@ -5957,7 +5963,7 @@ packages:
/@changesets/write@0.2.3: /@changesets/write@0.2.3:
resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==} resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==}
dependencies: dependencies:
'@babel/runtime': 7.22.6 '@babel/runtime': 7.22.10
'@changesets/types': 5.2.1 '@changesets/types': 5.2.1
fs-extra: 7.0.1 fs-extra: 7.0.1
human-id: 1.0.2 human-id: 1.0.2
@ -7698,7 +7704,7 @@ packages:
/@manypkg/get-packages@1.1.3: /@manypkg/get-packages@1.1.3:
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
dependencies: dependencies:
'@babel/runtime': 7.22.6 '@babel/runtime': 7.22.10
'@changesets/types': 4.1.0 '@changesets/types': 4.1.0
'@manypkg/find-root': 1.1.0 '@manypkg/find-root': 1.1.0
fs-extra: 8.1.0 fs-extra: 8.1.0