Skip to content

Revamp Notion plugin #220

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f42aad1
Increase default window size
madebyisaacr May 3, 2025
31b6026
Improve design of unsupported fields
madebyisaacr May 3, 2025
b231fa7
Add support for phone number field
madebyisaacr May 3, 2025
d04b194
Allow empty email fields, make default email type string
madebyisaacr May 3, 2025
b4f25de
Make field type labels match Framer
madebyisaacr May 3, 2025
ae21070
Fix importing status fields
madebyisaacr May 3, 2025
064d1aa
Make "Next" a primary button
madebyisaacr May 3, 2025
4b195ea
Prevent importing non-image files as images
madebyisaacr May 3, 2025
4ea15a0
Sync blockquotes from Notion
madebyisaacr May 3, 2025
f8968c2
Sync YouTube videos in formatted text
madebyisaacr May 3, 2025
2158f5c
Improve Notion code block languages
madebyisaacr May 3, 2025
88372ce
Add unique ID field support
madebyisaacr May 3, 2025
84a869d
Add title with link to Notion database
madebyisaacr May 3, 2025
3dd0988
Add support for Notion page cover images
madebyisaacr May 3, 2025
bee0c45
Remove warnings for missing file and URL values
madebyisaacr May 3, 2025
8742256
Move title field to first in list of properties
madebyisaacr May 3, 2025
1b8d219
Import missing number as 0
madebyisaacr May 3, 2025
6f4a572
Add support for people fields
madebyisaacr May 3, 2025
e471b70
Disable field type dropdown when only 1 value
madebyisaacr May 3, 2025
5890a28
Fix typo in file name
madebyisaacr May 3, 2025
101e900
User select none, non-draggable images
madebyisaacr May 3, 2025
6a036ba
Update to framer-plugin 3.1.0
madebyisaacr May 3, 2025
5e24fe5
Removed the ability to import page content as string
madebyisaacr May 3, 2025
58e5cf8
Add Notion theme colors, primary log in button
madebyisaacr May 3, 2025
cb74792
Combine identical property type switch cases
madebyisaacr May 3, 2025
cd16304
Button pressed colors, spinner color, pointer cursor
madebyisaacr May 3, 2025
3799b64
Formula and rollup field, more slug types
madebyisaacr May 3, 2025
7998b9b
Add pointer events none to overflow gradients
madebyisaacr May 3, 2025
e783e6b
Allow syncing select fields with no value
madebyisaacr May 3, 2025
804417f
Improve field type selector
madebyisaacr May 3, 2025
3e66144
More URL, email and phone number field types
madebyisaacr May 5, 2025
b318f44
Improve image URL validation
madebyisaacr May 5, 2025
7e9c0fd
Allow syncing relation as a single reference field
madebyisaacr May 5, 2025
b34a219
Return first enum case when select is empty
madebyisaacr May 5, 2025
b6802d4
Fix email and phone number link validation
madebyisaacr May 5, 2025
2b28fba
Consistent padding in map fields UI
madebyisaacr May 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions plugins/notion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
18 changes: 18 additions & 0 deletions plugins/notion/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@ main {
gap: 15px;
}

[data-framer-theme="light"] #root {
--framer-color-tint: #000;
--framer-color-tint-dark: #262626;
--framer-color-tint-extra-dark: #333333;
}

[data-framer-theme="dark"] #root {
--framer-color-tint: #fff;
--framer-color-tint-dark: #e3e3e3;
--framer-color-tint-extra-dark: #c3c3c3;
--framer-color-text-reversed: #000;
}

[data-framer-theme="dark"] input[type="checkbox"]:checked {
/* Copy of the checkbox SVG from the built-in Framer CSS, but with a black stroke color instead of white. */
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiI+PHBhdGggZD0iTTMgNmwyIDIgNC00IiBmaWxsPSJ0cmFuc3BhcmVudCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtZGFzaGFycmF5PSI4LjUiIHN0cm9rZS1kYXNob2Zmc2V0PSIwIj48L3BhdGg+PHBhdGggZD0iTTMgNmw2IDAiIGZpbGw9InRyYW5zcGFyZW50IiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtZGFzaGFycmF5PSI2IiBzdHJva2UtZGFzaG9mZnNldD0iNiIgPjwvcGF0aD48L3N2Zz4=) !important;
}

/* I know this is not good but I didn't have time to fight with Tailwind. */
[data-framer-theme="light"] .tailwind-hell-escape-hatch-checkbox[type="checkbox"]:not(:checked) {
background: rgba(0, 0, 0, 0.1);
Expand Down
4 changes: 2 additions & 2 deletions plugins/notion/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions plugins/notion/src/Authenticate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export function Authentication({ onAuthenticated, context }: AuthenticationProps
})
}
return (
<div className="w-full h-full flex flex-col items-center justify-center gap-[20px] pb-4 overflo">
<img src={loginIllustration} className="max-w-100% rounded-md flex-shrink-0 select-none pointer-events-none" />
<div className="w-full h-full flex flex-col items-center justify-center gap-[20px] pb-4 select-none">
<img src={loginIllustration} draggable={false} className="max-w-100% rounded-md flex-shrink-0 pointer-events-none" />

<div className="flex flex-col items-center gap-2 flex-1 justify-center w-full">
{isLoading ? (
Expand All @@ -77,7 +77,7 @@ export function Authentication({ onAuthenticated, context }: AuthenticationProps
)}
</div>

<Button onClick={handleAuth} isLoading={isLoading} disabled={isLoading}>
<Button onClick={handleAuth} isLoading={isLoading} disabled={isLoading} variant="primary">
Log In
</Button>
</div>
Expand Down
110 changes: 75 additions & 35 deletions plugins/notion/src/MapFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import classNames from "classnames"
import { ManagedCollectionField } from "framer-plugin"
import { Fragment, useMemo, useState } from "react"
import { Button } from "./components/Button"
import { CheckboxTextfield } from "./components/CheckboxTexfield"
import { CheckboxTextfield } from "./components/CheckboxTextfield"
import { IconChevron } from "./components/Icons"
import {
NotionProperty,
Expand All @@ -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
Expand Down Expand Up @@ -98,18 +104,18 @@ function getLastSynchronizedAtTimestamp(
}

const labelByFieldTypeOption: Record<ManagedCollectionField["type"], string> = {
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({
Expand Down Expand Up @@ -213,21 +219,30 @@ export function MapDatabaseFields({
})
}

const title = richTextToPlainText(database.title)
const plainTextTitle = richTextToPlainText(database.title)
const title = plainTextTitle.trim() ? plainTextTitle : "Untitled"

return (
<form onSubmit={handleSubmit} className="flex flex-col gap-2 flex-1">
<div className="tailwind-hell-escape-hatch-gradient-top" />
<form onSubmit={handleSubmit} className="flex flex-col gap-2 flex-1 select-none">
<div className="tailwind-hell-escape-hatch-gradient-top pointer-events-none" />

<div className="h-[1px] border-b border-divider mb-2 sticky top-0" />

<div className="flex-1 flex flex-col gap-6">
<a
href={database.url}
target="_blank"
className="group w-fit max-w-full flex flex-row items-center gap-1.5"
>
{database.icon && <DatabaseIcon icon={database.icon} size={24} />}
<span className="group-hover:underline text-primary text-base font-bold">{title}</span>
</a>
<div className="flex flex-col gap-2 w-full">
<label className="text-tertiary" htmlFor="collectionName">
Slug Field
</label>
<select
className="w-full"
className="w-full cursor-pointer"
value={slugFieldId ?? ""}
onChange={e => setSlugFieldId(e.target.value)}
required
Expand All @@ -241,7 +256,7 @@ export function MapDatabaseFields({
</div>

<div className="flex flex-col gap-[10px] w-full items-center justify-center pb-[10px]">
<div className="grid grid-cols-fieldPicker gap-3 w-full items-center justify-center text-tertiary mb-[-3px]">
<div className="grid grid-cols-fieldPicker gap-2.5 w-full items-center justify-center text-tertiary mb-[-3px]">
<span>Notion Property</span>
<span className="col-start-3">Field Name</span>
<span>Field Type</span>
Expand All @@ -257,7 +272,7 @@ export function MapDatabaseFields({
<CheckboxTextfield
value={property.name}
disabled={!isSupported}
checked={!disabledFieldIds.has(property.id)}
checked={isSupported ? !disabledFieldIds.has(property.id) : false}
onChange={() => {
handleFieldToggle(property.id)
}}
Expand All @@ -272,36 +287,37 @@ export function MapDatabaseFields({
</div>
<input
type="text"
className={classNames("w-full", !isSupported && "opacity-50")}
className={classNames("w-full", !isSupported && "opacity-50 col-span-2")}
disabled={!isSupported || disabledFieldIds.has(property.id)}
placeholder={property.name}
value={isSupported ? (fieldNameOverrides[property.id] ?? "") : "Unsupported"}
onChange={e => {
handleFieldNameChange(property.id, e.target.value)
}}
></input>
<select
className="w-full"
onChange={event =>
handleFieldTypeChange(
property.id,
event.target.value as ManagedCollectionField["type"]
)
}
disabled={!isSupported}
value={!isSupported ? "unsupported" : fieldTypeByFieldId[property.id]}
>
{!isSupported && (
<option value="unsupported" disabled>
Unsupported
</option>
)}
{fieldOptions?.map(fieldOption => (
<option key={fieldOption} value={fieldOption}>
{labelByFieldTypeOption[fieldOption]}
</option>
{isSupported &&
(fieldOptions?.length === 1 ? (
<div className="w-full self-stretch bg-tertiary text-primary flex flex-col justify-center rounded-lg px-2.5">
{labelByFieldTypeOption[fieldOptions[0]]}
</div>
) : (
<select
className="w-full cursor-pointer px-2.5"
onChange={event =>
handleFieldTypeChange(
property.id,
event.target.value as ManagedCollectionField["type"]
)
}
value={fieldTypeByFieldId[property.id]}
>
{fieldOptions?.map(fieldOption => (
<option key={fieldOption} value={fieldOption}>
{labelByFieldTypeOption[fieldOption]}
</option>
))}
</select>
))}
</select>
</Fragment>
)
})}
Expand All @@ -310,12 +326,36 @@ export function MapDatabaseFields({
</div>

<div className="left-0 bottom-0 pb-[15px] w-full flex justify-between sticky bg-primary pt-4 border-t border-divider border-opacity-20 items-center max-w-full">
<div className="tailwind-hell-escape-hatch-gradient-bottom" />
<div className="tailwind-hell-escape-hatch-gradient-bottom pointer-events-none" />

<Button variant="primary" isLoading={isLoading} disabled={!slugFieldId} className="w-full">
Import from {title.trim() ? title : "Untitled"}
Import from {title}
</Button>
</div>
</form>
)
}

interface DatabaseIconProps {
icon: {
type: string
emoji?: string
external?: { url: string }
file?: { url: string }
}
size?: number
}

function DatabaseIcon({ icon, size = 20 }: DatabaseIconProps) {
if (!icon) return null

switch (icon.type) {
case "emoji":
return <span style={{ fontSize: `${size}px` }}>{icon.emoji}</span>
case "external":
case "file":
return <img src={icon[icon.type]?.url} style={{ width: size, height: size }} className="rounded-sm" />
}

return null
}
11 changes: 6 additions & 5 deletions plugins/notion/src/SelectDatabase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -66,8 +67,8 @@ export function SelectDatabase({ onDatabaseSelected }: SelectDatabaseProps) {
const selectEnabled = !isLoadingOrFetching && Boolean(data && data.length > 0)

return (
<form className="flex flex-col gap-[10px] w-full h-full" onSubmit={handleSubmit}>
<img src={notionConnectSrc} className="rounded-md" />
<form className="flex flex-col gap-[10px] w-full h-full select-none" onSubmit={handleSubmit}>
<img src={notionConnectSrc} draggable={false} className="rounded-md" />

<p>
To manually connect a database, open it in Notion, click on the three dots icon in the top right corner,
Expand All @@ -79,7 +80,7 @@ export function SelectDatabase({ onDatabaseSelected }: SelectDatabaseProps) {
<select
value={selectedDatabase ?? ""}
onChange={event => setSelectedDatabase(event.target.value)}
className="flex-1 shrink-1"
className="flex-1 shrink-1 cursor-pointer"
disabled={!selectEnabled}
>
{isLoadingOrFetching && (
Expand All @@ -106,9 +107,9 @@ export function SelectDatabase({ onDatabaseSelected }: SelectDatabaseProps) {
</select>
</div>

<button type="submit" disabled={!selectedDatabase}>
<Button type="submit" variant="primary" disabled={!selectedDatabase}>
Next
</button>
</Button>
</div>
</form>
)
Expand Down
Loading