From b95ebb4ae5115b8b150f387c032603c294b535a6 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Wed, 17 Aug 2022 12:54:28 +0200 Subject: [PATCH 1/4] refactor: implement adapter pattern for the Figma Plugin API in order to make it easier to mock --- src/commands-setup/CommandsMapping.ts | 8 +++++++- src/domain/FigmaPluginApi.ts | 3 +++ src/infrastructure/OfficialFigmaPluginApi.ts | 9 +++++++++ src/scene-commands/cancel/CancelCommandHandler.ts | 5 +++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/domain/FigmaPluginApi.ts create mode 100644 src/infrastructure/OfficialFigmaPluginApi.ts diff --git a/src/commands-setup/CommandsMapping.ts b/src/commands-setup/CommandsMapping.ts index a63356e..e46cde8 100644 --- a/src/commands-setup/CommandsMapping.ts +++ b/src/commands-setup/CommandsMapping.ts @@ -1,4 +1,5 @@ import { NetworkRequestCommandHandler } from "../browser-commands/network-request/NetworkRequestCommandHandler"; +import { OfficialFigmaPluginApi } from "../infrastructure/OfficialFigmaPluginApi"; import { CancelCommandHandler } from "../scene-commands/cancel/CancelCommandHandler"; import { CreateShapesCommandHandler } from "../scene-commands/create-shapes/CreateShapesCommandHandler"; import { PaintCurrentUserAvatarCommandHandler } from "../scene-commands/paint-current-user-avatar/PaintCurrentUserAvatarCommandHandler"; @@ -8,8 +9,13 @@ import { CommandHandler } from "./CommandHandler"; // πŸ‘‹ Add below your new commands. // Define its arbitrary key and its corresponding Handler class. // Tip: Declare your Command and CommandHandler classes creating a folder inside the `src/scene-commands` or `src/browser-commands` ones depending on the things you need to get access to (see the README explanation) 😊 +const officialFigmaPluginApi = new OfficialFigmaPluginApi(figma); +const cancelCommandHandler = CancelCommandHandler.withFigmaAdapter( + officialFigmaPluginApi +); + export const CommandsMapping: Record CommandHandler> = { - cancel: () => new CancelCommandHandler(figma), + cancel: () => cancelCommandHandler, createShapes: () => new CreateShapesCommandHandler(figma), paintCurrentUserAvatar: () => new PaintCurrentUserAvatarCommandHandler(figma), networkRequest: () => new NetworkRequestCommandHandler(), diff --git a/src/domain/FigmaPluginApi.ts b/src/domain/FigmaPluginApi.ts new file mode 100644 index 0000000..8048dfe --- /dev/null +++ b/src/domain/FigmaPluginApi.ts @@ -0,0 +1,3 @@ +export interface FigmaPluginApi { + notify(message: string): void; +} diff --git a/src/infrastructure/OfficialFigmaPluginApi.ts b/src/infrastructure/OfficialFigmaPluginApi.ts new file mode 100644 index 0000000..c14c853 --- /dev/null +++ b/src/infrastructure/OfficialFigmaPluginApi.ts @@ -0,0 +1,9 @@ +import { FigmaPluginApi } from "../domain/FigmaPluginApi"; + +export class OfficialFigmaPluginApi implements FigmaPluginApi { + constructor(private readonly figma: PluginAPI) {} + + notify(message: string): void { + this.figma.notify(message); + } +} diff --git a/src/scene-commands/cancel/CancelCommandHandler.ts b/src/scene-commands/cancel/CancelCommandHandler.ts index a032ef3..31cdfe9 100644 --- a/src/scene-commands/cancel/CancelCommandHandler.ts +++ b/src/scene-commands/cancel/CancelCommandHandler.ts @@ -1,9 +1,14 @@ import { CommandHandler } from "../../commands-setup/CommandHandler"; +import { FigmaPluginApi } from "../../domain/FigmaPluginApi"; import { CancelCommand } from "./CancelCommand"; export class CancelCommandHandler implements CommandHandler { constructor(private readonly figma: PluginAPI) {} + static withFigmaAdapter(figma: FigmaPluginApi) { + return new this(figma as PluginAPI); + } + // `command` argument needed due to polymorphism. // eslint-disable-next-line @typescript-eslint/no-unused-vars handle(command: CancelCommand): void { From b3cb438407a3b67a9e2a2e0e6aa23299fcdf4418 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Wed, 17 Aug 2022 12:57:41 +0200 Subject: [PATCH 2/4] refactor: illustrative example of a parallel change refactoring process --- tests/scene-commands/CancelCommandHandler.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/scene-commands/CancelCommandHandler.test.ts b/tests/scene-commands/CancelCommandHandler.test.ts index af94aa9..d346132 100644 --- a/tests/scene-commands/CancelCommandHandler.test.ts +++ b/tests/scene-commands/CancelCommandHandler.test.ts @@ -1,14 +1,15 @@ import { mock } from "jest-mock-extended"; +import { FigmaPluginApi } from "../../src/domain/FigmaPluginApi"; import { CancelCommand } from "../../src/scene-commands/cancel/CancelCommand"; import { CancelCommandHandler } from "../../src/scene-commands/cancel/CancelCommandHandler"; describe("CancelCommandHandler", () => { it("can be instantiated without throwing errors", () => { - const figmaPluginApiMock = mock(); + const figmaPluginApiMock = mock(); const cancelCommandHandlerInstantiator = () => { - new CancelCommandHandler(figmaPluginApiMock); + CancelCommandHandler.withFigmaAdapter(figmaPluginApiMock); }; expect(cancelCommandHandlerInstantiator).not.toThrow(TypeError); From d4b0526eaba8e9b7deb639c8fb5e4f8f02e99b32 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Wed, 17 Aug 2022 12:59:50 +0200 Subject: [PATCH 3/4] refactor: finish the parallel change refactoring process --- src/commands-setup/CommandsMapping.ts | 4 +--- src/scene-commands/cancel/CancelCommandHandler.ts | 6 +----- tests/scene-commands/CancelCommandHandler.test.ts | 4 ++-- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/commands-setup/CommandsMapping.ts b/src/commands-setup/CommandsMapping.ts index e46cde8..e27d574 100644 --- a/src/commands-setup/CommandsMapping.ts +++ b/src/commands-setup/CommandsMapping.ts @@ -10,9 +10,7 @@ import { CommandHandler } from "./CommandHandler"; // Define its arbitrary key and its corresponding Handler class. // Tip: Declare your Command and CommandHandler classes creating a folder inside the `src/scene-commands` or `src/browser-commands` ones depending on the things you need to get access to (see the README explanation) 😊 const officialFigmaPluginApi = new OfficialFigmaPluginApi(figma); -const cancelCommandHandler = CancelCommandHandler.withFigmaAdapter( - officialFigmaPluginApi -); +const cancelCommandHandler = new CancelCommandHandler(officialFigmaPluginApi); export const CommandsMapping: Record CommandHandler> = { cancel: () => cancelCommandHandler, diff --git a/src/scene-commands/cancel/CancelCommandHandler.ts b/src/scene-commands/cancel/CancelCommandHandler.ts index 31cdfe9..dc48f20 100644 --- a/src/scene-commands/cancel/CancelCommandHandler.ts +++ b/src/scene-commands/cancel/CancelCommandHandler.ts @@ -3,11 +3,7 @@ import { FigmaPluginApi } from "../../domain/FigmaPluginApi"; import { CancelCommand } from "./CancelCommand"; export class CancelCommandHandler implements CommandHandler { - constructor(private readonly figma: PluginAPI) {} - - static withFigmaAdapter(figma: FigmaPluginApi) { - return new this(figma as PluginAPI); - } + constructor(private readonly figma: FigmaPluginApi) {} // `command` argument needed due to polymorphism. // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/tests/scene-commands/CancelCommandHandler.test.ts b/tests/scene-commands/CancelCommandHandler.test.ts index d346132..dfa6eaa 100644 --- a/tests/scene-commands/CancelCommandHandler.test.ts +++ b/tests/scene-commands/CancelCommandHandler.test.ts @@ -9,14 +9,14 @@ describe("CancelCommandHandler", () => { const figmaPluginApiMock = mock(); const cancelCommandHandlerInstantiator = () => { - CancelCommandHandler.withFigmaAdapter(figmaPluginApiMock); + new CancelCommandHandler(figmaPluginApiMock); }; expect(cancelCommandHandlerInstantiator).not.toThrow(TypeError); }); it("notifies the end used with a farewell message", () => { - const figmaPluginApiMock = mock(); + const figmaPluginApiMock = mock(); const cancelCommandHandler = new CancelCommandHandler(figmaPluginApiMock); const randomCancelCommand = new CancelCommand(); From b064d13cbc7192888d33193e82b088b3f0b905f3 Mon Sep 17 00:00:00 2001 From: Javier Ferrer Date: Mon, 29 Aug 2022 16:08:24 +0200 Subject: [PATCH 4/4] refactor: implement adapter pattern for the `currentUser`, `request` and `select` Co-authored-by: Isma Navarro --- .../network-request/NetworkRequestCommand.ts | 2 +- src/domain/FigmaPluginApi.ts | 18 ++++++ src/infrastructure/OfficialFigmaPluginApi.ts | 56 ++++++++++++++++++- .../PaintCurrentUserAvatarCommandHandler.ts | 41 ++++---------- 4 files changed, 85 insertions(+), 32 deletions(-) diff --git a/src/browser-commands/network-request/NetworkRequestCommand.ts b/src/browser-commands/network-request/NetworkRequestCommand.ts index e9fb499..5a2d2a5 100644 --- a/src/browser-commands/network-request/NetworkRequestCommand.ts +++ b/src/browser-commands/network-request/NetworkRequestCommand.ts @@ -1,6 +1,6 @@ import { Command } from "../../commands-setup/Command"; -type SupportedResponseTypes = "text" | "arraybuffer"; +export type SupportedResponseTypes = "application/json" | "arraybuffer"; export class NetworkRequestCommand implements Command { readonly type = "networkRequest"; diff --git a/src/domain/FigmaPluginApi.ts b/src/domain/FigmaPluginApi.ts index 8048dfe..2a869f3 100644 --- a/src/domain/FigmaPluginApi.ts +++ b/src/domain/FigmaPluginApi.ts @@ -1,3 +1,21 @@ +import { SupportedResponseTypes } from "../browser-commands/network-request/NetworkRequestCommand"; + +export type FigmaUser = { + photoUrl: string; + name: string; +}; + export interface FigmaPluginApi { + currentUser(): FigmaUser; + notify(message: string): void; + + select(message: string): void; + + request( + url: string, + responseType: ResponseType + ): Promise< + ResponseType extends "arraybuffer" ? ArrayBuffer : Record + >; } diff --git a/src/infrastructure/OfficialFigmaPluginApi.ts b/src/infrastructure/OfficialFigmaPluginApi.ts index c14c853..840befc 100644 --- a/src/infrastructure/OfficialFigmaPluginApi.ts +++ b/src/infrastructure/OfficialFigmaPluginApi.ts @@ -1,9 +1,63 @@ -import { FigmaPluginApi } from "../domain/FigmaPluginApi"; +import { + NetworkRequestCommand, + SupportedResponseTypes, +} from "../browser-commands/network-request/NetworkRequestCommand"; +import { executeCommand } from "../commands-setup/executeCommand"; +import { FigmaPluginApi, FigmaUser } from "../domain/FigmaPluginApi"; export class OfficialFigmaPluginApi implements FigmaPluginApi { constructor(private readonly figma: PluginAPI) {} + currentUser(): FigmaUser { + const currentUserAvatarUrl = this.figma.currentUser?.photoUrl; + const currentUserName = this.figma.currentUser?.name; + + if (currentUserAvatarUrl === undefined || currentUserAvatarUrl === null) { + throw new Error("Current user does not have a photo"); + } + + if (currentUserName === undefined || currentUserName === null) { + throw new Error("Current user does not have a name"); + } + + return { photoUrl: currentUserAvatarUrl, name: currentUserName }; + } + notify(message: string): void { this.figma.notify(message); } + + // 🚩🚩🚩 🚩🚩🚩 🚩🚩🚩 🚩🚩🚩 🚩🚩🚩 + // 🚩🚩🚩 Gente que se flipa desacoplando 🚩🚩🚩 + // 🚩🚩🚩 TendrΓ­amos que modelar nuestro SceneNode 🚩🚩🚩 + // 🚩🚩🚩 🚩🚩🚩 🚩🚩🚩 🚩🚩🚩 🚩🚩🚩 + select(elements: ReadonlyArray): void { + this.figma.currentPage.selection = elements; + } + + request( + url: string, + responseType: ResponseType + ): Promise< + ResponseType extends "arraybuffer" ? ArrayBuffer : Record + > { + executeCommand(new NetworkRequestCommand(url, responseType)); + + return new Promise((resolve) => { + this.figma.ui.onmessage = async (command) => { + this.ensureToOnlyReceiveNetworkRequestResponse(command); + + resolve(command.payload); + }; + }); + } + + private ensureToOnlyReceiveNetworkRequestResponse(command: { type: string }) { + if (command.type !== "networkRequestResponse") { + const errorMessage = + "Unexpected command received while performing the request for painting the user avatar."; + + throw new Error(errorMessage); + } + } } diff --git a/src/scene-commands/paint-current-user-avatar/PaintCurrentUserAvatarCommandHandler.ts b/src/scene-commands/paint-current-user-avatar/PaintCurrentUserAvatarCommandHandler.ts index b4cf5dc..af3cc34 100644 --- a/src/scene-commands/paint-current-user-avatar/PaintCurrentUserAvatarCommandHandler.ts +++ b/src/scene-commands/paint-current-user-avatar/PaintCurrentUserAvatarCommandHandler.ts @@ -1,6 +1,5 @@ -import { NetworkRequestCommand } from "../../browser-commands/network-request/NetworkRequestCommand"; import { CommandHandler } from "../../commands-setup/CommandHandler"; -import { executeCommand } from "../../commands-setup/executeCommand"; +import { FigmaPluginApi, FigmaUser } from "../../domain/FigmaPluginApi"; import { PaintCurrentUserAvatarCommand } from "./PaintCurrentUserAvatarCommand"; export class PaintCurrentUserAvatarCommandHandler @@ -8,45 +7,27 @@ export class PaintCurrentUserAvatarCommandHandler { private readonly avatarImageSize = 100; - constructor(private readonly figma: PluginAPI) {} + constructor(private readonly figma: FigmaPluginApi) {} // `command` argument needed due to polymorphism. // eslint-disable-next-line @typescript-eslint/no-unused-vars - handle(command: PaintCurrentUserAvatarCommand): Promise { - const currentUserAvatarUrl = this.figma.currentUser?.photoUrl; - const currentUserName = this.figma.currentUser?.name; + async handle(command: PaintCurrentUserAvatarCommand): Promise { + let currentUser: FigmaUser; - if (currentUserAvatarUrl === undefined || currentUserAvatarUrl === null) { + try { + currentUser = this.figma.currentUser(); + } catch (e) { this.figma.notify("Sorry but you do not have an avatar to add πŸ˜…"); return Promise.resolve(); } - const responseType = "arraybuffer"; - executeCommand( - new NetworkRequestCommand(currentUserAvatarUrl, responseType) + const response = await this.figma.request( + currentUser.photoUrl, + "arraybuffer" ); - return new Promise((resolve) => { - this.figma.ui.onmessage = async (command) => { - this.ensureToOnlyReceiveNetworkRequestResponse(command); - - await this.createAvatarBadge( - command.payload as ArrayBuffer, - currentUserName as string - ); - resolve(); - }; - }); - } - - private ensureToOnlyReceiveNetworkRequestResponse(command: { type: string }) { - if (command.type !== "networkRequestResponse") { - const errorMessage = - "Unexpected command received while performing the request for painting the user avatar."; - - throw new Error(errorMessage); - } + await this.createAvatarBadge(response, currentUser.name); } private async createAvatarBadge(