diff --git a/src/button/CaretForProvider.tsx b/src/button/CaretForProvider.tsx index 09e8e73..415e4c0 100644 --- a/src/button/CaretForProvider.tsx +++ b/src/button/CaretForProvider.tsx @@ -49,6 +49,17 @@ const GitLabCaret = () => { ); }; +const AzureDevOpsCaret = () => { + return ( + + + + ); +}; + type Props = { provider: SupportedApplication; }; @@ -61,6 +72,8 @@ export const CaretForProvider = ({ provider }: Props) => { return ; case "gitlab": return ; + case "azure-devops": + return ; default: return ( diff --git a/src/button/button-contributions.ts b/src/button/button-contributions.ts index d440366..0cfc08c 100644 --- a/src/button/button-contributions.ts +++ b/src/button/button-contributions.ts @@ -3,7 +3,7 @@ * Happy about anyone who's able to make this work with imports (i.e. run the tests in this project), but I couldn't figure it out and gave up. */ -export type SupportedApplication = "github" | "gitlab" | "bitbucket-server" | "bitbucket"; +export type SupportedApplication = "github" | "gitlab" | "bitbucket-server" | "bitbucket" | "azure-devops"; const resolveMetaAppName = (head: HTMLHeadElement): string | undefined => { const metaApplication = head.querySelector("meta[name=application-name]"); @@ -18,16 +18,24 @@ const resolveMetaAppName = (head: HTMLHeadElement): string | undefined => { return undefined; }; +export const DEFAULT_HOSTS = ["github.com", "gitlab.com", "bitbucket.org", "dev.azure.com"]; + /** * Provides a fast check to see if the current URL is on a supported site. */ export const isSiteSuitable = (): boolean => { + const isWhitelistedHost = DEFAULT_HOSTS.some((host) => location.host === host); + if (isWhitelistedHost) { + return true; + } + const appName = resolveMetaAppName(document.head); if (!appName) { return false; } const allowedApps = ["GitHub", "GitLab", "Bitbucket"]; - return allowedApps.includes(appName); + + return allowedApps.some((allowedApp) => appName.includes(allowedApp)); }; export interface ButtonContributionParams { @@ -51,7 +59,7 @@ export interface ButtonContributionParams { /** * The element in which the button should be inserted. * - * This element will be inserted into teh main document and allows for styling within the original page. + * This element will be inserted into the main document and allows for styling within the original page. * * The structure looks like this: * @@ -98,6 +106,12 @@ export interface ButtonContributionParams { * the classnames to remove and add. */ manipulations?: { element: string; remove?: string; add?: string; style?: Partial }[]; + + /** + * A function that can be used to transform the URL that should be opened when the Gitpod button is clicked. + * @returns The transformed URL. + */ + urlTransformer?: (originalURL: string) => string; } function createElement( @@ -113,6 +127,61 @@ function createElement( } export const buttonContributions: ButtonContributionParams[] = [ + // Azure DevOps + { + id: "ado-repo", + exampleUrls: [ + // "https://dev.azure.com/services-azure/_git/project2" + ], + selector: "div.repos-files-header-commandbar:nth-child(1)", + containerElement: createElement("div", {}), + application: "azure-devops", + insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-header-command-bar-menu-button"])`, + manipulations: [ + { + element: "div.repos-files-header-commandbar.scroll-hidden", + remove: "scroll-hidden", + }, + ], + urlTransformer(originalUrl) { + const url = new URL(originalUrl); + if (url.pathname.includes("version=GB")) { + return originalUrl; + } + // version=GBdevelop + const branchElement = document.evaluate( + "//div[contains(@class, 'version-dropdown')]//span[contains(@class, 'text-ellipsis')]", + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null, + ).singleNodeValue; + if (branchElement) { + const branch = branchElement.textContent?.trim(); + url.searchParams.set("version", `GB${branch}`); + } + + return url.toString(); + }, + }, + { + id: "ado-pr", + exampleUrls: [ + // "https://dev.azure.com/services-azure/test-project/_git/repo2/pullrequest/1" + ], + selector: ".repos-pr-header > div:nth-child(2) > div:nth-child(1)", + containerElement: createElement("div", {}), + application: "azure-devops", + insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-menu-button-"])`, + }, + { + id: "ado-repo-empty", + exampleUrls: [], + selector: "div.clone-with-application", + application: "azure-devops", + containerElement: createElement("div", { marginLeft: "4px", marginRight: "4px" }), + }, + // GitLab { id: "gl-repo", // also taking care of branches diff --git a/src/button/button.css b/src/button/button.css index 31faf27..a8bb6fc 100644 --- a/src/button/button.css +++ b/src/button/button.css @@ -315,3 +315,26 @@ 0 0 1px rgba(9, 30, 66, 0.31) ); } + +.azure-devops { + --font-family: "Segoe UI", "-apple-system", BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Ubuntu, Arial, + sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --primary-bg-color: var(--communication-background, rgba(0, 120, 212, 1)); + --primary-hover-bg-color: rgba(var(--palette-primary-darkened-6, 0, 103, 181), 1); + --primary-color: var(--text-on-communication-background, rgba(255, 255, 255, 1)); + --primary-hover-color: var(--text-on-communication-background, rgba(255, 255, 255, 1)); + --primary-separator-color: rgba(var(--palette-primary-darkened-10, 0, 91, 161), 1); + --font-weight: 600; + --primary-height: 32px; + + --dropdown-color: var(--text-primary-color, rgba(0, 0, 0, 0.9)); + --dropdown-bg-color: var(--callout-background-color, rgba(255, 255, 255, 1)); + --dropdown-hover-bg-color: var(--palette-black-alpha-4, rgba(0, 0, 0, 0.04)); + --dropdown-box-shadow: 0 3.2px 7.2px 0 var(--callout-shadow-color, rgba(0, 0, 0, 0.132)), + 0 0.6px 1.8px 0 var(--callout-shadow-secondary-color, rgba(0, 0, 0, 0.108)); + --dropdown-border-width: 0; + --dropdown-border-radius: 4px; + + --border-radius: 2px; + --border-width: 0px; +} diff --git a/src/button/button.tsx b/src/button/button.tsx index d5b0ea9..2c28108 100644 --- a/src/button/button.tsx +++ b/src/button/button.tsx @@ -9,13 +9,14 @@ import { DEFAULT_GITPOD_ENDPOINT, EVENT_CURRENT_URL_CHANGED } from "~constants"; import { STORAGE_KEY_ADDRESS, STORAGE_KEY_ALWAYS_OPTIONS, STORAGE_KEY_NEW_TAB } from "~storage"; import type { SupportedApplication } from "./button-contributions"; -import { BitbucketCaret, CaretForProvider, GitHubCaret } from "./CaretForProvider"; +import { CaretForProvider } from "./CaretForProvider"; type Props = { application: SupportedApplication; additionalClassNames?: string[]; + urlTransformer?: (url: string) => string; }; -export const GitpodButton = ({ application, additionalClassNames }: Props) => { +export const GitpodButton = ({ application, additionalClassNames, urlTransformer }: Props) => { const [address] = useStorage(STORAGE_KEY_ADDRESS, DEFAULT_GITPOD_ENDPOINT); const [openInNewTab] = useStorage(STORAGE_KEY_NEW_TAB, true); const [disableAutostart] = useStorage(STORAGE_KEY_ALWAYS_OPTIONS, false); @@ -36,19 +37,19 @@ export const GitpodButton = ({ application, additionalClassNames }: Props) => { }; }, []); - const actions = useMemo( - () => [ + const actions = useMemo(() => { + const parsedHref = !urlTransformer ? currentHref : urlTransformer(currentHref); + return [ { - href: `${address}/?autostart=${!disableAutostart}#${currentHref}`, + href: `${address}/?autostart=${!disableAutostart}#${parsedHref}`, label: "Open", }, { - href: `${address}/?autostart=false#${currentHref}`, + href: `${address}/?autostart=false#${parsedHref}`, label: "Open with options...", }, - ], - [address, disableAutostart, currentHref], - ); + ]; + }, [address, disableAutostart, currentHref, urlTransformer]); const dropdownRef = useRef(null); const firstActionRef = useRef(null); diff --git a/src/contents/button.tsx b/src/contents/button.tsx index 4d698cc..f7cade0 100644 --- a/src/contents/button.tsx +++ b/src/contents/button.tsx @@ -7,8 +7,9 @@ import { EVENT_CURRENT_URL_CHANGED } from "~constants"; import { GitpodButton } from "../button/button"; import { buttonContributions, isSiteSuitable, type ButtonContributionParams } from "../button/button-contributions"; +// keep in sync with DEFAULT_HOSTS in src/button/button-contributions.ts export const config: PlasmoCSConfig = { - matches: ["https://github.com/*", "https://gitlab.com/*", "https://bitbucket.org/*"], + matches: ["https://github.com/*", "https://gitlab.com/*", "https://bitbucket.org/*", "https://dev.azure.com/*"], }; export const getStyle = () => { @@ -45,6 +46,7 @@ class ButtonContributionManager { key={containerId} application={contribution.application} additionalClassNames={contribution.additionalClassNames} + urlTransformer={contribution.urlTransformer} />, ); } diff --git a/test/src/button-contributions-copy.ts b/test/src/button-contributions-copy.ts index 04c91b3..f3db6b1 100644 --- a/test/src/button-contributions-copy.ts +++ b/test/src/button-contributions-copy.ts @@ -3,7 +3,7 @@ * Happy about anyone who's able to make this work with imports (i.e. run the tests in this project), but I couldn't figure it out and gave up. */ -export type SupportedApplication = "github" | "gitlab" | "bitbucket-server" | "bitbucket"; +export type SupportedApplication = "github" | "gitlab" | "bitbucket-server" | "bitbucket" | "azure-devops"; const resolveMetaAppName = (head: HTMLHeadElement): string | undefined => { const metaApplication = head.querySelector("meta[name=application-name]"); @@ -18,16 +18,24 @@ const resolveMetaAppName = (head: HTMLHeadElement): string | undefined => { return undefined; }; +export const DEFAULT_HOSTS = ["github.com", "gitlab.com", "bitbucket.org", "dev.azure.com"]; + /** * Provides a fast check to see if the current URL is on a supported site. */ export const isSiteSuitable = (): boolean => { + const isWhitelistedHost = DEFAULT_HOSTS.some((host) => location.host === host); + if (isWhitelistedHost) { + return true; + } + const appName = resolveMetaAppName(document.head); if (!appName) { return false; } const allowedApps = ["GitHub", "GitLab", "Bitbucket"]; - return allowedApps.includes(appName); + + return allowedApps.some((allowedApp) => appName.includes(allowedApp)); }; export interface ButtonContributionParams { @@ -51,7 +59,7 @@ export interface ButtonContributionParams { /** * The element in which the button should be inserted. * - * This element will be inserted into teh main document and allows for styling within the original page. + * This element will be inserted into the main document and allows for styling within the original page. * * The structure looks like this: * @@ -98,6 +106,12 @@ export interface ButtonContributionParams { * the classnames to remove and add. */ manipulations?: { element: string; remove?: string; add?: string; style?: Partial }[]; + + /** + * A function that can be used to transform the URL that should be opened when the Gitpod button is clicked. + * @returns The transformed URL. + */ + urlTransformer?: (originalURL: string) => string; } function createElement( @@ -113,6 +127,61 @@ function createElement( } export const buttonContributions: ButtonContributionParams[] = [ + // Azure DevOps + { + id: "ado-repo", + exampleUrls: [ + // "https://dev.azure.com/services-azure/_git/project2" + ], + selector: "div.repos-files-header-commandbar:nth-child(1)", + containerElement: createElement("div", {}), + application: "azure-devops", + insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-header-command-bar-menu-button"])`, + manipulations: [ + { + element: "div.repos-files-header-commandbar.scroll-hidden", + remove: "scroll-hidden", + }, + ], + urlTransformer(originalUrl) { + const url = new URL(originalUrl); + if (url.pathname.includes("version=GB")) { + return originalUrl; + } + // version=GBdevelop + const branchElement = document.evaluate( + "//div[contains(@class, 'version-dropdown')]//span[contains(@class, 'text-ellipsis')]", + document, + null, + XPathResult.FIRST_ORDERED_NODE_TYPE, + null, + ).singleNodeValue; + if (branchElement) { + const branch = branchElement.textContent?.trim(); + url.searchParams.set("version", `GB${branch}`); + } + + return url.toString(); + }, + }, + { + id: "ado-pr", + exampleUrls: [ + // "https://dev.azure.com/services-azure/test-project/_git/repo2/pullrequest/1" + ], + selector: ".repos-pr-header > div:nth-child(2) > div:nth-child(1)", + containerElement: createElement("div", {}), + application: "azure-devops", + insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-menu-button-"])`, + }, + { + id: "ado-repo-empty", + exampleUrls: [], + selector: "div.clone-with-application", + application: "azure-devops", + containerElement: createElement("div", { marginLeft: "4px", marginRight: "4px" }), + }, + // GitLab { id: "gl-repo", // also taking care of branches