Skip to content

Commit

Permalink
Jira Kanban mode (#16)
Browse files Browse the repository at this point in the history
* added mode toggle to Jira client settings (allows Kanban only mode)

* renamed updateCurrentSprint to update

* Added exlude backlog flag to Kanban mode

---------

Co-authored-by: Aaron Buitenwerf <[email protected]>
  • Loading branch information
froznsm and BoxThatBeat authored Mar 6, 2023
1 parent 4def022 commit 9423b9f
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 86 deletions.
20 changes: 10 additions & 10 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,24 @@ export default class AgileTaskNotesPlugin extends Plugin {
await this.loadSettings();

// This creates an icon in the left ribbon for updating boards.
this.addRibbonIcon('dice', 'Update Current Sprint', () => {
this.tfsClientImplementations[this.settings.selectedTfsClient].updateCurrentSprint(this.settings);
new Notice('Updated current sprint successfully!');
this.addRibbonIcon('dice', 'Update TFS Tasks', () => {
this.tfsClientImplementations[this.settings.selectedTfsClient].update(this.settings);
new Notice('Updated current tasks successfully!');
});

this.addCommand({
id: 'aupdate-current-sprint',
name: 'Update Current Sprint',
id: 'update-tfs-tasks',
name: 'Update TFS Tasks',
callback: () => {
this.tfsClientImplementations[this.settings.selectedTfsClient].updateCurrentSprint(this.settings);
new Notice('Updated current sprint successfully!');
this.tfsClientImplementations[this.settings.selectedTfsClient].update(this.settings);
new Notice('Updated current tasks successfully!');
}
});

this.addSettingTab(new AgileTaskNotesPluginSettingTab(this.app, this));

if (this.settings.intervalMinutes > 0) {
this.registerInterval(window.setInterval(() => this.tfsClientImplementations[this.settings.selectedTfsClient].updateCurrentSprint(this.settings), this.settings.intervalMinutes * 60000));
this.registerInterval(window.setInterval(() => this.tfsClientImplementations[this.settings.selectedTfsClient].update(this.settings), this.settings.intervalMinutes * 60000));
}
}

Expand All @@ -74,7 +74,7 @@ export default class AgileTaskNotesPlugin extends Plugin {
}
}

class AgileTaskNotesPluginSettingTab extends PluginSettingTab {
export class AgileTaskNotesPluginSettingTab extends PluginSettingTab {
plugin: AgileTaskNotesPlugin;

constructor(app: App, plugin: AgileTaskNotesPlugin) {
Expand Down Expand Up @@ -102,7 +102,7 @@ class AgileTaskNotesPluginSettingTab extends PluginSettingTab {
});
});

plugin.tfsClientImplementations[plugin.settings.selectedTfsClient].setupSettings(containerEl, plugin);
plugin.tfsClientImplementations[plugin.settings.selectedTfsClient].setupSettings(containerEl, plugin, this);

containerEl.createEl('h2', {text: 'Vault Settings'});

Expand Down
8 changes: 4 additions & 4 deletions src/Clients/AzureDevopsClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import AgileTaskNotesPlugin, { AgileTaskNotesSettings } from 'main';
import { normalizePath, requestUrl, Setting, TFile, Vault } from 'obsidian';
import AgileTaskNotesPlugin, { AgileTaskNotesPluginSettingTab, AgileTaskNotesSettings } from 'main';
import { normalizePath, requestUrl, Setting } from 'obsidian';
import { VaultHelper } from 'src/VaultHelper'
import { ITfsClient } from './ITfsClient';
import { Task } from 'src/Task';
Expand Down Expand Up @@ -30,7 +30,7 @@ export class AzureDevopsClient implements ITfsClient{

clientName: string = 'AzureDevops';

public async updateCurrentSprint(settings: AgileTaskNotesSettings): Promise<void> {
public async update(settings: AgileTaskNotesSettings): Promise<void> {

const encoded64PAT = Buffer.from(`:${settings.azureDevopsSettings.accessToken}`).toString("base64");

Expand Down Expand Up @@ -82,7 +82,7 @@ export class AzureDevopsClient implements ITfsClient{
}
}

public setupSettings(container: HTMLElement, plugin: AgileTaskNotesPlugin): any {
public setupSettings(container: HTMLElement, plugin: AgileTaskNotesPlugin, settingsTab: AgileTaskNotesPluginSettingTab): any {
container.createEl('h2', {text: 'AzureDevops Remote Repo Settings'});

new Setting(container)
Expand Down
6 changes: 3 additions & 3 deletions src/Clients/ITfsClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import AgileTaskNotesPlugin from "main";
import AgileTaskNotesPlugin, { AgileTaskNotesPluginSettingTab } from "main";

/**
* An interface describing a TFS backend implementation
Expand All @@ -16,13 +16,13 @@ export interface ITfsClient {
* @param settings - The plugin settings
* @public
*/
updateCurrentSprint(settings: any): Promise<void>;
update(settings: any): Promise<void>;

/**
* Creates all the required UI elements for this client's settings
* @param container - The HTML container to build off of
* @param plugin - The plugin itself
* @public
*/
setupSettings(container: HTMLElement, plugin: AgileTaskNotesPlugin): any;
setupSettings(container: HTMLElement, plugin: AgileTaskNotesPlugin, settingsTab: AgileTaskNotesPluginSettingTab): any;
}
228 changes: 174 additions & 54 deletions src/Clients/JiraClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import AgileTaskNotesPlugin, { AgileTaskNotesSettings } from 'main';
import AgileTaskNotesPlugin, { AgileTaskNotesPluginSettingTab, AgileTaskNotesSettings } from 'main';
import { normalizePath, requestUrl, Setting, TFile } from 'obsidian';
import { Task } from 'src/Task';
import { VaultHelper } from 'src/VaultHelper'
Expand All @@ -11,7 +11,9 @@ export interface JiraSettings {
authmode: string,
apiToken: string,
boardId: string,
useSprintName: boolean
useSprintName: boolean,
mode: string,
excludeBacklog: boolean
}

export const JIRA_DEFAULT_SETTINGS: JiraSettings = {
Expand All @@ -22,73 +24,165 @@ export const JIRA_DEFAULT_SETTINGS: JiraSettings = {
apiToken: '',
boardId: '',
useSprintName: true,
mode: 'sprints',
excludeBacklog: false
}

export class JiraClient implements ITfsClient{

clientName: string = 'Jira';

public async updateCurrentSprint(settings: AgileTaskNotesSettings): Promise<void> {
public async update(settings: AgileTaskNotesSettings): Promise<void> {

var headers = {
"Authorization": '',
"Content-Type": "application/json"
}
if(settings.jiraSettings.authmode == 'basic') {
const encoded64Key = Buffer.from(`${settings.jiraSettings.email}:${settings.jiraSettings.apiToken}`).toString("base64");
headers.Authorization = `Basic ${encoded64Key}`
headers.Authorization = `Basic ${encoded64Key}`;
} else if(settings.jiraSettings.authmode = 'bearer') {
headers.Authorization = `Bearer ${settings.jiraSettings.apiToken}`
headers.Authorization = `Bearer ${settings.jiraSettings.apiToken}`;
}

const BaseURL = `https://${settings.jiraSettings.baseUrl}/rest/agile/1.0`;

try {
const sprintsResponse = await requestUrl({ method: 'GET', headers: headers, url: `${BaseURL}/board/${settings.jiraSettings.boardId}/sprint?state=active` })
const currentSprintId = sprintsResponse.json.values[0].id
const currentSprintName = sprintsResponse.json.values[0].name
.replace(/Sprint/, '')
.replace(/Board/, '')
.replace(/^\s+|\s+$/g, '')
.replace(/[^a-zA-Z0-9 -]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')

const sprintIdentifier = settings.jiraSettings.useSprintName ? currentSprintName : currentSprintId
const issuesResponse = await requestUrl({ method: 'GET', headers: headers, url: `${BaseURL}/board/${settings.jiraSettings.boardId}/sprint/${currentSprintId}/issue?jql=assignee=\"${settings.jiraSettings.name}\"` });

const assignedIssuesInSprint = issuesResponse.json.issues;

const normalizedFolderPath = normalizePath(settings.targetFolder + '/sprint-' + sprintIdentifier);

// Ensure folder structure created
VaultHelper.createFolders(normalizedFolderPath);

let tasks:Array<Task> = [];
assignedIssuesInSprint.forEach((task:any) => {
tasks.push(new Task(task.key, task.fields["status"]["statusCategory"]["name"], task.fields["summary"], task.fields["issuetype"]["name"], task.fields["assignee"]["displayName"], `https://${settings.jiraSettings.baseUrl}/browse/${task.key}`, task.fields["description"]));
});

// Create markdown files based on remote task in current sprint
await Promise.all(VaultHelper.createTaskNotes(normalizedFolderPath, tasks, settings.noteTemplate));
if (settings.jiraSettings.mode == 'sprints') {
const sprintsResponse = await requestUrl({ method: 'GET', headers: headers, url: `${BaseURL}/board/${settings.jiraSettings.boardId}/sprint?state=active` });
const currentSprintId = sprintsResponse.json.values[0].id;
const currentSprintName = sprintsResponse.json.values[0].name
.replace(/Sprint/, '')
.replace(/Board/, '')
.replace(/^\s+|\s+$/g, '')
.replace(/[^a-zA-Z0-9 -]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-');

if (settings.createKanban) {
const sprintIdentifier = settings.jiraSettings.useSprintName ? currentSprintName : currentSprintId;
const issuesResponse = await requestUrl(
{
method: 'GET',
headers: headers,
url: `${BaseURL}/board/${settings.jiraSettings.boardId}/sprint/${currentSprintId}/issue?jql=assignee=\"${settings.jiraSettings.name}\"&maxResults=1000`
}
);

const assignedIssuesInSprint = issuesResponse.json.issues;

const normalizedFolderPath = normalizePath(settings.targetFolder + '/sprint-' + sprintIdentifier);

// Ensure folder structure created
VaultHelper.createFolders(normalizedFolderPath);

let tasks:Array<Task> = [];
assignedIssuesInSprint.forEach((task:any) => {
tasks.push(new Task(
task.key,
task.fields["status"]["name"],
task.fields["summary"],
task.fields["issuetype"]["name"],
task.fields["assignee"]["displayName"],
`https://${settings.jiraSettings.baseUrl}/browse/${task.key}`,
task.fields["description"])
);
});

// Create markdown files based on remote task in current sprint
await Promise.all(VaultHelper.createTaskNotes(normalizedFolderPath, tasks, settings.noteTemplate));

// Get the column names from the Jira board
const boardConfigResponse = await requestUrl({ method: 'GET', headers: headers, url: `${BaseURL}/board/${settings.jiraSettings.boardId}/configuration` })
if (settings.createKanban) {

// Get the column names from the Jira board
const boardConfigResponse = await requestUrl(
{
method: 'GET',
headers: headers,
url: `${BaseURL}/board/${settings.jiraSettings.boardId}/configuration`
}
);
const columnIds = boardConfigResponse.json.columnConfig.columns.map((column:any) => column.name);

const columnIds = boardConfigResponse.json.columnConfig.columns.map((column:any) => column.name);
await VaultHelper.createKanbanBoard(normalizedFolderPath, tasks, columnIds, sprintIdentifier);
}

// Create or replace Kanban board of current sprint
await VaultHelper.createKanbanBoard(normalizedFolderPath, tasks, columnIds, sprintIdentifier);
}
} else if(settings.jiraSettings.mode == 'kanban') {

const completedFolder = settings.targetFolder + '/Completed/';
const normalizedBaseFolderPath = normalizePath(settings.targetFolder);
const normalizedCompletedfolderPath = normalizePath(completedFolder);

// Ensure folder structures created
VaultHelper.createFoldersFromList([normalizedBaseFolderPath, normalizedCompletedfolderPath]);

const issuesResponse = await requestUrl(
{
method: 'GET',
headers: headers,
url: `${BaseURL}/board/${settings.jiraSettings.boardId}/issue?jql=assignee=\"${settings.jiraSettings.name}\"&maxResults=1000`
}
);

const assignedIssues = issuesResponse.json.issues;

let activeTasks: Array<Task> = [];
let completedTasks: Array<Task> = [];

assignedIssues.forEach((task:any) => {
if (!settings.jiraSettings.excludeBacklog || settings.jiraSettings.excludeBacklog && task.fields["status"]["name"] !== 'Backlog') {
let taskObj = new Task(
task.key,
task.fields["status"]["name"],
task.fields["summary"],
task.fields["issuetype"]["name"],
task.fields["assignee"]["displayName"],
`https://${settings.jiraSettings.baseUrl}/browse/${task.key}`,
task.fields["description"]
);

if (task.fields["resolution"] != null) {
completedTasks.push(taskObj);
} else {
activeTasks.push(taskObj);
}
}
});

// Create markdown files
await Promise.all(VaultHelper.createTaskNotes(normalizedBaseFolderPath, activeTasks, settings.noteTemplate));
await Promise.all(VaultHelper.createTaskNotes(normalizedCompletedfolderPath, completedTasks, settings.noteTemplate));

// Move pre-existing notes that became resolved state into the Completed folder and vise versa
const completedTaskNoteFiles = completedTasks.map(task => VaultHelper.getFileByTaskId(settings.targetFolder, task.id)).filter((file): file is TFile => !!file);
completedTaskNoteFiles.forEach(file => app.vault.rename(file, normalizePath(completedFolder + file.name)));
const activeTaskNoteFiles = activeTasks.map(task => VaultHelper.getFileByTaskId(settings.targetFolder, task.id)).filter((file): file is TFile => !!file);
activeTaskNoteFiles.forEach(file => app.vault.rename(file, normalizePath(settings.targetFolder + '/' + file.name)));

if (settings.createKanban) {

// Get the column names from the Jira board
const boardConfigResponse = await requestUrl(
{
method: 'GET',
headers: headers,
url: `${BaseURL}/board/${settings.jiraSettings.boardId}/configuration`
}
);
var columnIds = boardConfigResponse.json.columnConfig.columns.map((column:any) => column.name);

if (settings.jiraSettings.excludeBacklog) {
columnIds = columnIds.filter((columnName:string) => columnName !== 'Backlog');
}

await VaultHelper.createKanbanBoard(normalizedBaseFolderPath, activeTasks.concat(completedTasks), columnIds, settings.jiraSettings.boardId);
}
}
} catch(e) {
VaultHelper.logError(e);
}
}

public setupSettings(container: HTMLElement, plugin: AgileTaskNotesPlugin): any {
public setupSettings(container: HTMLElement, plugin: AgileTaskNotesPlugin, settingsTab: AgileTaskNotesPluginSettingTab): any {
container.createEl('h2', {text: 'Jira Remote Repo Settings'});

new Setting(container)
Expand Down Expand Up @@ -149,24 +243,50 @@ export class JiraClient implements ITfsClient{
}));

new Setting(container)
.setName('Use Sprint Name (rather than id)')
.setDesc("Uses the Sprint's human assigned name")
.addToggle(text => text
.setValue(plugin.settings.jiraSettings.useSprintName)
.onChange(async (value) => {
plugin.settings.jiraSettings.useSprintName = value;
await plugin.saveSettings();
}));
.setName('Board ID')
.setDesc('The ID of your Scrum board (the number in the URL when viewing scrum board in browser) ')
.addText(text => text
.setPlaceholder('Enter Board ID')
.setValue(plugin.settings.jiraSettings.boardId)
.onChange(async (value) => {
plugin.settings.jiraSettings.boardId = value;
await plugin.saveSettings();
}));

new Setting(container)
.setName('Board ID')
.setDesc('The ID of your Scrum board (the number in the URL when viewing scrum board in browser) ')
.addText(text => text
.setPlaceholder('Enter Board ID')
.setValue(plugin.settings.jiraSettings.boardId)
.setName('Mode')
.setDesc('Set the mode corresponding to how you use Jira')
.addDropdown((dropdown) => {
dropdown.addOption("sprints", "Sprints");
dropdown.addOption("kanban", "Kanban");
dropdown.setValue(plugin.settings.jiraSettings.mode)
.onChange(async (value) => {
plugin.settings.jiraSettings.mode = value;
await plugin.saveSettings();
settingsTab.display() // Refresh settings to update view
});
});

if (plugin.settings.jiraSettings.mode === 'sprints') {
new Setting(container)
.setName('Use Sprint Name (rather than id)')
.setDesc("Uses the Sprint's human assigned name")
.addToggle(text => text
.setValue(plugin.settings.jiraSettings.useSprintName)
.onChange(async (value) => {
plugin.settings.jiraSettings.useSprintName = value;
await plugin.saveSettings();
}));
} else if (plugin.settings.jiraSettings.mode === 'kanban') {
new Setting(container)
.setName('Exclude Backlog')
.setDesc('Enable to prevent creation of issues from the backlog')
.addToggle(toggle => toggle
.setValue(plugin.settings.jiraSettings.excludeBacklog)
.onChange(async (value) => {
plugin.settings.jiraSettings.boardId = value;
plugin.settings.jiraSettings.excludeBacklog = value
await plugin.saveSettings();
}));
}
}
}
Loading

0 comments on commit 9423b9f

Please sign in to comment.