diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 932be760..12485b63 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -24,6 +24,7 @@ plugins/TransportForLondon/* @clarkd plugins/UniFi/* @adamkinniburgh plugins/UptimeRobot/* @kieranlangton plugins/WorldCup2026/* @TimWheeler-SQUP +plugins/Notion/* @clarkd plugins/Algolia/* @andrewmumblebee diff --git a/plugins/Notion/v1/configValidation.json b/plugins/Notion/v1/configValidation.json new file mode 100644 index 00000000..e8ae3382 --- /dev/null +++ b/plugins/Notion/v1/configValidation.json @@ -0,0 +1,11 @@ +{ + "steps": [ + { + "displayName": "Authenticate", + "dataStream": { "name": "currentUser" }, + "required": true, + "error": "Could not connect to Notion. Make sure you have signed in and authorised access to your workspace.", + "success": "Connected to Notion successfully." + } + ] +} diff --git a/plugins/Notion/v1/custom_types.json b/plugins/Notion/v1/custom_types.json new file mode 100644 index 00000000..df15bcb4 --- /dev/null +++ b/plugins/Notion/v1/custom_types.json @@ -0,0 +1,23 @@ +[ + { + "name": "Notion User", + "sourceType": "Notion User", + "icon": "user", + "singular": "User", + "plural": "Users" + }, + { + "name": "Notion Data Source", + "sourceType": "Notion Data Source", + "icon": "table", + "singular": "Data Source", + "plural": "Data Sources" + }, + { + "name": "Notion Page", + "sourceType": "Notion Page", + "icon": "file-lines", + "singular": "Page", + "plural": "Pages" + } +] diff --git a/plugins/Notion/v1/dataStreams/currentUser.json b/plugins/Notion/v1/dataStreams/currentUser.json new file mode 100644 index 00000000..8321d9ba --- /dev/null +++ b/plugins/Notion/v1/dataStreams/currentUser.json @@ -0,0 +1,21 @@ +{ + "name": "currentUser", + "displayName": "Current User", + "description": "The authenticated Notion bot user and its workspace", + "tags": ["Account"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "users/me", + "postRequestScript": "currentUser.js" + }, + "matches": "none", + "visibility": { "type": "hidden" }, + "metadata": [ + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "workspaceName", "displayName": "Workspace", "shape": "string" }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/dataSourceRows.json b/plugins/Notion/v1/dataStreams/dataSourceRows.json new file mode 100644 index 00000000..584b562c --- /dev/null +++ b/plugins/Notion/v1/dataStreams/dataSourceRows.json @@ -0,0 +1,31 @@ +{ + "name": "dataSourceRows", + "displayName": "Data Source Contents", + "description": "Pages within a data source with their property values", + "tags": ["Data Sources", "Pages"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "post", + "endpointPath": "data_sources/{{object.rawId}}/query", + "postBody": {}, + "postRequestScript": "dataSourceRows.js", + "paging": { + "mode": "token", + "pageSize": { "realm": "body", "path": "page_size", "value": 100 }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "body", "path": "start_cursor" } + } + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Data Source"] } + }, + "metadata": [ + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "lastEditedTime", "displayName": "Last Edited", "shape": "date" }, + { "name": "createdTime", "displayName": "Created", "shape": "date", "visible": false }, + { "name": "url", "displayName": "URL", "shape": ["url", { "label": "Open in Notion" }], "visible": false }, + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/dataSourceSchema.json b/plugins/Notion/v1/dataStreams/dataSourceSchema.json new file mode 100644 index 00000000..0a82a1fd --- /dev/null +++ b/plugins/Notion/v1/dataStreams/dataSourceSchema.json @@ -0,0 +1,22 @@ +{ + "name": "dataSourceSchema", + "displayName": "Data Source Schema", + "description": "Property names and types (schema) for a single data source", + "tags": ["Data Sources"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "data_sources/{{object.rawId}}", + "paging": { "mode": "none" }, + "postRequestScript": "dataSourceSchema.js" + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Data Source"] } + }, + "metadata": [ + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "details", "displayName": "Details", "shape": "string" } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/dataSourcesSearch.json b/plugins/Notion/v1/dataStreams/dataSourcesSearch.json new file mode 100644 index 00000000..3cace383 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/dataSourcesSearch.json @@ -0,0 +1,29 @@ +{ + "name": "dataSourcesSearch", + "displayName": "Data Sources", + "description": "Data sources (database tables) shared with the integration", + "tags": ["Data Sources"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "search", + "postBody": { "filter": { "property": "object", "value": "data_source" } }, + "postRequestScript": "dataSourcesSearch.js", + "paging": { + "mode": "token", + "pageSize": { "realm": "body", "path": "page_size", "value": 100 }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "body", "path": "start_cursor" } + } + }, + "matches": "none", + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "createdTime", "displayName": "Created", "shape": "date" }, + { "name": "lastEditedTime", "displayName": "Last Edited", "shape": "date" }, + { "name": "url", "displayName": "URL", "shape": ["url", { "label": "Open in Notion" }] }, + { "name": "parentDatabaseId", "displayName": "Parent Database ID", "shape": "string", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/pageComments.json b/plugins/Notion/v1/dataStreams/pageComments.json new file mode 100644 index 00000000..45af2433 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/pageComments.json @@ -0,0 +1,42 @@ +{ + "name": "pageComments", + "displayName": "Page Comments", + "description": "Comments and discussion threads on a single Notion page", + "tags": ["Comments"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "comments", + "getArgs": [ + { "key": "block_id", "value": "{{object.rawId}}" } + ], + "postRequestScript": "pageComments.js", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "100" }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "queryArg", "path": "start_cursor" } + }, + "errorHandling": { + "type": "script", + "script": "result = response.status === 403 ? 'Notion did not grant the Read comments capability. Enable \"Read comments\" on your Notion integration, then reconnect the plugin in SquaredUp.' : ((data && data.message) || ('Notion comments API returned status ' + response.status));" + } + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Page"] } + }, + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "visible": false }, + { "name": "createdTime", "displayName": "Created", "shape": "date" }, + { "name": "text", "displayName": "Comment", "shape": "string", "role": "label" }, + { "name": "createdById", "displayName": "Created By ID", "shape": "string", "visible": false }, + { + "name": "author", + "displayName": "Author", + "sourceId": "createdById", + "sourceType": "Notion User", + "objectPropertyPath": "name" + } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/pageContent.json b/plugins/Notion/v1/dataStreams/pageContent.json new file mode 100644 index 00000000..1634200e --- /dev/null +++ b/plugins/Notion/v1/dataStreams/pageContent.json @@ -0,0 +1,27 @@ +{ + "name": "pageContent", + "displayName": "Page Content", + "description": "Block-level content of a Notion page — paragraphs, headings, to-do items, lists, quotes, callouts and code blocks", + "tags": ["Pages"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "blocks/{{object.rawId}}/children", + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "100" }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "queryArg", "path": "start_cursor" } + }, + "postRequestScript": "pageContent.js" + }, + "matches": { "sourceType": { "type": "oneOf", "values": ["Notion Page"] } }, + "metadata": [ + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "text", "displayName": "Text", "shape": "string", "role": "label" }, + { "name": "checked", "displayName": "Checked", "shape": "boolean" }, + { "name": "hasChildren", "displayName": "Has Children", "shape": "boolean", "visible": false }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/pageProperties.json b/plugins/Notion/v1/dataStreams/pageProperties.json new file mode 100644 index 00000000..fc5e2364 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/pageProperties.json @@ -0,0 +1,25 @@ +{ + "name": "pageProperties", + "displayName": "Page Properties", + "description": "Property values for a single Notion page", + "tags": ["Pages"], + "baseDataSourceName": "httpRequestScopedSingle", + "config": { + "httpMethod": "get", + "endpointPath": "pages/{{object.rawId}}", + "postRequestScript": "pageProperties.js", + "paging": { "mode": "none" } + }, + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Page"] } + }, + "metadata": [ + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "lastEditedTime", "displayName": "Last Edited", "shape": "date" }, + { "name": "createdTime", "displayName": "Created", "shape": "date", "visible": false }, + { "name": "url", "displayName": "URL", "shape": ["url", { "label": "Open in Notion" }], "visible": false }, + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false }, + { "pattern": ".*" } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/pagesSearch.json b/plugins/Notion/v1/dataStreams/pagesSearch.json new file mode 100644 index 00000000..090449e7 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/pagesSearch.json @@ -0,0 +1,40 @@ +{ + "name": "pagesSearch", + "displayName": "Pages", + "description": "Pages shared with the integration, including database rows", + "tags": ["Pages"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "post", + "endpointPath": "search", + "postBody": { "filter": { "property": "object", "value": "page" } }, + "postRequestScript": "pagesSearch.js", + "paging": { + "mode": "token", + "pageSize": { "realm": "body", "path": "page_size", "value": 100 }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "body", "path": "start_cursor" } + } + }, + "matches": "none", + "ui": [ + { + "type": "objects", + "name": "scope", + "label": "Page or data source (optional)", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Page", "Notion Data Source"] } + } + } + ], + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "createdTime", "displayName": "Created", "shape": "date" }, + { "name": "lastEditedTime", "displayName": "Last Edited", "shape": "date" }, + { "name": "url", "displayName": "URL", "shape": ["url", { "label": "Open in Notion" }] }, + { "name": "parentType", "displayName": "Parent Type", "shape": "string", "visible": false }, + { "name": "parentDataSourceId", "displayName": "Parent Data Source ID", "shape": "string", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/dataStreams/scripts/currentUser.js b/plugins/Notion/v1/dataStreams/scripts/currentUser.js new file mode 100644 index 00000000..6417c8be --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/currentUser.js @@ -0,0 +1,10 @@ +// /v1/users/me returns a single user object (the integration's bot user), +// not an array — wrap it as one row and surface the workspace name. +result = [ + { + id: data.id, + name: data.name || (data.bot && data.bot.workspace_name) || "Notion bot", + type: data.type, + workspaceName: data.bot && data.bot.workspace_name + } +]; diff --git a/plugins/Notion/v1/dataStreams/scripts/dataSourceRows.js b/plugins/Notion/v1/dataStreams/scripts/dataSourceRows.js new file mode 100644 index 00000000..03016c2e --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/dataSourceRows.js @@ -0,0 +1,89 @@ +// POST /v1/data_sources/{id}/query returns the pages (rows) of a data source, +// accumulated across pages under data.results. Each page's `properties` is a bag +// of typed Notion property values keyed by the property name; flatten each to a +// scalar so the custom fields become plain columns. +const plain = (rich) => (rich || []).map((t) => t.plain_text).join(""); + +const valueOf = (prop) => { + if (!prop) return null; + switch (prop.type) { + case "title": + return plain(prop.title); + case "rich_text": + return plain(prop.rich_text); + case "number": + return prop.number; + case "select": + return prop.select ? prop.select.name : null; + case "status": + return prop.status ? prop.status.name : null; + case "multi_select": + return (prop.multi_select || []).map((s) => s.name).join(", "); + case "date": + return prop.date ? prop.date.start : null; + case "checkbox": + return prop.checkbox; + case "url": + return prop.url; + case "email": + return prop.email; + case "phone_number": + return prop.phone_number; + case "people": + return (prop.people || []).map((p) => p.name || p.id).join(", "); + case "files": + return (prop.files || []).map((f) => f.name).join(", "); + case "created_time": + return prop.created_time; + case "last_edited_time": + return prop.last_edited_time; + case "created_by": + return prop.created_by ? prop.created_by.name || prop.created_by.id : null; + case "last_edited_by": + return prop.last_edited_by ? prop.last_edited_by.name || prop.last_edited_by.id : null; + case "unique_id": + return prop.unique_id + ? (prop.unique_id.prefix ? prop.unique_id.prefix + "-" : "") + prop.unique_id.number + : null; + case "formula": + return prop.formula + ? prop.formula.string ?? + prop.formula.number ?? + prop.formula.boolean ?? + (prop.formula.date ? prop.formula.date.start : null) + : null; + case "rollup": + if (!prop.rollup) return null; + if (prop.rollup.type === "array") { + return (prop.rollup.array || []).map((item) => valueOf(item)).filter((v) => v != null).join(", "); + } + return prop.rollup.number ?? (prop.rollup.date ? prop.rollup.date.start : null); + case "relation": + return (prop.relation || []).map((r) => r.id).join(", "); + default: + return null; + } +}; + +result = (data.results || []).map((page) => { + const props = page.properties || {}; + const row = { + id: page.id, + url: page.url || null, + createdTime: page.created_time || null, + lastEditedTime: page.last_edited_time || null + }; + let title = ""; + for (const key of Object.keys(props)) { + const prop = props[key]; + const value = valueOf(prop); + // The title property is surfaced as `name` — don't also emit it as a duplicate column. + if (prop && prop.type === "title") { + title = value || title; + continue; + } + row[key] = value; + } + row.name = title || "Untitled"; + return row; +}); diff --git a/plugins/Notion/v1/dataStreams/scripts/dataSourceSchema.js b/plugins/Notion/v1/dataStreams/scripts/dataSourceSchema.js new file mode 100644 index 00000000..eb4d4351 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/dataSourceSchema.js @@ -0,0 +1,21 @@ +// dataStreams/scripts/dataSourceSchema.js +// data is the single data source object: { id, title, properties: { : { id, name, type, ... } } } +// Iterate properties (object keyed by property name) → one row per property. + +result = Object.values(data.properties || {}).map(function (prop) { + var details = ""; + var typeConfig = prop[prop.type]; + + if (prop.type === "select" || prop.type === "multi_select") { + var options = typeConfig && typeConfig.options ? typeConfig.options : []; + details = options.map(function (o) { return o.name; }).join(", "); + } else if (prop.type === "relation") { + details = (typeConfig && typeConfig.database_id) ? typeConfig.database_id : ""; + } + + return { + name: prop.name, + type: prop.type, + details: details + }; +}); diff --git a/plugins/Notion/v1/dataStreams/scripts/dataSourcesSearch.js b/plugins/Notion/v1/dataStreams/scripts/dataSourcesSearch.js new file mode 100644 index 00000000..5ca9a87e --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/dataSourcesSearch.js @@ -0,0 +1,10 @@ +// POST /v1/search (filter data_source) returns paged results accumulated under +// data.results. The title is a rich-text array; flatten it to plain text. +result = (data.results || []).map((ds) => ({ + id: ds.id, + name: (ds.title || []).map((t) => t.plain_text).join("").trim() || "Untitled", + createdTime: ds.created_time || null, + lastEditedTime: ds.last_edited_time || null, + url: ds.url || null, + parentDatabaseId: (ds.parent && ds.parent.database_id) || null +})); diff --git a/plugins/Notion/v1/dataStreams/scripts/pageComments.js b/plugins/Notion/v1/dataStreams/scripts/pageComments.js new file mode 100644 index 00000000..ad621494 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/pageComments.js @@ -0,0 +1,11 @@ +// GET /v1/comments?block_id= returns paged results under data.results. +// Each comment has: id, created_time, created_by: { object: "user", id }, +// rich_text: [{ plain_text, ... }], parent. +// Join all plain_text segments to produce a single text string per comment. + +result = (data.results || []).map((comment) => ({ + id: comment.id, + createdTime: comment.created_time || null, + createdById: (comment.created_by && comment.created_by.id) || null, + text: (comment.rich_text || []).map((rt) => rt.plain_text || "").join("").trim() || null +})); diff --git a/plugins/Notion/v1/dataStreams/scripts/pageContent.js b/plugins/Notion/v1/dataStreams/scripts/pageContent.js new file mode 100644 index 00000000..6abcdb85 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/pageContent.js @@ -0,0 +1,33 @@ +// blocks/{{pageId}}/children returns top-level blocks only (nested children require +// separate calls per block — not fetched here). Each block has a `type` key and a +// same-named property containing a `rich_text` array (for text-bearing types) and +// optionally a `checked` boolean (to_do blocks only). +const TEXT_TYPES = new Set([ + "paragraph", + "heading_1", + "heading_2", + "heading_3", + "bulleted_list_item", + "numbered_list_item", + "to_do", + "toggle", + "quote", + "callout", + "code" +]); + +const plain = (richText) => + (richText || []).map((t) => t.plain_text || "").join(""); + +result = (data.results || []).map((block) => { + const type = block.type; + const blockData = block[type] || {}; + const text = TEXT_TYPES.has(type) ? plain(blockData.rich_text) : ""; + const checked = type === "to_do" ? (blockData.checked === true) : null; + return { + type, + text, + checked, + hasChildren: block.has_children === true + }; +}); diff --git a/plugins/Notion/v1/dataStreams/scripts/pageProperties.js b/plugins/Notion/v1/dataStreams/scripts/pageProperties.js new file mode 100644 index 00000000..bfff238b --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/pageProperties.js @@ -0,0 +1,88 @@ +// GET /v1/pages/{id} returns a single page object. Flatten its properties bag +// into a plain row so custom fields become columns. Copied helpers from dataSourceRows.js. +const plain = (rich) => (rich || []).map((t) => t.plain_text).join(""); + +const valueOf = (prop) => { + if (!prop) return null; + switch (prop.type) { + case "title": + return plain(prop.title); + case "rich_text": + return plain(prop.rich_text); + case "number": + return prop.number; + case "select": + return prop.select ? prop.select.name : null; + case "status": + return prop.status ? prop.status.name : null; + case "multi_select": + return (prop.multi_select || []).map((s) => s.name).join(", "); + case "date": + return prop.date ? prop.date.start : null; + case "checkbox": + return prop.checkbox; + case "url": + return prop.url; + case "email": + return prop.email; + case "phone_number": + return prop.phone_number; + case "people": + return (prop.people || []).map((p) => p.name || p.id).join(", "); + case "files": + return (prop.files || []).map((f) => f.name).join(", "); + case "created_time": + return prop.created_time; + case "last_edited_time": + return prop.last_edited_time; + case "created_by": + return prop.created_by ? prop.created_by.name || prop.created_by.id : null; + case "last_edited_by": + return prop.last_edited_by ? prop.last_edited_by.name || prop.last_edited_by.id : null; + case "unique_id": + return prop.unique_id + ? (prop.unique_id.prefix ? prop.unique_id.prefix + "-" : "") + prop.unique_id.number + : null; + case "formula": + return prop.formula + ? prop.formula.string ?? + prop.formula.number ?? + prop.formula.boolean ?? + (prop.formula.date ? prop.formula.date.start : null) + : null; + case "rollup": + if (!prop.rollup) return null; + if (prop.rollup.type === "array") { + return (prop.rollup.array || []).map((item) => valueOf(item)).filter((v) => v != null).join(", "); + } + return prop.rollup.number ?? (prop.rollup.date ? prop.rollup.date.start : null); + case "relation": + return (prop.relation || []).map((r) => r.id).join(", "); + default: + return null; + } +}; + +// data is the single page object returned by GET /v1/pages/{id} +const page = data; +const props = page.properties || {}; +const row = { + id: page.id, + url: page.url || null, + createdTime: page.created_time || null, + lastEditedTime: page.last_edited_time || null +}; +let title = ""; +for (const key of Object.keys(props)) { + const prop = props[key]; + const value = valueOf(prop); + // The title property is surfaced as `name` — don't also emit it as a duplicate column. + if (prop && prop.type === "title") { + title = value || title; + continue; + } + row[key] = value; +} +row.name = title || "Untitled"; + +result = [row]; diff --git a/plugins/Notion/v1/dataStreams/scripts/pagesSearch.js b/plugins/Notion/v1/dataStreams/scripts/pagesSearch.js new file mode 100644 index 00000000..0ce369f9 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/scripts/pagesSearch.js @@ -0,0 +1,37 @@ +// POST /v1/search (filter page) returns paged results accumulated under +// data.results. A page's title lives in a dynamically-named property whose +// type is "title" — scan the properties bag to find it. +const titleOf = (page) => { + const props = page.properties || {}; + for (const key of Object.keys(props)) { + const p = props[key]; + if (p && p.type === "title" && Array.isArray(p.title)) { + return p.title.map((t) => t.plain_text).join("").trim(); + } + } + return ""; +}; + +const rows = (data.results || []).map((page) => { + const parent = page.parent || {}; + return { + id: page.id, + name: titleOf(page) || "Untitled", + createdTime: page.created_time || null, + lastEditedTime: page.last_edited_time || null, + url: page.url || null, + parentType: parent.type || null, + parentDataSourceId: parent.data_source_id || null + }; +}); + +// Optional scope filter: page(s) or data source(s) selected via the ui "scope" picker. +// context.config.scope is an array of object envelopes; each rawId may itself be a +// single-element array — unwrap it before comparing. +const unwrap = (v) => (Array.isArray(v) ? v[0] : v); +const selected = (context.config && context.config.scope) || []; +const selectedIds = new Set(selected.map((o) => unwrap(o.rawId)).filter(Boolean)); + +result = selectedIds.size + ? rows.filter((r) => selectedIds.has(r.id) || selectedIds.has(r.parentDataSourceId)) + : rows; diff --git a/plugins/Notion/v1/dataStreams/usersList.json b/plugins/Notion/v1/dataStreams/usersList.json new file mode 100644 index 00000000..f22c3594 --- /dev/null +++ b/plugins/Notion/v1/dataStreams/usersList.json @@ -0,0 +1,28 @@ +{ + "name": "usersList", + "displayName": "Users", + "description": "Members of the Notion workspace", + "tags": ["Users"], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "users", + "pathToData": "results", + "expandInnerObjects": true, + "paging": { + "mode": "token", + "pageSize": { "realm": "queryArg", "path": "page_size", "value": "100" }, + "in": { "realm": "payload", "path": "next_cursor" }, + "out": { "realm": "queryArg", "path": "start_cursor" } + } + }, + "matches": "none", + "metadata": [ + { "name": "id", "displayName": "ID", "shape": "string", "role": "id", "visible": false }, + { "name": "name", "displayName": "Name", "shape": "string", "role": "label" }, + { "name": "type", "displayName": "Type", "shape": "string" }, + { "name": "person.email", "displayName": "Email", "shape": "string" }, + { "name": "avatar_url", "displayName": "Avatar URL", "shape": "url", "visible": false } + ], + "timeframes": false +} diff --git a/plugins/Notion/v1/defaultContent/manifest.json b/plugins/Notion/v1/defaultContent/manifest.json new file mode 100644 index 00000000..00d2358c --- /dev/null +++ b/plugins/Notion/v1/defaultContent/manifest.json @@ -0,0 +1,8 @@ +{ + "items": [ + { "name": "overview", "type": "dashboard" }, + { "name": "notionUser", "type": "dashboard" }, + { "name": "notionDataSource", "type": "dashboard" }, + { "name": "notionPage", "type": "dashboard" } + ] +} diff --git a/plugins/Notion/v1/defaultContent/notionDataSource.dash.json b/plugins/Notion/v1/defaultContent/notionDataSource.dash.json new file mode 100644 index 00000000..c73b36ff --- /dev/null +++ b/plugins/Notion/v1/defaultContent/notionDataSource.dash.json @@ -0,0 +1,168 @@ +{ + "name": "Data Source", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": ["{{variables.[Notion Data Source]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "18b6c622-ae69-468e-8013-7083e2a8831d", + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Details", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Data Source]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Notion Data Sources]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Data Source]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "45fa56ff-b832-4e9c-9fb4-f71383ee3910", + "x": 0, + "y": 4, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pages in this Data Source", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Data Source]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pagesSearch]}}", + "name": "pagesSearch", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "scope": { + "variable": "{{variables.[Notion Data Source]}}", + "workspace": "{{workspaceId}}", + "scope": "{{scopes.[Notion Data Sources]}}" + } + }, + "sort": { + "by": [["lastEditedTime", "desc"]] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "lastEditedTime", "url"], + "hiddenColumns": ["id", "createdTime", "parentType", "parentDataSourceId"] + } + } + } + } + }, + { + "i": "7c1e9a04-2d5b-4f6a-9c3e-1b2a3c4d5e6f", + "x": 0, + "y": 8, + "w": 4, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Contents", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Data Source]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[dataSourceRows]}}", + "name": "dataSourceRows", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["lastEditedTime", "desc"]] + } + }, + "scope": { + "scope": "{{scopes.[Notion Data Sources]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Data Source]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "hiddenColumns": ["id", "createdTime"] + } + } + } + } + }, + { + "i": "9d2f4b81-6a3c-4e72-bf90-5c1d2e3f4a5b", + "x": 0, + "y": 13, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Fields", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Data Source]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[dataSourceSchema]}}", + "name": "dataSourceSchema", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Notion Data Sources]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Data Source]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "type", "details"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Notion/v1/defaultContent/notionPage.dash.json b/plugins/Notion/v1/defaultContent/notionPage.dash.json new file mode 100644 index 00000000..ad8b5c70 --- /dev/null +++ b/plugins/Notion/v1/defaultContent/notionPage.dash.json @@ -0,0 +1,125 @@ +{ + "name": "Page", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": ["{{variables.[Notion Page]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "60514e48-d346-4df3-b38e-9a620f254d25", + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Properties", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Page]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pageProperties]}}", + "name": "pageProperties", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Notion Pages]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Page]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + }, + { + "i": "a4d2f1b6-3c8e-4a90-bb71-2f5c6d7e8a91", + "x": 0, + "y": 4, + "w": 4, + "h": 5, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Content", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Page]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pageContent]}}", + "name": "pageContent", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Notion Pages]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Page]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["text", "type", "checked"], + "hiddenColumns": ["hasChildren"] + } + } + } + } + }, + { + "i": "c5e3a2d7-4f9b-4c01-9d82-3a6b7c8d9e02", + "x": 0, + "y": 9, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Comments", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion Page]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pageComments]}}", + "name": "pageComments", + "pluginConfigId": "{{configId}}" + }, + "scope": { + "scope": "{{scopes.[Notion Pages]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion Page]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["createdTime", "author", "text"], + "hiddenColumns": ["id", "createdById"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Notion/v1/defaultContent/notionUser.dash.json b/plugins/Notion/v1/defaultContent/notionUser.dash.json new file mode 100644 index 00000000..bc84b73c --- /dev/null +++ b/plugins/Notion/v1/defaultContent/notionUser.dash.json @@ -0,0 +1,48 @@ +{ + "name": "User", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "variables": ["{{variables.[Notion User]}}"], + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "56eb70db-4900-4fcc-91d1-1a6bf0f21ddc", + "x": 0, + "y": 0, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Details", + "description": "", + "timeframe": "none", + "variables": ["{{variables.[Notion User]}}"], + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "datastream-properties", + "name": "properties" + }, + "scope": { + "scope": "{{scopes.[Notion Users]}}", + "workspace": "{{workspaceId}}", + "variable": "{{variables.[Notion User]}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": true + } + } + } + } + } + ] + } +} diff --git a/plugins/Notion/v1/defaultContent/overview.dash.json b/plugins/Notion/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..6ab930e2 --- /dev/null +++ b/plugins/Notion/v1/defaultContent/overview.dash.json @@ -0,0 +1,219 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last24hours", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "6e8b4dc7-b902-4bb8-8f70-1f149eac93bc", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Users", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[usersList]}}", + "name": "usersList", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "679c5421-4490-4a8b-b501-05acdd1fed39", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Data Sources", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[dataSourcesSearch]}}", + "name": "dataSourcesSearch", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "0d924bf2-0b5a-46df-bb3e-d1a5b87e2d18", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Pages", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pagesSearch]}}", + "name": "pagesSearch", + "pluginConfigId": "{{configId}}", + "group": { + "by": [], + "aggregate": [{ "type": "count" }] + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none" + } + } + } + } + }, + { + "i": "f26a5ac7-b229-4cb1-827e-55b753c11125", + "x": 0, + "y": 2, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recently Edited Pages", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[pagesSearch]}}", + "name": "pagesSearch", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["lastEditedTime", "desc"]] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "lastEditedTime", "url"], + "hiddenColumns": ["id", "createdTime", "parentType", "parentDataSourceId"] + } + } + } + } + }, + { + "i": "c5b2aadc-a62f-4894-8d82-1f90ab71689e", + "x": 0, + "y": 6, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Users", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[usersList]}}", + "name": "usersList", + "pluginConfigId": "{{configId}}" + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "type", "person.email"], + "hiddenColumns": ["id", "avatar_url"] + } + } + } + } + }, + { + "i": "5e6b33f6-e0fd-4420-9778-e5b11110100d", + "x": 2, + "y": 6, + "w": 2, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Data Sources", + "description": "", + "timeframe": "none", + "activePluginConfigIds": ["{{configId}}"], + "dataStream": { + "id": "{{dataStreams.[dataSourcesSearch]}}", + "name": "dataSourcesSearch", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [["lastEditedTime", "desc"]] + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": ["name", "lastEditedTime", "url"], + "hiddenColumns": ["id", "createdTime", "parentDatabaseId"] + } + } + } + } + } + ] + } +} diff --git a/plugins/Notion/v1/defaultContent/scopes.json b/plugins/Notion/v1/defaultContent/scopes.json new file mode 100644 index 00000000..eddf9a55 --- /dev/null +++ b/plugins/Notion/v1/defaultContent/scopes.json @@ -0,0 +1,38 @@ +[ + { + "name": "Notion Users", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion User"] } + }, + "variable": { + "name": "Notion User", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Notion Data Sources", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Data Source"] } + }, + "variable": { + "name": "Notion Data Source", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + }, + { + "name": "Notion Pages", + "matches": { + "sourceType": { "type": "oneOf", "values": ["Notion Page"] } + }, + "variable": { + "name": "Notion Page", + "allowMultipleSelection": false, + "default": "none", + "type": "object" + } + } +] diff --git a/plugins/Notion/v1/docs/README.md b/plugins/Notion/v1/docs/README.md new file mode 100644 index 00000000..70b4c0dc --- /dev/null +++ b/plugins/Notion/v1/docs/README.md @@ -0,0 +1,57 @@ +# Notion + +Bring your Notion workspace into SquaredUp. This plugin indexes your workspace **users**, **data sources** (the tables/databases shared with the integration) and **pages**, and lets you explore page properties, page content, comments and database schemas — with dashboards that summarise your workspace, surface recently-edited content, and let you drill into any data source or page. + +## What this plugin monitors + +- **Users** — the people and bots that are members of your Notion workspace. +- **Data sources** — the individual tables of data that live under your Notion databases, for every database shared with the integration. +- **Pages** — the pages shared with the integration, including pages that are rows inside a data source. Each page records its title, URL, parent, and created / last-edited times. + +Out-of-the-box dashboards give you a workspace **Overview** plus a **perspective** for each user, data source and page. + +## Before you start — create a Notion integration + +This plugin connects using **OAuth 2.0**, so you need a Notion **public integration** to obtain a Client ID and Client Secret. + +1. Go to [Notion → My integrations](https://www.notion.so/my-integrations) and click **New connection**. +2. Give it a name (e.g. *SquaredUp*) and associate it with the workspace you want to monitor. +3. Select OAuth as the Authentication method +4. If required, select a specific workspace. +5. Under **Redirect URIs**, add the SquaredUp redirect URI for your region — copy the one that matches your tenant: + - **US:** `https://app.squaredup.com/settings/pluginsoauth2` + - **EU:** `https://eu.app.squaredup.com/settings/pluginsoauth2` +6. Click Create connection +7. Under **Capabilities**, uncheck 'Update content' and 'Insert content' - only read permissions are required. If you want to use the **Page Comments** data stream, also enable the **Read comments** capability. +8. Click Save connection +6. Under 'OAuth connection', copy the **Client ID** and **Client secret** — you'll paste these into SquaredUp. + +When you add the plugin in SquaredUp, paste the Client ID and Client Secret, then click **Sign in with Notion** and approve access to the pages/databases you selected. + +### Share content with the integration +Notion only returns content that has been explicitly shared with the integration. In each page or database you want monitored, open the **•••** menu → **Connections** → **Connect to** → select your integration. Sharing a parent page shares its children too. + +## Configuration fields + +| Field | Description | Required | +| --- | --- | --- | +| **Client ID** | The *OAuth client ID* from your Notion integration's Configuration tab. | Yes | +| **Client Secret** | The *OAuth client secret* from your Notion integration's Configuration tab. Stored securely. | Yes | +| **Sign in with Notion** | Launches the Notion OAuth consent screen. After approving, the connection is authenticated. | Yes | + +## What gets indexed + +| Object type | Description | +| --- | --- | +| **Notion User** | A member of the workspace — a person (with email where available) or a bot. | +| **Notion Data Source** | A table of data under a Notion database that has been shared with the integration. | +| **Notion Page** | A page shared with the integration, including database rows. Linked in the graph to its parent data source. | + +## Known limitations + +- **Only shared content is visible.** The Notion API only returns pages, databases and data sources that have been explicitly shared with the integration. Anything not shared will not appear in SquaredUp. Listing workspace **users** additionally requires an OAuth/integration token (personal access tokens cannot list users). +- **No historical data.** The Notion API does not expose a queryable time range, so all data is current-state. Tiles can still highlight stale or recently-edited content using each object's last-edited timestamp, but there are no trend/time-series charts. +- **Rate limits.** Notion limits requests to roughly **3 requests per second** per integration. Large workspaces import more slowly; bursts may be throttled (HTTP 429). +- **Page volume.** Every row in a database is itself a page, so workspaces with large databases can import a very large number of `Notion Page` objects. +- **Comments require an extra capability.** The Page Comments data stream needs the integration's **Read comments** capability (see setup step 5). Without it, the stream returns no data. +- **Page content is top-level only.** The Page Content data stream returns a page's top-level blocks; nested/child blocks (content inside toggles, columns, etc.) are not expanded. diff --git a/plugins/Notion/v1/icon.svg b/plugins/Notion/v1/icon.svg new file mode 100644 index 00000000..9fc27e22 --- /dev/null +++ b/plugins/Notion/v1/icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/plugins/Notion/v1/indexDefinitions/default.json b/plugins/Notion/v1/indexDefinitions/default.json new file mode 100644 index 00000000..69bc647e --- /dev/null +++ b/plugins/Notion/v1/indexDefinitions/default.json @@ -0,0 +1,52 @@ +{ + "steps": [ + { + "name": "users", + "dataStream": { "name": "usersList" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { "value": "Notion User" }, + "properties": [ + { "userType": "type" }, + { "email": "person.email" }, + { "avatarUrl": "avatar_url" } + ] + } + }, + { + "name": "dataSources", + "dataStream": { "name": "dataSourcesSearch" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { "value": "Notion Data Source" }, + "properties": [ + "createdTime", + "lastEditedTime", + "url", + "parentDatabaseId" + ] + } + }, + { + "name": "pages", + "dataStream": { "name": "pagesSearch" }, + "timeframe": "none", + "objectMapping": { + "id": "id", + "name": "name", + "type": { "value": "Notion Page" }, + "properties": [ + "createdTime", + "lastEditedTime", + "url", + "parentType", + "parentDataSourceId" + ] + } + } + ] +} diff --git a/plugins/Notion/v1/metadata.json b/plugins/Notion/v1/metadata.json new file mode 100644 index 00000000..7691255f --- /dev/null +++ b/plugins/Notion/v1/metadata.json @@ -0,0 +1,61 @@ +{ + "name": "notion", + "displayName": "Notion", + "version": "1.2.4", + "author": { + "name": "@clarkd", + "type": "community" + }, + "description": "Bring your Notion workspace into SquaredUp — index users, databases and pages, and surface page properties, content, comments and database schemas.", + "category": "Collaboration", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": false, + "restrictedToPlatforms": [], + "keywords": [ + "notion", + "workspace", + "pages", + "databases", + "knowledge base", + "collaboration" + ], + "objectTypes": [ + "Notion User", + "Notion Data Source", + "Notion Page" + ], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Notion/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/Notion/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "https://api.notion.com/v1", + "authMode": "oauth2", + "oauth2GrantType": "authCode", + "oauth2AuthUrl": "https://api.notion.com/v1/oauth/authorize", + "oauth2TokenUrl": "https://api.notion.com/v1/oauth/token", + "oauth2ClientId": "{{clientId}}", + "oauth2ClientSecret": "{{clientSecret}}", + "oauth2ClientSecretLocationDuringAuth": "header", + "oauth2AuthExtraArgs": [ + { "key": "owner", "value": "user" } + ], + "headers": [ + { "key": "Notion-Version", "value": "2026-03-11" } + ], + "queryArgs": [] + } + } +} diff --git a/plugins/Notion/v1/ui.json b/plugins/Notion/v1/ui.json new file mode 100644 index 00000000..ce3350e4 --- /dev/null +++ b/plugins/Notion/v1/ui.json @@ -0,0 +1,47 @@ +[ + { + "name": "clientId", + "label": "Client ID", + "type": "text", + "help": "Your Notion integration's **OAuth client ID**, found on the integration's Configuration tab. [Manage integrations](https://www.notion.so/my-integrations)", + "placeholder": "e.g. 000000000-0000-0000-0000-000000000000", + "validation": { + "required": true + } + }, + { + "name": "clientSecret", + "label": "Client Secret", + "type": "password", + "help": "Your Notion integration's **OAuth client secret**, found on the integration's Configuration tab. [Manage integrations](https://www.notion.so/my-integrations)", + "placeholder": "e.g. secret_00000000000000000000000000000000", + "validation": { + "required": true + } + }, + { + "type": "fieldGroup", + "name": "signInGroup", + "label": "Sign in", + "visible": { + "clientId": { + "type": "regex", + "pattern": "(.+)" + }, + "clientSecret": { + "type": "regex", + "pattern": "(.+)" + } + }, + "fields": [ + { + "type": "oAuth2", + "name": "oauth2AuthCodeSignIn", + "label": "Sign in with Notion", + "validation": { + "required": true + } + } + ] + } +]