Skip to content

Commit

Permalink
Add support to notion for relations via multi references
Browse files Browse the repository at this point in the history
  • Loading branch information
tom-james-watson committed Nov 14, 2024
1 parent 5734db0 commit 9265e22
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 15 deletions.
24 changes: 13 additions & 11 deletions plugins/notion/src/MapFields.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { isFullDatabase } from "@notionhq/client"
import { GetDatabaseResponse } from "@notionhq/client/build/src/api-endpoints"
import classNames from "classnames"
import { ManagedCollectionField } from "framer-plugin"
import { assert } from "./utils"
import { Fragment, useMemo, useState } from "react"
import { Button } from "./components/Button"
import { CheckboxTextfield } from "./components/CheckboxTexfield"
import { IconChevron } from "./components/Icons"
import {
NotionProperty,
PluginContext,
SynchronizeMutationOptions,
SynchronizeProgress,
getCollectionFieldForProperty,
getNotionProperties,
getPossibleSlugFields,
hasDatabaseFieldsChanged,
hasFieldConfigurationChanged,
isSupportedNotionProperty,
supportedCMSTypeByNotionPropertyType,
richTextToPlainText,
getNotionProperties,
hasFieldConfigurationChanged,
supportedCMSTypeByNotionPropertyType,
} from "./notion"
import { Fragment, useMemo, useState } from "react"
import classNames from "classnames"
import { IconChevron } from "./components/Icons"
import { Button } from "./components/Button"
import { isFullDatabase } from "@notionhq/client"
import { CheckboxTextfield } from "./components/CheckboxTexfield"
import { assert } from "./utils"

function getSortedProperties(database: GetDatabaseResponse): NotionProperty[] {
return getNotionProperties(database).sort((propertyA, propertyB) => {
Expand Down Expand Up @@ -108,6 +108,8 @@ const labelByFieldTypeOption: Record<ManagedCollectionField["type"], string> = {
image: "Image",
link: "Link",
string: "String",
collectionReference: "Reference",
multiCollectionReference: "Multi Reference",
}

export function MapDatabaseFields({
Expand Down Expand Up @@ -182,7 +184,7 @@ export function MapDatabaseFields({
const fieldType = fieldTypeByFieldId[property.id]
assert(fieldType)

const field = getCollectionFieldForProperty(property, fieldType)
const field = getCollectionFieldForProperty(property, fieldType, pluginContext.databaseIdMap)
if (!field) continue

const nameOverride = fieldNameOverrides[property.id]
Expand Down
54 changes: 50 additions & 4 deletions plugins/notion/src/notion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ import {
isFullPage,
isNotionClientError,
} from "@notionhq/client"
import pLimit from "p-limit"
import {
BlockObjectResponse,
GetDatabaseResponse,
PageObjectResponse,
RichTextItemResponse,
} from "@notionhq/client/build/src/api-endpoints"
import { assert, assertNever, formatDate, isDefined, isString, slugify } from "./utils"
import { ManagedCollectionField, CollectionItemData, framer, ManagedCollection } from "framer-plugin"
import { useMutation, useQuery } from "@tanstack/react-query"
import { CollectionItemData, framer, ManagedCollection, ManagedCollectionField } from "framer-plugin"
import pLimit from "p-limit"
import { blocksToHtml, richTextToHTML } from "./blocksToHTML"
import { assert, assertNever, formatDate, isDefined, isString, slugify } from "./utils"

export type FieldId = string

Expand All @@ -35,6 +35,7 @@ const pluginLastSyncedKey = "notionPluginLastSynced"
const ignoredFieldIdsKey = "notionPluginIgnoredFieldIds"
const pluginSlugIdKey = "notionPluginSlugId"
const databaseNameKey = "notionDatabaseName"
const databaseIdKey = "notionDatabaseId"

// Maximum number of concurrent requests to Notion API
// This is to prevent rate limiting.
Expand Down Expand Up @@ -185,6 +186,7 @@ export const supportedNotionPropertyTypes = [
"status",
"url",
"files",
"relation",
] satisfies ReadonlyArray<NotionProperty["type"]>

type SupportedPropertyType = (typeof supportedNotionPropertyTypes)[number]
Expand All @@ -207,6 +209,7 @@ export const supportedCMSTypeByNotionPropertyType = {
url: ["link"],
email: ["formattedText", "string"],
files: ["file", "image"],
relation: ["multiCollectionReference"],
} satisfies Record<SupportedPropertyType, ReadonlyArray<ManagedCollectionField["type"]>>

function assertFieldTypeMatchesPropertyType<T extends SupportedPropertyType>(
Expand All @@ -226,7 +229,11 @@ function assertFieldTypeMatchesPropertyType<T extends SupportedPropertyType>(
*/
export function getCollectionFieldForProperty<
TProperty extends Extract<NotionProperty, { type: SupportedPropertyType }>,
>(property: TProperty, fieldType: ManagedCollectionField["type"]): ManagedCollectionField | null {
>(
property: TProperty,
fieldType: ManagedCollectionField["type"],
databaseIdMap: DatabaseIdMap
): ManagedCollectionField | null {
switch (property.type) {
case "email":
case "rich_text": {
Expand Down Expand Up @@ -350,6 +357,26 @@ export function getCollectionFieldForProperty<
userEditable: false,
}
}
case "relation": {
assertFieldTypeMatchesPropertyType(property.type, fieldType)

const collectionId = databaseIdMap.get(property.relation.database_id)

if (!collectionId) {
// Database includes a relation to a database that hasn't been synced to Framer.
// TODO: It would be better to surface this error to the user in
// the UI instead of just skipping the field.
return null
}

return {
type: "multiCollectionReference",
id: property.id,
name: property.name,
collectionId: collectionId,
userEditable: false,
}
}
default: {
assertNever(property)
}
Expand Down Expand Up @@ -404,6 +431,9 @@ export function getPropertyValue(
case "date": {
return property.date?.start
}
case "relation": {
return property.relation.map(({ id }) => id)
}
case "files": {
const firstFile = property.files[0]
if (!firstFile) return null
Expand Down Expand Up @@ -654,6 +684,7 @@ export async function synchronizeDatabase(
collection.setPluginData(pluginLastSyncedKey, new Date().toISOString()),
collection.setPluginData(pluginSlugIdKey, slugFieldId),
collection.setPluginData(databaseNameKey, richTextToPlainText(database.title)),
collection.setPluginData(databaseIdKey, database.id),
])

return {
Expand Down Expand Up @@ -701,6 +732,7 @@ export interface PluginContextNew {
type: "new"
collection: ManagedCollection
isAuthenticated: boolean
databaseIdMap: DatabaseIdMap
}

export interface PluginContextUpdate {
Expand All @@ -713,12 +745,14 @@ export interface PluginContextUpdate {
ignoredFieldIds: FieldId[]
slugFieldId: string | null
isAuthenticated: boolean
databaseIdMap: DatabaseIdMap
}

export interface PluginContextError {
type: "error"
message: string
isAuthenticated: false
databaseIdMap: DatabaseIdMap
}

export type PluginContext = PluginContextNew | PluginContextUpdate | PluginContextError
Expand All @@ -735,17 +769,27 @@ function getIgnoredFieldIds(rawIgnoredFields: string | null) {
return parsed
}

export type DatabaseIdMap = Map<string, string>

export async function getPluginContext(): Promise<PluginContext> {
const collection = await framer.getManagedCollection()
const collectionFields = await collection.getFields()
const databaseId = await collection.getPluginData(pluginDatabaseIdKey)
const hasAuthToken = isAuthenticated()

const databaseIdMap: DatabaseIdMap = new Map()

for (const collection of await framer.getCollections()) {
const collectionDatabaseId = await collection.getPluginData(databaseIdKey)
if (collectionDatabaseId) databaseIdMap.set(collectionDatabaseId, collection.id)
}

if (!databaseId || !hasAuthToken) {
return {
type: "new",
collection,
isAuthenticated: hasAuthToken,
databaseIdMap,
}
}

Expand Down Expand Up @@ -773,6 +817,7 @@ export async function getPluginContext(): Promise<PluginContext> {
slugFieldId,
hasChangedFields: hasDatabaseFieldsChanged(collectionFields, database, ignoredFieldIds),
isAuthenticated: hasAuthToken,
databaseIdMap,
}
} catch (error) {
if (isNotionClientError(error) && error.code === APIErrorCode.ObjectNotFound) {
Expand All @@ -782,6 +827,7 @@ export async function getPluginContext(): Promise<PluginContext> {
type: "error",
message: `The database "${databaseName}" was not found. Log in with Notion and select the Database to sync.`,
isAuthenticated: false,
databaseIdMap,
}
}

Expand Down

0 comments on commit 9265e22

Please sign in to comment.