feat: add notion as issue provider#2660
Conversation
Greptile SummaryThis PR adds Notion as an issue provider, supporting both an "all-shared" scope (search everything the integration can access) and an explicit "data-sources" scope (specific pages/databases by URL or 32-char ID). It introduces a connection service, an issue provider with list/search/context fetching, a setup form with token-preservation on scope edits, and improved empty-state error display for any provider.
Confidence Score: 5/5Safe to merge — the Notion integration is well-contained, all API calls go through the existing encrypted secrets store and rate-limiting patterns, and the previous review concerns (concurrency, separator, double-sort) are all resolved. The credential service, issue provider, and UI forms are all thoroughly tested. The only noteworthy inconsistency is that the modal mode prop changes the dialog header but the form independently decides its own edit/connect state from a live query — a cosmetic mismatch in an unlikely edge case, not a functional regression. integration-setup-modal.tsx — the mode prop / hasCredentials split between the modal header and the form could eventually cause label drift if other providers adopt the edit pattern.
|
| Filename | Overview |
|---|---|
| apps/emdash-desktop/src/main/core/notion/notion-connection-service.ts | New credential management service; handles URL parsing, token validation via /users/me, and in-memory caching. The ', ' separator (not newline) for stored sourceUrls correctly round-trips through the single-line Input in the edit form. |
| apps/emdash-desktop/src/main/core/notion/notion-issue-provider.ts | Issue provider with all four code paths (all-shared + search, all-shared + list, data-sources + search, data-sources + list). All paths now use mapWithConcurrency(4); redundant double-sort removed; block context capped at 300 blocks / depth 3. |
| apps/emdash-desktop/src/renderer/features/integrations/integration-setup-modal.tsx | Adds mode prop (connect/edit) that changes the modal header, but the prop is not forwarded to the form. NotionSetupForm determines its own edit/connect state independently from hasCredentials, which can produce a title/button label mismatch in edge cases. |
| apps/emdash-desktop/src/renderer/features/integrations/NotionSetupForm.tsx | Setup form that fetches its own configuration state (staleTime:0). Edit mode detected via hasCredentials, not modal mode prop. Loading skeleton disables submit correctly. |
| apps/emdash-desktop/src/renderer/features/tasks/components/issue-selector/issue-search-empty-state.tsx | New component replacing the plain error string; dispatches to parseIssueSearchError for provider-aware title/description/action. Clean implementation. |
| apps/emdash-desktop/src/renderer/features/tasks/components/issue-selector/parse-issue-search-error.ts | Error parsing logic with provider-specific handling for Notion and GitHub; falls back to a generic error display. Well-tested. |
| apps/emdash-desktop/src/shared/telemetry.ts | Replaces hardcoded provider union literals with IssueProviderType, fixing several pre-existing stale enumerations (monday, trello, notion were missing from telemetry types). |
| apps/emdash-desktop/src/main/core/notion/notion-connection-service.test.ts | Comprehensive tests covering credential storage, URL parsing, token preservation, legacy migration, and API error normalization. |
| apps/emdash-desktop/src/main/core/notion/notion-issue-provider.test.ts | Good test coverage across all provider code paths including pagination, search, data-source queries, and block-context collection. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI as Renderer (NotionSetupForm)
participant RPC as RPC Router (notionController)
participant CS as NotionConnectionService
participant KS as EncryptedSecretsStore
participant NA as Notion API
UI->>RPC: "saveCredentials({token, databaseUrls})"
RPC->>CS: saveCredentials(input)
CS->>CS: parseDatabaseUrls(databaseUrls)
CS->>NA: GET /users/me (validate token)
NA-->>CS: "{id, bot.workspace_name}"
CS->>KS: setSecret("emdash-notion-credentials", JSON)
CS-->>RPC: "{success:true, displayName}"
RPC-->>UI: "{success:true, displayName}"
Note over UI,NA: Issue listing (data-sources scope)
UI->>RPC: "listIssues({limit:50})"
RPC->>CS: getStoredCredentials()
CS->>KS: getSecret (cached after first read)
KS-->>CS: credentials
CS-->>RPC: credentials
RPC->>NA: "POST /data_sources/{id}/query (x N, concurrency=4)"
NA-->>RPC: "{results:[pages]}"
RPC-->>UI: "{success:true, issues:[]}"
Note over UI,NA: getIssueContext
UI->>RPC: "getIssueContext({identifier})"
RPC->>NA: "GET /pages/{id}"
NA-->>RPC: page
RPC->>NA: "GET /blocks/{id}/children (paginated, depth<=3)"
NA-->>RPC: blocks
RPC-->>UI: "{success:true, issue:{...context}}"
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant UI as Renderer (NotionSetupForm)
participant RPC as RPC Router (notionController)
participant CS as NotionConnectionService
participant KS as EncryptedSecretsStore
participant NA as Notion API
UI->>RPC: "saveCredentials({token, databaseUrls})"
RPC->>CS: saveCredentials(input)
CS->>CS: parseDatabaseUrls(databaseUrls)
CS->>NA: GET /users/me (validate token)
NA-->>CS: "{id, bot.workspace_name}"
CS->>KS: setSecret("emdash-notion-credentials", JSON)
CS-->>RPC: "{success:true, displayName}"
RPC-->>UI: "{success:true, displayName}"
Note over UI,NA: Issue listing (data-sources scope)
UI->>RPC: "listIssues({limit:50})"
RPC->>CS: getStoredCredentials()
CS->>KS: getSecret (cached after first read)
KS-->>CS: credentials
CS-->>RPC: credentials
RPC->>NA: "POST /data_sources/{id}/query (x N, concurrency=4)"
NA-->>RPC: "{results:[pages]}"
RPC-->>UI: "{success:true, issues:[]}"
Note over UI,NA: getIssueContext
UI->>RPC: "getIssueContext({identifier})"
RPC->>NA: "GET /pages/{id}"
NA-->>RPC: page
RPC->>NA: "GET /blocks/{id}/children (paginated, depth<=3)"
NA-->>RPC: blocks
RPC-->>UI: "{success:true, issue:{...context}}"
Reviews (3): Last reviewed commit: "fix(notion): keep database urls single-l..." | Re-trigger Greptile

Description
Screenshot/Recording (if applicable)
https://streamable.com/gmawsc
Checklist
messages and, when possible, the PR title