Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions extensions/notion/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Notion Changelog

## [Multiple Notion accounts support] - {PR_MERGE_DATE}
- Add support for up to two Notion accounts

## [Use Clipboard in Create + Update Shortcuts] - 2025-12-01

- New `Preference` allowing to use Clipboard for auto-filling **Name (Title)** or **Content** (ref: [Issue #23086](https://github.com/raycast/extensions/issues/23086))
Expand Down
13 changes: 13 additions & 0 deletions extensions/notion/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ If you are not logging in through OAuth, you can still use the extension with an
3. Manually give the integration access to the specific pages or databases by [adding connections to them](https://www.notion.so/help/add-and-manage-connections-with-the-api#add-connections-to-pages)


## Using multiple accounts

You can connect two Notion accounts when using OAuth:

1. In Raycast settings, set `Authentication Type` to `OAuth (Recommended)`.
2. Fill both `Account 1 Label` and `Account 2 Label` (for example: Work, Personal).
3. Use the account selector in Create Database Page, Quick Capture, and Add Text to Page to switch accounts.
4. Search Notion will query both accounts and show the account label on the right.

Note: Internal Integration Secret mode is single-account only.

When using `@notion`, you can specify the account with the label (for example: “in Work”) and it will route to that account. If the account is ambiguous and multiple accounts are configured, it will ask which account to use.

## I can't find the Notion page or database from Raycast

If you have connected your Notion account to Raycast, you need to grant the Raycast Extension access to new root pages.
Expand Down
40 changes: 38 additions & 2 deletions extensions/notion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
}
],
"ai": {
"instructions": "-If asked to summarize or view a document, look for pages with the title of the document.\\n - When referring to a page or database, always include a link to the page or database. \\n - When creating a page, always include the database name. Ask for the database name if it's not provided.",
"instructions": "-If asked to summarize or view a document, look for pages with the title of the document.\\n - When referring to a page or database, always include a link to the page or database. \\n - When creating a page, always include the database name. Ask for the database name if it's not provided.\\n - If multiple Notion accounts are configured, infer the account from the user's request and the account labels in settings (e.g., Work/Personal).\\n - When calling tools, pass the matching accountLabel.\\n - If the account isn't clear, ask the user which account to use before calling tools.",
"evals": [
{
"input": "@notion Summarize the last marketing meeting notes",
Expand Down Expand Up @@ -251,12 +251,48 @@
]
},
"preferences": [
{
"name": "notion_auth_type",
"type": "dropdown",
"title": "Authentication Type",
"required": true,
"default": "oauth",
"data": [
{
"title": "OAuth (Recommended)",
"value": "oauth"
},
{
"title": "Internal Integration Secret",
"value": "internal"
}
],
"description": "OAuth supports multiple accounts. Internal integrations use a single account."
},
{
"name": "notion_account_1_label",
"type": "textfield",
"title": "Account 1 Label",
"required": false,
"default": "",
"placeholder": "Work",
"description": "Label shown in account selectors and search results. Fill both account labels to enable switching."
},
{
"name": "notion_account_2_label",
"type": "textfield",
"title": "Account 2 Label",
"required": false,
"default": "",
"placeholder": "Personal",
"description": "Leave empty to use a single account. Fill both account labels to enable switching."
},
{
"name": "notion_token",
"type": "password",
"title": "Internal Integration Secret",
"required": false,
"description": "In Notion, go to Settings & members > My connections > Develop or manage integrations > New integration",
"description": "Only used with Authentication Type = Internal Integration Secret. Create an integration at notion.so/my-integrations, copy the Internal Integration Secret under Secrets, and add the integration as a connection to the pages or databases you want to access.",
"placeholder": "ntn_FGDeSrZNodQuaJEqWzvxcTyrPlpBHYvdLpTZykrWEu"
},
{
Expand Down
45 changes: 39 additions & 6 deletions extensions/notion/src/add-text-to-page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { BlockObjectRequest } from "@notionhq/client/build/src/api-endpoints";
import { Action, ActionPanel, Form, Icon, LaunchProps, Toast, closeMainWindow, showToast } from "@raycast/api";
import { FormValidation, useForm, withAccessToken } from "@raycast/utils";
import { FormValidation, useForm } from "@raycast/utils";
import { markdownToBlocks } from "@tryfabric/martian";
import { useState } from "react";
import { useEffect, useState } from "react";

import { useSearchPages } from "./hooks";
import { appendBlockToPage, getPageIcon } from "./utils/notion";
import { notionService } from "./utils/notion/oauth";
import {
getActiveAccountId,
getDefaultAccountId,
getNotionAccounts,
NotionAccountId,
setActiveAccountId,
} from "./utils/notion/oauth";

type AddTextToPageValues = {
prepend: boolean;
Expand All @@ -17,9 +23,11 @@ type AddTextToPageValues = {

function AddTextToPage(props: LaunchProps<{ arguments: Arguments.AddTextToPage }>) {
const [searchText, setSearchText] = useState<string>("");
const { data, isLoading } = useSearchPages(searchText);
const accounts = getNotionAccounts();
const [accountId, setAccountId] = useState<NotionAccountId>(getDefaultAccountId());
const { data, isLoading } = useSearchPages(searchText, accountId);

const { itemProps, handleSubmit } = useForm<AddTextToPageValues>({
const { itemProps, handleSubmit, setValue } = useForm<AddTextToPageValues>({
async onSubmit(values) {
try {
await showToast({ style: Toast.Style.Animated, title: "Adding content to the page" });
Expand All @@ -37,6 +45,7 @@ function AddTextToPage(props: LaunchProps<{ arguments: Arguments.AddTextToPage }
children: content,
prepend: values.prepend,
addDateDivider: values.addDateDivider,
accountId,
});
await closeMainWindow();
await showToast({ style: Toast.Style.Success, title: "Added text to page" });
Expand All @@ -55,6 +64,12 @@ function AddTextToPage(props: LaunchProps<{ arguments: Arguments.AddTextToPage }

const searchPages = data?.pages.filter((page) => page.object === "page");

useEffect(() => {
getActiveAccountId()
.then(setAccountId)
.catch(() => undefined);
}, []);

return (
<Form
actions={
Expand All @@ -63,6 +78,24 @@ function AddTextToPage(props: LaunchProps<{ arguments: Arguments.AddTextToPage }
</ActionPanel>
}
>
{accounts.length > 1 ? (
<Form.Dropdown
id="accountId"
title="Account"
value={accountId}
onChange={(value) => {
const nextAccountId = value as NotionAccountId;
setAccountId(nextAccountId);
setActiveAccountId(nextAccountId);
setValue("page", "");
}}
storeValue
>
{accounts.map((account) => (
<Form.Dropdown.Item key={account.id} title={account.label} value={account.id} />
))}
</Form.Dropdown>
) : null}
<Form.Dropdown
{...itemProps.page}
title="Notion Page"
Expand Down Expand Up @@ -93,4 +126,4 @@ function AddTextToPage(props: LaunchProps<{ arguments: Arguments.AddTextToPage }
</Form>
);
}
export default withAccessToken(notionService)(AddTextToPage);
export default AddTextToPage;
16 changes: 11 additions & 5 deletions extensions/notion/src/components/DatabaseList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ import { useEffect, useState } from "react";

import { useDatabaseProperties, useDatabasesView } from "../hooks";
import { queryDatabase, getPageName, Page, User } from "../utils/notion";
import { NotionAccountId } from "../utils/notion/oauth";

import { DatabaseView } from "./DatabaseView";
import { CreatePageForm } from "./forms";

type DatabaseListProps = {
databasePage: Page;
setRecentPage: (page: Page) => Promise<void>;
removeRecentPage: (id: string) => Promise<void>;
removeRecentPage: (id: string, accountId?: NotionAccountId) => Promise<void>;
users?: User[];
};

export function DatabaseList({ databasePage, setRecentPage, removeRecentPage, users }: DatabaseListProps) {
const databaseId = databasePage.id;
const accountId = databasePage.accountId;
const databaseName = getPageName(databasePage);
const [searchText, setSearchText] = useState<string>();
const [sort, setSort] = useState<"last_edited_time" | "created_time">("last_edited_time");
Expand All @@ -25,10 +27,14 @@ export function DatabaseList({ databasePage, setRecentPage, removeRecentPage, us
isLoading,
mutate,
} = useCachedPromise(
(databaseId, searchText, sort) => queryDatabase(databaseId, searchText, sort),
[databaseId, searchText, sort],
(databaseId, searchText, sort, accountId) => queryDatabase(databaseId, searchText, sort, accountId),
[databaseId, searchText, sort, accountId],
);
const { data: databaseProperties, isLoading: isLoadingDatabaseProperties } = useDatabaseProperties(
databaseId,
undefined,
accountId,
);
const { data: databaseProperties, isLoading: isLoadingDatabaseProperties } = useDatabaseProperties(databaseId);
const { data: databaseView, isLoading: isLoadingDatabaseViews, setDatabaseView } = useDatabasesView(databaseId);

useEffect(() => {
Expand Down Expand Up @@ -83,7 +89,7 @@ export function DatabaseList({ databasePage, setRecentPage, removeRecentPage, us
title="Create New Page"
icon={Icon.Plus}
shortcut={Keyboard.Shortcut.Common.New}
target={<CreatePageForm defaults={{ database: databaseId }} mutate={mutate} />}
target={<CreatePageForm defaults={{ database: databaseId }} mutate={mutate} accountId={accountId} />}
/>
</ActionPanel>
}
Expand Down
4 changes: 3 additions & 1 deletion extensions/notion/src/components/DatabaseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { List, Image } from "@raycast/api";
import { JSX } from "react";

import { notionColorToTintColor, isType, Page, DatabaseProperty, PropertyConfig, User } from "../utils/notion";
import { NotionAccountId } from "../utils/notion/oauth";
import type { DatabaseView } from "../utils/types";

import { PageListItem } from "./PageListItem";
Expand All @@ -14,7 +15,7 @@ type DatabaseViewProps = {
databaseView?: DatabaseView;
setDatabaseView?: (view: DatabaseView) => Promise<void>;
setRecentPage: (page: Page) => Promise<void>;
removeRecentPage: (id: string) => Promise<void>;
removeRecentPage: (id: string, accountId?: NotionAccountId) => Promise<void>;
mutate: () => Promise<void>;
users?: User[];
sort?: "last_edited_time" | "created_time";
Expand Down Expand Up @@ -182,6 +183,7 @@ export function DatabaseView(props: DatabaseViewProps) {
pageId={p.id}
pageProperty={p.properties[propertyId]}
icon="./icon/kanban_status_started.png"
accountId={p.accountId}
shortcut={{
macOS: { modifiers: ["cmd", "shift"], key: "s" },
Windows: { modifiers: ["ctrl", "shift"], key: "s" },
Expand Down
6 changes: 3 additions & 3 deletions extensions/notion/src/components/PageDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ export function PageDetail({ page, setRecentPage, users }: PageDetailProps) {
const pageName = getPageName(page);

const { data, isLoading, mutate } = useCachedPromise(
async (id) => {
const fetchedPageContent = await fetchPageContent(id);
async (id, accountId) => {
const fetchedPageContent = await fetchPageContent(id, accountId);

const blocks = [];

Expand All @@ -84,7 +84,7 @@ export function PageDetail({ page, setRecentPage, users }: PageDetailProps) {

return { markdown: blocks.join("\n") };
},
[page.id],
[page.id, page.accountId],
);

useEffect(() => {
Expand Down
21 changes: 16 additions & 5 deletions extensions/notion/src/components/PageListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
PageProperty,
User,
} from "../utils/notion";
import { NotionAccountId } from "../utils/notion/oauth";
import { handleOnOpenPage } from "../utils/openPage";
import { DatabaseView } from "../utils/types";

Expand All @@ -39,11 +40,12 @@ type PageListItemProps = {
databaseProperties?: DatabaseProperty[];
setDatabaseView?: (databaseView: DatabaseView) => Promise<void>;
setRecentPage: (page: Page) => Promise<void>;
removeRecentPage: (id: string) => Promise<void>;
removeRecentPage: (id: string, accountId?: NotionAccountId) => Promise<void>;
mutate: () => Promise<void>;
users?: User[];
icon?: Image.ImageLike;
customActions?: JSX.Element[];
accountLabel?: string;
};

export function PageListItem({
Expand All @@ -57,6 +59,7 @@ export function PageListItem({
icon = getPageIcon(page),
users,
mutate,
accountLabel,
}: PageListItemProps) {
const accessories: List.Item.Accessory[] = [];

Expand Down Expand Up @@ -90,6 +93,13 @@ export function PageListItem({
});
}

if (accountLabel) {
accessories.push({
text: accountLabel,
tooltip: `Account: ${accountLabel}`,
});
}

const quickEditProperties = databaseProperties?.filter((property) =>
["checkbox", "status", "select", "multi_select", "status", "people"].includes(property.type),
);
Expand Down Expand Up @@ -173,6 +183,7 @@ export function PageListItem({
pageId={page.id}
pageProperty={page.properties[dp.id]}
mutate={mutate}
accountId={page.accountId}
/>
))}
</ActionPanel.Submenu>
Expand All @@ -192,7 +203,7 @@ export function PageListItem({
title="Create New Page"
icon={Icon.Plus}
shortcut={Keyboard.Shortcut.Common.New}
target={<CreatePageForm defaults={{ database: page.id }} mutate={mutate} />}
target={<CreatePageForm defaults={{ database: page.id }} mutate={mutate} accountId={page.accountId} />}
/>
)}

Expand All @@ -212,11 +223,11 @@ export function PageListItem({
})
) {
if (page.object === "database") {
deleteDatabase(page.id);
deleteDatabase(page.id, page.accountId);
} else {
deletePage(page.id);
deletePage(page.id, page.accountId);
}
await removeRecentPage(page.id);
await removeRecentPage(page.id, page.accountId);
await mutate();
}
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
PropertyConfig,
ReadablePropertyType,
} from "../../utils/notion";
import { NotionAccountId } from "../../utils/notion/oauth";

type EditPropertyOptions = PropertyConfig<"select" | "multi_select">["options"][number] & {
icon?: string;
Expand All @@ -24,6 +25,7 @@ export function ActionEditPageProperty({
mutate,
icon,
options,
accountId,
}: {
databaseProperty: DatabaseProperty;
pageId: string;
Expand All @@ -32,10 +34,11 @@ export function ActionEditPageProperty({
shortcut?: Keyboard.Shortcut;
icon?: Image.ImageLike;
options?: EditPropertyOptions[];
accountId?: NotionAccountId;
}) {
if (!icon) icon = getPropertyIcon(databaseProperty);

const { data: users } = useUsers();
const { data: users } = useUsers(accountId);

const title = "Set " + databaseProperty.name;

Expand All @@ -44,7 +47,7 @@ export function ActionEditPageProperty({
style: Toast.Style.Animated,
title: "Updating Property",
});
const updatedPage = await patchPage(pageId, patch);
const updatedPage = await patchPage(pageId, patch, accountId);
if (updatedPage && updatedPage.id) {
showToast({
title: "Property Updated",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function AppendToPageForm(props: { page: Page; onContentUpdate?: () => vo

pop();

await appendToPage(page.id, { content: values.content });
await appendToPage(page.id, { content: values.content }, page.accountId);
onContentUpdate?.();

await showToast({ style: Toast.Style.Success, title: "Added content to the page", message: pageTitle });
Expand Down
Loading
Loading