diff --git a/apps/client/src/features/editor/components/integration-link/integration-link-view.tsx b/apps/client/src/features/editor/components/integration-link/integration-link-view.tsx index 0d4a320d..65a502d0 100644 --- a/apps/client/src/features/editor/components/integration-link/integration-link-view.tsx +++ b/apps/client/src/features/editor/components/integration-link/integration-link-view.tsx @@ -18,6 +18,8 @@ const providerIcons: Record = { gitlab: "https://gitlab.com/assets/favicon-72a2cad5025aa931d6ea56c3201d1f18e68a8571da3c2571592f63571e0c5571.png", jira: "https://wac-cdn.atlassian.com/assets/img/favicons/atlassian/favicon.png", linear: "https://linear.app/favicon.ico", + google_docs: "https://ssl.gstatic.com/docs/documents/images/kix-favicon7.ico", + figma: "https://static.figma.com/app/icon/1/favicon.png", }; function IntegrationLinkView(props: any) { diff --git a/apps/server/src/core/integration/constants.ts b/apps/server/src/core/integration/constants.ts index 0a55246a..96ffdb8e 100644 --- a/apps/server/src/core/integration/constants.ts +++ b/apps/server/src/core/integration/constants.ts @@ -4,4 +4,6 @@ export enum IntegrationType { GITLAB = 'gitlab', JIRA = 'jira', LINEAR = 'linear', + GOOGLE_DOCS = 'google_docs', + FIGMA = 'figma', } diff --git a/apps/server/src/core/integration/oauth/oauth.controller.ts b/apps/server/src/core/integration/oauth/oauth.controller.ts index 328bafbc..86e29d2f 100644 --- a/apps/server/src/core/integration/oauth/oauth.controller.ts +++ b/apps/server/src/core/integration/oauth/oauth.controller.ts @@ -75,11 +75,11 @@ export class OAuthController { ); const appUrl = this.environmentService.getAppUrl(); - return res.redirect(`${appUrl}/settings/integrations`); + return res.redirect(`${appUrl}/settings/integrations`, 302).send(); } catch (err) { this.logger.error(`OAuth callback error for ${type}: ${(err as Error).message}`); const appUrl = this.environmentService.getAppUrl(); - return res.redirect(`${appUrl}/settings/integrations?error=oauth_failed`); + return res.redirect(`${appUrl}/settings/integrations?error=oauth_failed`, 302).send(); } } diff --git a/apps/server/src/core/integration/unfurl/unfurl.service.ts b/apps/server/src/core/integration/unfurl/unfurl.service.ts index 8c3551e7..cd5cca15 100644 --- a/apps/server/src/core/integration/unfurl/unfurl.service.ts +++ b/apps/server/src/core/integration/unfurl/unfurl.service.ts @@ -3,7 +3,10 @@ import { IntegrationRegistry } from '../registry/integration-registry'; import { IntegrationConnectionRepo } from '../repos/integration-connection.repo'; import { IntegrationRepo } from '../repos/integration.repo'; import { OAuthService } from '../oauth/oauth.service'; -import { UnfurlResult, IntegrationProvider } from '../registry/integration-provider.interface'; +import { + UnfurlResult, + IntegrationProvider, +} from '../registry/integration-provider.interface'; import { RedisService } from '@nestjs-labs/nestjs-ioredis'; import type { Redis } from 'ioredis'; import * as crypto from 'crypto'; @@ -38,6 +41,7 @@ export class UnfurlService { } const resolved = await this.resolveProvider(url, workspaceId); + if (!resolved) { return null; } @@ -58,7 +62,8 @@ export class UnfurlService { } try { - const accessToken = await this.oauthService.getValidAccessToken(connection); + const accessToken = + await this.oauthService.getValidAccessToken(connection); const unfurlResult = await provider.unfurl({ url, @@ -123,7 +128,11 @@ export class UnfurlService { } private buildCacheKey(workspaceId: string, url: string): string { - const hash = crypto.createHash('sha256').update(url).digest('hex').slice(0, 16); + const hash = crypto + .createHash('sha256') + .update(url) + .digest('hex') + .slice(0, 16); return `${UNFURL_CACHE_PREFIX}${workspaceId}:${hash}`; } } diff --git a/apps/server/src/ee b/apps/server/src/ee index 41b27c95..010a833e 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 41b27c951fff9b1a10118dc38f671dbff6b77cfc +Subproject commit 010a833e1a978faf02d4038baa9f9775ce2916e2 diff --git a/packages/editor-ext/src/lib/integration-link/integration-link-patterns.ts b/packages/editor-ext/src/lib/integration-link/integration-link-patterns.ts index ccfa2c90..ed9b40f3 100644 --- a/packages/editor-ext/src/lib/integration-link/integration-link-patterns.ts +++ b/packages/editor-ext/src/lib/integration-link/integration-link-patterns.ts @@ -4,28 +4,158 @@ export type IntegrationLinkPattern = { }; export const integrationLinkPatterns: IntegrationLinkPattern[] = [ - // GitHub (cloud + GHE): /:owner/:repo/pull/:num or /issues/:num + // GitHub PR commit (must be before generic PR pattern) { provider: "github", regex: - /^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/(pull|issues)\/(\d+)/, + /^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/pull\/(\d+)\/commits\/([a-f0-9]+)/, }, - // GitLab (cloud + self-hosted): /-/issues/:num or /-/merge_requests/:num + // GitHub PR (with optional /checks, /commits, /files sub-pages) + { + provider: "github", + regex: + /^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/pull\/(\d+)/, + }, + // GitHub issue + { + provider: "github", + regex: + /^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/issues\/(\d+)/, + }, + // GitHub commit + { + provider: "github", + regex: + /^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/commits?\/([a-f0-9]+)/, + }, + // GitHub file/blob + { + provider: "github", + regex: + /^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.+?)(?:#L(\d+)(?:-L(\d+))?)?$/, + }, + // GitHub pulls list + { + provider: "github", + regex: + /^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/pulls(?:\/.*)?(?:\?.*)?$/, + }, + // GitHub releases list + { + provider: "github", + regex: + /^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/releases(?:\/.*)?(?:\?.*)?$/, + }, + // GitHub issues list + { + provider: "github", + regex: + /^https?:\/\/[^\/]+\/([^\/]+)\/([^\/]+)\/issues(?:\/(?:created_by|assigned)\/[\w.\/-]+)?\/?(?:\?.*)?$/, + }, + // GitHub repo + { + provider: "github", + regex: + /^https?:\/\/[^\/]+\/([a-zA-Z0-9\-_.]+)\/([a-zA-Z0-9\-_.]+)\/?$/, + }, + // GitLab commit in MR diff (must be before generic MR pattern) { provider: "gitlab", regex: - /^https?:\/\/[^\/]+\/(.+)\/-\/(issues|merge_requests)\/(\d+)/, + /^https?:\/\/[^\/]+\/(.+)\/-\/merge_requests\/(\d+)\/diffs\?.*commit_id=([a-f0-9]+)/, + }, + // GitLab merge request + { + provider: "gitlab", + regex: + /^https?:\/\/[^\/]+\/(.+)\/-\/merge_requests\/(\d+)/, + }, + // GitLab issue + { + provider: "gitlab", + regex: + /^https?:\/\/[^\/]+\/(.+)\/-\/issues\/(\d+)/, + }, + // GitLab commit + { + provider: "gitlab", + regex: + /^https?:\/\/[^\/]+\/(.+)\/-\/commits?\/([a-f0-9]+)/, + }, + // GitLab issues list + { + provider: "gitlab", + regex: + /^https?:\/\/[^\/]+\/(.+)\/-\/issues\/?(?:\?.*)?$/, + }, + // GitLab merge requests list + { + provider: "gitlab", + regex: + /^https?:\/\/[^\/]+\/(.+)\/-\/merge_requests\/?(?:\?.*)?$/, + }, + // GitLab project + { + provider: "gitlab", + regex: + /^https?:\/\/[^\/]+\/([a-zA-Z0-9\-_.]+)\/([a-zA-Z0-9\-_]+)\/?$/, + }, + // Google Docs + { + provider: "google_docs", + regex: /^https?:\/\/docs\.google\.com\/document\/d\/([\w-]+)/, + }, + // Google Sheets + { + provider: "google_docs", + regex: /^https?:\/\/docs\.google\.com\/spreadsheets\/d\/([\w-]+)/, + }, + // Google Slides + { + provider: "google_docs", + regex: /^https?:\/\/docs\.google\.com\/presentation\/d\/([\w-]+)/, + }, + // Google Forms + { + provider: "google_docs", + regex: /^https?:\/\/docs\.google\.com\/forms\/d\/([\w-]+)/, + }, + // Google Drive file + { + provider: "google_docs", + regex: /^https?:\/\/drive\.google\.com\/file\/d\/([\w-]+)/, + }, + // Figma file (design, file, proto, board) + { + provider: "figma", + regex: + /^https?:\/\/([\w.-]+\.)?figma\.com\/(file|proto|board|design)\/([0-9a-zA-Z]{22,128})/, }, // Jira (cloud + server): /browse/KEY-123 { provider: "jira", regex: /^https?:\/\/[^\/]+\/browse\/([A-Z][A-Z0-9]+-\d+)/, }, - // Linear (cloud only): /team/issue/KEY-123 + // Linear issue: /team/issue/KEY-123(/:title-slug)? { provider: "linear", regex: /^https?:\/\/linear\.app\/([^\/]+)\/issue\/([A-Z]+-\d+)/, }, + // Linear project: /team/project/:slug(/:tab)? + { + provider: "linear", + regex: /^https?:\/\/linear\.app\/([^\/]+)\/project\/([^\/]+)/, + }, + // Linear initiative: /team/initiative/:slug(/:tab)? + { + provider: "linear", + regex: /^https?:\/\/linear\.app\/([^\/]+)\/initiative\/([^\/]+)/, + }, + // Linear view: /team/view/:id(/:tab)? + { + provider: "linear", + regex: /^https?:\/\/linear\.app\/([^\/]+)\/view\/([^\/]+)/, + }, ]; export function matchIntegrationLink(