Skip to content

Commit

Permalink
Table Generator Plugin (#2)
Browse files Browse the repository at this point in the history
Plugin to generate table with customizable header and body cells
- Generate table with any header and body cells
- Update data using CSV data
  • Loading branch information
origami-z authored Jun 12, 2023
1 parent 929690f commit 4d08165
Show file tree
Hide file tree
Showing 53 changed files with 3,112 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
8 changes: 8 additions & 0 deletions jest/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ class ComponentNode {
}

global.figma = {
ui: {
postMessage: jest.fn(),
},
clientStorage: {
keysAsync: jest.fn(),
getAsync: jest.fn(),
setAsync: jest.fn(),
},
createComponent: () => new ComponentNode(),
createText: jest.fn(),
};
36 changes: 35 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions packages/table-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Generic Table Generator

Generate table with any header and body cells

![Generate table screenshot](./docs/generate-table-screenshot.png)

Update data using CSV data

![Update data screenshot](./docs/update-table-data-screenshot.png)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions packages/table-generator/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "Table Generator",
"id": "1250025074199810777",
"api": "1.0.0",
"editorType": ["figma"],
"permissions": [],
"main": "dist/code.js",
"ui": "dist/index.html",
"relaunchButtons": [
{
"command": "edit-table",
"name": "Edit table"
}
]
}
50 changes: 50 additions & 0 deletions packages/table-generator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "figma-table-generator",
"version": "0.0.1",
"description": "Table Generator Figma Plugin",
"main": "dist/code.js",
"scripts": {
"test": "jest",
"tsc": "npm run tsc:main && npm run tsc:ui && npm run tsc:tests",
"tsc:main": "tsc --noEmit -p plugin-src",
"tsc:ui": "tsc --noEmit -p ui-src",
"tsc:tests": "tsc --noEmit -p plugin-src/__tests__",
"tsc:watch": "concurrently -n widget,iframe,tests \"npm run tsc:main -- --watch --preserveWatchOutput\" \"npm run tsc:ui -- --watch --preserveWatchOutput\" \"npm run tsc:tests -- --watch --preserveWatchOutput\"",
"build": "npm run build:ui && npm run build:main -- --minify",
"build:main": "esbuild plugin-src/code.ts --bundle --outfile=dist/code.js --target=es6",
"build:ui": "npx vite build --minify esbuild --emptyOutDir=false",
"build:watch": "concurrently -n widget,iframe \"npm run build:main -- --watch\" \"npm run build:ui -- --watch\"",
"dev": "concurrently -n tsc,build,vite 'npm:tsc:watch' 'npm:build:watch' 'vite'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/jpmorganchase/Figma-Plugins-and-Widgets.git"
},
"keywords": [
"figma-plugin"
],
"author": "JPMC",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/jpmorganchase/Figma-Plugins-and-Widgets/issues"
},
"homepage": "https://github.com/jpmorganchase/Figma-Plugins-and-Widgets#readme",
"dependencies": {
"@salt-ds/core": "^1.7.1",
"@salt-ds/icons": "^1.3.1",
"@salt-ds/lab": "^1.0.0-alpha.9",
"@salt-ds/theme": "^1.5.0",
"papaparse": "^5.3.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/papaparse": "^5.3.7"
},
"jest": {
"projects": [
"ui-src/jest.config.js",
"plugin-src/jest.config.js"
]
}
}
17 changes: 17 additions & 0 deletions packages/table-generator/plugin-src/__tests__/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["es6"],
"strict": true,
"skipLibCheck": true,
"types": ["jest", "plugin-typings"],
"typeRoots": [
"../../../../node_modules/@figma",
"../../../../node_modules/@jest",
"../../../../node_modules/@types"
],
"esModuleInterop": true,
"moduleResolution": "node"
},
"include": ["./**/*.ts"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
readUISetting,
sendUISettingToUI,
setUiSetting,
} from "../../utils/clientStorage";

describe("sendUISettingToUI", () => {
test("sends message to UI", () => {
const postMessageSpy = jest.spyOn(figma.ui, "postMessage");
const testSetting = {
syncCsvHeader: true,
autoPopulateCsvColumns: true,
};
sendUISettingToUI(testSetting);
expect(postMessageSpy).toBeCalledWith(
expect.objectContaining({
setting: testSetting,
type: "read-data-table-setting-result",
})
);
});
});

describe("readUISetting", () => {
afterEach(() => {
jest.resetAllMocks();
});
test("default settings to false when first launch", async () => {
jest.spyOn(figma.clientStorage, "keysAsync").mockResolvedValue([]);
expect(await readUISetting()).toEqual({
syncCsvHeader: false,
autoPopulateCsvColumns: false,
});
});
test("reads setting correctly when value set before", async () => {
jest
.spyOn(figma.clientStorage, "keysAsync")
.mockResolvedValue(["SYNC_CSV_HEADER", "AUTO_POPULATE_CSV_COLUMNS"]);
jest.spyOn(figma.clientStorage, "getAsync").mockResolvedValue(true);
expect(await readUISetting()).toEqual({
autoPopulateCsvColumns: true,
syncCsvHeader: true,
});
});
});

describe("setUiSetting", () => {
test("set setting correctly", async () => {
const mockStore: any = {};
jest
.spyOn(figma.clientStorage, "setAsync")
.mockImplementation(async (key, value) => {
mockStore[key] = value;
});
await setUiSetting({ syncCsvHeader: false, autoPopulateCsvColumns: true });
expect(mockStore).toEqual({
SYNC_CSV_HEADER: false,
AUTO_POPULATE_CSV_COLUMNS: true,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getPreferredTextInChild } from "../../utils/data-interface";

describe("getPreferredTextInChild", () => {
test("returns empty string when no nested TextNode", () => {
const actual = getPreferredTextInChild({ children: [] } as any);
expect(actual).toBe("");
});

test("returns content when there is only one nested TextNode", () => {
const actual = getPreferredTextInChild({
children: [
{ type: "TEXT", visible: true, characters: "ABC", name: "Any" },
],
} as any);
expect(actual).toBe("ABC");
});

test("returns content with preferred name when there are more than one nested TextNode", () => {
const actual = getPreferredTextInChild({
children: [
{ type: "TEXT", visible: true, characters: "ABC", name: "Any" },
{ type: "TEXT", visible: true, characters: "XYZ", name: "Cell" },
],
} as any);
expect(actual).toBe("XYZ");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { updateColumn } from "../../utils/generate-table";

describe("updateColumn", () => {
test("not adding or removing when config rows stays the same", () => {
const mockRemove = jest.fn();
const mockAppendChild = jest.fn();
const mockColumn = {
type: "FRAME",
appendChild: mockAppendChild,
children: [
{ type: "INSTANCE", remove: mockRemove }, // header
{ type: "INSTANCE", remove: mockRemove }, // cell 1
],
} as any;

const mockComponent = {
type: "COMPONENT",
createInstance: () => ({ type: "INSTANCE", layoutAlign: "INHERIT" }),
} as any;

updateColumn(mockColumn, 1, mockComponent);
expect(mockAppendChild).not.toBeCalled();
expect(mockRemove).not.toBeCalled();
});

test("removes excess cells when new config has less rows", () => {
const mockRemove = jest.fn();
const mockAppendChild = jest.fn();
const mockColumn = {
type: "FRAME",
appendChild: mockAppendChild,
children: [
{ type: "INSTANCE", remove: mockRemove }, // header
{ type: "INSTANCE", remove: mockRemove }, // cell 1
{ type: "INSTANCE", remove: mockRemove }, // cell 2
{ type: "INSTANCE", remove: mockRemove }, // cell 3
],
} as any;

const mockComponent = {
type: "COMPONENT",
createInstance: () => ({ type: "INSTANCE", layoutAlign: "INHERIT" }),
} as any;

updateColumn(mockColumn, 1, mockComponent);
expect(mockAppendChild).not.toBeCalled();
expect(mockRemove).toBeCalledTimes(2);
});

test("adds missing cells when new config has more rows", () => {
const mockRemove = jest.fn();
const mockAppendChild = jest.fn();
const mockColumn = {
type: "FRAME",
appendChild: mockAppendChild,
children: [
{ type: "INSTANCE", remove: mockRemove }, // header
{ type: "INSTANCE", remove: mockRemove }, // cell 1
],
} as any;

const mockComponent = {
type: "COMPONENT",
createInstance: () => ({ type: "INSTANCE", layoutAlign: "INHERIT" }),
} as any;

updateColumn(mockColumn, 3, mockComponent);
expect(mockAppendChild).toBeCalledTimes(2);
expect(mockRemove).not.toBeCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { setRelaunchButton } from "../../utils/relaunch";

describe("setRelaunchButton", () => {
test("sets relaunch data with empty string", () => {
const mockSet = jest.fn();
setRelaunchButton({ setRelaunchData: mockSet } as any);

expect(mockSet).toBeCalledWith({ "edit-table": "" });
});
});
Loading

0 comments on commit 4d08165

Please sign in to comment.