diff --git a/CHANGELOG.md b/CHANGELOG.md index e976ccf..64e0821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 0.15.2 + +### Bug Fixes + +- removed copy-paste lib and use new clipboard API +- solved issue in application state when there are multiple VsCode instances + ## 0.15.1 ### Bug Fixes diff --git a/package.json b/package.json index 41e4d79..65e7649 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "jira-plugin", "displayName": "Jira Plugin", "description": "Jira integration for vscode", - "version": "0.15.1", + "version": "0.15.2", "publisher": "gioboa", "icon": "images/icons/icon.png", "galleryBanner": { @@ -363,7 +363,6 @@ } }, "devDependencies": { - "@types/copy-paste": "^1.1.30", "@types/mocha": "^5.2.6", "@types/node": "^11.9.5", "husky": "^1.3.1", @@ -374,7 +373,6 @@ "vscode": "^1.1.30" }, "dependencies": { - "copy-paste": "^1.3.0", "jira-connector": "^2.10.0" } } diff --git a/src/commands/change-issue-assignee.ts b/src/commands/change-issue-assignee.ts index b78868e..da10a5c 100644 --- a/src/commands/change-issue-assignee.ts +++ b/src/commands/change-issue-assignee.ts @@ -1,23 +1,22 @@ import * as vscode from 'vscode'; import { IssueItem } from '../explorer/item/issue-item'; -import state, { canExecuteJiraAPI, isWorkingIssue } from '../store/state'; -import { selectValues, logger } from '../services'; +import { logger, selectValues, store } from '../services'; export default async function changeIssueAssigneeCommand(issueItem: IssueItem): Promise { try { - if (issueItem && issueItem.issue && canExecuteJiraAPI()) { + if (issueItem && issueItem.issue && store.canExecuteJiraAPI()) { let issue = issueItem.issue; // verify if it's the current working issue - if (!isWorkingIssue(issue.key)) { + if (!store.isWorkingIssue(issue.key)) { let assignee = await selectValues.selectAssignee(false, false, true, undefined); if (!!assignee) { // call Jira API - const res = await state.jira.setAssignIssue({ issueKey: issue.key, assignee: assignee }); + const res = await store.state.jira.setAssignIssue({ issueKey: issue.key, assignee: assignee }); await vscode.commands.executeCommand('jira-plugin.refresh'); } } } else { - if (canExecuteJiraAPI()) { + if (store.canExecuteJiraAPI()) { logger.printErrorMessageInOutputAndShowAlert('Use this command from Jira Plugin EXPLORER'); } } diff --git a/src/commands/change-issue-status.ts b/src/commands/change-issue-status.ts index c4151fb..79cebf6 100644 --- a/src/commands/change-issue-status.ts +++ b/src/commands/change-issue-status.ts @@ -1,18 +1,17 @@ import * as vscode from 'vscode'; import { IssueItem } from '../explorer/item/issue-item'; -import { logger, selectValues } from '../services'; -import state, { canExecuteJiraAPI, isWorkingIssue } from '../store/state'; +import { logger, selectValues, store } from '../services'; export default async function changeIssueStatusCommand(issueItem: IssueItem): Promise { try { - if (issueItem && issueItem.issue && canExecuteJiraAPI()) { + if (issueItem && issueItem.issue && store.canExecuteJiraAPI()) { let issue = issueItem.issue; // verify if it's the current working issue - if (!isWorkingIssue(issue.key)) { + if (!store.isWorkingIssue(issue.key)) { const newTransitionId = await selectValues.selectTransition(issue.key); if (newTransitionId) { // call Jira API - const result = await state.jira.setTransition({ + const result = await store.state.jira.setTransition({ issueKey: issue.key, transition: { transition: { @@ -24,7 +23,7 @@ export default async function changeIssueStatusCommand(issueItem: IssueItem): Pr } } } else { - if (canExecuteJiraAPI()) { + if (store.canExecuteJiraAPI()) { logger.printErrorMessageInOutputAndShowAlert('Use this command from Jira Plugin EXPLORER'); } } diff --git a/src/commands/create-issue.ts b/src/commands/create-issue.ts index 5149f78..22e9dfe 100644 --- a/src/commands/create-issue.ts +++ b/src/commands/create-issue.ts @@ -1,16 +1,15 @@ import * as vscode from 'vscode'; import { IssueItem } from '../explorer/item/issue-item'; -import { configuration, createIssue, logger, selectValues } from '../services'; +import { configuration, createIssue, logger, selectValues, store } from '../services'; import { IPickValue } from '../services/configuration.model'; import { CONFIG } from '../shared/constants'; -import state, { verifyCurrentProject } from '../store/state'; export default async function createIssueCommand(issueItem: IssueItem): Promise { const project = configuration.get(CONFIG.WORKING_PROJECT); - if (verifyCurrentProject(project)) { + if (store.verifyCurrentProject(project)) { try { // first of first we decide the type of the ticket - const availableTypes = await state.jira.getAllIssueTypesWithFields(project); + const availableTypes = await store.state.jira.getAllIssueTypesWithFields(project); if (!!availableTypes) { // here the user select which type of issue create createIssue.init(await selectValues.selectIssueType(false, availableTypes)); diff --git a/src/commands/favourites-filters.ts b/src/commands/favourites-filters.ts index 6cc9794..df9cc87 100644 --- a/src/commands/favourites-filters.ts +++ b/src/commands/favourites-filters.ts @@ -1,5 +1,5 @@ +import { logger, selectValues } from '../services'; import { SEARCH_MODE } from '../shared/constants'; -import { selectValues, logger } from '../services'; export default async function favouritesFiltersCommand(): Promise { try { diff --git a/src/commands/issue-add-comment.ts b/src/commands/issue-add-comment.ts index 53153b0..b71bc6b 100644 --- a/src/commands/issue-add-comment.ts +++ b/src/commands/issue-add-comment.ts @@ -1,12 +1,11 @@ import * as vscode from 'vscode'; import { IssueItem } from '../explorer/item/issue-item'; +import { configuration, logger, selectValues, store } from '../services'; import { CONFIG } from '../shared/constants'; -import state, { canExecuteJiraAPI } from '../store/state'; -import { selectValues, logger, configuration } from '../services'; export default async function issueAddCommentCommand(issueItem: IssueItem): Promise { try { - if (issueItem && issueItem.issue && canExecuteJiraAPI()) { + if (issueItem && issueItem.issue && store.canExecuteJiraAPI()) { let issue = issueItem.issue; let text = await vscode.window.showInputBox({ ignoreFocusOut: true, @@ -25,7 +24,7 @@ export default async function issueAddCommentCommand(issueItem: IssueItem): Prom } } // call Jira API - const response = await state.jira.addNewComment({ issueKey: issue.key, comment: { body: text } }); + const response = await store.state.jira.addNewComment({ issueKey: issue.key, comment: { body: text } }); await vscode.commands.executeCommand('jira-plugin.refresh'); // modal const action = await vscode.window.showInformationMessage('Comment created', 'Open in browser'); @@ -40,7 +39,7 @@ export default async function issueAddCommentCommand(issueItem: IssueItem): Prom } } } else { - if (canExecuteJiraAPI()) { + if (store.canExecuteJiraAPI()) { logger.printErrorMessageInOutputAndShowAlert('Use this command from Jira Plugin EXPLORER'); } } diff --git a/src/commands/issue-add-worklog.ts b/src/commands/issue-add-worklog.ts index b44b4a5..5dc4f1a 100644 --- a/src/commands/issue-add-worklog.ts +++ b/src/commands/issue-add-worklog.ts @@ -1,13 +1,12 @@ +import { logger, store } from '../services'; import { NO_WORKING_ISSUE } from '../shared/constants'; -import state, { canExecuteJiraAPI } from '../store/state'; -import { logger } from '../services'; export default async function issueAddWorklogCommand(issueKey: string, timeSpentSeconds: number, comment: string): Promise { try { if (issueKey !== NO_WORKING_ISSUE.key) { - if (canExecuteJiraAPI()) { + if (store.canExecuteJiraAPI()) { // call Jira API - const response = await state.jira.addWorkLog({ + const response = await store.state.jira.addWorkLog({ issueKey: issueKey, worklog: { timeSpentSeconds: Math.ceil(timeSpentSeconds / 60) * 60, comment } }); diff --git a/src/commands/set-working-issue.ts b/src/commands/set-working-issue.ts index 2eab5f4..2da51ef 100644 --- a/src/commands/set-working-issue.ts +++ b/src/commands/set-working-issue.ts @@ -1,9 +1,8 @@ import * as vscode from 'vscode'; -import { IIssue, IWorkingIssue } from '../services/http.model'; import NoWorkingIssuePick from '../picks/no-working-issue-pick'; -import { configuration, selectValues, statusBar, utilities } from '../services'; +import { configuration, selectValues, statusBar, store, utilities } from '../services'; +import { IIssue, IWorkingIssue } from '../services/http.model'; import { ACTIONS, CONFIG, NO_WORKING_ISSUE, TRACKING_TIME_MODE } from '../shared/constants'; -import state, { changeStateWorkingIssue } from '../store/state'; export default async function setWorkingIssueCommand(storedWorkingIssue: IWorkingIssue, preloadedIssue: IIssue): Promise { // run it's called from status bar there is a working issue in the storage @@ -13,19 +12,21 @@ export default async function setWorkingIssueCommand(storedWorkingIssue: IWorkin const issue = workingIssues.find((issue: IIssue) => issue.key === storedWorkingIssue.issue.key); if (!!issue) { // YES - restart tracking time for the stored working issue - state.workingIssue = storedWorkingIssue; + store.state.workingIssue = storedWorkingIssue; vscode.window.showInformationMessage( - `PENDING WORKING ISSUE: ${state.workingIssue.issue.key} | timeSpent: ${utilities.secondsToHHMMSS(state.workingIssue.trackingTime)}` + `PENDING WORKING ISSUE: ${store.state.workingIssue.issue.key} | timeSpent: ${utilities.secondsToHHMMSS( + store.state.workingIssue.trackingTime + )}` ); // set stored working issue - changeStateWorkingIssue(state.workingIssue.issue, state.workingIssue.trackingTime); + store.changeStateWorkingIssue(store.state.workingIssue.issue, store.state.workingIssue.trackingTime); } else { // NO - set no working issue - changeStateWorkingIssue(new NoWorkingIssuePick().pickValue, 0); + store.changeStateWorkingIssue(new NoWorkingIssuePick().pickValue, 0); } } else { // normal workflow, user must select a working issue - const workingIssue = state.workingIssue || new NoWorkingIssuePick().pickValue; + const workingIssue = store.state.workingIssue || new NoWorkingIssuePick().pickValue; const newIssue = preloadedIssue || (await selectValues.selectChangeWorkingIssue()); if (!!newIssue && newIssue.key !== workingIssue.issue.key) { if ( @@ -55,14 +56,14 @@ export default async function setWorkingIssueCommand(storedWorkingIssue: IWorkin if (action === ACTIONS.YES || action === ACTIONS.YES_WITH_COMMENT) { await vscode.commands.executeCommand( 'jira-plugin.issueAddWorklogCommand', - state.workingIssue.issue.key, - state.workingIssue.trackingTime, + store.state.workingIssue.issue.key, + store.state.workingIssue.trackingTime, comment || '' ); } } // set the new working issue - changeStateWorkingIssue(newIssue, 0); + store.changeStateWorkingIssue(newIssue, 0); } } } diff --git a/src/commands/set-working-project.ts b/src/commands/set-working-project.ts index 7574d9b..d546faf 100644 --- a/src/commands/set-working-project.ts +++ b/src/commands/set-working-project.ts @@ -1,6 +1,5 @@ -import { selectValues } from '../services'; -import { changeStateProject } from '../store/state'; +import { selectValues, store } from '../services'; export default async function setWorkingProjectCommand(): Promise { - changeStateProject(await selectValues.selectProject()); + store.changeStateProject(await selectValues.selectProject()); } diff --git a/src/commands/setup-credentials.ts b/src/commands/setup-credentials.ts index ff0b51c..aff910c 100644 --- a/src/commands/setup-credentials.ts +++ b/src/commands/setup-credentials.ts @@ -1,7 +1,6 @@ import * as vscode from 'vscode'; -import { configuration } from '../services'; +import { configuration, store } from '../services'; import { CONFIG } from '../shared/constants'; -import { connectToJira } from '../store/state'; export default async function setupCredentials(): Promise { const baseUrl = configuration.get(CONFIG.BASE_URL); @@ -41,5 +40,5 @@ export default async function setupCredentials(): Promise { }) ); - await connectToJira(); + await store.connectToJira(); } diff --git a/src/explorer/issues-explorer.ts b/src/explorer/issues-explorer.ts index 1d81851..fcc4dc3 100644 --- a/src/explorer/issues-explorer.ts +++ b/src/explorer/issues-explorer.ts @@ -1,8 +1,7 @@ import * as vscode from 'vscode'; -import { configuration, logger } from '../services'; +import { configuration, logger, store } from '../services'; import { IIssue } from '../services/http.model'; import { CONFIG, GROUP_BY_FIELDS, LOADING } from '../shared/constants'; -import state from '../store/state'; import { DividerItem } from './item/divider-item'; import { FilterInfoItem } from './item/filter-info-item'; import { IssueItem } from './item/issue-item'; @@ -115,7 +114,7 @@ export default class IssuesExplorer implements vscode.TreeDataProvider 0) { if (issues.some(issue => !issue.fields.hasOwnProperty(this.groupByField.value))) { @@ -148,7 +147,7 @@ export default class IssuesExplorer implements vscode.TreeDataProvider descB ? 1 : 0; }); // add in the firt possition 'filter-info-item' and then the 'divider-item' - items.unshift(new FilterInfoItem(project, state.currentFilter, issues.length), new DividerItem('------')); + items.unshift(new FilterInfoItem(project, store.state.currentSearch.filter, issues.length), new DividerItem('------')); // loop items and insert a separator when field value change this.addSeparators(items, this.groupByField); if (issues.length === configuration.get(CONFIG.NUMBER_ISSUES_IN_LIST)) { @@ -158,11 +157,15 @@ export default class IssuesExplorer implements vscode.TreeDataProvider => { - channel = vscode.window.createOutputChannel(CONFIG_NAME.toUpperCase()); - state.channel = channel; + const channel: vscode.OutputChannel = vscode.window.createOutputChannel(CONFIG_NAME.toUpperCase()); context.subscriptions.push(channel); - state.context = context; + store.state.channel = channel; + store.state.context = context; vscode.window.registerTreeDataProvider('issuesExplorer', issuesExplorer); context.subscriptions.push(statusBar); context.subscriptions.push(gitIntegration); context.subscriptions.push(...commands.register()); // create Jira Instance and try to connect - await connectToJira(); + await store.connectToJira(); }; diff --git a/src/services/configuration.service.ts b/src/services/configuration.service.ts index 5d11c20..66d9307 100644 --- a/src/services/configuration.service.ts +++ b/src/services/configuration.service.ts @@ -1,6 +1,5 @@ import * as vscode from 'vscode'; -import { logger } from '.'; -import { IWorkingIssue } from './http.model'; +import { store } from '.'; import { CONFIG, CONFIG_COUNTER, @@ -9,30 +8,24 @@ import { CREDENTIALS_SEPARATOR, DEFAULT_WORKING_ISSUE_STATUS } from '../shared/constants'; -import state from '../store/state'; import { IConfiguration } from './configuration.model'; +import { IStatus, IWorkingIssue } from './http.model'; export default class ConfigurationService { + // all the plugin settings + private settings: IConfiguration = { ...vscode.workspace.getConfiguration(CONFIG_NAME) }; + public isValid(): boolean { if (!this.settings) { return false; } - const { baseUrl } = this.settings; - const { username, password } = this.credentials; + const { username } = this.settings; + const { password } = this.credentials; return !!(baseUrl && username && password); } - // all the plugin settings - private get settings(): IConfiguration | undefined { - const config: IConfiguration | undefined = vscode.workspace.getConfiguration(CONFIG_NAME); - if (!config) { - logger.printErrorMessageInOutputAndShowAlert('No settings found. Probably an error in vscode'); - } - return config; - } - public get credentials(): { username: string; password: string } { const config = this.settings; const credentials: string = (config && this.globalState.get(`${CONFIG_NAME}:${config.baseUrl}`)) || ''; @@ -46,8 +39,7 @@ export default class ConfigurationService { if (!this.settings) { return fallbackValue; } - - return this.settings.get(entry, fallbackValue); + return this.settings[entry] || fallbackValue; } // used for set only one setting @@ -56,7 +48,9 @@ export default class ConfigurationService { if (entry === CONFIG.BASE_URL && typeof value === 'string') { value = value.replace(/\/$/, ''); } - + // update settings object - Fix issue #97 + (this.settings)[entry] = value; + // update VsCode settings return this.settings && this.settings.update(entry, value, true); } @@ -70,7 +64,7 @@ export default class ConfigurationService { // get inside VS Code local storage the settings public get globalState(): vscode.Memento { - return state.context.globalState; + return store.state.context.globalState; } // set inside VS Code local storage the last working issue @@ -100,11 +94,13 @@ export default class ConfigurationService { return this.globalState.get(`${CONFIG_NAME}:${CONFIG_COUNTER}`); } - public workingIssueStatuses(): string { + public workingIssueStatuses(statuses?: IStatus[]): string { let statusList = (this.get(CONFIG.WORKING_ISSUE_STATUSES) || DEFAULT_WORKING_ISSUE_STATUS) .split(',') .map((status: string) => status.trim()) - .filter((status: string) => state.statuses.some(stateStatus => stateStatus.name.toLowerCase() === status.toLowerCase())); + .filter((status: string) => + (statuses || store.state.statuses).some(stateStatus => stateStatus.name.toLowerCase() === status.toLowerCase()) + ); return statusList && statusList.length > 0 ? statusList.reduce((a: string, b: string) => (a === '' ? a + `'${b}'` : `${a},'${b}'`), '') : `'${DEFAULT_WORKING_ISSUE_STATUS}'`; diff --git a/src/services/create-issue.service.ts b/src/services/create-issue.service.ts index e9a27e2..bc9c0c3 100644 --- a/src/services/create-issue.service.ts +++ b/src/services/create-issue.service.ts @@ -1,9 +1,8 @@ import * as vscode from 'vscode'; -import { configuration, logger } from '.'; +import { configuration, logger, store } from '.'; import openIssueCommand from '../commands/open-issue'; -import { ICreateIssueEpicList, IField, IFieldSchema, IIssue, IIssueType, ILabel } from './http.model'; import { CONFIG, SEARCH_MAX_RESULTS } from '../shared/constants'; -import state from '../store/state'; +import { ICreateIssueEpicList, IField, IFieldSchema, IIssue, IIssueType, ILabel } from './http.model'; export default class CreateIssueService { // this object store all user choices @@ -133,11 +132,11 @@ export default class CreateIssueService { const field = this.getField(fieldName); if (this.isAssigneeOrReporterField(fieldName)) { // assignee autoCompleteUrl sometimes don't work, I use custom one - // this.preloadedListValues[fieldName] = await state.jira.customRequest('GET', field.autoCompleteUrl); - this.preloadedListValues[fieldName] = await state.jira.getAssignees(this.project); + // this.preloadedListValues[fieldName] = await store.state.jira.customRequest('GET', field.autoCompleteUrl); + this.preloadedListValues[fieldName] = await store.state.jira.getAssignees(this.project); } if (this.isEpicLinkFieldSchema(field.schema)) { - const response = await state.jira.getCreateIssueEpics(configuration.get(CONFIG.WORKING_PROJECT), SEARCH_MAX_RESULTS); + const response = await store.state.jira.getCreateIssueEpics(configuration.get(CONFIG.WORKING_PROJECT), SEARCH_MAX_RESULTS); // format issues in standard way if (!!response && !!response.epicLists) { const list: IIssue[] = []; @@ -165,11 +164,11 @@ export default class CreateIssueService { } } if (this.isSprintFieldSchema(field.schema)) { - const response = await state.jira.getSprints(); + const response = await store.state.jira.getSprints(); this.preloadedListValues[fieldName] = response.suggestions || []; } if (this.isLabelsField(fieldName)) { - const response = await state.jira.customRequest('GET', field.autoCompleteUrl); + const response = await store.state.jira.customRequest('GET', field.autoCompleteUrl); this.preloadedListValues[fieldName] = (response.suggestions || []).map((entry: ILabel) => { entry.key = entry.label; entry.description = ''; @@ -177,7 +176,7 @@ export default class CreateIssueService { }); } if (this.isIssuelinksField(fieldName)) { - const response = await state.jira.customRequest('GET', field.autoCompleteUrl); + const response = await store.state.jira.customRequest('GET', field.autoCompleteUrl); for (const [key, value] of Object.entries(response)) { if (value instanceof Array) { if (!!value[0] && !!value[0].issues && value[0].issues instanceof Array) { @@ -187,7 +186,7 @@ export default class CreateIssueService { } if (!!this.preloadedListValues[fieldName]) { // issueLinkedType field - const types = await state.jira.getAvailableLinkIssuesType(); + const types = await store.state.jira.getAvailableLinkIssuesType(); this.preloadedListValues[this.NEW_ISSUE_FIELDS.ISSUE_LINKS_TYPES.field] = types.issueLinkTypes || []; } } @@ -214,7 +213,7 @@ export default class CreateIssueService { if (!this.preloadedListValues[fieldName] && !!field.autoCompleteUrl) { try { // here the Jira API call - const response = await state.jira.customRequest('GET', field.autoCompleteUrl); + const response = await store.state.jira.customRequest('GET', field.autoCompleteUrl); for (const value of Object.values(response)) { // I assume those are the values because it's an array if (value instanceof Array) { @@ -303,7 +302,7 @@ export default class CreateIssueService { if (!!update) { payload = { ...payload, update: { ...update } }; } - const createdIssue = await state.jira.createIssue(payload); + const createdIssue = await store.state.jira.createIssue(payload); if (!!createdIssue && !!createdIssue.key) { // if the response is ok, we will open the created issue const action = await vscode.window.showInformationMessage('Issue created', 'Open in browser'); diff --git a/src/services/git-integration.service.ts b/src/services/git-integration.service.ts index 8cf44ca..76b56fe 100644 --- a/src/services/git-integration.service.ts +++ b/src/services/git-integration.service.ts @@ -1,9 +1,7 @@ import { EventEmitter } from 'events'; import * as vscode from 'vscode'; -import { gitIntegration, logger } from '.'; +import { configuration, gitIntegration, logger, store } from '.'; import { ACTIONS, CONFIG } from '../shared/constants'; -import state, { changeStateProject } from '../store/state'; -import ConfigurationService from './configuration.service'; import { IIssue } from './http.model'; /** @@ -50,10 +48,10 @@ export default class GitIntegrationService { private gitExtension: vscode.Extension; get isGitIntegrationEnabled(): boolean { - return !!this.configuration.get(CONFIG.GIT_INTEGRATION_ENABLED); + return !!configuration.get(CONFIG.GIT_INTEGRATION_ENABLED); } - constructor(private configuration: ConfigurationService) { + constructor() { this.gitExtension = vscode.extensions.getExtension('vscode.git') || (undefined as any); vscode.commands.executeCommand('setContext', 'gitEnabled', '0'); if (!!this.gitExtension) { @@ -113,9 +111,9 @@ export default class GitIntegrationService { if (!!newBranch) { const ticket = this.parseTicket(newBranch); if (ticket) { - const issue = await state.jira.getIssueByKey(ticket.issue); + const issue = await store.state.jira.getIssueByKey(ticket.issue); // if issue exist and is different from current working issue - const workingIssueKey = state.workingIssue.issue.key; + const workingIssueKey = store.state.workingIssue.issue.key; if (!!issue && issue.key !== workingIssueKey) { // modal const action = await vscode.window.showInformationMessage( @@ -165,7 +163,7 @@ export default class GitIntegrationService { private async setCurrentWorkingProjectAndIssue(ticket: { project: string; issue: string }, issue: IIssue): Promise { try { - changeStateProject(ticket.project); + store.changeStateProject(ticket.project); vscode.commands.executeCommand('jira-plugin.setWorkingIssueCommand', undefined, issue); } catch (e) { logger.printErrorMessageInOutputAndShowAlert(e); diff --git a/src/services/index.ts b/src/services/index.ts index f338eb3..a368ea6 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,19 +1,21 @@ import IssuesExplorer from '../explorer/issues-explorer'; import ConfigurationService from './configuration.service'; +import CreateIssueService from './create-issue.service'; import GitIntegrationService from './git-integration.service'; import LoggerService from './logger.service'; import NotificationService from './notifications.service'; import SelectValuesService from './select-values.service'; import StatusBarService from './status-bar.service'; +import StoreService from './store.service'; import UtilitiesService from './utilities.service'; -import CreateIssueService from './create-issue.service'; +export const store = new StoreService(); export const configuration = new ConfigurationService(); export const issuesExplorer = new IssuesExplorer(); export const logger = new LoggerService(); export const utilities = new UtilitiesService(); export const selectValues = new SelectValuesService(); -export const gitIntegration = new GitIntegrationService(configuration); -export const statusBar = new StatusBarService(configuration); -export const notifications = new NotificationService(configuration); +export const gitIntegration = new GitIntegrationService(); +export const statusBar = new StatusBarService(); +export const notifications = new NotificationService(); export const createIssue = new CreateIssueService(); diff --git a/src/services/logger.service.ts b/src/services/logger.service.ts index f4ecfc3..08b7403 100644 --- a/src/services/logger.service.ts +++ b/src/services/logger.service.ts @@ -1,17 +1,17 @@ import * as vscode from 'vscode'; -import state from '../store/state'; +import { store } from '.'; export default class LoggerService { public printErrorMessageInOutputAndShowAlert(err: any) { - if (state.channel) { + if (store.state.channel) { vscode.window.showErrorMessage(`Error: Check logs in Jira Plugin terminal output.`); - state.channel.append(`Error: ${err}\n`); + store.state.channel.append(`Error: ${err}\n`); } } public printErrorMessageInOutput(err: any) { - if (state.channel) { - state.channel.append(`Error: ${err}\n`); + if (store.state.channel) { + store.state.channel.append(`Error: ${err}\n`); } } @@ -27,8 +27,8 @@ export default class LoggerService { } public jiraPluginDebugLog(message: string, value: any) { - if (this.debugMode() && state.channel) { - state.channel.append(`${message}: ${value}\n`); + if (this.debugMode() && store.state.channel) { + store.state.channel.append(`${message}: ${value}\n`); } } } diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts index 555ee97..74b830d 100644 --- a/src/services/notifications.service.ts +++ b/src/services/notifications.service.ts @@ -1,28 +1,24 @@ import * as vscode from 'vscode'; -import { logger } from '.'; +import { configuration, logger, store } from '.'; import openIssueCommand from '../commands/open-issue'; -import { INotification, INotifications } from './http.model'; import { ACTIONS, CONFIG } from '../shared/constants'; -import state from '../store/state'; -import ConfigurationService from './configuration.service'; +import { INotification, INotifications } from './http.model'; export default class NotificationService { private notifications: INotification[] = []; private showedIds: string[] = []; get isEnabled(): boolean { - return !!this.configuration.get(CONFIG.CHECK_FOR_NOTIFICATIONS_ENABLE); + return !!configuration.get(CONFIG.CHECK_FOR_NOTIFICATIONS_ENABLE); } - constructor(private configuration: ConfigurationService) {} - public async startNotificationsWatcher(): Promise { if (this.isEnabled) { try { let goOn = true; let lastId = ''; while (goOn) { - const response: INotifications = await state.jira.getNotifications(lastId); + const response: INotifications = await store.state.jira.getNotifications(lastId); // check if it's enmpty if (!!response.data && !!response.data.length) { for (let notification of response.data) { @@ -95,7 +91,7 @@ export default class NotificationService { this.showedIds = this.showedIds.filter(id => id !== notification.id); break; case ACTIONS.MARK_AS_READ: - const response = await state.jira.markNotificationsAsReadUnread({ + const response = await store.state.jira.markNotificationsAsReadUnread({ ids: [notification.id], toState: 'READ' }); diff --git a/src/services/select-values.service.ts b/src/services/select-values.service.ts index 248c236..b4fa25e 100644 --- a/src/services/select-values.service.ts +++ b/src/services/select-values.service.ts @@ -1,5 +1,5 @@ import * as vscode from 'vscode'; -import { configuration, issuesExplorer, logger, utilities } from '.'; +import { configuration, issuesExplorer, logger, store, utilities } from '.'; import BackPick from '../picks/back-pick'; import NoWorkingIssuePick from '../picks/no-working-issue-pick'; import UnassignedAssigneePick from '../picks/unassigned-assignee-pick'; @@ -13,19 +13,18 @@ import { SEARCH_MODE, UNASSIGNED } from '../shared/constants'; -import state, { canExecuteJiraAPI, changeStateIssues, verifyCurrentProject } from '../store/state'; import { IAssignee, IFavouriteFilter, IIssue, IIssueType } from './http.model'; export default class SelectValuesService { // selection for projects public async selectProject(): Promise { try { - if (canExecuteJiraAPI()) { - if (state.projects.length === 0) { - state.projects = utilities.hideProjects(await state.jira.getProjects()); - utilities.createDocumentLinkProvider(state.projects); + if (store.canExecuteJiraAPI()) { + if (store.state.projects.length === 0) { + store.state.projects = utilities.hideProjects(await store.state.jira.getProjects()); + utilities.createDocumentLinkProvider(store.state.projects); } - const picks = state.projects.map(project => ({ + const picks = store.state.projects.map(project => ({ pickValue: project.key, label: project.key, description: project.name @@ -41,8 +40,8 @@ export default class SelectValuesService { // selection for statuses public async selectStatus(): Promise { - if (canExecuteJiraAPI()) { - const picks = state.statuses.map(status => ({ + if (store.canExecuteJiraAPI()) { + const picks = store.state.statuses.map(status => ({ pickValue: status.name, label: utilities.addStatusIcon(status.name, true), description: status.description @@ -127,7 +126,7 @@ export default class SelectValuesService { break; } case SEARCH_MODE.REFRESH: { - return [state.currentFilter, state.currentJQL]; + return [store.state.currentSearch.filter, store.state.currentSearch.jql]; } case SEARCH_MODE.MY_WORKING_ISSUES: { const statuses = configuration.workingIssueStatuses(); @@ -149,16 +148,16 @@ export default class SelectValuesService { // perform the search calling Jira API public async selectIssue(mode: string, filterAndJQL?: string[]): Promise { try { - if (canExecuteJiraAPI()) { + if (store.canExecuteJiraAPI()) { const project = configuration.get(CONFIG.WORKING_PROJECT); - if (verifyCurrentProject(project)) { + if (store.verifyCurrentProject(project)) { const [filter, jql] = filterAndJQL || (await this.getFilterAndJQL(mode, project)); - changeStateIssues(LOADING.text, '', []); + store.changeStateIssues(LOADING.text, '', []); if (!!jql) { logger.jiraPluginDebugLog(`${filter} jql`, jql); // call Jira API with the generated JQL const maxResults = Math.min(configuration.get(CONFIG.NUMBER_ISSUES_IN_LIST), SEARCH_MAX_RESULTS); - const searchResult = await state.jira.search({ + const searchResult = await store.state.jira.search({ jql, maxResults }); @@ -166,24 +165,24 @@ export default class SelectValuesService { if (!!searchResult && !!searchResult.issues && searchResult.issues.length > 0) { // exclude issues with project key different from current working project searchResult.issues = searchResult.issues.filter((issue: IIssue) => (issue.fields.project.key || '') === project); - changeStateIssues(filter, jql, searchResult.issues); + store.changeStateIssues(filter, jql, searchResult.issues); } else { - changeStateIssues(filter, jql, []); + store.changeStateIssues(filter, jql, []); vscode.window.showInformationMessage(`No issues found for ${project} project`); } } else { - changeStateIssues('', '', []); + store.changeStateIssues('', '', []); throw new Error(`Wrong parameter. No issues found for ${project} project.`); } } else { - changeStateIssues('', '', []); + store.changeStateIssues('', '', []); throw new Error(`Working project not correct. Select working project in the status bar.`); } } else { - changeStateIssues('', '', []); + store.changeStateIssues('', '', []); } } catch (e) { - changeStateIssues('', '', []); + store.changeStateIssues('', '', []); logger.printErrorMessageInOutputAndShowAlert(e); } } @@ -192,12 +191,12 @@ export default class SelectValuesService { public async selectWorkingIssues(): Promise { let issues: IIssue[] = []; try { - if (canExecuteJiraAPI()) { + if (store.canExecuteJiraAPI()) { const project = configuration.get(CONFIG.WORKING_PROJECT); - if (verifyCurrentProject(project)) { + if (store.verifyCurrentProject(project)) { const [filter, jql] = await this.getFilterAndJQL(SEARCH_MODE.MY_WORKING_ISSUES, project); if (!!jql) { - const result = await state.jira.search({ jql, maxResults: SEARCH_MAX_RESULTS }); + const result = await store.state.jira.search({ jql, maxResults: SEARCH_MAX_RESULTS }); issues = result.issues || []; } } @@ -211,13 +210,13 @@ export default class SelectValuesService { // selection for working issues public async selectChangeWorkingIssue(): Promise { try { - if (canExecuteJiraAPI()) { + if (store.canExecuteJiraAPI()) { const project = configuration.get(CONFIG.WORKING_PROJECT); - if (verifyCurrentProject(project)) { + if (store.verifyCurrentProject(project)) { const [filter, jql] = await this.getFilterAndJQL(SEARCH_MODE.MY_WORKING_ISSUES, project); if (!!jql) { // call Jira API - const issues = await state.jira.search({ jql, maxResults: SEARCH_MAX_RESULTS }); + const issues = await store.state.jira.search({ jql, maxResults: SEARCH_MAX_RESULTS }); if (issues.issues && issues.issues.length > 0) { const picks = issues.issues.map(issue => ({ pickValue: issue, @@ -233,7 +232,7 @@ export default class SelectValuesService { } else { vscode.window.showInformationMessage(`No ${filter} issues found for your user in ${project} project`); // limit case, there is a working issue selected but the user has no more ${filter} issue. i.e: change of status of the working issue - if (state.workingIssue.issue.key !== NO_WORKING_ISSUE.key) { + if (store.state.workingIssue.issue.key !== NO_WORKING_ISSUE.key) { const picks = [new NoWorkingIssuePick()]; const selected = await vscode.window.showQuickPick(picks, { placeHolder: `Your working issue list`, @@ -260,8 +259,8 @@ export default class SelectValuesService { ): Promise { try { const project = configuration.get(CONFIG.WORKING_PROJECT); - if (verifyCurrentProject(project)) { - const assignees = preLoadedPicks || (await state.jira.getAssignees(project)); + if (store.verifyCurrentProject(project)) { + const assignees = preLoadedPicks || (await store.state.jira.getAssignees(project)); const picks = (assignees || []) .filter((assignee: IAssignee) => assignee.active === true) .map((assignee: IAssignee) => { @@ -295,7 +294,7 @@ export default class SelectValuesService { // only the possible transitions public async selectTransition(issueKey: string): Promise { try { - const transitions = await state.jira.getTransitions(issueKey); + const transitions = await store.state.jira.getTransitions(issueKey); const picks = transitions.transitions.map(transition => ({ pickValue: transition.id, label: transition.name, @@ -316,7 +315,7 @@ export default class SelectValuesService { public async doubleSelection( firstSelection: Function, secondSelection: Function - ): Promise<{ firstChoise: string; secondChoise: string }> { + ): Promise<{ firstChoise: string; secondChoise: string | IAssignee }> { let ok = false; let firstChoise = ''; let secondChoise = ''; @@ -334,12 +333,12 @@ export default class SelectValuesService { public async selectStatusAndAssignee(): Promise<{ status: string; assignee: string }> { const project = configuration.get(CONFIG.WORKING_PROJECT); - if (verifyCurrentProject(project)) { + if (store.verifyCurrentProject(project)) { const { firstChoise, secondChoise } = await this.doubleSelection( this.selectStatus, - async () => await this.selectAssignee(true, true, true, undefined) + async () => await this.selectAssignee(true, true, false, undefined) ); - return { status: firstChoise, assignee: secondChoise }; + return { status: firstChoise, assignee: (secondChoise).name }; } else { throw new Error(`Working project not correct, please select one valid project. ("Set working project" command)`); } @@ -347,7 +346,7 @@ export default class SelectValuesService { public async selectIssueType(ignoreFocusOut: boolean, preLoadedPicks: IIssueType[]): Promise { try { - const types = preLoadedPicks || (await state.jira.getAllIssueTypes()); + const types = preLoadedPicks || (await store.state.jira.getAllIssueTypes()); const picks = (types || []).map(type => ({ pickValue: type, label: type.name, @@ -369,10 +368,10 @@ export default class SelectValuesService { // selection for Favorite Filters public async selectFavoriteFilters(): Promise { try { - if (canExecuteJiraAPI()) { + if (store.canExecuteJiraAPI()) { const project = configuration.get(CONFIG.WORKING_PROJECT); - if (verifyCurrentProject(project)) { - const favFilters = await state.jira.getFavoriteFilters(); + if (store.verifyCurrentProject(project)) { + const favFilters = await store.state.jira.getFavoriteFilters(); if (favFilters && favFilters.length > 0) { const selected = await vscode.window.showQuickPick( favFilters.map(filter => { diff --git a/src/services/status-bar.service.ts b/src/services/status-bar.service.ts index b5bcfbc..2268466 100644 --- a/src/services/status-bar.service.ts +++ b/src/services/status-bar.service.ts @@ -1,16 +1,14 @@ import * as vscode from 'vscode'; -import { configuration, utilities } from '.'; -import { IWorkingIssue } from './http.model'; +import { configuration, store, utilities } from '.'; import { CONFIG, NO_WORKING_ISSUE, TRACKING_TIME_MODE } from '../shared/constants'; -import state, { incrementStateWorkingIssueTimePerSecond } from '../store/state'; -import ConfigurationService from './configuration.service'; +import { IWorkingIssue } from './http.model'; export default class StatusBarService { private workingProjectItem: vscode.StatusBarItem; private workingIssueItem: vscode.StatusBarItem; private intervalId: NodeJS.Timer | undefined; private awayTimeout = 30 * 60; // Default to 30 minutes - constructor(configuration: ConfigurationService) { + constructor() { this.workingIssueItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); this.workingProjectItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 200); this.awayTimeout = configuration.get(CONFIG.TRACKING_TIME_MODE_HYBRID_TIMEOUT) * 60; @@ -18,7 +16,7 @@ export default class StatusBarService { // setup working project item public async updateWorkingProjectItem(project: string): Promise { - if (!state.jira) { + if (!store.state.jira) { return; } if (!project) { @@ -69,7 +67,7 @@ export default class StatusBarService { } this.clearWorkingIssueInterval(); - if (state.workingIssue.issue.key !== NO_WORKING_ISSUE.key) { + if (store.state.workingIssue.issue.key !== NO_WORKING_ISSUE.key) { if (configuration.get(CONFIG.TRACKING_TIME_MODE) !== TRACKING_TIME_MODE.NEVER) { this.startWorkingIssueInterval(); } @@ -77,10 +75,10 @@ export default class StatusBarService { // if user select NO_WORKING_ISSUE clear the stored working issue configuration.setGlobalWorkingIssue(undefined); } - this.workingIssueItem.tooltip = this.workingIssueItemTooltip(state.workingIssue); + this.workingIssueItem.tooltip = this.workingIssueItemTooltip(store.state.workingIssue); this.workingIssueItem.command = 'jira-plugin.setWorkingIssueCommand'; - state.workingIssue.awayTime = 0; - this.workingIssueItem.text = this.workingIssueItemText(state.workingIssue); + store.state.workingIssue.awayTime = 0; + this.workingIssueItem.text = this.workingIssueItemText(store.state.workingIssue); this.workingIssueItem.show(); } @@ -97,27 +95,27 @@ export default class StatusBarService { if (configuration.get(CONFIG.TRACKING_TIME_MODE) === TRACKING_TIME_MODE.HYBRID) { // If we are coming back from an away period catch up our logging time // If the away time was > awayTimeout, workingIssue.awayTime will be -1, so we won't log the away time. - if (state.workingIssue.awayTime && state.workingIssue.awayTime > 0) { - state.workingIssue.trackingTime += state.workingIssue.awayTime; + if (store.state.workingIssue.awayTime && store.state.workingIssue.awayTime > 0) { + store.state.workingIssue.trackingTime += store.state.workingIssue.awayTime; } // Clear the away timer - state.workingIssue.awayTime = 0; + store.state.workingIssue.awayTime = 0; } // Update as normal - incrementStateWorkingIssueTimePerSecond(); + store.incrementStateWorkingIssueTimePerSecond(); } else if (configuration.get(CONFIG.TRACKING_TIME_MODE) === TRACKING_TIME_MODE.HYBRID) { // If we are away from the Window capture the time, if it's less than our awayTimeout - if (state.workingIssue.awayTime >= 0) { - if (this.awayTimeout - state.workingIssue.awayTime > 0) { - state.workingIssue.awayTime++; + if (store.state.workingIssue.awayTime >= 0) { + if (this.awayTimeout - store.state.workingIssue.awayTime > 0) { + store.state.workingIssue.awayTime++; } else { // We've been away longer than the away timeout, we are probably working on something else // we set the away timer to -1 to disable it until the next away period - state.workingIssue.awayTime = -1; + store.state.workingIssue.awayTime = -1; } } } - this.workingIssueItem.text = this.workingIssueItemText(state.workingIssue); + this.workingIssueItem.text = this.workingIssueItemText(store.state.workingIssue); }, 1000); } diff --git a/src/services/store.model.ts b/src/services/store.model.ts new file mode 100644 index 0000000..66f8afb --- /dev/null +++ b/src/services/store.model.ts @@ -0,0 +1,14 @@ +import * as vscode from 'vscode'; +import { IIssue, IJira, IProject, IStatus, IWorkingIssue } from './http.model'; + +export interface IState { + jira: IJira; + context: vscode.ExtensionContext; + channel: vscode.OutputChannel; + documentLinkDisposable: vscode.Disposable; + statuses: IStatus[]; + projects: IProject[]; + issues: IIssue[]; + currentSearch: { filter: string; jql: string }; + workingIssue: IWorkingIssue; +} diff --git a/src/services/store.service.ts b/src/services/store.service.ts new file mode 100644 index 0000000..48c084a --- /dev/null +++ b/src/services/store.service.ts @@ -0,0 +1,138 @@ +import * as vscode from 'vscode'; +import { configuration, gitIntegration, issuesExplorer, logger, notifications, statusBar, utilities } from '.'; +import NoWorkingIssuePick from '../picks/no-working-issue-pick'; +import { CONFIG, LOADING, NO_WORKING_ISSUE } from '../shared/constants'; +import { IIssue, IProject } from './http.model'; +import { Jira } from './http.service'; +import { IState } from './store.model'; + +export default class StoreService { + // initial state + public state: IState = { + jira: undefined as any, + context: undefined as any, + channel: undefined as any, + documentLinkDisposable: undefined as any, + statuses: [], + projects: [], + issues: [], + currentSearch: { + filter: LOADING.text, + jql: '' + }, + workingIssue: { + issue: new NoWorkingIssuePick().pickValue, + trackingTime: 0, + awayTime: 0 + } + }; + + public async connectToJira(): Promise { + try { + this.state.jira = new Jira(); + // save statuses and projects in the global state + this.state.statuses = await this.state.jira.getStatuses(); + this.addAdditionalStatuses(); + this.state.statuses.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); + + this.state.projects = utilities.hideProjects(await this.state.jira.getProjects()); + utilities.createDocumentLinkProvider(this.state.projects); + + const project = configuration.get(CONFIG.WORKING_PROJECT); + statusBar.updateWorkingProjectItem(project); + // refresh Jira explorer list + if (project) { + // start notification service + notifications.startNotificationsWatcher(); + await vscode.commands.executeCommand('jira-plugin.defaultIssuesCommand'); + } else { + vscode.window.showWarningMessage("Working project isn't set."); + } + } catch (err) { + configuration.set(CONFIG.WORKING_PROJECT, ''); + setTimeout(() => { + statusBar.updateWorkingProjectItem(''); + }, 1000); + this.changeStateIssues('', '', []); + logger.printErrorMessageInOutputAndShowAlert(err); + } + } + + public canExecuteJiraAPI(): boolean { + return this.state.jira && configuration.isValid(); + } + + public verifyCurrentProject(project: string | undefined): boolean { + return !!project && this.state.projects.filter((prj: IProject) => prj.key === project).length > 0; + } + + public changeStateProject(project: string): void { + if (!!project) { + if (configuration.get(CONFIG.WORKING_PROJECT) !== project) { + configuration.set(CONFIG.WORKING_PROJECT, project); + // update project item in the status bar + statusBar.updateWorkingProjectItem(project); + // loading in Jira explorer + this.changeStateIssues(LOADING.text, '', []); + // start notification service + notifications.startNotificationsWatcher(); + // launch search for the new project + setTimeout(() => vscode.commands.executeCommand('jira-plugin.defaultIssuesCommand'), 1000); + } + } + } + + public changeStateIssues(filter: string, jql: string, issues: IIssue[]): void { + this.state.currentSearch.filter = filter; + this.state.currentSearch.jql = jql; + this.state.issues = issues; + issuesExplorer.refresh(); + } + + public async changeStateWorkingIssue(issue: IIssue, trackingTime: number): Promise { + if (issue.key !== NO_WORKING_ISSUE.key) { + await gitIntegration.switchToWorkingTicketBranch(issue); + } + const awayTime: number = 0; // FIXME: We don't need awayTime when changing issues, not sure best way to handle this. + this.state.workingIssue = { issue, trackingTime, awayTime }; + statusBar.updateWorkingIssueItem(false); + } + + public incrementStateWorkingIssueTimePerSecond(): void { + this.state.workingIssue.trackingTime += 1; + // prevent writing to much on storage + if (this.state.workingIssue.trackingTime % 60 === 0) { + if (this.state.workingIssue.issue.key !== NO_WORKING_ISSUE.key) { + configuration.setGlobalWorkingIssue(this.state.workingIssue); + } + } + } + + // verify if it's the current working issue + public isWorkingIssue(issueKey: string): boolean { + if (issueKey === this.state.workingIssue.issue.key) { + vscode.window.showErrorMessage(`Issue ${issueKey} has pending worklog. Resolve the conflict and retry the action.`); + } + return issueKey === this.state.workingIssue.issue.key; + } + + public addAdditionalStatuses() { + try { + const additionalStatuses = configuration.get(CONFIG.ADDITIONAL_STATUSES); + if (!!additionalStatuses) { + const list = additionalStatuses.split(','); + list.forEach((status: string) => { + const newStatus = status.trim(); + if (!!newStatus && !this.state.statuses.find(el => el.name.toLowerCase() === newStatus.toLowerCase())) { + this.state.statuses.push({ + description: newStatus, + name: newStatus + }); + } + }); + } + } catch (err) { + logger.printErrorMessageInOutputAndShowAlert(err); + } + } +} diff --git a/src/services/utilities.service.ts b/src/services/utilities.service.ts index 373b6b4..85e621d 100644 --- a/src/services/utilities.service.ts +++ b/src/services/utilities.service.ts @@ -1,12 +1,10 @@ -const copyPaste = require('copy-paste'); import * as path from 'path'; import * as vscode from 'vscode'; -import { configuration, logger } from '.'; +import { configuration, logger, store } from '.'; import { IssueItem } from '../explorer/item/issue-item'; import { ACTIONS, CONFIG, STATUS_ICONS } from '../shared/constants'; import { IssueLinkProvider } from '../shared/document-link-provider'; -import state from '../store/state'; -import { IIssue, IProject } from './http.model'; +import { IProject } from './http.model'; export default class UtilitiesService { // generate icon + status @@ -63,7 +61,7 @@ export default class UtilitiesService { copyToClipboard(issue: IssueItem) { if (issue) { - copyPaste.copy(issue.label); + vscode.env.clipboard.writeText(issue.label || ''); vscode.window.showInformationMessage('Jira Plugin - Copied to clipboard'); } else { logger.printErrorMessageInOutputAndShowAlert('Use this command from Jira Plugin EXPLORER'); @@ -72,9 +70,9 @@ export default class UtilitiesService { insertWorkingIssueComment() { const editor = vscode.window.activeTextEditor; - if (editor && state.workingIssue) { + if (editor && store.state.workingIssue) { editor.edit(edit => { - const workingIssue = state.workingIssue; + const workingIssue = store.state.workingIssue; edit.insert(editor.selection.active, `// ${workingIssue.issue.key} - ${workingIssue.issue.fields.summary}`); }); } else { @@ -83,10 +81,10 @@ export default class UtilitiesService { } createDocumentLinkProvider(projects: IProject[]) { - if (!!state.documentLinkDisposable) { - state.documentLinkDisposable.dispose(); + if (!!store.state.documentLinkDisposable) { + store.state.documentLinkDisposable.dispose(); } - state.documentLinkDisposable = vscode.languages.registerDocumentLinkProvider({ scheme: '*' }, new IssueLinkProvider(projects)); + store.state.documentLinkDisposable = vscode.languages.registerDocumentLinkProvider({ scheme: '*' }, new IssueLinkProvider(projects)); } hideProjects(projects: IProject[]): IProject[] { diff --git a/src/store/state.ts b/src/store/state.ts deleted file mode 100644 index 674d839..0000000 --- a/src/store/state.ts +++ /dev/null @@ -1,146 +0,0 @@ -import * as vscode from 'vscode'; -import NoWorkingIssuePick from '../picks/no-working-issue-pick'; -import { configuration, gitIntegration, issuesExplorer, logger, notifications, statusBar, utilities } from '../services'; -import { IIssue, IJira, IProject, IStatus, IWorkingIssue } from '../services/http.model'; -import { Jira } from '../services/http.service'; -import { CONFIG, LOADING, NO_WORKING_ISSUE } from '../shared/constants'; - -export interface IState { - jira: IJira; - context: vscode.ExtensionContext; - channel: vscode.OutputChannel; - documentLinkDisposable: vscode.Disposable; - statuses: IStatus[]; - projects: IProject[]; - issues: IIssue[]; - currentFilter: string; - currentJQL: string; - workingIssue: IWorkingIssue; -} - -// initial state -const state: IState = { - jira: undefined as any, - context: undefined as any, - channel: undefined as any, - documentLinkDisposable: undefined as any, - statuses: [], - projects: [], - issues: [], - currentFilter: LOADING.text, - currentJQL: '', - workingIssue: { - issue: new NoWorkingIssuePick().pickValue, - trackingTime: 0, - awayTime: 0 - } -}; - -export default state; - -export const connectToJira = async (): Promise => { - try { - state.jira = new Jira(); - // save statuses and projects in the global state - state.statuses = await state.jira.getStatuses(); - addAdditionalStatuses(); - state.statuses.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); - - state.projects = utilities.hideProjects(await state.jira.getProjects()); - utilities.createDocumentLinkProvider(state.projects); - statusBar.updateWorkingProjectItem(''); - - const project = configuration.get(CONFIG.WORKING_PROJECT); - // refresh Jira explorer list - if (project) { - // start notification service - notifications.startNotificationsWatcher(); - await vscode.commands.executeCommand('jira-plugin.defaultIssuesCommand'); - } else { - vscode.window.showWarningMessage("Working project isn't set."); - } - } catch (err) { - configuration.set(CONFIG.WORKING_PROJECT, ''); - setTimeout(() => { - statusBar.updateWorkingProjectItem(''); - }, 1000); - changeStateIssues('', '', []); - logger.printErrorMessageInOutputAndShowAlert(err); - } -}; - -export const canExecuteJiraAPI = (): boolean => { - return state.jira && configuration.isValid(); -}; - -export const verifyCurrentProject = (project: string | undefined): boolean => { - return !!project && state.projects.filter((prj: IProject) => prj.key === project).length > 0; -}; - -export const changeStateProject = (project: string): void => { - if (configuration.get(CONFIG.WORKING_PROJECT) !== project) { - configuration.set(CONFIG.WORKING_PROJECT, project); - // update project item in the status bar - statusBar.updateWorkingProjectItem(project); - // loading in Jira explorer - changeStateIssues(LOADING.text, '', []); - // start notification service - notifications.startNotificationsWatcher(); - // launch search for the new project - setTimeout(() => vscode.commands.executeCommand('jira-plugin.defaultIssuesCommand'), 1000); - } -}; - -export const changeStateIssues = (filter: string, jql: string, issues: IIssue[]): void => { - state.currentFilter = filter; - state.currentJQL = jql; - state.issues = issues; - issuesExplorer.refresh(); -}; - -export const changeStateWorkingIssue = async (issue: IIssue, trackingTime: number): Promise => { - if (issue.key !== NO_WORKING_ISSUE.key) { - await gitIntegration.switchToWorkingTicketBranch(issue); - } - const awayTime: number = 0; // FIXME: We don't need awayTime when changing issues, not sure best way to handle this. - state.workingIssue = { issue, trackingTime, awayTime }; - statusBar.updateWorkingIssueItem(false); -}; - -export const incrementStateWorkingIssueTimePerSecond = (): void => { - state.workingIssue.trackingTime += 1; - // prevent writing to much on storage - if (state.workingIssue.trackingTime % 60 === 0) { - if (state.workingIssue.issue.key !== NO_WORKING_ISSUE.key) { - configuration.setGlobalWorkingIssue(state.workingIssue); - } - } -}; - -// verify if it's the current working issue -export const isWorkingIssue = (issueKey: string): boolean => { - if (issueKey === state.workingIssue.issue.key) { - vscode.window.showErrorMessage(`Issue ${issueKey} has pending worklog. Resolve the conflict and retry the action.`); - } - return issueKey === state.workingIssue.issue.key; -}; - -export const addAdditionalStatuses = () => { - try { - const additionalStatuses = configuration.get(CONFIG.ADDITIONAL_STATUSES); - if (!!additionalStatuses) { - const list = additionalStatuses.split(','); - list.forEach((status: string) => { - const newStatus = status.trim(); - if (!!newStatus && !state.statuses.find(el => el.name.toLowerCase() === newStatus.toLowerCase())) { - state.statuses.push({ - description: newStatus, - name: newStatus - }); - } - }); - } - } catch (err) { - logger.printErrorMessageInOutputAndShowAlert(err); - } -}; diff --git a/test/tests/configuration.test.ts b/test/tests/configuration.test.ts index a49c34f..d1279b3 100644 --- a/test/tests/configuration.test.ts +++ b/test/tests/configuration.test.ts @@ -1,12 +1,13 @@ import * as assert from 'assert'; import ConfigurationService from '../../src/services/configuration.service'; import { IWorkingIssue } from '../../src/services/http.model'; +import StoreService from '../../src/services/store.service'; import { CONFIG, DEFAULT_WORKING_ISSUE_STATUS } from '../../src/shared/constants'; -import state from '../../src/store/state'; import { backupSettings, restoreSettings } from '../utils/utils'; suite('Configuration', () => { const configurationService = new ConfigurationService(); + const store = new StoreService(); const tests = [ { title: `${CONFIG.BASE_URL} 1`, @@ -123,50 +124,30 @@ suite('Configuration', () => { }); test(`WorkingIssueStatuses in statuses list`, async () => { - state.statuses = [ - { - description: 'In Progress', - name: 'In Progress' - }, - { - description: 'Closed', - name: 'Closed' - } - ]; + console.log('store.state.statuses', store.state.statuses); await configurationService.set(CONFIG.WORKING_ISSUE_STATUSES, 'In Progress, Closed'); - const statuses = configurationService.workingIssueStatuses(); + const statuses = configurationService.workingIssueStatuses([ + { description: 'In Progress', name: 'In Progress' }, + { description: 'Closed', name: 'Closed' } + ]); assert.strictEqual(statuses, `'In Progress','Closed'`); }); test(`WorkingIssueStatuses only one in statuses list`, async () => { - state.statuses = [ - { - description: 'In Progress', - name: 'In Progress' - }, - { - description: 'Closed', - name: 'Closed' - } - ]; await configurationService.set(CONFIG.WORKING_ISSUE_STATUSES, 'In Progress, Abc'); - const statuses = configurationService.workingIssueStatuses(); + const statuses = configurationService.workingIssueStatuses([ + { description: 'In Progress', name: 'In Progress' }, + { description: 'Closed', name: 'Closed' } + ]); assert.strictEqual(statuses, `'In Progress'`); }); test(`WorkingIssueStatuses not in statuses list`, async () => { - state.statuses = [ - { - description: 'In Progress', - name: 'In Progress' - }, - { - description: 'Closed', - name: 'Closed' - } - ]; await configurationService.set(CONFIG.WORKING_ISSUE_STATUSES, 'Abc'); - const statuses = configurationService.workingIssueStatuses(); + const statuses = configurationService.workingIssueStatuses([ + { description: 'In Progress', name: 'In Progress' }, + { description: 'Closed', name: 'Closed' } + ]); assert.strictEqual(statuses, `'${DEFAULT_WORKING_ISSUE_STATUS}'`); }); diff --git a/test/tests/http._test.ts b/test/tests/http._test.ts index a9f31bb..e937e45 100644 --- a/test/tests/http._test.ts +++ b/test/tests/http._test.ts @@ -3,14 +3,15 @@ import NoWorkingIssuePick from '../../src/picks/no-working-issue-pick'; import ConfigurationService from '../../src/services/configuration.service'; import { IAssignee, IIssue, INotification, ISetTransition } from '../../src/services/http.model'; import { Jira } from '../../src/services/http.service'; +import StoreService from '../../src/services/store.service'; import { LOADING } from '../../src/shared/constants'; -import { IState } from '../../src/store/state'; import { settings } from '../utils/settings'; import { backupSettings, restoreSettings } from '../utils/utils'; suite('Jira API', () => { const configurationService = new ConfigurationService(); - const state: IState = { + const store = new StoreService(); + store.state = { jira: undefined as any, context: undefined as any, channel: undefined as any, @@ -18,8 +19,7 @@ suite('Jira API', () => { statuses: [], projects: [], issues: [], - currentFilter: LOADING.text, - currentJQL: '', + currentSearch: { filter: LOADING.text, jql: '' }, workingIssue: { issue: new NoWorkingIssuePick().pickValue, trackingTime: 0, @@ -80,7 +80,7 @@ suite('Jira API', () => { test(`Setup Test Settings`, async () => { await restoreSettings(configurationService, settings); project = settings.workingProject; - state.jira = new Jira(); + store.state.jira = new Jira(); assert.strictEqual(1, 1); }); @@ -100,7 +100,7 @@ suite('Jira API', () => { key: project }, issuetype: { - id: '10007' + id: '10004' }, summary: 'VsCode npm test', description: 'created by VsCode npm test' @@ -145,15 +145,19 @@ suite('Jira API', () => { tests.forEach(t => { test(t.name, async () => { - const response = await (state.jira)[t.name](preparePaylod(t)); - storeResponse(t.name, response); - if (t.name === 'getIssueByKey') { - if (response.key !== issueKey || response.fields.assignee.key !== assigneeKey) { - throw new Error('getIssueByKey -> issue not correct'); + if (t.name !== 'markNotificationsAsReadUnread' || !!notification) { + const response = await (store.state.jira)[t.name](preparePaylod(t)); + storeResponse(t.name, response); + if (t.name === 'getIssueByKey') { + if (response.key !== issueKey || response.fields.assignee.key !== assigneeKey) { + throw new Error('getIssueByKey -> issue not correct'); + } } + // console.log(JSON.stringify(response)); + assert.strictEqual(1, 1); + } else { + assert.strictEqual(1, 1); } - // console.log(JSON.stringify(response)); - assert.strictEqual(1, 1); }); });