From 7c49a0db452b2c2f7a4e6284064f330605a0d487 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Mon, 5 May 2025 07:43:25 -0400 Subject: [PATCH 01/16] Increase plugin UI size --- plugins/notion/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/notion/src/App.tsx b/plugins/notion/src/App.tsx index 6a64dc31..d592eac3 100644 --- a/plugins/notion/src/App.tsx +++ b/plugins/notion/src/App.tsx @@ -23,8 +23,8 @@ export function AuthenticatedApp({ context }: AuthenticatedAppProps) { ) useEffect(() => { - const width = databaseConfig ? 360 : 325 - const height = databaseConfig ? 425 : 370 + const width = databaseConfig ? 600 : 325 + const height = databaseConfig ? 500 : 370 framer.showUI({ width, height, From b00e8f653f167298b4bc8253dfe5ecfa2c0df53c Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Mon, 5 May 2025 07:58:05 -0400 Subject: [PATCH 02/16] Update to framer-plugin 3.1.0 --- plugins/notion/package.json | 4 ++-- plugins/notion/src/notion.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/notion/package.json b/plugins/notion/package.json index bcd7e2c9..62f88a1a 100644 --- a/plugins/notion/package.json +++ b/plugins/notion/package.json @@ -15,7 +15,7 @@ "@notionhq/client": "^2.2.15", "@tanstack/react-query": "^5.29.2", "classnames": "^2.5.1", - "framer-plugin": "^2.0.4", + "framer-plugin": "^3.1.0", "react": "^18", "react-dom": "^18", "react-error-boundary": "^4.0.13", @@ -35,6 +35,6 @@ "tailwindcss": "^3.4.3", "typescript": "^5.3.3", "vite": "^6", - "vite-plugin-framer": "^1.0.1" + "vite-plugin-framer": "^1.0.7" } } diff --git a/plugins/notion/src/notion.ts b/plugins/notion/src/notion.ts index 129ef31d..a629b6b8 100644 --- a/plugins/notion/src/notion.ts +++ b/plugins/notion/src/notion.ts @@ -14,7 +14,7 @@ import { RichTextItemResponse, } from "@notionhq/client/build/src/api-endpoints" import { useMutation, useQuery } from "@tanstack/react-query" -import { CollectionItemData, framer, ManagedCollection, ManagedCollectionField } from "framer-plugin" +import { CollectionItemData, framer, ManagedCollection, ManagedCollectionField, FieldData } from "framer-plugin" import pLimit from "p-limit" import { blocksToHtml, richTextToHTML } from "./blocksToHTML" import { assert, assertNever, formatDate, isDefined, isString, slugify } from "./utils" @@ -506,7 +506,7 @@ async function processItem( ): Promise { let slugValue: null | string = null - const fieldData: Record = {} + const fieldData: FieldData = {} assert(isFullPage(item)) @@ -539,12 +539,12 @@ async function processItem( }) } - fieldData[field.id] = fieldValue + fieldData[field.id] = { type: field.type, value: fieldValue } } if (fieldsById.has(pageContentProperty.id) && item.id) { const contentHTML = await getPageBlocksAsRichText(item.id) - fieldData[pageContentProperty.id] = contentHTML + fieldData[pageContentProperty.id] = { type: "formattedText", value: contentHTML } } if (!slugValue) { From 6d849ef2b927b22db24dc3cf25d1fb5d765a17ea Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Fri, 9 May 2025 22:17:25 -0400 Subject: [PATCH 03/16] Move notion to notion-legacy --- plugins/{notion => notion-legacy}/.eslintrc.cjs | 0 plugins/{notion => notion-legacy}/.gitignore | 0 plugins/{notion => notion-legacy}/README.md | 0 plugins/{notion => notion-legacy}/framer.json | 0 plugins/{notion => notion-legacy}/index.html | 0 plugins/{notion => notion-legacy}/package.json | 0 plugins/{notion => notion-legacy}/postcss.config.js | 0 plugins/{notion => notion-legacy}/public/icon.png | Bin plugins/{notion => notion-legacy}/src/App.css | 0 plugins/{notion => notion-legacy}/src/App.tsx | 0 .../{notion => notion-legacy}/src/Authenticate.tsx | 0 plugins/{notion => notion-legacy}/src/MapFields.tsx | 0 .../src/SelectDatabase.tsx | 0 .../src/assets/notion-connect.png | Bin .../src/assets/notion-doodle.png | Bin .../src/assets/notion-login.png | Bin .../{notion => notion-legacy}/src/blocksToHTML.ts | 0 .../src/components/Button.tsx | 0 .../src/components/CenteredSpinner.tsx | 0 .../src/components/CheckboxTexfield.tsx | 0 .../src/components/ErrorBoundaryFallback.tsx | 0 .../src/components/Icons.tsx | 0 .../src/components/Spinner.tsx | 0 .../src/components/spinner.module.css | 0 plugins/{notion => notion-legacy}/src/debug.ts | 0 plugins/{notion => notion-legacy}/src/globals.css | 0 plugins/{notion => notion-legacy}/src/main.tsx | 0 plugins/{notion => notion-legacy}/src/notion.ts | 0 plugins/{notion => notion-legacy}/src/utils.ts | 0 plugins/{notion => notion-legacy}/src/vite-env.d.ts | 0 .../{notion => notion-legacy}/tailwind.config.js | 0 plugins/{notion => notion-legacy}/tsconfig.json | 0 plugins/{notion => notion-legacy}/vite.config.ts | 0 33 files changed, 0 insertions(+), 0 deletions(-) rename plugins/{notion => notion-legacy}/.eslintrc.cjs (100%) rename plugins/{notion => notion-legacy}/.gitignore (100%) rename plugins/{notion => notion-legacy}/README.md (100%) rename plugins/{notion => notion-legacy}/framer.json (100%) rename plugins/{notion => notion-legacy}/index.html (100%) rename plugins/{notion => notion-legacy}/package.json (100%) rename plugins/{notion => notion-legacy}/postcss.config.js (100%) rename plugins/{notion => notion-legacy}/public/icon.png (100%) rename plugins/{notion => notion-legacy}/src/App.css (100%) rename plugins/{notion => notion-legacy}/src/App.tsx (100%) rename plugins/{notion => notion-legacy}/src/Authenticate.tsx (100%) rename plugins/{notion => notion-legacy}/src/MapFields.tsx (100%) rename plugins/{notion => notion-legacy}/src/SelectDatabase.tsx (100%) rename plugins/{notion => notion-legacy}/src/assets/notion-connect.png (100%) rename plugins/{notion => notion-legacy}/src/assets/notion-doodle.png (100%) rename plugins/{notion => notion-legacy}/src/assets/notion-login.png (100%) rename plugins/{notion => notion-legacy}/src/blocksToHTML.ts (100%) rename plugins/{notion => notion-legacy}/src/components/Button.tsx (100%) rename plugins/{notion => notion-legacy}/src/components/CenteredSpinner.tsx (100%) rename plugins/{notion => notion-legacy}/src/components/CheckboxTexfield.tsx (100%) rename plugins/{notion => notion-legacy}/src/components/ErrorBoundaryFallback.tsx (100%) rename plugins/{notion => notion-legacy}/src/components/Icons.tsx (100%) rename plugins/{notion => notion-legacy}/src/components/Spinner.tsx (100%) rename plugins/{notion => notion-legacy}/src/components/spinner.module.css (100%) rename plugins/{notion => notion-legacy}/src/debug.ts (100%) rename plugins/{notion => notion-legacy}/src/globals.css (100%) rename plugins/{notion => notion-legacy}/src/main.tsx (100%) rename plugins/{notion => notion-legacy}/src/notion.ts (100%) rename plugins/{notion => notion-legacy}/src/utils.ts (100%) rename plugins/{notion => notion-legacy}/src/vite-env.d.ts (100%) rename plugins/{notion => notion-legacy}/tailwind.config.js (100%) rename plugins/{notion => notion-legacy}/tsconfig.json (100%) rename plugins/{notion => notion-legacy}/vite.config.ts (100%) diff --git a/plugins/notion/.eslintrc.cjs b/plugins/notion-legacy/.eslintrc.cjs similarity index 100% rename from plugins/notion/.eslintrc.cjs rename to plugins/notion-legacy/.eslintrc.cjs diff --git a/plugins/notion/.gitignore b/plugins/notion-legacy/.gitignore similarity index 100% rename from plugins/notion/.gitignore rename to plugins/notion-legacy/.gitignore diff --git a/plugins/notion/README.md b/plugins/notion-legacy/README.md similarity index 100% rename from plugins/notion/README.md rename to plugins/notion-legacy/README.md diff --git a/plugins/notion/framer.json b/plugins/notion-legacy/framer.json similarity index 100% rename from plugins/notion/framer.json rename to plugins/notion-legacy/framer.json diff --git a/plugins/notion/index.html b/plugins/notion-legacy/index.html similarity index 100% rename from plugins/notion/index.html rename to plugins/notion-legacy/index.html diff --git a/plugins/notion/package.json b/plugins/notion-legacy/package.json similarity index 100% rename from plugins/notion/package.json rename to plugins/notion-legacy/package.json diff --git a/plugins/notion/postcss.config.js b/plugins/notion-legacy/postcss.config.js similarity index 100% rename from plugins/notion/postcss.config.js rename to plugins/notion-legacy/postcss.config.js diff --git a/plugins/notion/public/icon.png b/plugins/notion-legacy/public/icon.png similarity index 100% rename from plugins/notion/public/icon.png rename to plugins/notion-legacy/public/icon.png diff --git a/plugins/notion/src/App.css b/plugins/notion-legacy/src/App.css similarity index 100% rename from plugins/notion/src/App.css rename to plugins/notion-legacy/src/App.css diff --git a/plugins/notion/src/App.tsx b/plugins/notion-legacy/src/App.tsx similarity index 100% rename from plugins/notion/src/App.tsx rename to plugins/notion-legacy/src/App.tsx diff --git a/plugins/notion/src/Authenticate.tsx b/plugins/notion-legacy/src/Authenticate.tsx similarity index 100% rename from plugins/notion/src/Authenticate.tsx rename to plugins/notion-legacy/src/Authenticate.tsx diff --git a/plugins/notion/src/MapFields.tsx b/plugins/notion-legacy/src/MapFields.tsx similarity index 100% rename from plugins/notion/src/MapFields.tsx rename to plugins/notion-legacy/src/MapFields.tsx diff --git a/plugins/notion/src/SelectDatabase.tsx b/plugins/notion-legacy/src/SelectDatabase.tsx similarity index 100% rename from plugins/notion/src/SelectDatabase.tsx rename to plugins/notion-legacy/src/SelectDatabase.tsx diff --git a/plugins/notion/src/assets/notion-connect.png b/plugins/notion-legacy/src/assets/notion-connect.png similarity index 100% rename from plugins/notion/src/assets/notion-connect.png rename to plugins/notion-legacy/src/assets/notion-connect.png diff --git a/plugins/notion/src/assets/notion-doodle.png b/plugins/notion-legacy/src/assets/notion-doodle.png similarity index 100% rename from plugins/notion/src/assets/notion-doodle.png rename to plugins/notion-legacy/src/assets/notion-doodle.png diff --git a/plugins/notion/src/assets/notion-login.png b/plugins/notion-legacy/src/assets/notion-login.png similarity index 100% rename from plugins/notion/src/assets/notion-login.png rename to plugins/notion-legacy/src/assets/notion-login.png diff --git a/plugins/notion/src/blocksToHTML.ts b/plugins/notion-legacy/src/blocksToHTML.ts similarity index 100% rename from plugins/notion/src/blocksToHTML.ts rename to plugins/notion-legacy/src/blocksToHTML.ts diff --git a/plugins/notion/src/components/Button.tsx b/plugins/notion-legacy/src/components/Button.tsx similarity index 100% rename from plugins/notion/src/components/Button.tsx rename to plugins/notion-legacy/src/components/Button.tsx diff --git a/plugins/notion/src/components/CenteredSpinner.tsx b/plugins/notion-legacy/src/components/CenteredSpinner.tsx similarity index 100% rename from plugins/notion/src/components/CenteredSpinner.tsx rename to plugins/notion-legacy/src/components/CenteredSpinner.tsx diff --git a/plugins/notion/src/components/CheckboxTexfield.tsx b/plugins/notion-legacy/src/components/CheckboxTexfield.tsx similarity index 100% rename from plugins/notion/src/components/CheckboxTexfield.tsx rename to plugins/notion-legacy/src/components/CheckboxTexfield.tsx diff --git a/plugins/notion/src/components/ErrorBoundaryFallback.tsx b/plugins/notion-legacy/src/components/ErrorBoundaryFallback.tsx similarity index 100% rename from plugins/notion/src/components/ErrorBoundaryFallback.tsx rename to plugins/notion-legacy/src/components/ErrorBoundaryFallback.tsx diff --git a/plugins/notion/src/components/Icons.tsx b/plugins/notion-legacy/src/components/Icons.tsx similarity index 100% rename from plugins/notion/src/components/Icons.tsx rename to plugins/notion-legacy/src/components/Icons.tsx diff --git a/plugins/notion/src/components/Spinner.tsx b/plugins/notion-legacy/src/components/Spinner.tsx similarity index 100% rename from plugins/notion/src/components/Spinner.tsx rename to plugins/notion-legacy/src/components/Spinner.tsx diff --git a/plugins/notion/src/components/spinner.module.css b/plugins/notion-legacy/src/components/spinner.module.css similarity index 100% rename from plugins/notion/src/components/spinner.module.css rename to plugins/notion-legacy/src/components/spinner.module.css diff --git a/plugins/notion/src/debug.ts b/plugins/notion-legacy/src/debug.ts similarity index 100% rename from plugins/notion/src/debug.ts rename to plugins/notion-legacy/src/debug.ts diff --git a/plugins/notion/src/globals.css b/plugins/notion-legacy/src/globals.css similarity index 100% rename from plugins/notion/src/globals.css rename to plugins/notion-legacy/src/globals.css diff --git a/plugins/notion/src/main.tsx b/plugins/notion-legacy/src/main.tsx similarity index 100% rename from plugins/notion/src/main.tsx rename to plugins/notion-legacy/src/main.tsx diff --git a/plugins/notion/src/notion.ts b/plugins/notion-legacy/src/notion.ts similarity index 100% rename from plugins/notion/src/notion.ts rename to plugins/notion-legacy/src/notion.ts diff --git a/plugins/notion/src/utils.ts b/plugins/notion-legacy/src/utils.ts similarity index 100% rename from plugins/notion/src/utils.ts rename to plugins/notion-legacy/src/utils.ts diff --git a/plugins/notion/src/vite-env.d.ts b/plugins/notion-legacy/src/vite-env.d.ts similarity index 100% rename from plugins/notion/src/vite-env.d.ts rename to plugins/notion-legacy/src/vite-env.d.ts diff --git a/plugins/notion/tailwind.config.js b/plugins/notion-legacy/tailwind.config.js similarity index 100% rename from plugins/notion/tailwind.config.js rename to plugins/notion-legacy/tailwind.config.js diff --git a/plugins/notion/tsconfig.json b/plugins/notion-legacy/tsconfig.json similarity index 100% rename from plugins/notion/tsconfig.json rename to plugins/notion-legacy/tsconfig.json diff --git a/plugins/notion/vite.config.ts b/plugins/notion-legacy/vite.config.ts similarity index 100% rename from plugins/notion/vite.config.ts rename to plugins/notion-legacy/vite.config.ts From 4707c87457d00da70b5fcec0e99cea86f72470b7 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Fri, 9 May 2025 22:19:42 -0400 Subject: [PATCH 04/16] Create CMS starter for new Notion plugin --- package-lock.json | 1276 +++++++++++++++++++- plugins/notion-legacy/framer.json | 2 +- plugins/notion-legacy/package.json | 2 +- plugins/notion/.gitignore | 3 + plugins/notion/.prettierrc.json | 7 + plugins/notion/README.md | 19 + plugins/notion/eslint.config.ts | 21 + plugins/notion/framer.json | 6 + plugins/notion/index.html | 14 + plugins/notion/package.json | 40 + plugins/notion/public/data/articles.json | 56 + plugins/notion/public/data/categories.json | 30 + plugins/notion/public/icon.png | Bin 0 -> 3938 bytes plugins/notion/src/App.css | 180 +++ plugins/notion/src/App.tsx | 76 ++ plugins/notion/src/FieldMapping.tsx | 229 ++++ plugins/notion/src/SelectDataSource.tsx | 71 ++ plugins/notion/src/data.ts | 186 +++ plugins/notion/src/main.tsx | 33 + plugins/notion/tsconfig.json | 22 + plugins/notion/vite.config.ts | 6 + 21 files changed, 2241 insertions(+), 38 deletions(-) create mode 100644 plugins/notion/.gitignore create mode 100644 plugins/notion/.prettierrc.json create mode 100644 plugins/notion/README.md create mode 100644 plugins/notion/eslint.config.ts create mode 100644 plugins/notion/framer.json create mode 100644 plugins/notion/index.html create mode 100644 plugins/notion/package.json create mode 100644 plugins/notion/public/data/articles.json create mode 100644 plugins/notion/public/data/categories.json create mode 100644 plugins/notion/public/icon.png create mode 100644 plugins/notion/src/App.css create mode 100644 plugins/notion/src/App.tsx create mode 100644 plugins/notion/src/FieldMapping.tsx create mode 100644 plugins/notion/src/SelectDataSource.tsx create mode 100644 plugins/notion/src/data.ts create mode 100644 plugins/notion/src/main.tsx create mode 100644 plugins/notion/tsconfig.json create mode 100644 plugins/notion/vite.config.ts diff --git a/package-lock.json b/package-lock.json index 198a70ad..7baca6ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -837,6 +837,16 @@ "node": "*" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", @@ -935,19 +945,32 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.13.0", "levn": "^0.4.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.9", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", @@ -1282,6 +1305,28 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.1.tgz", + "integrity": "sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.3", + "eventsource": "^3.0.2", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@mole-inc/bin-wrapper": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@mole-inc/bin-wrapper/-/bin-wrapper-8.0.1.tgz", @@ -4627,6 +4672,43 @@ "node": ">=6.5" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", @@ -5094,6 +5176,27 @@ "integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==", "license": "MIT" }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -5230,6 +5333,16 @@ "node": ">=8.0.0" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cacheable-lookup": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", @@ -5288,6 +5401,23 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -5597,6 +5727,16 @@ "node": ">= 0.6" } }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5604,6 +5744,40 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5970,6 +6144,16 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/design-system": { "resolved": "plugins/design-system", "link": true @@ -6116,6 +6300,13 @@ "dev": true, "license": "MIT" }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.102", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz", @@ -6129,6 +6320,16 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -6255,6 +6456,13 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -6509,6 +6717,16 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/event-target-shim": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", @@ -6535,6 +6753,29 @@ "node": ">=0.8.x" } }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz", + "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/execa": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", @@ -6633,6 +6874,101 @@ "node": ">=4" } }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": "^4.11 || 5 || ^5.0.0-beta.1" + } + }, + "node_modules/express/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -6823,6 +7159,24 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6949,6 +7303,16 @@ "node": ">= 0.12" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -7012,6 +7376,16 @@ "framer-plugin-tools": "index.js" } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7071,17 +7445,17 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -7383,6 +7757,23 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http2-wrapper": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", @@ -7411,6 +7802,19 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -7505,6 +7909,16 @@ "node": ">=12" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -7600,6 +8014,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -8243,6 +8664,16 @@ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "license": "CC0-1.0" }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/meow": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", @@ -8263,12 +8694,25 @@ "node": ">=4" } }, - "node_modules/merge-stream": { + "node_modules/merge-descriptors": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", @@ -8480,6 +8924,16 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -8565,6 +9019,10 @@ "resolved": "plugins/notion", "link": true }, + "node_modules/notion-legacy": { + "resolved": "plugins/notion-legacy", + "link": true + }, "node_modules/npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -8616,12 +9074,38 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ogl": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ogl/-/ogl-1.0.11.tgz", "integrity": "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA==", "license": "Unlicense" }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8779,6 +9263,16 @@ "node": ">=4" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-data-parser": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", @@ -8845,6 +9339,16 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -8926,6 +9430,16 @@ "@napi-rs/nice": "^1.0.1" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/points-on-curve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", @@ -9173,6 +9687,20 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -9207,6 +9735,22 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -9320,6 +9864,32 @@ } } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -9982,6 +10552,23 @@ "points-on-path": "^0.2.1" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/rss-feeds": { "resolved": "plugins/rss-feeds", "link": true @@ -10043,6 +10630,13 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/sander": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", @@ -10128,6 +10722,75 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -10151,6 +10814,82 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -10277,6 +11016,16 @@ "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", "license": "CC0-1.0" }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -10868,6 +11617,16 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/token-types": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz", @@ -11081,6 +11840,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -11330,6 +12127,16 @@ "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unsplash": { "resolved": "plugins/unsplash", "link": true @@ -11475,6 +12282,16 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -11794,6 +12611,26 @@ "dev": true, "license": "MIT" }, + "node_modules/zod": { + "version": "3.24.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.4.tgz", + "integrity": "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.5", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", + "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "plugins/airtable": { "version": "0.0.0", "dependencies": { @@ -11848,8 +12685,18 @@ "vite-plugin-framer": "^1" } }, - "plugins/airtable/node_modules/@eslint/eslintrc": { - "version": "3.3.0", + "plugins/airtable/node_modules/@eslint/config-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", + "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "plugins/airtable/node_modules/@eslint/eslintrc": { + "version": "3.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", "dev": true, @@ -13641,6 +14488,16 @@ "vite-plugin-framer": "^1.0.7" } }, + "plugins/locale-sync/node_modules/@eslint/config-helpers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.1.0.tgz", + "integrity": "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "plugins/locale-sync/node_modules/@eslint/eslintrc": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", @@ -13871,12 +14728,39 @@ } }, "plugins/notion": { + "version": "0.0.0", + "dependencies": { + "framer-plugin": "^3.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react-swc": "^3.8.0", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "framer-plugin-tools": "^1.0.0", + "globals": "^16.0.0", + "jiti": "^2.4.2", + "prettier": "^3.5.2", + "typescript": "^5.7.3", + "typescript-eslint": "^8.25.0", + "vite": "^6.2.6", + "vite-plugin-framer": "^1.0.7", + "vite-plugin-mkcert": "^1.17.7" + } + }, + "plugins/notion-legacy": { "version": "0.0.0", "dependencies": { "@notionhq/client": "^2.2.15", "@tanstack/react-query": "^5.29.2", "classnames": "^2.5.1", - "framer-plugin": "^2.0.4", + "framer-plugin": "^3.1.0", "react": "^18", "react-dom": "^18", "react-error-boundary": "^4.0.13", @@ -13896,10 +14780,10 @@ "tailwindcss": "^3.4.3", "typescript": "^5.3.3", "vite": "^6", - "vite-plugin-framer": "^1.0.1" + "vite-plugin-framer": "^1.0.7" } }, - "plugins/notion/node_modules/@typescript-eslint/eslint-plugin": { + "plugins/notion-legacy/node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", @@ -13933,7 +14817,7 @@ } } }, - "plugins/notion/node_modules/@typescript-eslint/parser": { + "plugins/notion-legacy/node_modules/@typescript-eslint/parser": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", @@ -13962,7 +14846,7 @@ } } }, - "plugins/notion/node_modules/@typescript-eslint/scope-manager": { + "plugins/notion-legacy/node_modules/@typescript-eslint/scope-manager": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", @@ -13980,7 +14864,7 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "plugins/notion/node_modules/@typescript-eslint/type-utils": { + "plugins/notion-legacy/node_modules/@typescript-eslint/type-utils": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", @@ -14008,7 +14892,7 @@ } } }, - "plugins/notion/node_modules/@typescript-eslint/types": { + "plugins/notion-legacy/node_modules/@typescript-eslint/types": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", @@ -14022,7 +14906,7 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "plugins/notion/node_modules/@typescript-eslint/typescript-estree": { + "plugins/notion-legacy/node_modules/@typescript-eslint/typescript-estree": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", @@ -14051,7 +14935,7 @@ } } }, - "plugins/notion/node_modules/@typescript-eslint/utils": { + "plugins/notion-legacy/node_modules/@typescript-eslint/utils": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", @@ -14074,7 +14958,7 @@ "eslint": "^8.56.0" } }, - "plugins/notion/node_modules/@typescript-eslint/visitor-keys": { + "plugins/notion-legacy/node_modules/@typescript-eslint/visitor-keys": { "version": "7.18.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", @@ -14092,6 +14976,336 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "plugins/notion-legacy/node_modules/framer-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/framer-plugin/-/framer-plugin-3.2.0.tgz", + "integrity": "sha512-KPFjygdYlI3fEqIIR1yElK8XSVe75XxIlWYXOSlYc0oHKI430yANqfmrLHlpdgEyjaf/nJqmjnget4YOvtgvfA==", + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "plugins/notion/node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "plugins/notion/node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "plugins/notion/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "plugins/notion/node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "plugins/notion/node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "plugins/notion/node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "plugins/notion/node_modules/@eslint/js": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz", + "integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "plugins/notion/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "plugins/notion/node_modules/eslint": { + "version": "9.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.26.0.tgz", + "integrity": "sha512-Hx0MOjPh6uK9oq9nVsATZKE/Wlbai7KFjfCuw9UHaguDW3x+HF0O5nIi3ud39TWgrTjTO5nHxmL3R1eANinWHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.13.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.26.0", + "@eslint/plugin-kit": "^0.2.8", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@modelcontextprotocol/sdk": "^1.8.0", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "zod": "^3.24.2" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "plugins/notion/node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "plugins/notion/node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "plugins/notion/node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "plugins/notion/node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "plugins/notion/node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "plugins/notion/node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "plugins/notion/node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "plugins/notion/node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "plugins/notion/node_modules/framer-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/framer-plugin/-/framer-plugin-3.2.0.tgz", + "integrity": "sha512-KPFjygdYlI3fEqIIR1yElK8XSVe75XxIlWYXOSlYc0oHKI430yANqfmrLHlpdgEyjaf/nJqmjnget4YOvtgvfA==", + "peerDependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "plugins/notion/node_modules/globals": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", + "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "plugins/notion/node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "plugins/phosphor": { "version": "0.0.0", "dependencies": { @@ -14216,16 +15430,6 @@ "vite-plugin-framer": "^1.0.7" } }, - "plugins/redirect-sync/node_modules/@eslint/config-helpers": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", - "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "plugins/redirect-sync/node_modules/@eslint/eslintrc": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", diff --git a/plugins/notion-legacy/framer.json b/plugins/notion-legacy/framer.json index 596b2de3..c23cc3d8 100644 --- a/plugins/notion-legacy/framer.json +++ b/plugins/notion-legacy/framer.json @@ -1,6 +1,6 @@ { "id": "e00c8d", - "name": "Notion", + "name": "Notion (Legacy)", "modes": ["configureManagedCollection", "syncManagedCollection"], "icon": "/icon.png" } diff --git a/plugins/notion-legacy/package.json b/plugins/notion-legacy/package.json index 62f88a1a..0d1cff21 100644 --- a/plugins/notion-legacy/package.json +++ b/plugins/notion-legacy/package.json @@ -1,5 +1,5 @@ { - "name": "notion", + "name": "notion-legacy", "private": true, "version": "0.0.0", "type": "module", diff --git a/plugins/notion/.gitignore b/plugins/notion/.gitignore new file mode 100644 index 00000000..3d845b84 --- /dev/null +++ b/plugins/notion/.gitignore @@ -0,0 +1,3 @@ +/dist +/node_modules +/plugin.zip diff --git a/plugins/notion/.prettierrc.json b/plugins/notion/.prettierrc.json new file mode 100644 index 00000000..47573df4 --- /dev/null +++ b/plugins/notion/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "semi": false, + "trailingComma": "es5", + "arrowParens": "avoid" +} diff --git a/plugins/notion/README.md b/plugins/notion/README.md new file mode 100644 index 00000000..b449f8c3 --- /dev/null +++ b/plugins/notion/README.md @@ -0,0 +1,19 @@ +# CMS Starter + +This is a starter for building a CMS plugin for Framer. + +Run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +And [open in Framer](https://www.framer.com/developers/plugins/quick-start#opening-in-framer). + +Learn more: https://www.framer.com/developers/plugins/introduction diff --git a/plugins/notion/eslint.config.ts b/plugins/notion/eslint.config.ts new file mode 100644 index 00000000..a1ba15df --- /dev/null +++ b/plugins/notion/eslint.config.ts @@ -0,0 +1,21 @@ +import js from "@eslint/js" +import * as reactHooks from "eslint-plugin-react-hooks" +import reactRefresh from "eslint-plugin-react-refresh" +import globals from "globals" +import * as tseslint from "typescript-eslint" + +export default tseslint.config( + { ignores: ["dist"] }, + { + files: ["**/*.{ts,tsx}"], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs["recommended-latest"], + reactRefresh.configs.vite, + ], + languageOptions: { + globals: globals.browser, + }, + } +) diff --git a/plugins/notion/framer.json b/plugins/notion/framer.json new file mode 100644 index 00000000..596b2de3 --- /dev/null +++ b/plugins/notion/framer.json @@ -0,0 +1,6 @@ +{ + "id": "e00c8d", + "name": "Notion", + "modes": ["configureManagedCollection", "syncManagedCollection"], + "icon": "/icon.png" +} diff --git a/plugins/notion/index.html b/plugins/notion/index.html new file mode 100644 index 00000000..b1c9463c --- /dev/null +++ b/plugins/notion/index.html @@ -0,0 +1,14 @@ + + + + + + + Notion + + +
+ + + + diff --git a/plugins/notion/package.json b/plugins/notion/package.json new file mode 100644 index 00000000..8d5bd53d --- /dev/null +++ b/plugins/notion/package.json @@ -0,0 +1,40 @@ +{ + "name": "notion", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "check": "npm run typecheck && npm run lint", + "dev": "vite", + "format": "prettier . --write", + "lint": "eslint . --max-warnings 0", + "pack": "framer-plugin-tools pack", + "preview": "vite preview", + "typecheck": "tsc" + }, + "dependencies": { + "framer-plugin": "^3.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react-swc": "^3.8.0", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "framer-plugin-tools": "^1.0.0", + "globals": "^16.0.0", + "jiti": "^2.4.2", + "prettier": "^3.5.2", + "typescript": "^5.7.3", + "typescript-eslint": "^8.25.0", + "vite": "^6.2.6", + "vite-plugin-framer": "^1.0.7", + "vite-plugin-mkcert": "^1.17.7" + } +} diff --git a/plugins/notion/public/data/articles.json b/plugins/notion/public/data/articles.json new file mode 100644 index 00000000..391a3d93 --- /dev/null +++ b/plugins/notion/public/data/articles.json @@ -0,0 +1,56 @@ +{ + "id": "articles", + "fields": [ + { "name": "Title", "type": "string" }, + { "name": "Content", "type": "formattedText" }, + { "name": "Reading Time", "type": "number" }, + { "name": "Featured", "type": "boolean" } + ], + "items": [ + { + "Title": { "type": "string", "value": "Getting Started" }, + "Reading Time": { "type": "number", "value": 3 }, + "Featured": { "type": "boolean", "value": true }, + "Content": { + "type": "formattedText", + "value": "Learn how to set up content fields, add CMS content to your canvas, and create pages that automatically populate with your data. With the right setup, you can quickly build index and detail pages for your collection." + } + }, + { + "Title": { "type": "string", "value": "Latest Updates" }, + "Reading Time": { "type": "number", "value": 2 }, + "Featured": { "type": "boolean", "value": false }, + "Content": { + "type": "formattedText", + "value": "We’ve added powerful new features including reference fields and filtering capabilities for your collections. These updates allow you to keep content in a single collection while customizing how it’s presented across different pages." + } + }, + { + "Title": { "type": "string", "value": "Styling Elements" }, + "Reading Time": { "type": "number", "value": 4 }, + "Featured": { "type": "boolean", "value": false }, + "Content": { + "type": "formattedText", + "value": "Latest improvements to the canvas and layer panel make styling and layout work easier than ever. New features include enhanced component controls and automatic tinting support." + } + }, + { + "Title": { "type": "string", "value": "Importing Content" }, + "Reading Time": { "type": "number", "value": 6 }, + "Featured": { "type": "boolean", "value": false }, + "Content": { + "type": "formattedText", + "value": "Learn how to prepare your CSV file for import, including formatting for rich text, images, dates, colors, and toggle fields. Ensure each field in your CSV has a matching field in your CMS collection with the same name and compatible data type." + } + }, + { + "Title": { "type": "string", "value": "Best Practices" }, + "Reading Time": { "type": "number", "value": 8 }, + "Featured": { "type": "boolean", "value": true }, + "Content": { + "type": "formattedText", + "value": "Choose compelling topics based on audience needs and industry trends, while organizing your content with clear structure using proper HTML tags. Consider adding pagination for extensive content lists to enhance performance and improve SEO by reducing load times." + } + } + ] +} diff --git a/plugins/notion/public/data/categories.json b/plugins/notion/public/data/categories.json new file mode 100644 index 00000000..efa652e9 --- /dev/null +++ b/plugins/notion/public/data/categories.json @@ -0,0 +1,30 @@ +{ + "id": "categories", + "fields": [ + { "name": "Title", "type": "string" }, + { "name": "Description", "type": "string" }, + { "name": "Color", "type": "color" } + ], + "items": [ + { + "Title": { "type": "string", "value": "CMS" }, + "Description": { "type": "string", "value": "Content Management System" }, + "Color": { "type": "color", "value": "orange" } + }, + { + "Title": { "type": "string", "value": "Basics" }, + "Description": { "type": "string", "value": "Basic content management" }, + "Color": { "type": "color", "value": "red" } + }, + { + "Title": { "type": "string", "value": "Updates" }, + "Description": { "type": "string", "value": "Updates to the CMS" }, + "Color": { "type": "color", "value": "blue" } + }, + { + "Title": { "type": "string", "value": "Pro Tips" }, + "Description": { "type": "string", "value": "Tips for using the CMS" }, + "Color": { "type": "color", "value": "green" } + } + ] +} diff --git a/plugins/notion/public/icon.png b/plugins/notion/public/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c20846f2f5b38533a7f59d2aebcf05b834bca452 GIT binary patch literal 3938 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 + const [isLoadingDataSource, setIsLoadingDataSource] = useState(Boolean(previousDataSourceId)) + + useLayoutEffect(() => { + const hasDataSourceSelected = Boolean(dataSource) + + framer.showUI({ + width: hasDataSourceSelected ? 360 : 260, + height: hasDataSourceSelected ? 425 : 340, + minWidth: hasDataSourceSelected ? 360 : undefined, + minHeight: hasDataSourceSelected ? 425 : undefined, + resizable: hasDataSourceSelected, + }) + }, [dataSource]) + + useEffect(() => { + if (!previousDataSourceId) { + return + } + + const abortController = new AbortController() + + setIsLoadingDataSource(true) + getDataSource(previousDataSourceId, abortController.signal) + .then(setDataSource) + .catch(error => { + if (abortController.signal.aborted) return + + console.error(error) + framer.notify( + `Error loading previously configured data source “${previousDataSourceId}”. Check the logs for more details.`, + { + variant: "error", + } + ) + }) + .finally(() => { + if (abortController.signal.aborted) return + + setIsLoadingDataSource(false) + }) + + return () => { + abortController.abort() + } + }, [previousDataSourceId]) + + if (isLoadingDataSource) { + return ( +
+
+
+ ) + } + + if (!dataSource) { + return + } + + return +} diff --git a/plugins/notion/src/FieldMapping.tsx b/plugins/notion/src/FieldMapping.tsx new file mode 100644 index 00000000..6a67237a --- /dev/null +++ b/plugins/notion/src/FieldMapping.tsx @@ -0,0 +1,229 @@ +import { type EditableManagedCollectionField, framer, type ManagedCollection } from "framer-plugin" +import { useEffect, useState } from "react" +import { type DataSource, dataSourceOptions, mergeFieldsWithExistingFields, syncCollection } from "./data" + +interface FieldMappingRowProps { + field: EditableManagedCollectionField + originalFieldName: string | undefined + disabled: boolean + onToggleDisabled: (fieldId: string) => void + onNameChange: (fieldId: string, name: string) => void +} + +function FieldMappingRow({ field, originalFieldName, disabled, onToggleDisabled, onNameChange }: FieldMappingRowProps) { + return ( + <> + + + + + onNameChange(field.id, event.target.value)} + onKeyDown={event => { + if (event.key === "Enter") { + event.preventDefault() + } + }} + /> + + ) +} + +const initialManagedCollectionFields: EditableManagedCollectionField[] = [] +const initialFieldIds: ReadonlySet = new Set() + +interface FieldMappingProps { + collection: ManagedCollection + dataSource: DataSource + initialSlugFieldId: string | null +} + +export function FieldMapping({ collection, dataSource, initialSlugFieldId }: FieldMappingProps) { + const [status, setStatus] = useState<"mapping-fields" | "loading-fields" | "syncing-collection">( + initialSlugFieldId ? "loading-fields" : "mapping-fields" + ) + const isSyncing = status === "syncing-collection" + const isLoadingFields = status === "loading-fields" + + const [possibleSlugFields] = useState(() => dataSource.fields.filter(field => field.type === "string")) + + const [selectedSlugField, setSelectedSlugField] = useState( + possibleSlugFields.find(field => field.id === initialSlugFieldId) ?? possibleSlugFields[0] ?? null + ) + + const [fields, setFields] = useState(initialManagedCollectionFields) + const [ignoredFieldIds, setIgnoredFieldIds] = useState(initialFieldIds) + + const dataSourceName = dataSourceOptions.find(option => option.id === dataSource.id)?.name ?? dataSource.id + + useEffect(() => { + const abortController = new AbortController() + + collection + .getFields() + .then(collectionFields => { + if (abortController.signal.aborted) return + + setFields(mergeFieldsWithExistingFields(dataSource.fields, collectionFields)) + + const existingFieldIds = new Set(collectionFields.map(field => field.id)) + const ignoredFields = dataSource.fields.filter(sourceField => !existingFieldIds.has(sourceField.id)) + + if (initialSlugFieldId) { + setIgnoredFieldIds(new Set(ignoredFields.map(field => field.id))) + } + + setStatus("mapping-fields") + }) + .catch(error => { + if (!abortController.signal.aborted) { + console.error("Failed to fetch collection fields:", error) + framer.notify("Failed to load collection fields", { variant: "error" }) + } + }) + + return () => { + abortController.abort() + } + }, [initialSlugFieldId, dataSource, collection]) + + const changeFieldName = (fieldId: string, name: string) => { + setFields(prevFields => { + const updatedFields = prevFields.map(field => { + if (field.id !== fieldId) return field + return { ...field, name } + }) + return updatedFields + }) + } + + const toggleFieldDisabledState = (fieldId: string) => { + setIgnoredFieldIds(previousIgnoredFieldIds => { + const updatedIgnoredFieldIds = new Set(previousIgnoredFieldIds) + + if (updatedIgnoredFieldIds.has(fieldId)) { + updatedIgnoredFieldIds.delete(fieldId) + } else { + updatedIgnoredFieldIds.add(fieldId) + } + + return updatedIgnoredFieldIds + }) + } + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault() + + if (!selectedSlugField) { + // This can't happen because the form will not submit if no slug field is selected + // but TypeScript can't infer that. + console.error("There is no slug field selected. Sync will not be performed") + framer.notify("Please select a slug field before importing.", { variant: "warning" }) + return + } + + try { + setStatus("syncing-collection") + + const fieldsToSync = fields.filter(field => !ignoredFieldIds.has(field.id)) + + await syncCollection(collection, dataSource, fieldsToSync, selectedSlugField) + await framer.closePlugin("Synchronization successful", { variant: "success" }) + } catch (error) { + console.error(error) + framer.notify(`Failed to sync collection “${dataSource.id}”. Check the logs for more details.`, { + variant: "error", + }) + } finally { + setStatus("mapping-fields") + } + } + + if (isLoadingFields) { + return ( +
+
+
+ ) + } + + return ( +
+
+
+ + +
+ Column + Field + {fields.map(field => ( + sourceField.id === field.id)?.name} + disabled={ignoredFieldIds.has(field.id)} + onToggleDisabled={toggleFieldDisabledState} + onNameChange={changeFieldName} + /> + ))} +
+ +
+
+ +
+
+
+ ) +} diff --git a/plugins/notion/src/SelectDataSource.tsx b/plugins/notion/src/SelectDataSource.tsx new file mode 100644 index 00000000..e0a2a5ca --- /dev/null +++ b/plugins/notion/src/SelectDataSource.tsx @@ -0,0 +1,71 @@ +import { framer } from "framer-plugin" +import { useState } from "react" +import { type DataSource, getDataSource, dataSourceOptions } from "./data" + +interface SelectDataSourceProps { + onSelectDataSource: (dataSource: DataSource) => void +} + +export function SelectDataSource({ onSelectDataSource }: SelectDataSourceProps) { + const [selectedDataSourceId, setSelectedDataSourceId] = useState(dataSourceOptions[0].id) + const [isLoading, setIsLoading] = useState(false) + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault() + + try { + setIsLoading(true) + + const dataSource = await getDataSource(selectedDataSourceId) + onSelectDataSource(dataSource) + } catch (error) { + console.error(error) + framer.notify(`Failed to load data source “${selectedDataSourceId}”. Check the logs for more details.`, { + variant: "error", + }) + } finally { + setIsLoading(false) + } + } + + return ( +
+
+
+ + + +
+
+

CMS Starter

+

Everything you need to get started with a CMS Plugin.

+
+
+ +
+ + +
+
+ ) +} diff --git a/plugins/notion/src/data.ts b/plugins/notion/src/data.ts new file mode 100644 index 00000000..5c269bb9 --- /dev/null +++ b/plugins/notion/src/data.ts @@ -0,0 +1,186 @@ +import { + type EditableManagedCollectionField, + type FieldDataInput, + framer, + type ManagedCollection, + type ManagedCollectionItemInput, +} from "framer-plugin" + +export const PLUGIN_KEYS = { + DATA_SOURCE_ID: "dataSourceId", + SLUG_FIELD_ID: "slugFieldId", +} as const + +export interface DataSource { + id: string + fields: readonly EditableManagedCollectionField[] + items: FieldDataInput[] +} + +export const dataSourceOptions = [ + { id: "articles", name: "Articles" }, + { id: "categories", name: "Categories" }, +] as const + +/** + * Retrieve data and process it into a structured format. + * + * @example + * { + * id: "articles", + * fields: [ + * { id: "title", name: "Title", type: "string" }, + * { id: "content", name: "Content", type: "formattedText" } + * ], + * items: [ + * { title: "My First Article", content: "Hello world" }, + * { title: "Another Article", content: "More content here" } + * ] + * } + */ +export async function getDataSource(dataSourceId: string, abortSignal?: AbortSignal): Promise { + // Fetch from your data source + const dataSourceResponse = await fetch(`/data/${dataSourceId}.json`, { signal: abortSignal }) + const dataSource = await dataSourceResponse.json() + + // Map your source fields to supported field types in Framer + const fields: EditableManagedCollectionField[] = [] + for (const field of dataSource.fields) { + switch (field.type) { + case "string": + case "number": + case "boolean": + case "color": + case "formattedText": + case "date": + case "link": + fields.push({ + id: field.name, + name: field.name, + type: field.type, + }) + break + case "image": + case "file": + case "enum": + case "collectionReference": + case "multiCollectionReference": + console.warn(`Support for field type "${field.type}" is not implemented in this Plugin.`) + break + default: { + console.warn(`Unknown field type "${field.type}".`) + } + } + } + + const items = dataSource.items as FieldDataInput[] + + return { + id: dataSource.id, + fields, + items, + } +} + +export function mergeFieldsWithExistingFields( + sourceFields: readonly EditableManagedCollectionField[], + existingFields: readonly EditableManagedCollectionField[] +): EditableManagedCollectionField[] { + return sourceFields.map(sourceField => { + const existingField = existingFields.find(existingField => existingField.id === sourceField.id) + if (existingField) { + return { ...sourceField, name: existingField.name } + } + return sourceField + }) +} + +export async function syncCollection( + collection: ManagedCollection, + dataSource: DataSource, + fields: readonly EditableManagedCollectionField[], + slugField: EditableManagedCollectionField +) { + const sanitizedFields = fields.map(field => ({ + ...field, + name: field.name.trim() || field.id, + })) + + const items: ManagedCollectionItemInput[] = [] + const unsyncedItems = new Set(await collection.getItemIds()) + + for (let i = 0; i < dataSource.items.length; i++) { + const item = dataSource.items[i] + if (!item) throw new Error("Logic error") + + const slugValue = item[slugField.id] + if (!slugValue || typeof slugValue.value !== "string") { + console.warn(`Skipping item at index ${i} because it doesn't have a valid slug`) + continue + } + + unsyncedItems.delete(slugValue.value) + + const fieldData: FieldDataInput = {} + for (const [fieldName, value] of Object.entries(item)) { + const field = sanitizedFields.find(field => field.id === fieldName) + + // Field is in the data but skipped based on selected fields. + if (!field) continue + + // For details on expected field value, see: + // https://www.framer.com/developers/plugins/cms#collections + fieldData[field.id] = value + } + + items.push({ + id: slugValue.value, + slug: slugValue.value, + draft: false, + fieldData, + }) + } + + await collection.setFields(sanitizedFields) + await collection.removeItems(Array.from(unsyncedItems)) + await collection.addItems(items) + + await collection.setPluginData(PLUGIN_KEYS.DATA_SOURCE_ID, dataSource.id) + await collection.setPluginData(PLUGIN_KEYS.SLUG_FIELD_ID, slugField.id) +} + +export async function syncExistingCollection( + collection: ManagedCollection, + previousDataSourceId: string | null, + previousSlugFieldId: string | null +): Promise<{ didSync: boolean }> { + if (!previousDataSourceId) { + return { didSync: false } + } + + if (framer.mode !== "syncManagedCollection" || !previousSlugFieldId) { + return { didSync: false } + } + + try { + const dataSource = await getDataSource(previousDataSourceId) + const existingFields = await collection.getFields() + + const slugField = dataSource.fields.find(field => field.id === previousSlugFieldId) + if (!slugField) { + framer.notify(`No field matches the slug field id “${previousSlugFieldId}”. Sync will not be performed.`, { + variant: "error", + }) + return { didSync: false } + } + + await syncCollection(collection, dataSource, existingFields, slugField) + return { didSync: true } + } catch (error) { + console.error(error) + framer.notify(`Failed to sync collection “${previousDataSourceId}”. Check browser console for more details.`, { + variant: "error", + }) + return { didSync: false } + } +} diff --git a/plugins/notion/src/main.tsx b/plugins/notion/src/main.tsx new file mode 100644 index 00000000..91181aaa --- /dev/null +++ b/plugins/notion/src/main.tsx @@ -0,0 +1,33 @@ +import "framer-plugin/framer.css" + +import { framer } from "framer-plugin" +import { StrictMode } from "react" +import { createRoot } from "react-dom/client" +import { App } from "./App.tsx" +import { PLUGIN_KEYS, syncExistingCollection } from "./data" + +const activeCollection = await framer.getActiveManagedCollection() + +const previousDataSourceId = await activeCollection.getPluginData(PLUGIN_KEYS.DATA_SOURCE_ID) +const previousSlugFieldId = await activeCollection.getPluginData(PLUGIN_KEYS.SLUG_FIELD_ID) + +const { didSync } = await syncExistingCollection(activeCollection, previousDataSourceId, previousSlugFieldId) + +if (didSync) { + await framer.closePlugin("Synchronization successful", { + variant: "success", + }) +} else { + const root = document.getElementById("root") + if (!root) throw new Error("Root element not found") + + createRoot(root).render( + + + + ) +} diff --git a/plugins/notion/tsconfig.json b/plugins/notion/tsconfig.json new file mode 100644 index 00000000..b583cd50 --- /dev/null +++ b/plugins/notion/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["src", "*"], + "compilerOptions": { + "allowImportingTsExtensions": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2023", + "verbatimModuleSyntax": true + } +} diff --git a/plugins/notion/vite.config.ts b/plugins/notion/vite.config.ts new file mode 100644 index 00000000..08b427f5 --- /dev/null +++ b/plugins/notion/vite.config.ts @@ -0,0 +1,6 @@ +import react from "@vitejs/plugin-react-swc" +import { defineConfig } from "vite" +import framer from "vite-plugin-framer" +import mkcert from "vite-plugin-mkcert" + +export default defineConfig({ plugins: [react(), mkcert(), framer()] }) From ee26a1ea9c9a31014414fa6f525023858bee15cd Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Fri, 9 May 2025 22:30:00 -0400 Subject: [PATCH 05/16] Update select data source page --- plugins/notion/public/notion-connect.png | Bin 0 -> 33382 bytes plugins/notion/src/App.css | 16 +++++++--------- plugins/notion/src/App.tsx | 2 +- plugins/notion/src/SelectDataSource.tsx | 19 ++++++------------- 4 files changed, 14 insertions(+), 23 deletions(-) create mode 100644 plugins/notion/public/notion-connect.png diff --git a/plugins/notion/public/notion-connect.png b/plugins/notion/public/notion-connect.png new file mode 100644 index 0000000000000000000000000000000000000000..8a997cc0c7a31816323d681f1202afa90074aee1 GIT binary patch literal 33382 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 V>Z@Nl?JM9TBk@_hTvY%2{{iPLo1*{# literal 0 HcmV?d00001 diff --git a/plugins/notion/src/App.css b/plugins/notion/src/App.css index 2c4e096f..8cee1671 100644 --- a/plugins/notion/src/App.css +++ b/plugins/notion/src/App.css @@ -64,20 +64,18 @@ form { width: 100%; flex: 1; text-align: center; - max-width: 160px; gap: 15px; } -.intro h2 { - line-height: 1; - font-size: 1em; - margin-bottom: 10px; -} - .intro p { color: var(--framer-color-text-tertiary); } +.intro img { + width: 100%; + border-radius: 8px; +} + .setup label { display: flex; flex-direction: row; @@ -147,11 +145,11 @@ form { box-shadow: none; } -[data-framer-theme=light] .mapping .source-field input[type="checkbox"]:not(:checked) { +[data-framer-theme="light"] .mapping .source-field input[type="checkbox"]:not(:checked) { background: #ccc; } -[data-framer-theme=dark] .mapping .source-field input[type="checkbox"]:not(:checked) { +[data-framer-theme="dark"] .mapping .source-field input[type="checkbox"]:not(:checked) { background: #666; } diff --git a/plugins/notion/src/App.tsx b/plugins/notion/src/App.tsx index c5151f54..9cfcf919 100644 --- a/plugins/notion/src/App.tsx +++ b/plugins/notion/src/App.tsx @@ -21,7 +21,7 @@ export function App({ collection, previousDataSourceId, previousSlugFieldId }: A framer.showUI({ width: hasDataSourceSelected ? 360 : 260, - height: hasDataSourceSelected ? 425 : 340, + height: hasDataSourceSelected ? 425 : 345, minWidth: hasDataSourceSelected ? 360 : undefined, minHeight: hasDataSourceSelected ? 425 : undefined, resizable: hasDataSourceSelected, diff --git a/plugins/notion/src/SelectDataSource.tsx b/plugins/notion/src/SelectDataSource.tsx index e0a2a5ca..82c66074 100644 --- a/plugins/notion/src/SelectDataSource.tsx +++ b/plugins/notion/src/SelectDataSource.tsx @@ -31,18 +31,11 @@ export function SelectDataSource({ onSelectDataSource }: SelectDataSourceProps) return (
-
- - - -
-
-

CMS Starter

-

Everything you need to get started with a CMS Plugin.

-
+ +

+ To manually connect a database, open it in Notion, click on the three dots icon in the top right + corner, then select Connections and connect to Framer. +

@@ -53,7 +46,7 @@ export function SelectDataSource({ onSelectDataSource }: SelectDataSourceProps) value={selectedDataSourceId} > {dataSourceOptions.map(({ id, name }) => (
diff --git a/plugins/notion/src/api.ts b/plugins/notion/src/api.ts new file mode 100644 index 00000000..adf6905e --- /dev/null +++ b/plugins/notion/src/api.ts @@ -0,0 +1,164 @@ +import { + APIErrorCode, + Client, + collectPaginatedAPI, + isFullBlock, + isFullDatabase, + isFullPage, + isNotionClientError, +} from "@notionhq/client" +import type { + BlockObjectResponse, + GetDatabaseResponse, + PageObjectResponse, + RichTextItemResponse, +} from "@notionhq/client/build/src/api-endpoints" +import { useMutation, useQuery } from "@tanstack/react-query" +import { + framer, + type CollectionItemData, + type ManagedCollection, + type ManagedCollectionField, + type FieldData, +} from "framer-plugin" +import pLimit from "p-limit" +import { blocksToHtml, richTextToHTML } from "./blocksToHTML" +import { assert, assertNever, formatDate, isDefined, isString, slugify } from "./utils" + +export const API_BASE_URL = "https://notion-plugin-api.framer-team.workers.dev" +export const PLUGIN_KEYS = { + DATABASE_ID: "notionPluginDatabaseId", + LAST_SYNCED: "notionPluginLastSynced", + IGNORED_FIELD_IDS: "notionPluginIgnoredFieldIds", + SLUG_FIELD_ID: "notionPluginSlugId", + DATABASE_NAME: "notionDatabaseName", + BEARER_TOKEN: "notionBearerToken", +} as const + +export type FieldId = string + +// Maximum number of concurrent requests to Notion API +// This is to prevent rate limiting. +const CONCURRENCY_LIMIT = 5 + +export type NotionProperty = GetDatabaseResponse["properties"][string] + +// Every page has content which can be fetched as blocks. We add it as a +// property so it displays in the list where you can configure properties to be +// synced with the CMS +const pageContentId = "page-content" +export const pageContentProperty: SupportedNotionProperty = { + type: "rich_text", + id: pageContentId, + name: "Content", + description: "Page Content", + rich_text: {}, +} + +// Naive implementation to be authenticated, a token could be expired. +// For simplicity we just close the plugin and clear storage in that case. +export function isAuthenticated() { + return localStorage.getItem(PLUGIN_KEYS.BEARER_TOKEN) !== null +} + +let notion: Client | null = null +if (isAuthenticated()) { + initNotionClient() +} + +export function initNotionClient() { + const token = localStorage.getItem(PLUGIN_KEYS.BEARER_TOKEN) + if (!token) throw new Error("Notion API token is missing") + + notion = new Client({ + fetch: async (url, fetchInit) => { + const urlObj = new URL(url) + + try { + const resp = await fetch(`${API_BASE_URL}/notion${urlObj.pathname}${urlObj.search}`, fetchInit) + + // If status is unauthorized, clear the token + // And we close the plugin (for now) + // TODO: Improve this flow in the plugin. + if (resp.status === 401) { + localStorage.removeItem(PLUGIN_KEYS.BEARER_TOKEN) + await framer.closePlugin("Notion Authorization Failed. Re-open the plugin to re-authorize.", { + variant: "error", + }) + return resp + } + + return resp + } catch (error) { + console.log("Notion API error", error) + throw error + } + }, + auth: token, + }) +} + +export const supportedNotionPropertyTypes = [ + "email", + "rich_text", + "date", + "last_edited_time", + "select", + "number", + "checkbox", + "created_time", + "title", + "status", + "url", + "files", + "relation", +] satisfies ReadonlyArray + +type SupportedPropertyType = (typeof supportedNotionPropertyTypes)[number] +type SupportedNotionProperty = Extract + +export function isSupportedNotionProperty(property: NotionProperty): property is SupportedNotionProperty { + return supportedNotionPropertyTypes.includes(property.type as SupportedPropertyType) +} + +export async function getNotionDatabases() { + if (!notion) { + initNotionClient() + } + + const results = await collectPaginatedAPI(notion!.search, { + filter: { + property: "object", + value: "database", + }, + }) + + return results.filter(isFullDatabase) +} + +export const supportedCMSTypeByNotionPropertyType = { + checkbox: ["boolean"], + date: ["date"], + number: ["number"], + title: ["string"], + rich_text: ["formattedText", "string"], + created_time: ["date"], + last_edited_time: ["date"], + select: ["enum"], + status: ["enum"], + url: ["link"], + email: ["formattedText", "string"], + files: ["file", "image"], + relation: ["multiCollectionReference"], +} satisfies Record> + +function assertFieldTypeMatchesPropertyType( + propertyType: T, + fieldType: ManagedCollectionField["type"] +): asserts fieldType is (typeof supportedCMSTypeByNotionPropertyType)[T][number] { + const allowedFieldTypes = supportedCMSTypeByNotionPropertyType[propertyType] + + if (!allowedFieldTypes.includes(fieldType as never)) { + throw new Error(`Field type '${fieldType}' is not valid for property type '${propertyType}'.`) + } +} diff --git a/plugins/notion/src/auth.ts b/plugins/notion/src/auth.ts index d0928047..ce63f73f 100644 --- a/plugins/notion/src/auth.ts +++ b/plugins/notion/src/auth.ts @@ -1,11 +1,7 @@ import { generateRandomId } from "./utils" +import { PLUGIN_KEYS, API_BASE_URL } from "./api" -interface Tokens { - bearer_token: string -} - -interface StoredTokens { - createdAt: number +type Tokens = { bearer_token: string } @@ -16,10 +12,8 @@ interface Authorize { } class Auth { - private readonly PLUGIN_TOKENS_KEY = "notionBearerToken" - private readonly AUTH_URI = "https://notion-plugin-api.framer-team.workers.dev" private readonly NOTION_CLIENT_ID = "3504c5a7-9f75-4f87-aa1b-b735f8480432" - storedTokens?: StoredTokens | null + storedTokens?: Tokens | null logout() { this.tokens.clear() @@ -29,11 +23,11 @@ class Auth { const tokens = this.tokens.get() if (!tokens) return null - return this.tokens.get() + return tokens } async fetchTokens(readKey: string) { - const res = await fetch(`${this.AUTH_URI}/auth/authorize/${readKey}`, { + const res = await fetch(`${API_BASE_URL}/auth/authorize/${readKey}`, { method: "GET", }) @@ -42,18 +36,15 @@ class Auth { } const { token } = await res.json() - const tokens: Tokens = { - bearer_token: token, - } - this.tokens.save(tokens) - return tokens + this.tokens.save({ bearer_token: token }) + return token } async authorize(): Promise { const writeKey = generateRandomId() const readKey = generateRandomId() - const res = await fetch(`${this.AUTH_URI}/auth/authorize`, { + const res = await fetch(`${API_BASE_URL}/auth/authorize`, { method: "POST", headers: { "Content-Type": "application/json", @@ -65,7 +56,7 @@ class Auth { throw new Error("Failed to generate OAuth URL") } - const oauthRedirectUrl = encodeURIComponent(`${this.AUTH_URI}/auth/authorize/callback`) + const oauthRedirectUrl = encodeURIComponent(`${API_BASE_URL}/auth/authorize/callback`) return { writeKey, @@ -77,7 +68,7 @@ class Auth { async isWorkerAlive(): Promise { return true // try { - // const res = await fetch(`${this.AUTH_URI}/auth/authorize`, { + // const res = await fetch(`${API_BASE_URL}/auth/authorize`, { // method: "POST", // }) // console.log("res", res) @@ -90,32 +81,23 @@ class Auth { private readonly tokens = { save: (tokens: Tokens) => { - const storedTokens: StoredTokens = { - createdAt: Date.now(), - bearer_token: tokens.bearer_token, - } - - this.storedTokens = storedTokens - localStorage.setItem(this.PLUGIN_TOKENS_KEY, JSON.stringify(storedTokens)) + this.storedTokens = tokens + localStorage.setItem(PLUGIN_KEYS.BEARER_TOKEN, tokens.bearer_token) }, get: () => { if (this.storedTokens) return this.storedTokens - const serializedTokens = localStorage.getItem(this.PLUGIN_TOKENS_KEY) - if (!serializedTokens) return null + const bearerToken = localStorage.getItem(PLUGIN_KEYS.BEARER_TOKEN) + if (!bearerToken) return null - try { - const storedTokens = JSON.parse(serializedTokens) as StoredTokens - this.storedTokens = storedTokens + const storedTokens = { bearer_token: bearerToken } as Tokens + this.storedTokens = storedTokens - return storedTokens - } catch { - return null - } + return storedTokens }, clear: () => { this.storedTokens = null - localStorage.removeItem(this.PLUGIN_TOKENS_KEY) + localStorage.removeItem(PLUGIN_KEYS.BEARER_TOKEN) }, } } diff --git a/plugins/notion/src/blocksToHTML.ts b/plugins/notion/src/blocksToHTML.ts new file mode 100644 index 00000000..3a2758a3 --- /dev/null +++ b/plugins/notion/src/blocksToHTML.ts @@ -0,0 +1,104 @@ +import type { BlockObjectResponse, RichTextItemResponse } from "@notionhq/client/build/src/api-endpoints" +import { assert } from "./utils" + +export function richTextToHTML(texts: RichTextItemResponse[]) { + return texts + .map(({ plain_text, annotations, href }) => { + let html = plain_text + + // Apply formatting based on annotations + if (annotations.bold) { + html = `${html}` + } + if (annotations.italic) { + html = `${html}` + } + if (annotations.strikethrough) { + html = `${html}` + } + if (annotations.underline) { + html = `${html}` + } + + if (annotations.code) { + html = `${html}` + } + + if (annotations.color !== "default") { + const color = annotations.color.replace("_", "") + html = `${html}` + } + + if (href) { + html = `${html}` + } + + return html + }) + .join("") +} + +export function blocksToHtml(blocks: BlockObjectResponse[]) { + let htmlContent = "" + + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i] + assert(block) + + switch (block.type) { + case "paragraph": + htmlContent += `

${richTextToHTML(block.paragraph.rich_text)}

` + break + case "heading_1": + htmlContent += `

${richTextToHTML(block.heading_1.rich_text)}

` + break + case "heading_2": + htmlContent += `

${richTextToHTML(block.heading_2.rich_text)}

` + break + case "heading_3": + htmlContent += `

${richTextToHTML(block.heading_3.rich_text)}

` + break + case "divider": + htmlContent += "
" + break + case "image": + switch (block.image.type) { + case "external": + htmlContent += `${block.image.caption[0]?.plain_text}` + break + case "file": + htmlContent += `${block.image.caption[0]?.plain_text}` + break + } + break + case "bulleted_list_item": + case "numbered_list_item": { + const tag = block.type === "bulleted_list_item" ? "ul" : "ol" + + // Start the list if it's the first item of its type or the previous item isn't a list of the same type + if (i === 0 || blocks[i - 1].type !== block.type) htmlContent += `<${tag}>` + + if (block.type === "bulleted_list_item") { + htmlContent += `
  • ${richTextToHTML(block.bulleted_list_item.rich_text)}
  • ` + } else { + // Add the list item + htmlContent += `
  • ${richTextToHTML(block.numbered_list_item.rich_text)}
  • ` + } + + // If next block is not the same type, close the list + if (i === blocks.length - 1 || blocks[i + 1].type !== block.type) { + htmlContent += `` + } + break + } + case "code": + htmlContent += `
    ${richTextToHTML(block.code.rich_text)}
    ` + break + default: + // TODO: More block types can be added here! + break + } + } + + return htmlContent +} diff --git a/plugins/notion/src/data.ts b/plugins/notion/src/data.ts index 5b47902b..57ec79ae 100644 --- a/plugins/notion/src/data.ts +++ b/plugins/notion/src/data.ts @@ -1,30 +1,36 @@ import { + framer, + ManagedCollection, type ManagedCollectionFieldInput, + type CollectionItemData, + type ManagedCollectionField, + type FieldData, type FieldDataInput, - framer, - type ManagedCollection, type ManagedCollectionItemInput, } from "framer-plugin" - -export const dataSourceOptions = [ - { id: "articles", name: "Articles" }, - { id: "categories", name: "Categories" }, -] as const - -export const PLUGIN_KEYS = { - DATABASE_ID: "notionPluginDatabaseId", - LAST_SYNCED: "notionPluginLastSynced", - IGNORED_FIELD_IDS: "notionPluginIgnoredFieldIds", - SLUG_FIELD_ID: "notionPluginSlugId", - DATABASE_NAME: "notionDatabaseName", -} as const +import { Client, collectPaginatedAPI, isFullDatabase } from "@notionhq/client" +import { initNotionClient, getNotionDatabases } from "./api" +import type { DatabaseObjectResponse } from "@notionhq/client/build/src/api-endpoints" +import { PLUGIN_KEYS } from "./api" export interface DataSource { id: string + name: string fields: readonly ManagedCollectionFieldInput[] items: FieldDataInput[] } +export async function getDataSources(): Promise { + const databases = await getNotionDatabases() + + return databases.map((db: DatabaseObjectResponse) => ({ + id: db.id, + name: db.title[0]?.plain_text || "Untitled Database", + fields: [], // Fields will be populated when needed + items: [], // Items will be populated when needed + })) +} + /** * Retrieve data and process it into a structured format. * @@ -80,6 +86,7 @@ export async function getDataSource(dataSourceId: string, abortSignal?: AbortSig return { id: dataSource.id, + name: dataSource.name, fields, items, } diff --git a/plugins/notion/src/main.tsx b/plugins/notion/src/main.tsx index 68189852..2d5f362b 100644 --- a/plugins/notion/src/main.tsx +++ b/plugins/notion/src/main.tsx @@ -6,7 +6,8 @@ import ReactDOM from "react-dom/client" import { App } from "./App.tsx" import auth from "./auth" -import { syncExistingCollection, PLUGIN_KEYS } from "./data" +import { syncExistingCollection } from "./data" +import { PLUGIN_KEYS } from "./api" import { Authenticate } from "./Login.tsx" const activeCollection = await framer.getActiveManagedCollection() diff --git a/plugins/notion/src/utils.ts b/plugins/notion/src/utils.ts index 3e25e09c..3bccd514 100644 --- a/plugins/notion/src/utils.ts +++ b/plugins/notion/src/utils.ts @@ -1,3 +1,72 @@ +export function assert(condition: unknown, ...msg: unknown[]): asserts condition { + if (condition) return + + const e = Error("Assertion Error" + (msg.length > 0 ? ": " + msg.join(" ") : "")) + // Hack the stack so the assert call itself disappears. Works in jest and in chrome. + if (e.stack) { + try { + const lines = e.stack.split("\n") + if (lines[1]?.includes("assert")) { + lines.splice(1, 1) + e.stack = lines.join("\n") + } else if (lines[0]?.includes("assert")) { + lines.splice(0, 1) + e.stack = lines.join("\n") + } + } catch { + // nothing + } + } + throw e +} + +export function isDefined(value: T): value is NonNullable { + return value !== undefined && value !== null +} + +export function isString(value: unknown): value is string { + return typeof value === "string" +} + +// Match everything except for letters, numbers and parentheses. +const nonSlugCharactersRegExp = /[^\p{Letter}\p{Number}()]+/gu +// Match leading/trailing dashes, for trimming purposes. +const trimSlugRegExp = /^-+|-+$/gu + +/** + * Takes a freeform string and removes all characters except letters, numbers, + * and parentheses. Also makes it lower case, and separates words by dashes. + * This makes the value URL safe. + */ +export function slugify(value: string): string { + return value.toLowerCase().replace(nonSlugCharactersRegExp, "-").replace(trimSlugRegExp, "") +} + +export function isURL(value: string): boolean { + try { + new URL(value) + + return true + } catch { + return false + } +} + +export function formatDate(isoDateString: string) { + const date = new Date(isoDateString) + + // Example: Format as 'April 26, 2024 15:30' + return date.toLocaleString("en-US", { + day: "2-digit", + month: "numeric", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }) +} + export function generateRandomId() { const array = new Uint8Array(16) window.crypto.getRandomValues(array) @@ -9,3 +78,7 @@ export function generateRandomId() { return id } + +export function assertNever(x: never, error?: unknown): never { + throw error || new Error((x as unknown) ? `Unexpected value: ${x}` : "Application entered invalid state") +} From eceab8615462fc86dfc7ec631810db6a31a4630f Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 10 May 2025 12:53:18 -0400 Subject: [PATCH 10/16] Get database --- plugins/notion/src/api.ts | 15 +++++++++ plugins/notion/src/blocksToHTML.ts | 2 +- plugins/notion/src/data.ts | 54 ++++++------------------------ 3 files changed, 27 insertions(+), 44 deletions(-) diff --git a/plugins/notion/src/api.ts b/plugins/notion/src/api.ts index adf6905e..629375da 100644 --- a/plugins/notion/src/api.ts +++ b/plugins/notion/src/api.ts @@ -162,3 +162,18 @@ function assertFieldTypeMatchesPropertyType( throw new Error(`Field type '${fieldType}' is not valid for property type '${propertyType}'.`) } } + +export async function getDatabase(databaseId: string) { + if (!notion) { + initNotionClient() + } + + assert(notion, "Notion client is not initialized") + const database = await notion.databases.retrieve({ database_id: databaseId }) + + return database +} + +export function richTextToPlainText(richText: RichTextItemResponse[]) { + return richText.map(value => value.plain_text).join("") +} diff --git a/plugins/notion/src/blocksToHTML.ts b/plugins/notion/src/blocksToHTML.ts index 3a2758a3..9783ff61 100644 --- a/plugins/notion/src/blocksToHTML.ts +++ b/plugins/notion/src/blocksToHTML.ts @@ -38,7 +38,7 @@ export function richTextToHTML(texts: RichTextItemResponse[]) { .join("") } -export function blocksToHtml(blocks: BlockObjectResponse[]) { +export function blocksToHTML(blocks: BlockObjectResponse[]) { let htmlContent = "" for (let i = 0; i < blocks.length; i++) { diff --git a/plugins/notion/src/data.ts b/plugins/notion/src/data.ts index 57ec79ae..0d8c9c45 100644 --- a/plugins/notion/src/data.ts +++ b/plugins/notion/src/data.ts @@ -8,8 +8,7 @@ import { type FieldDataInput, type ManagedCollectionItemInput, } from "framer-plugin" -import { Client, collectPaginatedAPI, isFullDatabase } from "@notionhq/client" -import { initNotionClient, getNotionDatabases } from "./api" +import { getNotionDatabases, getDatabase, richTextToPlainText } from "./api" import type { DatabaseObjectResponse } from "@notionhq/client/build/src/api-endpoints" import { PLUGIN_KEYS } from "./api" @@ -18,16 +17,18 @@ export interface DataSource { name: string fields: readonly ManagedCollectionFieldInput[] items: FieldDataInput[] + database: DatabaseObjectResponse } export async function getDataSources(): Promise { const databases = await getNotionDatabases() - return databases.map((db: DatabaseObjectResponse) => ({ - id: db.id, - name: db.title[0]?.plain_text || "Untitled Database", + return databases.map((database: DatabaseObjectResponse) => ({ + id: database.id, + name: database.title[0]?.plain_text || "Untitled Database", fields: [], // Fields will be populated when needed items: [], // Items will be populated when needed + database, })) } @@ -49,46 +50,13 @@ export async function getDataSources(): Promise { */ export async function getDataSource(dataSourceId: string, abortSignal?: AbortSignal): Promise { // Fetch from your data source - const dataSourceResponse = await fetch(`/data/${dataSourceId}.json`, { signal: abortSignal }) - const dataSource = await dataSourceResponse.json() - - // Map your source fields to supported field types in Framer - const fields: ManagedCollectionFieldInput[] = [] - for (const field of dataSource.fields) { - switch (field.type) { - case "string": - case "number": - case "boolean": - case "color": - case "formattedText": - case "date": - case "link": - fields.push({ - id: field.name, - name: field.name, - type: field.type, - }) - break - case "image": - case "file": - case "enum": - case "collectionReference": - case "multiCollectionReference": - console.warn(`Support for field type "${field.type}" is not implemented in this Plugin.`) - break - default: { - console.warn(`Unknown field type "${field.type}".`) - } - } - } - - const items = dataSource.items as FieldDataInput[] + const database = await getDatabase(dataSourceId) return { - id: dataSource.id, - name: dataSource.name, - fields, - items, + id: database.id, + name: richTextToPlainText(database.title) || "Untitled Database", + fields: [], + items: [], } } From 166df5f2da8311c9a11cf47114d027fc56c613b7 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 10 May 2025 12:58:57 -0400 Subject: [PATCH 11/16] Avoid refetching database --- plugins/notion/src/SelectDataSource.tsx | 9 +++++++-- plugins/notion/src/api.ts | 4 ++-- plugins/notion/src/data.ts | 9 +++++---- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/plugins/notion/src/SelectDataSource.tsx b/plugins/notion/src/SelectDataSource.tsx index 9c4b7046..b16e745a 100644 --- a/plugins/notion/src/SelectDataSource.tsx +++ b/plugins/notion/src/SelectDataSource.tsx @@ -1,6 +1,6 @@ import { framer } from "framer-plugin" import { useEffect, useState } from "react" -import { type DataSource, getDataSource, getDataSources } from "./data" +import { type DataSource, getDataSources } from "./data" interface SelectDataSourceProps { onSelectDataSource: (dataSource: DataSource) => void @@ -43,7 +43,12 @@ export function SelectDataSource({ onSelectDataSource }: SelectDataSourceProps) try { setStatus(Status.Loading) - const dataSource = await getDataSource(selectedDataSourceId) + const dataSource = dataSources.find(dataSource => dataSource.id === selectedDataSourceId) + if (!dataSource) { + framer.notify("Database not found", { variant: "error" }) + return + } + onSelectDataSource(dataSource) } catch (error) { console.error(error) diff --git a/plugins/notion/src/api.ts b/plugins/notion/src/api.ts index 629375da..29b7724d 100644 --- a/plugins/notion/src/api.ts +++ b/plugins/notion/src/api.ts @@ -174,6 +174,6 @@ export async function getDatabase(databaseId: string) { return database } -export function richTextToPlainText(richText: RichTextItemResponse[]) { - return richText.map(value => value.plain_text).join("") +export function richTextToPlainText(richText: RichTextItemResponse[] | undefined) { + return Array.isArray(richText) ? richText.map(value => value.plain_text).join("") : "" } diff --git a/plugins/notion/src/data.ts b/plugins/notion/src/data.ts index 0d8c9c45..7ddad483 100644 --- a/plugins/notion/src/data.ts +++ b/plugins/notion/src/data.ts @@ -9,7 +9,7 @@ import { type ManagedCollectionItemInput, } from "framer-plugin" import { getNotionDatabases, getDatabase, richTextToPlainText } from "./api" -import type { DatabaseObjectResponse } from "@notionhq/client/build/src/api-endpoints" +import type { GetDatabaseResponse } from "@notionhq/client/build/src/api-endpoints" import { PLUGIN_KEYS } from "./api" export interface DataSource { @@ -17,15 +17,15 @@ export interface DataSource { name: string fields: readonly ManagedCollectionFieldInput[] items: FieldDataInput[] - database: DatabaseObjectResponse + database: GetDatabaseResponse } export async function getDataSources(): Promise { const databases = await getNotionDatabases() - return databases.map((database: DatabaseObjectResponse) => ({ + return databases.map((database: GetDatabaseResponse) => ({ id: database.id, - name: database.title[0]?.plain_text || "Untitled Database", + name: richTextToPlainText(database.title) || "Untitled Database", fields: [], // Fields will be populated when needed items: [], // Items will be populated when needed database, @@ -57,6 +57,7 @@ export async function getDataSource(dataSourceId: string, abortSignal?: AbortSig name: richTextToPlainText(database.title) || "Untitled Database", fields: [], items: [], + database, } } From 45559fd293b499bdffbc5f0cd76f641fb5aa85bd Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 10 May 2025 13:15:03 -0400 Subject: [PATCH 12/16] Progress on Notion plugin --- plugins/notion/src/FieldMapping.tsx | 31 ++++- plugins/notion/src/SelectDataSource.tsx | 2 +- plugins/notion/src/api.ts | 166 +++++++++++++++++++++--- 3 files changed, 174 insertions(+), 25 deletions(-) diff --git a/plugins/notion/src/FieldMapping.tsx b/plugins/notion/src/FieldMapping.tsx index a103ad46..1b139d86 100644 --- a/plugins/notion/src/FieldMapping.tsx +++ b/plugins/notion/src/FieldMapping.tsx @@ -1,16 +1,37 @@ -import { type EditableManagedCollectionField, framer, type ManagedCollection } from "framer-plugin" +import { type ManagedCollectionField, type ManagedCollectionFieldInput. framer, type ManagedCollection } from "framer-plugin" import { useEffect, useState } from "react" import { type DataSource, mergeFieldsWithExistingFields, syncCollection } from "./data" +const labelByFieldTypeOption: Record = { + boolean: "Toggle", + date: "Date", + number: "Number", + formattedText: "Formatted Text", + color: "Color", + enum: "Option", + file: "File", + image: "Image", + link: "Link", + string: "Plain Text", + collectionReference: "Reference", + multiCollectionReference: "Multi-Reference", +} + interface FieldMappingRowProps { - field: EditableManagedCollectionField + field: ManagedCollectionFieldInput originalFieldName: string | undefined disabled: boolean onToggleDisabled: (fieldId: string) => void onNameChange: (fieldId: string, name: string) => void } -function FieldMappingRow({ field, originalFieldName, disabled, onToggleDisabled, onNameChange }: FieldMappingRowProps) { +function FieldMappingRow({ + field, + originalFieldName, + disabled, + onToggleDisabled, + onNameChange, +}: FieldMappingRowProps) { return ( <> onNameChange(field.id, event.target.value)} + disabled={disabled || isUnsupported} + placeholder={originalName ?? id} + value={isUnsupported ? "Unsupported" : name} + onChange={event => onNameChange(id, event.target.value)} onKeyDown={event => { if (event.key === "Enter") { event.preventDefault() } }} + className={"field-input" + (isUnsupported ? " unsupported" : "")} /> + {!isUnsupported && + (Array.isArray(allowedTypes) && allowedTypes.length > 1 ? ( + + ) : ( +
    {labelByFieldTypeOption[allowedTypes[0]]}
    + ))} ) } @@ -87,16 +102,17 @@ export function FieldMapping({ collection, dataSource, initialSlugFieldId }: Fie const isSyncing = status === "syncing-collection" const isLoadingFields = status === "loading-fields" - const [possibleSlugFields] = useState(() => dataSource.fields.filter(field => field.type === "string")) + const fieldsInfo = useMemo(() => getDataSourceFieldsInfo(dataSource.database), [dataSource.database]) + const possibleSlugFieldIds = useMemo(() => getPossibleSlugFieldIds(dataSource.database), [dataSource.database]) - const [selectedSlugField, setSelectedSlugField] = useState( - possibleSlugFields.find(field => field.id === initialSlugFieldId) ?? possibleSlugFields[0] ?? null + const [selectedSlugFieldId, setSelectedSlugFieldId]: FieldId | null = useState( + initialSlugFieldId ?? possibleSlugFieldIds[0] ?? null ) const [fields, setFields] = useState(initialManagedCollectionFields) const [ignoredFieldIds, setIgnoredFieldIds] = useState(initialFieldIds) - const dataSourceName = "" + const dataSourceName = dataSource.name useEffect(() => { const abortController = new AbortController() @@ -156,7 +172,7 @@ export function FieldMapping({ collection, dataSource, initialSlugFieldId }: Fie const handleSubmit = async (event: React.FormEvent) => { event.preventDefault() - if (!selectedSlugField) { + if (!selectedSlugFieldId) { // This can't happen because the form will not submit if no slug field is selected // but TypeScript can't infer that. console.error("There is no slug field selected. Sync will not be performed") @@ -169,7 +185,7 @@ export function FieldMapping({ collection, dataSource, initialSlugFieldId }: Fie const fieldsToSync = fields.filter(field => !ignoredFieldIds.has(field.id)) - await syncCollection(collection, dataSource, fieldsToSync, selectedSlugField) + await syncCollection(collection, dataSource, fieldsToSync, selectedSlugFieldId) await framer.closePlugin("Synchronization successful", { variant: "success" }) } catch (error) { console.error(error) @@ -199,18 +215,18 @@ export function FieldMapping({ collection, dataSource, initialSlugFieldId }: Fie required name="slugField" className="field-input" - value={selectedSlugField ? selectedSlugField.id : ""} + value={selectedSlugFieldId ?? ""} onChange={event => { - const selectedFieldId = event.target.value - const selectedField = possibleSlugFields.find(field => field.id === selectedFieldId) - if (!selectedField) return - setSelectedSlugField(selectedField) + setSelectedSlugFieldId( + possibleSlugFieldIds.includes(event.target.value) ? event.target.value : null + ) }} > - {possibleSlugFields.map(possibleSlugField => { + {possibleSlugFieldIds.map(possibleSlugFieldId => { return ( - ) })} @@ -218,30 +234,25 @@ export function FieldMapping({ collection, dataSource, initialSlugFieldId }: Fie
    - Column - Field - {fields.map(field => ( - sourceField.id === field.id)?.name} - disabled={ignoredFieldIds.has(field.id)} - onToggleDisabled={toggleFieldDisabledState} - onNameChange={changeFieldName} - /> - ))} + Notion Property + Field Name + Field Type + {fieldsInfo.map(fieldInfo => { + return ( + + ) + })}

    diff --git a/plugins/notion/src/api.ts b/plugins/notion/src/api.ts index f3cbba26..2a5cd5cd 100644 --- a/plugins/notion/src/api.ts +++ b/plugins/notion/src/api.ts @@ -22,7 +22,7 @@ import { type FieldData, } from "framer-plugin" import pLimit from "p-limit" -import { blocksToHtml, richTextToHTML } from "./blocksToHTML" +import { blocksToHTML, richTextToHTML } from "./blocksToHTML" import { assert, assertNever, formatDate, isDefined, isString, slugify } from "./utils" export const API_BASE_URL = "https://notion-plugin-api.framer-team.workers.dev" @@ -37,6 +37,15 @@ export const PLUGIN_KEYS = { export type FieldId = string +export interface FieldInfo { + id: FieldId + name: string + originalName: string + type: ManagedCollectionField["type"] | null + allowedTypes: ManagedCollectionField["type"][] + disabled: boolean +} + // Maximum number of concurrent requests to Notion API // This is to prevent rate limiting. const CONCURRENCY_LIMIT = 5 @@ -46,13 +55,13 @@ export type NotionProperty = GetDatabaseResponse["properties"][string] // Every page has content which can be fetched as blocks. We add it as a // property so it displays in the list where you can configure properties to be // synced with the CMS -const pageContentId = "page-content" -export const pageContentProperty: SupportedNotionProperty = { - type: "rich_text", - id: pageContentId, +export const pageContentProperty: FieldInfo = { + id: "page-content", + type: "formattedText", name: "Content", - description: "Page Content", - rich_text: {}, + originalName: "Content", + allowedTypes: ["formattedText"], + disabled: false, } // The order in which we display slug fields @@ -181,8 +190,8 @@ export function richTextToPlainText(richText: RichTextItemResponse[] | undefined return Array.isArray(richText) ? richText.map(value => value.plain_text).join("") : "" } -export function getNotionProperties(database: GetDatabaseResponse) { - const result: NotionProperty[] = [] +export function getDatabaseFieldsInfo(database: GetDatabaseResponse) { + const result: FieldInfo[] = [] // This property is always there but not included in `"database.properties" result.push(pageContentProperty) @@ -191,7 +200,16 @@ export function getNotionProperties(database: GetDatabaseResponse) { const property = database.properties[key] assert(property) - result.push(property) + const allowedTypes = supportedCMSTypeByNotionPropertyType[property.type] ?? [] + + result.push({ + id: property.id, + name: property.name, + originalName: property.name, + type: allowedTypes[0] ?? null, + allowedTypes, + disabled: false, + }) } return result @@ -201,7 +219,7 @@ export function getNotionProperties(database: GetDatabaseResponse) { * Given a Notion Database returns a list of possible fields that can be used as * a slug. And a suggested field id to use as a slug. */ -export function getPossibleSlugFields(database: GetDatabaseResponse) { +export function getPossibleSlugFieldIds(database: GetDatabaseResponse) { const options: NotionProperty[] = [] for (const key in database.properties) { @@ -223,7 +241,7 @@ export function getPossibleSlugFields(database: GetDatabaseResponse) { options.sort((a, b) => getOrderIndex(a.type) - getOrderIndex(b.type)) - return options + return options.map(property => property.id) } export function getPropertyValue( @@ -303,5 +321,5 @@ async function getPageBlocksAsRichText(pageId: string) { assert(blocks.every(isFullBlock), "Response is not a full block") - return blocksToHtml(blocks) + return blocksToHTML(blocks) } diff --git a/plugins/notion/src/data.ts b/plugins/notion/src/data.ts index 7ddad483..2eee1d85 100644 --- a/plugins/notion/src/data.ts +++ b/plugins/notion/src/data.ts @@ -8,28 +8,37 @@ import { type FieldDataInput, type ManagedCollectionItemInput, } from "framer-plugin" -import { getNotionDatabases, getDatabase, richTextToPlainText } from "./api" +import { + getNotionDatabases, + getDatabase, + richTextToPlainText, + pageContentProperty, + getDatabaseFieldsInfo, + PLUGIN_KEYS, + type FieldInfo, +} from "./api" import type { GetDatabaseResponse } from "@notionhq/client/build/src/api-endpoints" -import { PLUGIN_KEYS } from "./api" export interface DataSource { id: string name: string - fields: readonly ManagedCollectionFieldInput[] - items: FieldDataInput[] database: GetDatabaseResponse } export async function getDataSources(): Promise { const databases = await getNotionDatabases() - return databases.map((database: GetDatabaseResponse) => ({ - id: database.id, - name: richTextToPlainText(database.title) || "Untitled Database", - fields: [], // Fields will be populated when needed - items: [], // Items will be populated when needed - database, - })) + return databases.map((database: GetDatabaseResponse) => { + return { + id: database.id, + name: richTextToPlainText(database.title) || "Untitled Database", + database, + } + }) +} + +export function getDataSourceFieldsInfo(database: GetDatabaseResponse): FieldInfo[] { + return getDatabaseFieldsInfo(database) } /** From 071c2677a7db0e9e782241b2bac50644396c4872 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 10 May 2025 14:35:09 -0400 Subject: [PATCH 14/16] Change field types and names --- plugins/notion/src/App.css | 12 +++++ plugins/notion/src/App.tsx | 4 +- plugins/notion/src/FieldMapping.tsx | 73 ++++++++++++++++++++--------- plugins/notion/src/api.ts | 1 - 4 files changed, 64 insertions(+), 26 deletions(-) diff --git a/plugins/notion/src/App.css b/plugins/notion/src/App.css index 68e84784..e48fddc1 100644 --- a/plugins/notion/src/App.css +++ b/plugins/notion/src/App.css @@ -29,6 +29,10 @@ form { flex-shrink: 1; } +.field-input[disabled] { + opacity: 0.5; +} + .field-input.unsupported { grid-column: span 2; } @@ -177,6 +181,10 @@ form { align-items: center; } +.mapping .single-field-type.disabled { + opacity: 0.5; +} + .mapping footer { position: sticky; bottom: 0; @@ -201,6 +209,10 @@ form { pointer-events: none; } +select { + padding-left: 10px; +} + /* Login */ .login { diff --git a/plugins/notion/src/App.tsx b/plugins/notion/src/App.tsx index 12393cf6..1aaef4ac 100644 --- a/plugins/notion/src/App.tsx +++ b/plugins/notion/src/App.tsx @@ -44,9 +44,7 @@ export function App({ collection, previousDataSourceId, previousSlugFieldId }: A console.error(error) framer.notify( `Error loading previously configured data source “${previousDataSourceId}”. Check the logs for more details.`, - { - variant: "error", - } + { variant: "error" } ) }) .finally(() => { diff --git a/plugins/notion/src/FieldMapping.tsx b/plugins/notion/src/FieldMapping.tsx index 9222e1d1..354a3152 100644 --- a/plugins/notion/src/FieldMapping.tsx +++ b/plugins/notion/src/FieldMapping.tsx @@ -7,8 +7,11 @@ import { import { useEffect, useMemo, useState } from "react" import { type DataSource, getDataSourceFieldsInfo, mergeFieldsWithExistingFields, syncCollection } from "./data" import { getPossibleSlugFieldIds, type FieldId, type FieldInfo } from "./api" +import classNames from "classnames" -const labelByFieldTypeOption: Record = { +type FieldType = ManagedCollectionField["type"] + +const labelByFieldTypeOption: Record = { boolean: "Toggle", date: "Date", number: "Number", @@ -25,22 +28,30 @@ const labelByFieldTypeOption: Record = { interface FieldMappingRowProps { fieldInfo: FieldInfo - onToggleDisabled: (fieldId: string) => void + ignored: boolean + onToggleIgnored: (fieldId: string) => void onNameChange: (fieldId: string, name: string) => void + onFieldTypeChange: (fieldId: string, type: FieldType) => void } -function FieldMappingRow({ fieldInfo, onToggleDisabled, onNameChange }: FieldMappingRowProps) { +function FieldMappingRow({ + fieldInfo, + ignored, + onToggleIgnored, + onNameChange, + onFieldTypeChange, +}: FieldMappingRowProps) { const { id, name, originalName, type, allowedTypes } = fieldInfo const isUnsupported = !Array.isArray(allowedTypes) || allowedTypes.length === 0 - const disabled = isUnsupported || fieldInfo.disabled + const disabled = isUnsupported || ignored return ( <>