diff --git a/.changeset/yellow-bees-enjoy.md b/.changeset/yellow-bees-enjoy.md new file mode 100644 index 00000000..d1b695a8 --- /dev/null +++ b/.changeset/yellow-bees-enjoy.md @@ -0,0 +1,5 @@ +--- +"@saleor/app-sdk": patch +--- + +Added new API for communication around forms (TODO) diff --git a/.gitignore b/.gitignore index 9dfb9565..dc9c6993 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,5 @@ dist .idea/ .vscode/ + +coverage.json \ No newline at end of file diff --git a/README.md b/README.md index 48f34212..97904db1 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Supports Saleor version 3.20+ - Create a legacy release branch (e.g. `v1.x` branch) - Mark changeset to `main` with `major` change, which will start counting next `main` releases as `2.x.x` - Do not merge release PR until it's ready to be merged - + ### Deploying test snapshots PRs can be pushed to NPM by adding label to PR `release dev tag`. Workflow will run and print version that has been released. diff --git a/src/APL/redis/redis-apl.test.ts b/src/APL/redis/redis-apl.test.ts index 85759825..226c2691 100644 --- a/src/APL/redis/redis-apl.test.ts +++ b/src/APL/redis/redis-apl.test.ts @@ -84,7 +84,7 @@ describe("RedisAPL", () => { await defaultApl.get(mockAuthData.saleorApiUrl); expect(mockRedisClient.hGet).toHaveBeenCalledWith( "saleor_app_auth", - mockAuthData.saleorApiUrl + mockAuthData.saleorApiUrl, ); }); }); @@ -117,7 +117,7 @@ describe("RedisAPL", () => { expect(mockHSet).toHaveBeenCalledWith( mockHashKey, mockAuthData.saleorApiUrl, - JSON.stringify(mockAuthData) + JSON.stringify(mockAuthData), ); }); diff --git a/src/APL/redis/redis-apl.ts b/src/APL/redis/redis-apl.ts index da670466..3fad7184 100644 --- a/src/APL/redis/redis-apl.ts +++ b/src/APL/redis/redis-apl.ts @@ -112,7 +112,7 @@ export class RedisAPL implements APL { throw e; } - } + }, ); } @@ -136,7 +136,7 @@ export class RedisAPL implements APL { await this.client.hSet( this.hashCollectionKey, authData.saleorApiUrl, - JSON.stringify(authData) + JSON.stringify(authData), ); this.debug("Successfully set auth data in Redis"); @@ -155,7 +155,7 @@ export class RedisAPL implements APL { throw e; } - } + }, ); } @@ -193,7 +193,7 @@ export class RedisAPL implements APL { throw e; } - } + }, ); } @@ -232,7 +232,7 @@ export class RedisAPL implements APL { throw e; } - } + }, ); } diff --git a/src/APL/saleor-cloud/paginator.ts b/src/APL/saleor-cloud/paginator.ts index 172ffb35..099a203e 100644 --- a/src/APL/saleor-cloud/paginator.ts +++ b/src/APL/saleor-cloud/paginator.ts @@ -12,7 +12,7 @@ export class Paginator { constructor( private readonly url: string, private readonly fetchOptions: RequestInit, - private readonly fetchFn = fetch + private readonly fetchFn = fetch, ) {} public async fetchAll() { diff --git a/src/APL/saleor-cloud/saleor-cloud-apl-errors.ts b/src/APL/saleor-cloud/saleor-cloud-apl-errors.ts index 6bf25ac8..06added4 100644 --- a/src/APL/saleor-cloud/saleor-cloud-apl-errors.ts +++ b/src/APL/saleor-cloud/saleor-cloud-apl-errors.ts @@ -1,5 +1,8 @@ export class SaleorCloudAplError extends Error { - constructor(public code: string, message: string) { + constructor( + public code: string, + message: string, + ) { super(message); this.name = "SaleorCloudAplError"; } diff --git a/src/APL/vercel-kv/vercel-kv-apl.ts b/src/APL/vercel-kv/vercel-kv-apl.ts index d5e93056..354794ff 100644 --- a/src/APL/vercel-kv/vercel-kv-apl.ts +++ b/src/APL/vercel-kv/vercel-kv-apl.ts @@ -76,7 +76,7 @@ export class VercelKvApl implements APL { throw e; } - } + }, ); } @@ -118,7 +118,7 @@ export class VercelKvApl implements APL { throw e; } - } + }, ); } @@ -157,7 +157,7 @@ export class VercelKvApl implements APL { throw e; } - } + }, ); } diff --git a/src/app-bridge/actions.test.ts b/src/app-bridge/actions.test.ts index fe618291..2f479b85 100644 --- a/src/app-bridge/actions.test.ts +++ b/src/app-bridge/actions.test.ts @@ -39,6 +39,42 @@ describe("actions.ts", () => { }); }); + describe("actions.FormPayloadUpdate", () => { + it("Constructs action with \"formPayloadUpdate\" type, random id and payload for product translation", () => { + const payload = { + form: "product-translate" as const, + fields: { + productName: { value: "Updated Product Name" }, + productDescription: { value: "Updated Description" }, + seoName: { value: "Updated SEO Name" }, + }, + }; + + const action = actions.FormPayloadUpdate(payload); + + expect(action.type).toBe("formPayloadUpdate"); + expect(action.payload.actionId).toEqual(expect.any(String)); + expect(action.payload).toEqual(expect.objectContaining(payload)); + }); + + it("Constructs action with field value results", () => { + const payload = { + form: "product-translate" as const, + fields: { + productName: { value: "New Name" }, + productDescription: { value: "New Description" }, + seoName: { value: "New SEO" }, + seoDescription: { value: "New SEO Description" }, + }, + }; + + const action = actions.FormPayloadUpdate(payload); + + expect(action.payload.fields.productName).toEqual({ value: "New Name" }); + expect(action.payload.fields.productDescription).toEqual({ value: "New Description" }); + }); + }); + it("Throws custom error if crypto is not available", () => { vi.stubGlobal("crypto", { ...globalThis.crypto, diff --git a/src/app-bridge/actions.ts b/src/app-bridge/actions.ts index f250616a..73a0560a 100644 --- a/src/app-bridge/actions.ts +++ b/src/app-bridge/actions.ts @@ -1,3 +1,8 @@ +import { + AllFormPayloadUpdatePayloads, + formPayloadUpdateActionName, +} from "@/app-bridge/form-payload"; + import { AppPermission } from "../types"; import { Values } from "./helpers"; @@ -25,6 +30,12 @@ export const ActionType = { * Available from 3.15 */ requestPermission: "requestPermissions", + /** + * Apply form fields in active context. + * + * EXPERIMENTAL + */ + formPayloadUpdate: formPayloadUpdateActionName, } as const; export type ActionType = Values; @@ -127,6 +138,11 @@ export type RequestPermissions = ActionWithId< } >; +export type FormPayloadUpdate = ActionWithId< + typeof formPayloadUpdateActionName, + AllFormPayloadUpdatePayloads +>; + function createRequestPermissionsAction( permissions: AppPermission[], redirectPath: string, @@ -140,11 +156,20 @@ function createRequestPermissionsAction( }); } +function createFormPayloadUpdateAction(payload: AllFormPayloadUpdatePayloads): FormPayloadUpdate { + return withActionId({ + type: formPayloadUpdateActionName, + // @ts-ignore - TODO: For some reason TS is failing here, but this is internal implementation so it doesn't change the public API + payload, + }); +} + export type Actions = | RedirectAction | NotificationAction | UpdateRouting | NotifyReady + | FormPayloadUpdate | RequestPermissions; export const actions = { @@ -153,4 +178,5 @@ export const actions = { UpdateRouting: createUpdateRoutingAction, NotifyReady: createNotifyReadyAction, RequestPermissions: createRequestPermissionsAction, + FormPayloadUpdate: createFormPayloadUpdateAction, }; diff --git a/src/app-bridge/app-bridge-provider.tsx b/src/app-bridge/app-bridge-provider.tsx index a138845d..fd446591 100644 --- a/src/app-bridge/app-bridge-provider.tsx +++ b/src/app-bridge/app-bridge-provider.tsx @@ -42,7 +42,7 @@ export function AppBridgeProvider({ appBridgeInstance, ...props }: React.PropsWi appBridge, mounted: true, }), - [appBridge] + [appBridge], ); return ; @@ -51,7 +51,7 @@ export function AppBridgeProvider({ appBridgeInstance, ...props }: React.PropsWi export const useAppBridge = () => { const { appBridge, mounted } = useContext(AppContext); const [appBridgeState, setAppBridgeState] = useState(() => - appBridge ? appBridge.getState() : null + appBridge ? appBridge.getState() : null, ); if (typeof window !== "undefined" && !mounted) { diff --git a/src/app-bridge/app-bridge-state.ts b/src/app-bridge/app-bridge-state.ts index bc84b0d0..4a3bc860 100644 --- a/src/app-bridge/app-bridge-state.ts +++ b/src/app-bridge/app-bridge-state.ts @@ -1,3 +1,5 @@ +import { AllFormPayloads } from "@/app-bridge/form-payload"; + import { LocaleCode } from "../locales"; import { AppPermission, Permission } from "../types"; import { ThemeType } from "./events"; @@ -24,6 +26,7 @@ export type AppBridgeState = { email: string; }; appPermissions?: AppPermission[]; + formContext?: AllFormPayloads; }; type Options = { diff --git a/src/app-bridge/app-bridge.test.ts b/src/app-bridge/app-bridge.test.ts index 2dc3f9e1..6a5cacc1 100644 --- a/src/app-bridge/app-bridge.test.ts +++ b/src/app-bridge/app-bridge.test.ts @@ -325,4 +325,167 @@ describe("AppBridge", () => { expect(appBridge.getState().saleorVersion).toEqual("3.15.0"); expect(appBridge.getState().dashboardVersion).toEqual("3.15.1"); }); + + describe("Form payload handling", () => { + it("Updates state with form context when form payload event is received", () => { + expect(appBridge.getState().formContext).toBeUndefined(); + + const formPayload = { + form: "product-translate" as const, + productId: "product-123", + translationLanguage: "es", + currentLanguage: "en", + fields: { + productName: { + fieldName: "productName", + originalValue: "Original Product", + translatedValue: "Producto Original", + currentValue: "Original Product", + type: "short-text" as const, + }, + }, + }; + + const formEvent = DashboardEventFactory.createFormEvent(formPayload); + + fireEvent( + window, + new MessageEvent("message", { + data: formEvent, + origin, + }), + ); + + expect(appBridge.getState().formContext).toEqual(formPayload); + expect(appBridge.getState().formContext?.form).toBe("product-translate"); + expect(appBridge.getState().formContext?.productId).toBe("product-123"); + }); + + it("Subscribes to form payload event and executes callback", () => { + const callback = vi.fn(); + const unsubscribe = appBridge.subscribe("formPayload", callback); + + expect(callback).not.toHaveBeenCalled(); + + const formPayload = { + form: "product-translate" as const, + productId: "product-456", + translationLanguage: "fr", + currentLanguage: "en", + fields: { + productDescription: { + fieldName: "productDescription", + originalValue: "Description", + translatedValue: "Description en français", + currentValue: "Description", + type: "editorjs" as const, + }, + }, + }; + + const formEvent = DashboardEventFactory.createFormEvent(formPayload); + + fireEvent( + window, + new MessageEvent("message", { + data: formEvent, + origin, + }), + ); + + expect(callback).toHaveBeenCalledOnce(); + expect(callback).toHaveBeenCalledWith(formPayload); + + unsubscribe(); + + // After unsubscribe, callback should not be called again + fireEvent( + window, + new MessageEvent("message", { + data: formEvent, + origin, + }), + ); + + expect(callback).toHaveBeenCalledOnce(); + }); + + it("Dispatches form payload update action", () => { + const payload = { + form: "product-translate" as const, + fields: { + productName: { value: "Updated Product Name" }, + productDescription: { value: "Updated Description" }, + seoName: { value: "Updated SEO Name" }, + seoDescription: { value: "Updated SEO Name" }, + }, + }; + + const action = actions.FormPayloadUpdate(payload); + + mockDashboardActionResponse(action.type, action.payload.actionId); + + return expect(appBridge.dispatch(action)).resolves.toBeUndefined(); + }); + + it("Updates form context with new fields when multiple form events are received", () => { + const firstFormPayload = { + form: "product-translate" as const, + productId: "product-1", + translationLanguage: "es", + currentLanguage: "en", + fields: { + productName: { + fieldName: "productName", + originalValue: "Product 1", + translatedValue: "Producto 1", + currentValue: "Product 1", + type: "short-text" as const, + }, + }, + }; + + fireEvent( + window, + new MessageEvent("message", { + data: DashboardEventFactory.createFormEvent(firstFormPayload), + origin, + }), + ); + + expect(appBridge.getState().formContext?.productId).toBe("product-1"); + + const secondFormPayload = { + form: "product-translate" as const, + productId: "product-2", + translationLanguage: "fr", + currentLanguage: "en", + fields: { + productName: { + fieldName: "productName", + originalValue: "Product 2", + translatedValue: "Produit 2", + currentValue: "Product 2", + type: "short-text" as const, + }, + }, + }; + + fireEvent( + window, + new MessageEvent("message", { + data: DashboardEventFactory.createFormEvent(secondFormPayload), + origin, + }), + ); + + const appBridgeState = appBridge.getState(); + + expect(appBridgeState.formContext?.productId).toBe("product-2"); + + if (appBridgeState.formContext?.form === "product-translate") { + expect(appBridgeState.formContext?.translationLanguage).toBe("fr"); + } + }); + }); }); diff --git a/src/app-bridge/app-bridge.ts b/src/app-bridge/app-bridge.ts index 7e5bf010..0111ad54 100644 --- a/src/app-bridge/app-bridge.ts +++ b/src/app-bridge/app-bridge.ts @@ -61,6 +61,12 @@ function eventStateReducer(state: AppBridgeState, event: Events) { token: event.payload.token, }; } + case EventType.formPayload: { + return { + ...state, + formContext: event.payload, + }; + } case EventType.response: { return state; } @@ -82,6 +88,7 @@ const createEmptySubscribeMap = (): SubscribeMap => ({ theme: {}, localeChanged: {}, tokenRefresh: {}, + formPayload: {}, }); export type AppBridgeOptions = { @@ -318,6 +325,7 @@ export class AppBridge { ({ origin, data }: Omit & { data: Events }) => { debug("Received message from origin: %s and data: %j", origin, data); + // todo: we dont need this if and referer at all if (origin !== this.refererOrigin) { debug("Origin from message doesn't match refererOrigin. Function will return now"); // TODO what should happen here - be explicit @@ -335,7 +343,7 @@ export class AppBridge { if (EventType[type]) { Object.getOwnPropertySymbols(this.subscribeMap[type]).forEach((key) => { - debug("Executing listener for event: %s and payload %j", type, payload); + debug("Executing listener for event: '%s' and payload %j", type, payload); // @ts-ignore fixme this.subscribeMap[type][key](payload); }); diff --git a/src/app-bridge/events.test.ts b/src/app-bridge/events.test.ts index 937470c7..aa5316c5 100644 --- a/src/app-bridge/events.test.ts +++ b/src/app-bridge/events.test.ts @@ -8,7 +8,7 @@ describe("DashboardEventFactory", () => { DashboardEventFactory.createHandshakeEvent("mock-token", 1, { dashboard: "3.15.3", core: "3.15.1", - }) + }), ).toEqual({ payload: { token: "mock-token", @@ -65,4 +65,67 @@ describe("DashboardEventFactory", () => { type: "tokenRefresh", }); }); + + it("Creates form payload event for product translation", () => { + const formPayload = { + form: "product-translate" as const, + productId: "product-123", + translationLanguage: "es", + currentLanguage: "en", + fields: { + productName: { + fieldName: "productName", + originalValue: "Original Product", + translatedValue: "Producto Original", + currentValue: "Original Product", + type: "short-text" as const, + }, + productDescription: { + fieldName: "productDescription", + originalValue: "Original description", + translatedValue: "Descripción original", + currentValue: "Original description", + type: "editorjs" as const, + }, + }, + }; + + expect(DashboardEventFactory.createFormEvent(formPayload)).toEqual({ + type: "formPayload", + payload: formPayload, + }); + }); + + it("Creates form payload event with all translation field types", () => { + const formPayload = { + form: "product-translate" as const, + productId: "product-456", + translationLanguage: "fr", + currentLanguage: "en", + fields: { + shortTextField: { + fieldName: "shortTextField", + originalValue: "Short text", + translatedValue: "Texte court", + currentValue: "Short text", + type: "short-text" as const, + }, + editorField: { + fieldName: "editorField", + originalValue: "{\"blocks\": []}", + translatedValue: "{\"blocks\": []}", + currentValue: "{\"blocks\": []}", + type: "editorjs" as const, + }, + }, + }; + + const event = DashboardEventFactory.createFormEvent(formPayload); + + expect(event.type).toBe("formPayload"); + if (event.payload.form === "product-translate") { + expect(event.payload.fields.shortTextField.type).toBe("short-text"); + expect(event.payload.fields.editorField.type).toBe("editorjs"); + } + }); }); diff --git a/src/app-bridge/events.ts b/src/app-bridge/events.ts index 3f69c392..7b206815 100644 --- a/src/app-bridge/events.ts +++ b/src/app-bridge/events.ts @@ -1,3 +1,5 @@ +import { AllFormPayloads, formPayloadEventName } from "@/app-bridge/form-payload"; + import { LocaleCode } from "../locales"; import { Values } from "./helpers"; @@ -10,6 +12,7 @@ export const EventType = { theme: "theme", localeChanged: "localeChanged", tokenRefresh: "tokenRefresh", + formPayload: formPayloadEventName, } as const; export type EventType = Values; @@ -66,17 +69,20 @@ export type TokenRefreshEvent = Event< } >; +export type FormDataEvent = Event; + export type Events = | HandshakeEvent | DispatchResponseEvent | RedirectEvent | ThemeEvent | LocaleChangedEvent - | TokenRefreshEvent; + | TokenRefreshEvent + | FormDataEvent; export type PayloadOfEvent< TEventType extends EventType, - TEvent extends Events = Events + TEvent extends Events = Events, // @ts-ignore TODO - why this is not working with this tsconfig? Fixme > = TEvent extends Event ? TEvent["payload"] : never; @@ -113,7 +119,7 @@ export const DashboardEventFactory = { saleorVersions?: { dashboard: string; core: string; - } + }, ): HandshakeEvent { return { type: "handshake", @@ -141,4 +147,11 @@ export const DashboardEventFactory = { }, }; }, + // EXPERIMENTAL + createFormEvent(formPayload: AllFormPayloads): FormDataEvent { + return { + type: formPayloadEventName, + payload: formPayload, + }; + }, }; diff --git a/src/app-bridge/form-payload.ts b/src/app-bridge/form-payload.ts new file mode 100644 index 00000000..9bcdcd0d --- /dev/null +++ b/src/app-bridge/form-payload.ts @@ -0,0 +1,80 @@ +export const formPayloadUpdateActionName = "formPayloadUpdate"; +export const formPayloadEventName = "formPayload"; + +export type FormPayloadUpdateSingleFieldResult = { + value: string; +}; + +export type BaseFormPayloadUpdatePayload = { + /** + * Whether POPUP should be closed after this event is emitted. Default true. For non-popup extensions will be ignored. + */ + closePopup?: boolean; + fields: Record; +}; + +type ProductPayloadBase = { + productId: string; +}; + +/** + * TRANSLATIONS + */ + +type TranslationField = { + fieldName: string; + originalValue: string; + translatedValue: string; + currentValue: string; + type: "short-text" | "editorjs"; +}; +type TranslationPayloadBase = { + translationLanguage: string; + currentLanguage: string; + fields: Record; +}; + +export type FormPayloadProductTranslate = TranslationPayloadBase & + ProductPayloadBase & { + form: "product-translate"; + }; + +export type FormPayloadUpdatePayloadProductTranslate = BaseFormPayloadUpdatePayload & { + form: "product-translate"; + fields: { + productName?: FormPayloadUpdateSingleFieldResult; + productDescription?: FormPayloadUpdateSingleFieldResult; + seoName?: FormPayloadUpdateSingleFieldResult; + seoDescription?: FormPayloadUpdateSingleFieldResult; + }; +}; + +/** + * PRODUCT + */ +export type FormPayloadProductEdit = ProductPayloadBase & { + form: "product-edit"; + fields: Record< + "productName" | "productDescription", + { + fieldName: string; + originalValue: string; + currentValue: string; + type: "short-text" | "editorjs"; + } + >; +}; + +export type FormPayloadUpdatePayloadProductEdit = BaseFormPayloadUpdatePayload & { + form: "product-edit"; + fields: { + productName?: FormPayloadUpdateSingleFieldResult; + productDescription?: FormPayloadUpdateSingleFieldResult; + }; +}; + +export type AllFormPayloads = FormPayloadProductTranslate | FormPayloadProductEdit; + +export type AllFormPayloadUpdatePayloads = + | FormPayloadUpdatePayloadProductTranslate + | FormPayloadUpdatePayloadProductEdit; diff --git a/src/app-bridge/index.ts b/src/app-bridge/index.ts index ddb56733..9dc5b3ae 100644 --- a/src/app-bridge/index.ts +++ b/src/app-bridge/index.ts @@ -7,6 +7,7 @@ export * from "./app-bridge-provider"; export * from "./app-iframe-params"; export * from "./events"; export * from "./fetch"; +export * from "./form-payload"; export * from "./types"; export * from "./use-dashboard-token"; export * from "./with-authorization"; diff --git a/src/app-bridge/next/route-propagator.tsx b/src/app-bridge/next/route-propagator.tsx index 305a605c..53387bc7 100644 --- a/src/app-bridge/next/route-propagator.tsx +++ b/src/app-bridge/next/route-propagator.tsx @@ -28,7 +28,7 @@ export const useRoutePropagator = () => { ?.dispatch( actions.UpdateRouting({ newRoute: url, - }) + }), ) .catch(() => { console.error("Error dispatching action"); diff --git a/src/handlers/platforms/aws-lambda/create-protected-handler.ts b/src/handlers/platforms/aws-lambda/create-protected-handler.ts index c4298a0d..127c0535 100644 --- a/src/handlers/platforms/aws-lambda/create-protected-handler.ts +++ b/src/handlers/platforms/aws-lambda/create-protected-handler.ts @@ -12,7 +12,7 @@ import { AwsLambdaAdapter, AWSLambdaHandler } from "./platform-adapter"; export type AwsLambdaProtectedHandler = ( event: APIGatewayProxyEventV2, context: Context, - saleorContext: ProtectedHandlerContext + saleorContext: ProtectedHandlerContext, ) => Promise | APIGatewayProxyStructuredResultV2; /** @@ -23,7 +23,7 @@ export const createProtectedHandler = ( handlerFn: AwsLambdaProtectedHandler, apl: APL, - requiredPermissions?: Permission[] + requiredPermissions?: Permission[], ): AWSLambdaHandler => async (event, context) => { const adapter = new AwsLambdaAdapter(event, context); diff --git a/src/handlers/platforms/aws-lambda/platform-adapter.ts b/src/handlers/platforms/aws-lambda/platform-adapter.ts index 606388de..834487c8 100644 --- a/src/handlers/platforms/aws-lambda/platform-adapter.ts +++ b/src/handlers/platforms/aws-lambda/platform-adapter.ts @@ -13,7 +13,7 @@ import { export type AwsLambdaHandlerInput = APIGatewayProxyEventV2; export type AWSLambdaHandler = ( event: APIGatewayProxyEventV2, - context: Context + context: Context, ) => Promise; /** PlatformAdapter for AWS Lambda HTTP events @@ -28,7 +28,10 @@ export type AWSLambdaHandler = ( export class AwsLambdaAdapter implements PlatformAdapterInterface { public request: AwsLambdaHandlerInput; - constructor(private event: APIGatewayProxyEventV2, private context: Context) { + constructor( + private event: APIGatewayProxyEventV2, + private context: Context, + ) { this.request = event; } diff --git a/src/handlers/platforms/aws-lambda/saleor-webhooks/saleor-sync-webhook.ts b/src/handlers/platforms/aws-lambda/saleor-webhooks/saleor-sync-webhook.ts index 809a2858..28300224 100644 --- a/src/handlers/platforms/aws-lambda/saleor-webhooks/saleor-sync-webhook.ts +++ b/src/handlers/platforms/aws-lambda/saleor-webhooks/saleor-sync-webhook.ts @@ -3,9 +3,7 @@ import { SyncWebhookEventType } from "@/types"; import { AWSLambdaHandler } from "../platform-adapter"; import { AwsLambdaWebhookHandler, SaleorWebApiWebhook, WebhookConfig } from "./saleor-webhook"; -export type AwsLambdaSyncWebhookHandler< - TPayload, -> = AwsLambdaWebhookHandler; +export type AwsLambdaSyncWebhookHandler = AwsLambdaWebhookHandler; export class SaleorSyncWebhook< TPayload = unknown, @@ -15,7 +13,6 @@ export class SaleorSyncWebhook< protected readonly eventType = "sync" as const; - constructor(configuration: WebhookConfig) { super(configuration); diff --git a/src/handlers/platforms/aws-lambda/test-utils.ts b/src/handlers/platforms/aws-lambda/test-utils.ts index a05d9158..b4f138a2 100644 --- a/src/handlers/platforms/aws-lambda/test-utils.ts +++ b/src/handlers/platforms/aws-lambda/test-utils.ts @@ -21,7 +21,7 @@ export function createLambdaEvent( requestContext?: Partial; path?: string; method?: "POST" | "GET"; - } = {} + } = {}, ): APIGatewayProxyEventV2 { const { path = "/some-path", diff --git a/src/handlers/platforms/next/create-app-register-handler.ts b/src/handlers/platforms/next/create-app-register-handler.ts index 425ad58c..d29163da 100644 --- a/src/handlers/platforms/next/create-app-register-handler.ts +++ b/src/handlers/platforms/next/create-app-register-handler.ts @@ -1,6 +1,4 @@ -import { - RegisterActionHandler, -} from "@/handlers/actions/register-action-handler"; +import { RegisterActionHandler } from "@/handlers/actions/register-action-handler"; import { GenericCreateAppRegisterHandlerOptions } from "@/handlers/shared/create-app-register-handler-types"; import { NextJsAdapter, NextJsHandler, NextJsHandlerInput } from "./platform-adapter"; @@ -23,9 +21,9 @@ export type CreateAppRegisterHandlerOptions = * */ export const createAppRegisterHandler = (config: CreateAppRegisterHandlerOptions): NextJsHandler => - async (req, res) => { - const adapter = new NextJsAdapter(req, res); - const actionHandler = new RegisterActionHandler(adapter); - const result = await actionHandler.handleAction(config); - return adapter.send(result); - }; + async (req, res) => { + const adapter = new NextJsAdapter(req, res); + const actionHandler = new RegisterActionHandler(adapter); + const result = await actionHandler.handleAction(config); + return adapter.send(result); + }; diff --git a/src/handlers/platforms/next/create-protected-handler.ts b/src/handlers/platforms/next/create-protected-handler.ts index c2a5920c..29fd7d71 100644 --- a/src/handlers/platforms/next/create-protected-handler.ts +++ b/src/handlers/platforms/next/create-protected-handler.ts @@ -12,7 +12,7 @@ import { NextJsAdapter } from "./platform-adapter"; export type NextJsProtectedApiHandler = ( req: NextApiRequest, res: NextApiResponse, - ctx: ProtectedHandlerContext + ctx: ProtectedHandlerContext, ) => unknown | Promise; /** @@ -23,24 +23,24 @@ export const createProtectedHandler = ( handlerFn: NextJsProtectedApiHandler, apl: APL, - requiredPermissions?: Permission[] + requiredPermissions?: Permission[], ): NextApiHandler => - async (req, res) => { - const adapter = new NextJsAdapter(req, res); - const actionValidator = new ProtectedActionValidator(adapter); - const validationResult = await actionValidator.validateRequest({ - apl, - requiredPermissions, - }); + async (req, res) => { + const adapter = new NextJsAdapter(req, res); + const actionValidator = new ProtectedActionValidator(adapter); + const validationResult = await actionValidator.validateRequest({ + apl, + requiredPermissions, + }); - if (validationResult.result === "failure") { - return adapter.send(validationResult.value); - } + if (validationResult.result === "failure") { + return adapter.send(validationResult.value); + } - const context = validationResult.value; - try { - return handlerFn(req, res, context); - } catch (err) { - return res.status(500).end(); - } - }; + const context = validationResult.value; + try { + return handlerFn(req, res, context); + } catch (err) { + return res.status(500).end(); + } + }; diff --git a/src/handlers/platforms/next/platform-adapter.test.ts b/src/handlers/platforms/next/platform-adapter.test.ts index 32b99469..a4a4496f 100644 --- a/src/handlers/platforms/next/platform-adapter.test.ts +++ b/src/handlers/platforms/next/platform-adapter.test.ts @@ -11,8 +11,8 @@ describe("NextJsAdapter", () => { it("should return single header value", () => { const { req, res } = createMocks({ headers: { - "content-type": "application/json" - } + "content-type": "application/json", + }, }); const adapter = new NextJsAdapter(req, res); expect(adapter.getHeader("content-type")).toBe("application/json"); @@ -22,8 +22,8 @@ describe("NextJsAdapter", () => { const { req, res } = createMocks({ headers: { // @ts-expect-error node-mocks-http types != real NextJsRequest - "accept": ["text/html", "application/json"] - } + accept: ["text/html", "application/json"], + }, }); const adapter = new NextJsAdapter(req, res); expect(adapter.getHeader("accept")).toBe("text/html, application/json"); @@ -40,7 +40,7 @@ describe("NextJsAdapter", () => { it("should return request body", async () => { const { req, res } = createMocks({ method: "POST", - body: { data: "test" } + body: { data: "test" }, }); const adapter = new NextJsAdapter(req, res); const body = await adapter.getBody(); @@ -52,8 +52,8 @@ describe("NextJsAdapter", () => { it("should return raw body string", async () => { const { req, res } = createMocks({ headers: { - "content-length": "10" - } + "content-length": "10", + }, }); const adapter = new NextJsAdapter(req, res); @@ -70,8 +70,8 @@ describe("NextJsAdapter", () => { const { req, res } = createMocks({ headers: { host: "example.com", - "x-forwarded-proto": "https" - } + "x-forwarded-proto": "https", + }, }); const adapter = new NextJsAdapter(req, res); expect(adapter.getBaseUrl()).toBe("https://example.com"); @@ -81,8 +81,8 @@ describe("NextJsAdapter", () => { const { req, res } = createMocks({ headers: { host: "example.com", - "x-forwarded-proto": "http,https,wss" - } + "x-forwarded-proto": "http,https,wss", + }, }); const adapter = new NextJsAdapter(req, res); expect(adapter.getBaseUrl()).toBe("https://example.com"); @@ -92,8 +92,8 @@ describe("NextJsAdapter", () => { const { req, res } = createMocks({ headers: { host: "example.com", - "x-forwarded-proto": "wss,http" - } + "x-forwarded-proto": "wss,http", + }, }); const adapter = new NextJsAdapter(req, res); expect(adapter.getBaseUrl()).toBe("http://example.com"); @@ -103,8 +103,8 @@ describe("NextJsAdapter", () => { const { req, res } = createMocks({ headers: { host: "example.com", - "x-forwarded-proto": "wss,ftp" - } + "x-forwarded-proto": "wss,ftp", + }, }); const adapter = new NextJsAdapter(req, res); expect(adapter.getBaseUrl()).toBe("wss://example.com"); @@ -114,7 +114,7 @@ describe("NextJsAdapter", () => { describe("method", () => { it("should return POST method when used in request", () => { const { req, res } = createMocks({ - method: "POST" + method: "POST", }); const adapter = new NextJsAdapter(req, res); expect(adapter.method).toBe("POST"); @@ -122,11 +122,10 @@ describe("NextJsAdapter", () => { it("should return GET method when used in request", () => { const { req, res } = createMocks({ - method: "GET" + method: "GET", }); const adapter = new NextJsAdapter(req, res); expect(adapter.method).toBe("GET"); }); }); }); - diff --git a/src/handlers/platforms/next/platform-adapter.ts b/src/handlers/platforms/next/platform-adapter.ts index 9973aea8..a9003db2 100644 --- a/src/handlers/platforms/next/platform-adapter.ts +++ b/src/handlers/platforms/next/platform-adapter.ts @@ -23,11 +23,14 @@ export type NextJsHandler = (req: NextApiRequest, res: NextApiResponse) => Promi export class NextJsAdapter implements PlatformAdapterInterface { readonly type = "next" as const; - constructor(public request: NextApiRequest, private res: NextApiResponse) {} + constructor( + public request: NextApiRequest, + private res: NextApiResponse, + ) {} getHeader(name: string) { const header = this.request.headers[name]; - return Array.isArray(header) ? header.join(", ") : header ?? null; + return Array.isArray(header) ? header.join(", ") : (header ?? null); } getBody(): Promise { diff --git a/src/handlers/shared/create-app-register-handler-types.ts b/src/handlers/shared/create-app-register-handler-types.ts index edfe2e17..b04c04df 100644 --- a/src/handlers/shared/create-app-register-handler-types.ts +++ b/src/handlers/shared/create-app-register-handler-types.ts @@ -26,7 +26,7 @@ export type GenericCreateAppRegisterHandlerOptions = HasAPL & { authToken?: string; saleorApiUrl?: string; respondWithError: CallbackErrorHandler; - } + }, ): Promise; /** * Run after all security checks @@ -36,7 +36,7 @@ export type GenericCreateAppRegisterHandlerOptions = HasAPL & { context: { authData: AuthData; respondWithError: CallbackErrorHandler; - } + }, ): Promise; /** * Run after APL successfully AuthData, assuming that APL.set will reject a Promise in case of error @@ -46,7 +46,7 @@ export type GenericCreateAppRegisterHandlerOptions = HasAPL & { context: { authData: AuthData; respondWithError: CallbackErrorHandler; - } + }, ): Promise; /** * Run after APL fails to set AuthData @@ -57,6 +57,6 @@ export type GenericCreateAppRegisterHandlerOptions = HasAPL & { authData: AuthData; error: unknown; respondWithError: CallbackErrorHandler; - } + }, ): Promise; }; diff --git a/src/handlers/shared/generic-adapter-use-case-types.ts b/src/handlers/shared/generic-adapter-use-case-types.ts index 26371b1f..1dc7d284 100644 --- a/src/handlers/shared/generic-adapter-use-case-types.ts +++ b/src/handlers/shared/generic-adapter-use-case-types.ts @@ -7,7 +7,7 @@ export const HTTPMethod = { OPTIONS: "OPTIONS", DELETE: "DELETE", } as const; -export type HTTPMethod = typeof HTTPMethod[keyof typeof HTTPMethod]; +export type HTTPMethod = (typeof HTTPMethod)[keyof typeof HTTPMethod]; /** Status code of the result, for most platforms it's mapped to HTTP status code * however when request is not HTTP it can be mapped to something else */ @@ -17,15 +17,15 @@ export type ResultStatusCodes = number; * that is then translated by adapter to a valid platform response */ export type ActionHandlerResult = | { - status: ResultStatusCodes; - body: Body; - bodyType: "json"; - } + status: ResultStatusCodes; + body: Body; + bodyType: "json"; + } | { - status: ResultStatusCodes; - body: string; - bodyType: "string"; - }; + status: ResultStatusCodes; + body: string; + bodyType: "string"; + }; /** * Interface for adapters that translate specific platform objects (e.g. Web API, Next.js) diff --git a/src/handlers/shared/validate-allow-saleor-urls.test.ts b/src/handlers/shared/validate-allow-saleor-urls.test.ts index d4cfbbcc..fb355104 100644 --- a/src/handlers/shared/validate-allow-saleor-urls.test.ts +++ b/src/handlers/shared/validate-allow-saleor-urls.test.ts @@ -21,7 +21,7 @@ describe("validateAllowSaleorUrls", () => { it("Validates against custom function provided to allow list", () => { expect(validateAllowSaleorUrls(saleorCloudUrlMock, [saleorCloudRegexValidator])).toBe(true); expect(validateAllowSaleorUrls(onPremiseSaleorUrlMock, [saleorCloudRegexValidator])).toBe( - false + false, ); }); @@ -30,13 +30,13 @@ describe("validateAllowSaleorUrls", () => { validateAllowSaleorUrls(saleorCloudUrlMock, [ saleorCloudRegexValidator, onPremiseSaleorUrlMock, - ]) + ]), ).toBe(true); expect( validateAllowSaleorUrls(onPremiseSaleorUrlMock, [ saleorCloudRegexValidator, onPremiseSaleorUrlMock, - ]) + ]), ).toBe(true); }); }); diff --git a/src/handlers/shared/validate-allow-saleor-urls.ts b/src/handlers/shared/validate-allow-saleor-urls.ts index 58a91fc4..9fa0cc4d 100644 --- a/src/handlers/shared/validate-allow-saleor-urls.ts +++ b/src/handlers/shared/validate-allow-saleor-urls.ts @@ -2,7 +2,7 @@ import { CreateAppRegisterHandlerOptions } from "../platforms/next/create-app-re export const validateAllowSaleorUrls = ( saleorApiUrl: string, - allowedUrls: CreateAppRegisterHandlerOptions["allowedSaleorUrls"] + allowedUrls: CreateAppRegisterHandlerOptions["allowedSaleorUrls"], ) => { if (!allowedUrls || allowedUrls.length === 0) { return true; diff --git a/src/has-prop.ts b/src/has-prop.ts index b409356d..95117ed8 100644 --- a/src/has-prop.ts +++ b/src/has-prop.ts @@ -5,7 +5,7 @@ */ export function hasProp( obj: unknown, - key: K | null | undefined + key: K | null | undefined, ): obj is Record { return key != null && obj != null && typeof obj === "object" && key in obj; } diff --git a/src/test-utils/mock-adapter.ts b/src/test-utils/mock-adapter.ts index 725aca00..20bf63ce 100644 --- a/src/test-utils/mock-adapter.ts +++ b/src/test-utils/mock-adapter.ts @@ -1,8 +1,10 @@ -import { HTTPMethod, PlatformAdapterInterface } from "@/handlers/shared/generic-adapter-use-case-types"; +import { + HTTPMethod, + PlatformAdapterInterface, +} from "@/handlers/shared/generic-adapter-use-case-types"; export class MockAdapter implements PlatformAdapterInterface { - constructor(public config: { mockHeaders?: Record; baseUrl?: string }) { - } + constructor(public config: { mockHeaders?: Record; baseUrl?: string }) {} send() { throw new Error("Method not implemented."); diff --git a/src/types.ts b/src/types.ts index 5fdde927..33e768bc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,8 @@ */ export type AppExtensionTarget = "POPUP" | "APP_PAGE" | "NEW_TAB" | "WIDGET"; +type AppExtensionMount3_23 = "TRANSLATION_DETAILS"; + // Available mounts in Saleor 3.22 and newer type AppExtensionMount3_22 = | "CATEGORY_OVERVIEW_CREATE" @@ -46,6 +48,7 @@ type AppExtensionMount3_22 = export type AppExtensionMount = | AppExtensionMount3_22 + | AppExtensionMount3_23 | "CUSTOMER_OVERVIEW_CREATE" | "CUSTOMER_OVERVIEW_MORE_ACTIONS" | "CUSTOMER_DETAILS_MORE_ACTIONS" diff --git a/tsconfig.json b/tsconfig.json index 517c62b1..c9ad42cd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,7 @@ "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "target": "ES2021", - "lib": [ - "dom", - "ES2021" - ], + "lib": ["dom", "ES2021"], "jsx": "react", "useDefineForClassFields": false, "module": "commonjs", @@ -16,9 +13,7 @@ "skipLibCheck": true, "baseUrl": ".", "paths": { - "@/*": [ - "src/*" - ] + "@/*": ["src/*"] } } } diff --git a/vitest.config.mts b/vitest.config.mts index be665089..fe7a85e0 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,5 +1,5 @@ import react from "@vitejs/plugin-react"; -import tsconfigPaths from "vite-tsconfig-paths" +import tsconfigPaths from "vite-tsconfig-paths"; import { defineConfig } from "vitest/config"; // https://vitejs.dev/config/