Skip to content

Commit

Permalink
Merge pull request #20 from BlueBasher/feature/select-branch
Browse files Browse the repository at this point in the history
Allow select of source branch
  • Loading branch information
BlueBasher authored Jan 15, 2024
2 parents 4c84a87 + 74019fe commit 632b090
Show file tree
Hide file tree
Showing 11 changed files with 394 additions and 234 deletions.
2 changes: 1 addition & 1 deletion configs/dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"name": "Easy Branch Creator Dev",
"id": "bluebasher-easy-branch-creator-dev",
"baseUri": "https://localhost:3000",
"version": "0.0.0.10",
"version": "0.0.0.11",
"public": false
}
18 changes: 10 additions & 8 deletions src/branch-creator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ import SettingsDocument from "./settingsDocument";

export class BranchCreator {

public async createBranch(workItemId: number, repositoryId: string, repositoryName: string, project: IProjectInfo, gitBaseUrl: string): Promise<void> {
public async createBranch(workItemId: number, repositoryId: string, sourceBranchName: string, project: IProjectInfo, gitBaseUrl: string): Promise<void> {
const navigationService = await SDK.getService<IHostNavigationService>(CommonServiceIds.HostNavigationService);
const globalMessagesSvc = await SDK.getService<IGlobalMessagesService>(CommonServiceIds.GlobalMessagesService);
const gitRestClient = getClient(GitRestClient);
const workItemTrackingRestClient = getClient(WorkItemTrackingRestClient);
const storageService = new StorageService();
const settingsDocument = await storageService.getSettings();

const repository = await gitRestClient.getRepository(repositoryId, project.name);

const branchName = await this.getBranchName(workItemTrackingRestClient, settingsDocument, workItemId, project.name);
const branchUrl = `${gitBaseUrl}/${repositoryName}?version=GB${encodeURI(branchName)}`;
const branchUrl = `${gitBaseUrl}/${repository.name}?version=GB${encodeURI(branchName)}`;

if (await this.branchExists(gitRestClient, repositoryId, project.name, branchName)) {
console.info(`Branch ${branchName} aready exists in repository ${repositoryName}`);
console.info(`Branch ${branchName} aready exists in repository ${repository.name}`);

globalMessagesSvc.addToast({
duration: 3000,
Expand All @@ -34,16 +36,16 @@ export class BranchCreator {
return;
}

const defaultBranch = (await gitRestClient.getBranches(repositoryId, project.name)).find((x) => x.isBaseVersion);
if (!defaultBranch) {
console.warn(`Default branch ${branchName} not found`);
const branch = (await gitRestClient.getBranches(repositoryId, project.name)).find((x) => x.name === sourceBranchName);
if (!branch) {
console.warn(`Branch ${sourceBranchName} not found`);
return;
}

await this.createRef(gitRestClient, repositoryId, defaultBranch.commit.commitId, branchName);
await this.createRef(gitRestClient, repositoryId, branch.commit.commitId, branchName);
await this.linkBranchToWorkItem(workItemTrackingRestClient, project.id, repositoryId, workItemId, branchName);
await this.updateWorkItemState(workItemTrackingRestClient, settingsDocument, project.id, workItemId);
console.log(`Branch ${branchName} created in repository ${repositoryName}`);
console.log(`Branch ${branchName} created in repository ${repository.name}`);

globalMessagesSvc.addToast({
duration: 3000,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<div id="root"></div>
<script type="text/javascript" src="select-branch-details.js" charset="utf-8"></script>
<script type="text/javascript" src="branch-details-form.js" charset="utf-8"></script>
</body>
</html>
35 changes: 35 additions & 0 deletions src/branch-details-form/branch-details-form.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@import "node_modules/azure-devops-ui/Core/_platformCommon.scss";


#root {
height: 100%;
width: 100%;
display: flex;
}

.branch-details-form {
font-size: $fontSizeM;
}

.ms-Icon--GitLogo {
color: rgb(240, 81, 41);
margin-right: 4px;
}

.ms-Icon--BranchMerge {
margin-right: 4px;
}

.branch-details-form .branchNames {
height: 60px;
}

.branch-details-form .branchNames ul {
margin-block-start: 0px;
margin-block-end: 0px;
}

.branch-details-form .branchNames li {
list-style-type: disc;
list-style-position: inside;
}
137 changes: 137 additions & 0 deletions src/branch-details-form/branch-details-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import "./branch-details-form.scss";

import * as React from "react";
import * as ReactDOM from "react-dom";
import * as SDK from "azure-devops-extension-sdk";
import { getClient } from "azure-devops-extension-api";

import { Button } from "azure-devops-ui/Button";
import { ButtonGroup } from "azure-devops-ui/ButtonGroup";
import { WorkItemTrackingRestClient } from "azure-devops-extension-api/WorkItemTracking";
import { BranchCreator } from "../branch-creator";
import { StorageService } from "../storage-service";
import { RepositorySelect } from "../repository-select/repository-select";
import { BranchSelect } from "../branch-select/branch-select";

export interface ISelectBranchDetailsResult {
repositoryId: string;
sourceBranchName: string;
}

interface ISelectBranchDetailsState {
projectName?: string;
workItems: number[];
selectedRepositoryId?: string;
sourceBranchName?: string;
ready: boolean;
branchNames: string[];
}

class BranchDetailsForm extends React.Component<{}, ISelectBranchDetailsState> {
constructor(props: {}) {
super(props);
this.state = { workItems: [], branchNames: [], ready: false };
}

public componentDidMount() {
SDK.init();

SDK.ready().then(async () => {
const config = SDK.getConfiguration();
if (config.dialog) {
SDK.resize(undefined, 275);
}

this.setState({ projectName: config.projectName, workItems: config.workItems, selectedRepositoryId: config.initialValue, ready: false, branchNames: [] });

await this.setBranchNames();

this.setState(prevState => ({
...prevState,
ready: true
}));
});
}

public render(): JSX.Element {
return (
<div className="branch-details-form flex-column flex-grow rhythm-vertical-16">
<div className="flex-grow">
<RepositorySelect
projectName={this.state.projectName}
onRepositoryChange={(newRepositoryId) => this.onRepositoryChange(newRepositoryId)} />
<BranchSelect
projectName={this.state.projectName}
repositoryId={this.state.selectedRepositoryId}
onBranchChange={(newBranchName) => this.onSourceBranchNameChange(newBranchName)} />
<p>Branch Name</p>
<div className="branchNames flex-column scroll-auto">
<div>
<ul>
{this.state.branchNames.map(b => <li key={b}>{b}</li>)}
</ul>
</div>
</div>
</div>
<ButtonGroup className="branch-details-form-button-bar ">
<Button
disabled={!this.state.selectedRepositoryId}
primary={true}
text="Create Branch"
onClick={() => this.close(this.state.selectedRepositoryId && this.state.sourceBranchName ? {
repositoryId: this.state.selectedRepositoryId,
sourceBranchName: this.state.sourceBranchName
} : undefined)}
/>
<Button
text="Cancel"
onClick={() => this.close(undefined)}
/>
</ButtonGroup>
</div>
);
}

private close(result: ISelectBranchDetailsResult | undefined) {
const config = SDK.getConfiguration();
if (config.dialog) {
config.dialog.close(result);
}
}

private onRepositoryChange(newRepositoryId?: string | undefined): void {
this.setState(prevState => ({
...prevState,
selectedRepositoryId: newRepositoryId
}));
}

private onSourceBranchNameChange(newBranchName?: string | undefined): void {
this.setState(prevState => ({
...prevState,
sourceBranchName: newBranchName
}));
}

private async setBranchNames() {
if (this.state.projectName) {
const workItemTrackingRestClient = getClient(WorkItemTrackingRestClient);
const storageService = new StorageService();
const settingsDocument = await storageService.getSettings();

const branchCreator = new BranchCreator();
let branchNames: string[] = [];
for await (const workItemId of this.state.workItems) {
const branchName = await branchCreator.getBranchName(workItemTrackingRestClient, settingsDocument, workItemId, this.state.projectName!);
branchNames.push(branchName);
}

this.setState(prevState => ({
...prevState,
branchNames: branchNames
}));
}
}
}

ReactDOM.render(<BranchDetailsForm />, document.getElementById("root"));
106 changes: 106 additions & 0 deletions src/branch-select/branch-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as React from "react";
import { getClient } from "azure-devops-extension-api";
import { GitRestClient } from "azure-devops-extension-api/Git";

import { EditableDropdown } from "azure-devops-ui/EditableDropdown";
import { DropdownSelection } from "azure-devops-ui/Utilities/DropdownSelection";
import { ObservableArray } from "azure-devops-ui/Core/Observable";
import { IListBoxItem } from "azure-devops-ui/ListBox";
import { ITableColumn, SimpleTableCell } from "azure-devops-ui/Table";
import { Icon } from "azure-devops-ui/Icon";

export interface IBranchSelectProps {
projectName?: string;
repositoryId?: string;
onBranchChange: (newBranchName?: string) => void;
}

interface IBranchSelectState {
ready: boolean;
}

export class BranchSelect extends React.Component<IBranchSelectProps, IBranchSelectState> {
private branches = new ObservableArray<IListBoxItem<string>>();
private branchSelection = new DropdownSelection();

constructor(props: { onBranchChange: (newBranchName?: string) => void }) {
super(props);
this.state = { ready: false };
}

public async componentDidMount() {
await this.loadBranches();

this.setState(prevState => ({
...prevState,
ready: true
}));
}

public async componentDidUpdate(prevProps: IBranchSelectProps) {
if (prevProps.repositoryId !== this.props.repositoryId) {
await this.loadBranches();
}
}

public render(): JSX.Element {
return (
<div className="flex-column">
<label className="bolt-formitem-label body-m">Based on</label>
<EditableDropdown<string>
disabled={!this.state.ready}
items={this.branches}
selection={this.branchSelection}
onValueChange={(item?: IListBoxItem<string>) => {
this.setSelectedBranchName(item?.data);
}}
renderItem={(rowIndex: number, columnIndex: number, tableColumn: ITableColumn<IListBoxItem<string>>, tableItem: IListBoxItem<string>) => {
return (
<SimpleTableCell
columnIndex={columnIndex}
key={tableItem.id}
tableColumn={tableColumn}
>
<div className="bolt-list-box-cell-container"
>
<span className="bolt-list-cell-text">
<span className="text-ellipsis body-m">
<Icon iconName="BranchMerge" />
{tableItem.text}
</span>
</span>
</div>
</SimpleTableCell>
);
}}
/>
</div>
);
}

private async loadBranches() {
if (!!!this.props.repositoryId || !!!this.props.projectName) {
return;
}

const gitRestClient = getClient(GitRestClient);
const branches = await gitRestClient.getBranches(this.props.repositoryId, this.props.projectName);
this.branches.removeAll();
this.branches.push(...branches.map(t => { return { id: t.name, data: t.name, text: t.name } }));

if (this.branches.length > 0) {
const repository = await gitRestClient.getRepository(this.props.repositoryId, this.props.projectName);
let branchIndex = repository ? this.branches.value.findIndex((x) => x.data === repository.defaultBranch.replace('refs/heads/', '')) : 0;
if (branchIndex === -1) {
branchIndex = 0;
}

this.setSelectedBranchName(branches[branchIndex].name);
this.branchSelection.select(branchIndex);
}
}

private setSelectedBranchName(branchName?: string) {
this.props.onBranchChange(branchName);
}
}
8 changes: 4 additions & 4 deletions src/create-branch/create-branch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ILocationService, CommonServiceIds, IProjectPageService, IProjectInfo,
import { CoreRestClient } from "azure-devops-extension-api/Core";

import { BranchCreator } from "../branch-creator";
import { ISelectBranchDetailsResult } from "../select-branch-details/select-branch-details";
import { ISelectBranchDetailsResult } from "../branch-details-form/branch-details-form";

function createBranchFromWorkItem() {
"use strict";
Expand All @@ -24,17 +24,17 @@ function createBranchFromWorkItem() {
const branchCreator = new BranchCreator();
const dialogService = await SDK.getService<IHostPageLayoutService>(CommonServiceIds.HostPageLayoutService);
const workItems = getWorkItemIds(actionContext);
dialogService.openCustomDialog<ISelectBranchDetailsResult | undefined>(SDK.getExtensionContext().id + ".select-branch-details", {
dialogService.openCustomDialog<ISelectBranchDetailsResult | undefined>(SDK.getExtensionContext().id + ".branch-details-form", {
title: "Select Branch Details",
lightDismiss: false,
configuration: {
projectName: project.name,
workItems: workItems
},
onClose: (result: ISelectBranchDetailsResult | undefined) => {
if (result !== undefined && result.repositoryId !== undefined && result.repositoryName !== undefined) {
if (result !== undefined) {
workItems.forEach((id: number) => {
branchCreator.createBranch(id, result.repositoryId!, result.repositoryName!, project, gitBaseUrl);
branchCreator.createBranch(id, result.repositoryId, result.sourceBranchName, project, gitBaseUrl);
});
}
}
Expand Down
Loading

0 comments on commit 632b090

Please sign in to comment.