Skip to content
Merged
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
13 changes: 13 additions & 0 deletions src/button/CaretForProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ const GitLabCaret = () => {
);
};

const AzureDevOpsCaret = () => {
return (
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
d="M4.22 8.47a.75.75 0 0 1 1.06 0L12 15.19l6.72-6.72a.75.75 0 1 1 1.06 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L4.22 9.53a.75.75 0 0 1 0-1.06Z"
fill="currentColor"
/>
</svg>
);
};

type Props = {
provider: SupportedApplication;
};
Expand All @@ -61,6 +72,8 @@ export const CaretForProvider = ({ provider }: Props) => {
return <BitbucketCaret />;
case "gitlab":
return <GitLabCaret />;
case "azure-devops":
return <AzureDevOpsCaret />;
default:
return (
<svg width="16" viewBox="0 0 24 24" className={classNames("chevron-icon")}>
Expand Down
75 changes: 72 additions & 3 deletions src/button/button-contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]");
Expand All @@ -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 {
Expand All @@ -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:
*
Expand Down Expand Up @@ -98,6 +106,12 @@ export interface ButtonContributionParams {
* the classnames to remove and add.
*/
manipulations?: { element: string; remove?: string; add?: string; style?: Partial<CSSStyleDeclaration> }[];

/**
* 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(
Expand All @@ -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
Expand Down
23 changes: 23 additions & 0 deletions src/button/button.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
19 changes: 10 additions & 9 deletions src/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>(STORAGE_KEY_ADDRESS, DEFAULT_GITPOD_ENDPOINT);
const [openInNewTab] = useStorage<boolean>(STORAGE_KEY_NEW_TAB, true);
const [disableAutostart] = useStorage<boolean>(STORAGE_KEY_ALWAYS_OPTIONS, false);
Expand All @@ -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<HTMLDivElement | null>(null);
const firstActionRef = useRef<HTMLAnchorElement | null>(null);

Expand Down
4 changes: 3 additions & 1 deletion src/contents/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -45,6 +46,7 @@ class ButtonContributionManager {
key={containerId}
application={contribution.application}
additionalClassNames={contribution.additionalClassNames}
urlTransformer={contribution.urlTransformer}
/>,
);
}
Expand Down
75 changes: 72 additions & 3 deletions test/src/button-contributions-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]");
Expand All @@ -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 {
Expand All @@ -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:
*
Expand Down Expand Up @@ -98,6 +106,12 @@ export interface ButtonContributionParams {
* the classnames to remove and add.
*/
manipulations?: { element: string; remove?: string; add?: string; style?: Partial<CSSStyleDeclaration> }[];

/**
* 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(
Expand All @@ -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
Expand Down
Loading