Skip to content

Commit 7138321

Browse files
Azure DevOps support (#157)
Co-authored-by: Huiwen <[email protected]>
1 parent 77e2030 commit 7138321

File tree

6 files changed

+193
-16
lines changed

6 files changed

+193
-16
lines changed

src/button/CaretForProvider.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ const GitLabCaret = () => {
4949
);
5050
};
5151

52+
const AzureDevOpsCaret = () => {
53+
return (
54+
<svg width="16" height="16" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
55+
<path
56+
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"
57+
fill="currentColor"
58+
/>
59+
</svg>
60+
);
61+
};
62+
5263
type Props = {
5364
provider: SupportedApplication;
5465
};
@@ -61,6 +72,8 @@ export const CaretForProvider = ({ provider }: Props) => {
6172
return <BitbucketCaret />;
6273
case "gitlab":
6374
return <GitLabCaret />;
75+
case "azure-devops":
76+
return <AzureDevOpsCaret />;
6477
default:
6578
return (
6679
<svg width="16" viewBox="0 0 24 24" className={classNames("chevron-icon")}>

src/button/button-contributions.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* 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.
44
*/
55

6-
export type SupportedApplication = "github" | "gitlab" | "bitbucket-server" | "bitbucket";
6+
export type SupportedApplication = "github" | "gitlab" | "bitbucket-server" | "bitbucket" | "azure-devops";
77

88
const resolveMetaAppName = (head: HTMLHeadElement): string | undefined => {
99
const metaApplication = head.querySelector("meta[name=application-name]");
@@ -18,16 +18,24 @@ const resolveMetaAppName = (head: HTMLHeadElement): string | undefined => {
1818
return undefined;
1919
};
2020

21+
export const DEFAULT_HOSTS = ["github.com", "gitlab.com", "bitbucket.org", "dev.azure.com"];
22+
2123
/**
2224
* Provides a fast check to see if the current URL is on a supported site.
2325
*/
2426
export const isSiteSuitable = (): boolean => {
27+
const isWhitelistedHost = DEFAULT_HOSTS.some((host) => location.host === host);
28+
if (isWhitelistedHost) {
29+
return true;
30+
}
31+
2532
const appName = resolveMetaAppName(document.head);
2633
if (!appName) {
2734
return false;
2835
}
2936
const allowedApps = ["GitHub", "GitLab", "Bitbucket"];
30-
return allowedApps.includes(appName);
37+
38+
return allowedApps.some((allowedApp) => appName.includes(allowedApp));
3139
};
3240

3341
export interface ButtonContributionParams {
@@ -51,7 +59,7 @@ export interface ButtonContributionParams {
5159
/**
5260
* The element in which the button should be inserted.
5361
*
54-
* This element will be inserted into teh main document and allows for styling within the original page.
62+
* This element will be inserted into the main document and allows for styling within the original page.
5563
*
5664
* The structure looks like this:
5765
*
@@ -98,6 +106,12 @@ export interface ButtonContributionParams {
98106
* the classnames to remove and add.
99107
*/
100108
manipulations?: { element: string; remove?: string; add?: string; style?: Partial<CSSStyleDeclaration> }[];
109+
110+
/**
111+
* A function that can be used to transform the URL that should be opened when the Gitpod button is clicked.
112+
* @returns The transformed URL.
113+
*/
114+
urlTransformer?: (originalURL: string) => string;
101115
}
102116

103117
function createElement(
@@ -113,6 +127,61 @@ function createElement(
113127
}
114128

115129
export const buttonContributions: ButtonContributionParams[] = [
130+
// Azure DevOps
131+
{
132+
id: "ado-repo",
133+
exampleUrls: [
134+
// "https://dev.azure.com/services-azure/_git/project2"
135+
],
136+
selector: "div.repos-files-header-commandbar:nth-child(1)",
137+
containerElement: createElement("div", {}),
138+
application: "azure-devops",
139+
insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-header-command-bar-menu-button"])`,
140+
manipulations: [
141+
{
142+
element: "div.repos-files-header-commandbar.scroll-hidden",
143+
remove: "scroll-hidden",
144+
},
145+
],
146+
urlTransformer(originalUrl) {
147+
const url = new URL(originalUrl);
148+
if (url.pathname.includes("version=GB")) {
149+
return originalUrl;
150+
}
151+
// version=GBdevelop
152+
const branchElement = document.evaluate(
153+
"//div[contains(@class, 'version-dropdown')]//span[contains(@class, 'text-ellipsis')]",
154+
document,
155+
null,
156+
XPathResult.FIRST_ORDERED_NODE_TYPE,
157+
null,
158+
).singleNodeValue;
159+
if (branchElement) {
160+
const branch = branchElement.textContent?.trim();
161+
url.searchParams.set("version", `GB${branch}`);
162+
}
163+
164+
return url.toString();
165+
},
166+
},
167+
{
168+
id: "ado-pr",
169+
exampleUrls: [
170+
// "https://dev.azure.com/services-azure/test-project/_git/repo2/pullrequest/1"
171+
],
172+
selector: ".repos-pr-header > div:nth-child(2) > div:nth-child(1)",
173+
containerElement: createElement("div", {}),
174+
application: "azure-devops",
175+
insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-menu-button-"])`,
176+
},
177+
{
178+
id: "ado-repo-empty",
179+
exampleUrls: [],
180+
selector: "div.clone-with-application",
181+
application: "azure-devops",
182+
containerElement: createElement("div", { marginLeft: "4px", marginRight: "4px" }),
183+
},
184+
116185
// GitLab
117186
{
118187
id: "gl-repo", // also taking care of branches

src/button/button.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,26 @@
315315
0 0 1px rgba(9, 30, 66, 0.31)
316316
);
317317
}
318+
319+
.azure-devops {
320+
--font-family: "Segoe UI", "-apple-system", BlinkMacSystemFont, Roboto, "Helvetica Neue", Helvetica, Ubuntu, Arial,
321+
sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
322+
--primary-bg-color: var(--communication-background, rgba(0, 120, 212, 1));
323+
--primary-hover-bg-color: rgba(var(--palette-primary-darkened-6, 0, 103, 181), 1);
324+
--primary-color: var(--text-on-communication-background, rgba(255, 255, 255, 1));
325+
--primary-hover-color: var(--text-on-communication-background, rgba(255, 255, 255, 1));
326+
--primary-separator-color: rgba(var(--palette-primary-darkened-10, 0, 91, 161), 1);
327+
--font-weight: 600;
328+
--primary-height: 32px;
329+
330+
--dropdown-color: var(--text-primary-color, rgba(0, 0, 0, 0.9));
331+
--dropdown-bg-color: var(--callout-background-color, rgba(255, 255, 255, 1));
332+
--dropdown-hover-bg-color: var(--palette-black-alpha-4, rgba(0, 0, 0, 0.04));
333+
--dropdown-box-shadow: 0 3.2px 7.2px 0 var(--callout-shadow-color, rgba(0, 0, 0, 0.132)),
334+
0 0.6px 1.8px 0 var(--callout-shadow-secondary-color, rgba(0, 0, 0, 0.108));
335+
--dropdown-border-width: 0;
336+
--dropdown-border-radius: 4px;
337+
338+
--border-radius: 2px;
339+
--border-width: 0px;
340+
}

src/button/button.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import { DEFAULT_GITPOD_ENDPOINT, EVENT_CURRENT_URL_CHANGED } from "~constants";
99
import { STORAGE_KEY_ADDRESS, STORAGE_KEY_ALWAYS_OPTIONS, STORAGE_KEY_NEW_TAB } from "~storage";
1010

1111
import type { SupportedApplication } from "./button-contributions";
12-
import { BitbucketCaret, CaretForProvider, GitHubCaret } from "./CaretForProvider";
12+
import { CaretForProvider } from "./CaretForProvider";
1313

1414
type Props = {
1515
application: SupportedApplication;
1616
additionalClassNames?: string[];
17+
urlTransformer?: (url: string) => string;
1718
};
18-
export const GitpodButton = ({ application, additionalClassNames }: Props) => {
19+
export const GitpodButton = ({ application, additionalClassNames, urlTransformer }: Props) => {
1920
const [address] = useStorage<string>(STORAGE_KEY_ADDRESS, DEFAULT_GITPOD_ENDPOINT);
2021
const [openInNewTab] = useStorage<boolean>(STORAGE_KEY_NEW_TAB, true);
2122
const [disableAutostart] = useStorage<boolean>(STORAGE_KEY_ALWAYS_OPTIONS, false);
@@ -36,19 +37,19 @@ export const GitpodButton = ({ application, additionalClassNames }: Props) => {
3637
};
3738
}, []);
3839

39-
const actions = useMemo(
40-
() => [
40+
const actions = useMemo(() => {
41+
const parsedHref = !urlTransformer ? currentHref : urlTransformer(currentHref);
42+
return [
4143
{
42-
href: `${address}/?autostart=${!disableAutostart}#${currentHref}`,
44+
href: `${address}/?autostart=${!disableAutostart}#${parsedHref}`,
4345
label: "Open",
4446
},
4547
{
46-
href: `${address}/?autostart=false#${currentHref}`,
48+
href: `${address}/?autostart=false#${parsedHref}`,
4749
label: "Open with options...",
4850
},
49-
],
50-
[address, disableAutostart, currentHref],
51-
);
51+
];
52+
}, [address, disableAutostart, currentHref, urlTransformer]);
5253
const dropdownRef = useRef<HTMLDivElement | null>(null);
5354
const firstActionRef = useRef<HTMLAnchorElement | null>(null);
5455

src/contents/button.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { EVENT_CURRENT_URL_CHANGED } from "~constants";
77
import { GitpodButton } from "../button/button";
88
import { buttonContributions, isSiteSuitable, type ButtonContributionParams } from "../button/button-contributions";
99

10+
// keep in sync with DEFAULT_HOSTS in src/button/button-contributions.ts
1011
export const config: PlasmoCSConfig = {
11-
matches: ["https://github.com/*", "https://gitlab.com/*", "https://bitbucket.org/*"],
12+
matches: ["https://github.com/*", "https://gitlab.com/*", "https://bitbucket.org/*", "https://dev.azure.com/*"],
1213
};
1314

1415
export const getStyle = () => {
@@ -45,6 +46,7 @@ class ButtonContributionManager {
4546
key={containerId}
4647
application={contribution.application}
4748
additionalClassNames={contribution.additionalClassNames}
49+
urlTransformer={contribution.urlTransformer}
4850
/>,
4951
);
5052
}

test/src/button-contributions-copy.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* 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.
44
*/
55

6-
export type SupportedApplication = "github" | "gitlab" | "bitbucket-server" | "bitbucket";
6+
export type SupportedApplication = "github" | "gitlab" | "bitbucket-server" | "bitbucket" | "azure-devops";
77

88
const resolveMetaAppName = (head: HTMLHeadElement): string | undefined => {
99
const metaApplication = head.querySelector("meta[name=application-name]");
@@ -18,16 +18,24 @@ const resolveMetaAppName = (head: HTMLHeadElement): string | undefined => {
1818
return undefined;
1919
};
2020

21+
export const DEFAULT_HOSTS = ["github.com", "gitlab.com", "bitbucket.org", "dev.azure.com"];
22+
2123
/**
2224
* Provides a fast check to see if the current URL is on a supported site.
2325
*/
2426
export const isSiteSuitable = (): boolean => {
27+
const isWhitelistedHost = DEFAULT_HOSTS.some((host) => location.host === host);
28+
if (isWhitelistedHost) {
29+
return true;
30+
}
31+
2532
const appName = resolveMetaAppName(document.head);
2633
if (!appName) {
2734
return false;
2835
}
2936
const allowedApps = ["GitHub", "GitLab", "Bitbucket"];
30-
return allowedApps.includes(appName);
37+
38+
return allowedApps.some((allowedApp) => appName.includes(allowedApp));
3139
};
3240

3341
export interface ButtonContributionParams {
@@ -51,7 +59,7 @@ export interface ButtonContributionParams {
5159
/**
5260
* The element in which the button should be inserted.
5361
*
54-
* This element will be inserted into teh main document and allows for styling within the original page.
62+
* This element will be inserted into the main document and allows for styling within the original page.
5563
*
5664
* The structure looks like this:
5765
*
@@ -98,6 +106,12 @@ export interface ButtonContributionParams {
98106
* the classnames to remove and add.
99107
*/
100108
manipulations?: { element: string; remove?: string; add?: string; style?: Partial<CSSStyleDeclaration> }[];
109+
110+
/**
111+
* A function that can be used to transform the URL that should be opened when the Gitpod button is clicked.
112+
* @returns The transformed URL.
113+
*/
114+
urlTransformer?: (originalURL: string) => string;
101115
}
102116

103117
function createElement(
@@ -113,6 +127,61 @@ function createElement(
113127
}
114128

115129
export const buttonContributions: ButtonContributionParams[] = [
130+
// Azure DevOps
131+
{
132+
id: "ado-repo",
133+
exampleUrls: [
134+
// "https://dev.azure.com/services-azure/_git/project2"
135+
],
136+
selector: "div.repos-files-header-commandbar:nth-child(1)",
137+
containerElement: createElement("div", {}),
138+
application: "azure-devops",
139+
insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-header-command-bar-menu-button"])`,
140+
manipulations: [
141+
{
142+
element: "div.repos-files-header-commandbar.scroll-hidden",
143+
remove: "scroll-hidden",
144+
},
145+
],
146+
urlTransformer(originalUrl) {
147+
const url = new URL(originalUrl);
148+
if (url.pathname.includes("version=GB")) {
149+
return originalUrl;
150+
}
151+
// version=GBdevelop
152+
const branchElement = document.evaluate(
153+
"//div[contains(@class, 'version-dropdown')]//span[contains(@class, 'text-ellipsis')]",
154+
document,
155+
null,
156+
XPathResult.FIRST_ORDERED_NODE_TYPE,
157+
null,
158+
).singleNodeValue;
159+
if (branchElement) {
160+
const branch = branchElement.textContent?.trim();
161+
url.searchParams.set("version", `GB${branch}`);
162+
}
163+
164+
return url.toString();
165+
},
166+
},
167+
{
168+
id: "ado-pr",
169+
exampleUrls: [
170+
// "https://dev.azure.com/services-azure/test-project/_git/repo2/pullrequest/1"
171+
],
172+
selector: ".repos-pr-header > div:nth-child(2) > div:nth-child(1)",
173+
containerElement: createElement("div", {}),
174+
application: "azure-devops",
175+
insertBefore: `div.bolt-header-command-item-button:has(button[id^="__bolt-menu-button-"])`,
176+
},
177+
{
178+
id: "ado-repo-empty",
179+
exampleUrls: [],
180+
selector: "div.clone-with-application",
181+
application: "azure-devops",
182+
containerElement: createElement("div", { marginLeft: "4px", marginRight: "4px" }),
183+
},
184+
116185
// GitLab
117186
{
118187
id: "gl-repo", // also taking care of branches

0 commit comments

Comments
 (0)