From f42aad15286af98e5a7e899fc20e9be450572847 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 10:51:19 -0400 Subject: [PATCH 01/36] Increase default window 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 31b6026c120cc1fbbd538067f41399e1c21754fd Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 10:54:40 -0400 Subject: [PATCH 02/36] Improve design of unsupported fields Show "Unsupported" text once per row. Uncheck unsupported fields. --- plugins/notion/src/MapFields.tsx | 46 +++++++++++++++----------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/plugins/notion/src/MapFields.tsx b/plugins/notion/src/MapFields.tsx index b1bdb0d3..bbb175bc 100644 --- a/plugins/notion/src/MapFields.tsx +++ b/plugins/notion/src/MapFields.tsx @@ -257,7 +257,7 @@ export function MapDatabaseFields({ { handleFieldToggle(property.id) }} @@ -272,36 +272,32 @@ export function MapDatabaseFields({ { handleFieldNameChange(property.id, e.target.value) }} > - + {isSupported && ( + + )} ) })} From b231fa7bdc67e689bda72418c97f5c16093b2ad3 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 11:08:03 -0400 Subject: [PATCH 03/36] Add support for phone number field --- plugins/notion/src/notion.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugins/notion/src/notion.ts b/plugins/notion/src/notion.ts index 129ef31d..ffd534e6 100644 --- a/plugins/notion/src/notion.ts +++ b/plugins/notion/src/notion.ts @@ -186,6 +186,7 @@ export const supportedNotionPropertyTypes = [ "url", "files", "relation", + "phone_number", ] satisfies ReadonlyArray type SupportedPropertyType = (typeof supportedNotionPropertyTypes)[number] @@ -209,6 +210,7 @@ export const supportedCMSTypeByNotionPropertyType = { email: ["formattedText", "string"], files: ["file", "image"], relation: ["multiCollectionReference"], + phone_number: ["string"], } satisfies Record> function assertFieldTypeMatchesPropertyType( @@ -376,6 +378,16 @@ export function getCollectionFieldForProperty< userEditable: false, } } + case "phone_number": { + assertFieldTypeMatchesPropertyType(property.type, fieldType) + + return { + type: "string", + id: property.id, + name: property.name, + userEditable: false, + } + } default: { assertNever(property) } @@ -444,6 +456,11 @@ export function getPropertyValue( if (firstFile.type === "file") { return firstFile.file.url } + + return null + } + case "phone_number": { + return property.phone_number ?? "" } } } From d04b1947cd7a0522be785450092dd45a913a78f1 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 11:23:28 -0400 Subject: [PATCH 04/36] Allow empty email fields, make default email type string --- plugins/notion/src/notion.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/notion/src/notion.ts b/plugins/notion/src/notion.ts index ffd534e6..641704b8 100644 --- a/plugins/notion/src/notion.ts +++ b/plugins/notion/src/notion.ts @@ -207,7 +207,7 @@ export const supportedCMSTypeByNotionPropertyType = { select: ["enum"], status: ["enum"], url: ["link"], - email: ["formattedText", "string"], + email: ["string", "formattedText"], files: ["file", "image"], relation: ["multiCollectionReference"], phone_number: ["string"], @@ -419,6 +419,13 @@ export function getPropertyValue( return richTextToPlainText(property.rich_text) } + case "email": { + if (supportsHtml) { + return `

${property.email ?? ""}

` + } + + return property.email ?? "" + } case "select": { if (!property.select) return null @@ -548,7 +555,7 @@ async function processItem( } const fieldValue = getPropertyValue(property, { supportsHtml: field.type === "formattedText" }) - if (!fieldValue) { + if (fieldValue === null || fieldValue === undefined) { status.warnings.push({ url: item.url, fieldId: field.id, From b4f25de4cad6329dcc819adb3ea01f9a52bbf917 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 11:26:14 -0400 Subject: [PATCH 05/36] Make field type labels match Framer --- plugins/notion/src/MapFields.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/notion/src/MapFields.tsx b/plugins/notion/src/MapFields.tsx index bbb175bc..519edb03 100644 --- a/plugins/notion/src/MapFields.tsx +++ b/plugins/notion/src/MapFields.tsx @@ -98,18 +98,18 @@ function getLastSynchronizedAtTimestamp( } const labelByFieldTypeOption: Record = { - boolean: "Boolean", + boolean: "Toggle", date: "Date", number: "Number", formattedText: "Formatted Text", color: "Color", - enum: "Enum", + enum: "Option", file: "File", image: "Image", link: "Link", string: "String", collectionReference: "Reference", - multiCollectionReference: "Multi Reference", + multiCollectionReference: "Multi-Reference", } export function MapDatabaseFields({ From ae210705e4c7e8e8574660df63566a17fcdad0a7 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 11:32:01 -0400 Subject: [PATCH 06/36] Fix importing status fields --- plugins/notion/src/notion.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/notion/src/notion.ts b/plugins/notion/src/notion.ts index 641704b8..68a1a50d 100644 --- a/plugins/notion/src/notion.ts +++ b/plugins/notion/src/notion.ts @@ -319,10 +319,10 @@ export function getCollectionFieldForProperty< type: "enum", id: property.id, name: property.name, - cases: property.status.groups.map(group => { + cases: property.status.options.map(option => { return { - id: group.id, - name: group.name, + id: option.id, + name: option.name, } }), userEditable: false, @@ -431,6 +431,11 @@ export function getPropertyValue( return property.select.id } + case "status": { + if (!property.status) return null + + return property.status.id + } case "title": if (supportsHtml) { return richTextToHTML(property.title) From 064d1aa573e1a3d458d5ba88a5ee73ab8ee19500 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 11:34:00 -0400 Subject: [PATCH 07/36] Make "Next" a primary button --- plugins/notion/src/SelectDatabase.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/notion/src/SelectDatabase.tsx b/plugins/notion/src/SelectDatabase.tsx index caf9be38..dab74764 100644 --- a/plugins/notion/src/SelectDatabase.tsx +++ b/plugins/notion/src/SelectDatabase.tsx @@ -3,6 +3,7 @@ import { richTextToPlainText, useDatabasesQuery } from "./notion" import { FormEvent, useEffect, useRef, useState } from "react" import notionConnectSrc from "./assets/notion-connect.png" import { assert } from "./utils" +import { Button } from "./components/Button" interface SelectDatabaseProps { onDatabaseSelected: (database: GetDatabaseResponse) => void @@ -106,9 +107,9 @@ export function SelectDatabase({ onDatabaseSelected }: SelectDatabaseProps) { - + ) From 4b195ea42af26376ca33b9f09ff9cef2953bef6f Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 11:47:28 -0400 Subject: [PATCH 08/36] Prevent importing non-image files as images --- plugins/notion/src/notion.ts | 37 ++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/plugins/notion/src/notion.ts b/plugins/notion/src/notion.ts index 68a1a50d..97fc2678 100644 --- a/plugins/notion/src/notion.ts +++ b/plugins/notion/src/notion.ts @@ -54,6 +54,8 @@ export const pageContentProperty: SupportedNotionProperty = { rich_text: {}, } +export const imageFileExtensions = ["jpg", "jpeg", "png", "gif", "apng", "webp", "svg"] + // 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() { @@ -400,8 +402,10 @@ export function richTextToPlainText(richText: RichTextItemResponse[]) { export function getPropertyValue( property: PageObjectResponse["properties"][string], - { supportsHtml }: { supportsHtml: boolean } + { fieldType }: { fieldType: string } ): unknown | undefined { + const supportsHtml = fieldType === "formattedText" + switch (property.type) { case "checkbox": { return property.checkbox @@ -436,12 +440,13 @@ export function getPropertyValue( return property.status.id } - case "title": + case "title": { if (supportsHtml) { return richTextToHTML(property.title) } return richTextToPlainText(property.title) + } case "number": { return property.number } @@ -458,17 +463,25 @@ export function getPropertyValue( return property.relation.map(({ id }) => id) } case "files": { - const firstFile = property.files[0] - if (!firstFile) return null + for (const file of property.files) { + let url = null - if (firstFile.type === "external") { - return firstFile.external.url - } + if (file.type === "external") { + url = file.external.url + } else if (file.type === "file") { + url = file.file.url + } - if (firstFile.type === "file") { - return firstFile.file.url - } + if (!url) continue + if (fieldType === "image") { + if (!imageFileExtensions.some(ext => url.toLowerCase().endsWith(ext))) { + continue + } + } + + return url + } return null } case "phone_number": { @@ -544,7 +557,7 @@ async function processItem( assert(property) if (property.id === slugFieldId) { - const resolvedSlug = getPropertyValue(property, { supportsHtml: false }) + const resolvedSlug = getPropertyValue(property, { fieldType: "string" }) if (!resolvedSlug || typeof resolvedSlug !== "string") { continue } @@ -559,7 +572,7 @@ async function processItem( continue } - const fieldValue = getPropertyValue(property, { supportsHtml: field.type === "formattedText" }) + const fieldValue = getPropertyValue(property, { fieldType: field.type }) if (fieldValue === null || fieldValue === undefined) { status.warnings.push({ url: item.url, From 4ea15a006b2352f259c5836ad3f05850cf62909c Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 11:50:56 -0400 Subject: [PATCH 09/36] Sync blockquotes from Notion --- plugins/notion/src/blocksToHTML.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/notion/src/blocksToHTML.ts b/plugins/notion/src/blocksToHTML.ts index c15abd4b..29c26c4f 100644 --- a/plugins/notion/src/blocksToHTML.ts +++ b/plugins/notion/src/blocksToHTML.ts @@ -94,6 +94,9 @@ export function blocksToHtml(blocks: BlockObjectResponse[]) { case "code": htmlContent += `
${richTextToHTML(block.code.rich_text)}
` break + case "quote": + htmlContent += `
${richTextToHTML(block.quote.rich_text)}
` + break default: // TODO: More block types can be added here! break From f8968c27eb7bdfd2fdc178f5ddef2eb4bf3a9306 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 12:03:49 -0400 Subject: [PATCH 10/36] Sync YouTube videos in formatted text --- plugins/notion/src/blocksToHTML.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plugins/notion/src/blocksToHTML.ts b/plugins/notion/src/blocksToHTML.ts index 29c26c4f..b629b4e1 100644 --- a/plugins/notion/src/blocksToHTML.ts +++ b/plugins/notion/src/blocksToHTML.ts @@ -1,6 +1,8 @@ import { BlockObjectResponse, RichTextItemResponse } from "@notionhq/client/build/src/api-endpoints" import { assert } from "./utils" +const youtubeIdRegex = /(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/))(?[^?&]+)/u + export function richTextToHTML(texts: RichTextItemResponse[]) { return texts .map(({ plain_text, annotations, href }) => { @@ -97,6 +99,16 @@ export function blocksToHtml(blocks: BlockObjectResponse[]) { case "quote": htmlContent += `
${richTextToHTML(block.quote.rich_text)}
` break + case "video": + if (block.video.type === "external") { + const url = block.video.external.url + const youtubeId = url.match(youtubeIdRegex)?.groups?.videoId + if (youtubeId) { + htmlContent += `` + break + } + } + break default: // TODO: More block types can be added here! break From 2158f5c827443380f05e654058db11e944276e52 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 12:06:14 -0400 Subject: [PATCH 11/36] Improve Notion code block languages --- plugins/notion/src/blocksToHTML.ts | 84 +++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/plugins/notion/src/blocksToHTML.ts b/plugins/notion/src/blocksToHTML.ts index b629b4e1..d8f5e9d2 100644 --- a/plugins/notion/src/blocksToHTML.ts +++ b/plugins/notion/src/blocksToHTML.ts @@ -94,7 +94,14 @@ export function blocksToHtml(blocks: BlockObjectResponse[]) { break } case "code": - htmlContent += `
${richTextToHTML(block.code.rich_text)}
` + const language = codeLanguageMapping[block.code.language] + if (language) { + htmlContent += `
${richTextToHTML(
+                        block.code.rich_text
+                    )}
` + } else { + htmlContent += `
${richTextToHTML(block.code.rich_text)}
` + } break case "quote": htmlContent += `
${richTextToHTML(block.quote.rich_text)}
` @@ -117,3 +124,78 @@ export function blocksToHtml(blocks: BlockObjectResponse[]) { return htmlContent } + +const codeLanguageMapping: Record = { + abap: null, + arduino: null, + bash: "Shell", + basic: null, + c: "C", + clojure: null, + coffeescript: null, + "c++": "C++", + "c#": "C#", + css: "CSS", + dart: null, + diff: null, + docker: null, + elixir: null, + elm: null, + erlang: null, + flow: null, + fortran: null, + "f#": null, + gherkin: null, + glsl: null, + go: "Go", + graphql: null, + groovy: null, + haskell: "Haskell", + html: "HTML", + java: "Java", + javascript: "JavaScript", + json: null, + julia: "Julia", + kotlin: "Kotlin", + latex: null, + less: "Less", + lisp: null, + livescript: null, + lua: "Lua", + makefile: null, + markdown: "Markdown", + markup: null, + matlab: "MATLAB", + mermaid: null, + nix: null, + "objective-c": "Objective-C", + ocaml: null, + pascal: null, + perl: "Perl", + php: "PHP", + "plain text": null, + powershell: null, + prolog: null, + protobuf: null, + python: "Python", + r: null, + reason: null, + ruby: "Ruby", + rust: "Rust", + sass: null, + scala: "Scala", + scheme: null, + scss: "SCSS", + shell: "Shell", + sql: "SQL", + swift: "Swift", + typescript: "TypeScript", + "vb.net": null, + verilog: null, + vhdl: null, + "visual basic": null, + webassembly: null, + xml: null, + yaml: "YAML", + "java/c/c++/c#": null, +} From 88372ce71ea11af009bb01e68bb4fac4c082434c Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 12:17:35 -0400 Subject: [PATCH 12/36] Add unique ID field support --- plugins/notion/src/notion.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/plugins/notion/src/notion.ts b/plugins/notion/src/notion.ts index 97fc2678..4ada0270 100644 --- a/plugins/notion/src/notion.ts +++ b/plugins/notion/src/notion.ts @@ -189,6 +189,7 @@ export const supportedNotionPropertyTypes = [ "files", "relation", "phone_number", + "unique_id", ] satisfies ReadonlyArray type SupportedPropertyType = (typeof supportedNotionPropertyTypes)[number] @@ -213,6 +214,7 @@ export const supportedCMSTypeByNotionPropertyType = { files: ["file", "image"], relation: ["multiCollectionReference"], phone_number: ["string"], + unique_id: ["string", "number"], } satisfies Record> function assertFieldTypeMatchesPropertyType( @@ -390,6 +392,16 @@ export function getCollectionFieldForProperty< userEditable: false, } } + case "unique_id": { + assertFieldTypeMatchesPropertyType(property.type, fieldType) + + return { + type: fieldType, + id: property.id, + name: property.name, + userEditable: false, + } + } default: { assertNever(property) } @@ -454,6 +466,12 @@ export function getPropertyValue( return property.url } case "unique_id": { + if (fieldType === "string") { + return property.unique_id.prefix + ? `${property.unique_id.prefix}-${property.unique_id.number}` + : String(property.unique_id.number) + } + return property.unique_id.number } case "date": { From 84a869d752b3f57640a1579f26e0d81b39cbdc15 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 12:41:41 -0400 Subject: [PATCH 13/36] Add title with link to Notion database --- plugins/notion/src/MapFields.tsx | 37 ++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/plugins/notion/src/MapFields.tsx b/plugins/notion/src/MapFields.tsx index 519edb03..ac2f49ca 100644 --- a/plugins/notion/src/MapFields.tsx +++ b/plugins/notion/src/MapFields.tsx @@ -213,7 +213,8 @@ export function MapDatabaseFields({ }) } - const title = richTextToPlainText(database.title) + const plainTextTitle = richTextToPlainText(database.title) + const title = plainTextTitle.trim() ? plainTextTitle : "Untitled" return (
@@ -222,6 +223,14 @@ export function MapDatabaseFields({
+ + {database.icon && } + {title} +
-
+
Notion Property Field Name Field Type diff --git a/plugins/notion/src/notion.ts b/plugins/notion/src/notion.ts index 4ada0270..4b652600 100644 --- a/plugins/notion/src/notion.ts +++ b/plugins/notion/src/notion.ts @@ -54,6 +54,14 @@ export const pageContentProperty: SupportedNotionProperty = { rich_text: {}, } +const pageCoverImageId = "page-cover" +export const pageCoverImageProperty: SupportedNotionProperty = { + type: "cover-image", + id: pageCoverImageId, + name: "Cover Image", + description: "Page Cover Image", +} + export const imageFileExtensions = ["jpg", "jpeg", "png", "gif", "apng", "webp", "svg"] // Naive implementation to be authenticated, a token could be expired. @@ -70,8 +78,8 @@ if (isAuthenticated()) { export function getNotionProperties(database: GetDatabaseResponse) { const result: NotionProperty[] = [] - // This property is always there but not included in `"database.properties" - result.push(pageContentProperty) + // These properties are always there but not included in `"database.properties" + result.push(pageContentProperty, pageCoverImageProperty) for (const key in database.properties) { const property = database.properties[key] @@ -193,9 +201,16 @@ export const supportedNotionPropertyTypes = [ ] satisfies ReadonlyArray type SupportedPropertyType = (typeof supportedNotionPropertyTypes)[number] -type SupportedNotionProperty = Extract +type CustomPropertyType = "cover-image" -export function isSupportedNotionProperty(property: NotionProperty): property is SupportedNotionProperty { +type SupportedNotionProperty = + | Extract + | { type: CustomPropertyType; id: string; name: string; description: string } + +export function isSupportedNotionProperty( + property: NotionProperty | { type: CustomPropertyType } +): property is SupportedNotionProperty { + if (property.type === "cover-image") return true return supportedNotionPropertyTypes.includes(property.type as SupportedPropertyType) } @@ -215,7 +230,8 @@ export const supportedCMSTypeByNotionPropertyType = { relation: ["multiCollectionReference"], phone_number: ["string"], unique_id: ["string", "number"], -} satisfies Record> + "cover-image": ["image"], +} satisfies Record> function assertFieldTypeMatchesPropertyType( propertyType: T, @@ -233,7 +249,7 @@ function assertFieldTypeMatchesPropertyType( * That maps the Notion Property to the Framer CMS collection property type */ export function getCollectionFieldForProperty< - TProperty extends Extract, + TProperty extends Extract >( property: TProperty, fieldType: ManagedCollectionField["type"], @@ -402,6 +418,16 @@ export function getCollectionFieldForProperty< userEditable: false, } } + case "cover-image": { + assertFieldTypeMatchesPropertyType(property.type, fieldType) + + return { + type: "image", + id: property.id, + name: property.name, + userEditable: false, + } + } default: { assertNever(property) } @@ -607,6 +633,14 @@ async function processItem( fieldData[pageContentProperty.id] = contentHTML } + if (fieldsById.has(pageCoverImageProperty.id) && item.cover) { + if (item.cover.type === "external") { + fieldData[pageCoverImageProperty.id] = item.cover.external.url + } else if (item.cover.type === "file") { + fieldData[pageCoverImageProperty.id] = item.cover.file.url + } + } + if (!slugValue) { status.warnings.push({ url: item.url, From bee0c45ed5bdaec229a80e0b9928db13acbff377 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 13:04:12 -0400 Subject: [PATCH 15/36] Remove warnings for missing file and URL values --- plugins/notion/src/notion.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/notion/src/notion.ts b/plugins/notion/src/notion.ts index 4b652600..d311cdf1 100644 --- a/plugins/notion/src/notion.ts +++ b/plugins/notion/src/notion.ts @@ -489,7 +489,7 @@ export function getPropertyValue( return property.number } case "url": { - return property.url + return property.url ?? "" } case "unique_id": { if (fieldType === "string") { @@ -508,7 +508,7 @@ export function getPropertyValue( } case "files": { for (const file of property.files) { - let url = null + let url = "" if (file.type === "external") { url = file.external.url @@ -526,7 +526,7 @@ export function getPropertyValue( return url } - return null + return "" } case "phone_number": { return property.phone_number ?? "" From 8742256f09f192ed6b9ab0e7f27cca411a2869c7 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 13:06:42 -0400 Subject: [PATCH 16/36] Move title field to first in list of properties --- plugins/notion/src/MapFields.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/notion/src/MapFields.tsx b/plugins/notion/src/MapFields.tsx index 2acc5c84..076c991b 100644 --- a/plugins/notion/src/MapFields.tsx +++ b/plugins/notion/src/MapFields.tsx @@ -23,7 +23,13 @@ import { import { assert } from "./utils" function getSortedProperties(database: GetDatabaseResponse): NotionProperty[] { - return getNotionProperties(database).sort((propertyA, propertyB) => { + const properties = getNotionProperties(database) + return properties.sort((propertyA, propertyB) => { + // Put title field first + if (propertyA.type === "title") return -1 + if (propertyB.type === "title") return 1 + + // Then sort by supported status const a = isSupportedNotionProperty(propertyA) ? -1 : 0 const b = isSupportedNotionProperty(propertyB) ? -1 : 0 return a - b From 1b8d219eeb06afa5d86196d45d44283d22938af2 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 13:07:56 -0400 Subject: [PATCH 17/36] Import missing number as 0 --- plugins/notion/src/notion.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/notion/src/notion.ts b/plugins/notion/src/notion.ts index d311cdf1..c943e52f 100644 --- a/plugins/notion/src/notion.ts +++ b/plugins/notion/src/notion.ts @@ -486,7 +486,7 @@ export function getPropertyValue( return richTextToPlainText(property.title) } case "number": { - return property.number + return property.number ?? 0 } case "url": { return property.url ?? "" From 6f4a57292ae73422b947863a622b828adc20c4d3 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 13:15:02 -0400 Subject: [PATCH 18/36] Add support for people fields --- plugins/notion/src/notion.ts | 44 ++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/plugins/notion/src/notion.ts b/plugins/notion/src/notion.ts index c943e52f..b50cb574 100644 --- a/plugins/notion/src/notion.ts +++ b/plugins/notion/src/notion.ts @@ -198,6 +198,9 @@ export const supportedNotionPropertyTypes = [ "relation", "phone_number", "unique_id", + "people", + "created_by", + "last_edited_by", ] satisfies ReadonlyArray type SupportedPropertyType = (typeof supportedNotionPropertyTypes)[number] @@ -230,6 +233,9 @@ export const supportedCMSTypeByNotionPropertyType = { relation: ["multiCollectionReference"], phone_number: ["string"], unique_id: ["string", "number"], + people: ["string"], + created_by: ["string"], + last_edited_by: ["string"], "cover-image": ["image"], } satisfies Record> @@ -428,6 +434,33 @@ export function getCollectionFieldForProperty< userEditable: false, } } + case "people": { + assertFieldTypeMatchesPropertyType(property.type, fieldType) + return { + type: "string", + id: property.id, + name: property.name, + userEditable: false, + } + } + case "created_by": { + assertFieldTypeMatchesPropertyType(property.type, fieldType) + return { + type: "string", + id: property.id, + name: property.name, + userEditable: false, + } + } + case "last_edited_by": { + assertFieldTypeMatchesPropertyType(property.type, fieldType) + return { + type: "string", + id: property.id, + name: property.name, + userEditable: false, + } + } default: { assertNever(property) } @@ -531,6 +564,17 @@ export function getPropertyValue( case "phone_number": { return property.phone_number ?? "" } + case "people": { + const firstUser = property.people[0] + if (!firstUser) return "" + return "name" in firstUser ? firstUser.name : "" + } + case "created_by": { + return "name" in property.created_by ? property.created_by.name : "" + } + case "last_edited_by": { + return "name" in property.last_edited_by ? property.last_edited_by.name : "" + } } } From e471b70ca842ca1c9f36c2418cd7c1eaa12791f5 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 13:17:15 -0400 Subject: [PATCH 19/36] Disable field type dropdown when only 1 value --- plugins/notion/src/MapFields.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/notion/src/MapFields.tsx b/plugins/notion/src/MapFields.tsx index 076c991b..ac1782e5 100644 --- a/plugins/notion/src/MapFields.tsx +++ b/plugins/notion/src/MapFields.tsx @@ -28,7 +28,7 @@ function getSortedProperties(database: GetDatabaseResponse): NotionProperty[] { // Put title field first if (propertyA.type === "title") return -1 if (propertyB.type === "title") return 1 - + // Then sort by supported status const a = isSupportedNotionProperty(propertyA) ? -1 : 0 const b = isSupportedNotionProperty(propertyB) ? -1 : 0 @@ -297,7 +297,7 @@ export function MapDatabaseFields({ > {isSupported && ( setSlugFieldId(e.target.value)} required @@ -297,7 +297,10 @@ export function MapDatabaseFields({ > {isSupported && ( setSelectedDatabase(event.target.value)} - className="flex-1 shrink-1" + className="flex-1 shrink-1 cursor-pointer" disabled={!selectEnabled} > {isLoadingOrFetching && ( diff --git a/plugins/notion/src/components/spinner.module.css b/plugins/notion/src/components/spinner.module.css index d97fdc1c..65dc6960 100644 --- a/plugins/notion/src/components/spinner.module.css +++ b/plugins/notion/src/components/spinner.module.css @@ -7,7 +7,7 @@ } .lightStyle { - background-color: #fff; + background-color: var(--framer-color-text-reversed); } .darkStyle { From 3799b645ff2d425e0d87d41acc0aa6daadda44da Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 15:53:13 -0400 Subject: [PATCH 27/36] Formula and rollup field, more slug types --- plugins/notion/src/notion.ts | 170 ++++++++++++++++++++++++++++++----- 1 file changed, 149 insertions(+), 21 deletions(-) diff --git a/plugins/notion/src/notion.ts b/plugins/notion/src/notion.ts index 71511442..0e9d46db 100644 --- a/plugins/notion/src/notion.ts +++ b/plugins/notion/src/notion.ts @@ -46,7 +46,7 @@ export type NotionProperty = GetDatabaseResponse["properties"][string] // 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 = { +const pageContentProperty: SupportedNotionProperty = { type: "page-content", id: pageContentId, name: "Content", @@ -54,14 +54,26 @@ export const pageContentProperty: SupportedNotionProperty = { } const pageCoverImageId = "page-cover" -export const pageCoverImageProperty: SupportedNotionProperty = { +const pageCoverImageProperty: SupportedNotionProperty = { type: "cover-image", id: pageCoverImageId, name: "Cover Image", description: "Page Cover Image", } -export const imageFileExtensions = ["jpg", "jpeg", "png", "gif", "apng", "webp", "svg"] +const imageFileExtensions = ["jpg", "jpeg", "png", "gif", "apng", "webp", "svg"] + +const defaultValueForFieldType: Record = { + string: "", + number: 0, + boolean: false, + date: null, + formattedText: "", + image: null, + link: "", + file: null, + enum: null, +} // Naive implementation to be authenticated, a token could be expired. // For simplicity we just close the plugin and clear storage in that case. @@ -123,7 +135,7 @@ export function initNotionClient() { } // The order in which we display slug fields -const preferedSlugFieldOrder: NotionProperty["type"][] = ["title", "rich_text"] +const preferedSlugFieldOrder: NotionProperty["type"][] = ["title", "rich_text", "unique_id", "formula", "rollup"] /** * Given a Notion Database returns a list of possible fields that can be used as @@ -136,11 +148,8 @@ export function getPossibleSlugFields(database: GetDatabaseResponse) { const property = database.properties[key] assert(property) - switch (property.type) { - case "title": - case "rich_text": - options.push(property) - break + if (preferedSlugFieldOrder.includes(property.type)) { + options.push(property) } } @@ -200,6 +209,8 @@ export const supportedNotionPropertyTypes = [ "people", "created_by", "last_edited_by", + "formula", + "rollup", ] satisfies ReadonlyArray type SupportedPropertyType = (typeof supportedNotionPropertyTypes)[number] @@ -226,7 +237,7 @@ export const supportedCMSTypeByNotionPropertyType = { last_edited_time: ["date"], select: ["enum"], status: ["enum"], - url: ["link"], + url: ["link", "string"], email: ["string", "formattedText"], files: ["file", "image"], relation: ["multiCollectionReference"], @@ -235,6 +246,8 @@ export const supportedCMSTypeByNotionPropertyType = { people: ["string"], created_by: ["string"], last_edited_by: ["string"], + formula: ["string", "number", "boolean", "date", "link", "image", "file"], + rollup: ["string", "number", "boolean", "date", "link", "image", "file"], "cover-image": ["image"], "page-content": ["formattedText"], } satisfies Record> @@ -264,7 +277,8 @@ export function getCollectionFieldForProperty< switch (property.type) { case "email": case "rich_text": - case "unique_id": { + case "unique_id": + case "url": { assertFieldTypeMatchesPropertyType(property.type, fieldType) return { @@ -274,6 +288,27 @@ export function getCollectionFieldForProperty< userEditable: false, } } + case "formula": + case "rollup": { + assertFieldTypeMatchesPropertyType(property.type, fieldType) + + if (fieldType === "file") { + return { + type: fieldType, + id: property.id, + name: property.name, + userEditable: false, + allowedFileTypes: [], + } + } + + return { + type: fieldType, + id: property.id, + name: property.name, + userEditable: false, + } + } case "date": case "last_edited_time": case "created_time": { @@ -350,16 +385,6 @@ export function getCollectionFieldForProperty< userEditable: false, } } - case "url": { - assertFieldTypeMatchesPropertyType(property.type, fieldType) - - return { - type: "link", - id: property.id, - name: property.name, - userEditable: false, - } - } case "files": { assertFieldTypeMatchesPropertyType(property.type, fieldType) @@ -530,7 +555,89 @@ export function getPropertyValue( case "last_edited_by": { return "name" in property.last_edited_by ? property.last_edited_by.name : "" } + + case "formula": { + const value = property.formula + + if (!value) { + return defaultValueForFieldType[fieldType] ?? null + } + + switch (fieldType) { + case "string": + return String(value[value.type] ?? "") + case "link": + case "image": + case "file": + const url = String(value[value.type] ?? "") + if (url && isValidUrl(url)) { + return url + } + return "" + case "number": + return Number(value[value.type] ?? 0) + case "date": + return value.type == "date" ? formatDateString(value.date) : null + case "boolean": + return value.type == "boolean" ? value.boolean : Boolean(value[value.type]) + default: + return defaultValueForFieldType[fieldType] ?? null + } + } + case "rollup": { + const value = property.rollup + + let result = null + + switch (value?.type) { + case "array": + const item = value.array[0] + result = item ? getPropertyValue(item, { fieldType }) : defaultValueForFieldType[fieldType] ?? null + break + case "number": + result = value.number ?? 0 + break + case "date": + result = formatDateString(value.date) + break + default: + result = defaultValueForFieldType[fieldType] ?? null + break + } + + switch (fieldType) { + case "string": + case "date": + if (typeof result !== "string") { + result = "" + } + break + case "image": + case "link": + case "file": + if (!result || !isValidUrl(result)) { + result = "" + } + break + case "number": + if (typeof result !== "number") { + result = 0 + } + break + case "boolean": + if (typeof result !== "boolean") { + result = false + } + break + default: + break + } + + return result + } } + + return null } export interface SynchronizeProgress { @@ -1033,3 +1140,24 @@ export async function* iteratePaginatedAPI( nextCursor = response.next_cursor } while (nextCursor) } + +function formatDateString(value: string) { + if (!value) return null + + // Remove time from ISO string to prevent time zone issues when importing to Framer. + const date = new Date(value) + + // Set the time to midnight (00:00:00.000) + date.setHours(0, 0, 0, 0) + + return date.toISOString() +} + +function isValidUrl(url: string): boolean { + try { + new URL(url) + return true + } catch { + return false + } +} From 7998b9be58059464c62d56d1e09d5ff8b7f77f04 Mon Sep 17 00:00:00 2001 From: Isaac Roberts <119639439+madebyisaacr@users.noreply.github.com> Date: Sat, 3 May 2025 15:53:29 -0400 Subject: [PATCH 28/36] Add pointer events none to overflow gradients --- plugins/notion/src/MapFields.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/notion/src/MapFields.tsx b/plugins/notion/src/MapFields.tsx index 782a513f..a8a66236 100644 --- a/plugins/notion/src/MapFields.tsx +++ b/plugins/notion/src/MapFields.tsx @@ -224,7 +224,7 @@ export function MapDatabaseFields({ return (
-
+
@@ -325,7 +325,7 @@ export function MapDatabaseFields({
-
+