├── .prettierignore ├── .vscode ├── extensions.json ├── settings.json └── launch.json ├── images ├── icons │ ├── icon.png │ ├── dark │ │ ├── x.png │ │ ├── alert.png │ │ ├── book.png │ │ ├── check.png │ │ ├── cloud.png │ │ ├── dash.png │ │ ├── eye.png │ │ ├── flame.png │ │ ├── info.png │ │ ├── beaker.png │ │ ├── checklist.png │ │ ├── plus_nav.png │ │ ├── thumbsup.png │ │ ├── gist-secret.png │ │ └── refresh.svg │ ├── light │ │ ├── x.png │ │ ├── book.png │ │ ├── dash.png │ │ ├── eye.png │ │ ├── info.png │ │ ├── alert.png │ │ ├── beaker.png │ │ ├── check.png │ │ ├── cloud.png │ │ ├── flame.png │ │ ├── checklist.png │ │ ├── plus_nav.png │ │ ├── thumbsup.png │ │ ├── gist-secret.png │ │ └── refresh.svg │ └── jira-explorer-icon.svg └── readme │ ├── setup.gif │ ├── explorer.png │ ├── DuMont_logo.jpg │ ├── status-bar.png │ ├── issue-commands.png │ ├── working-issue.gif │ ├── jira-issue-link.png │ └── set-working-project.gif ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── --thank-you.md │ ├── feature_request.md │ └── issue_template.md └── workflows │ └── stale.yml ├── .prettierrc ├── .vscodeignore ├── src ├── commands │ ├── toggle-working-issue-timer.ts │ ├── set-working-project.ts │ ├── open-github-repo.ts │ ├── open-issue.ts │ ├── favourites-filters.ts │ ├── change-issue-assignee.ts │ ├── change-issue-status.ts │ ├── issue-add-worklog.ts │ ├── setup-credentials.ts │ ├── issue-add-comment.ts │ ├── stop-working-issue.ts │ ├── set-working-issue.ts │ ├── index.ts │ └── create-issue.ts ├── explorer │ ├── item │ │ ├── divider-item.ts │ │ ├── loading-item.ts │ │ ├── no-result-item.ts │ │ ├── filter-info-item.ts │ │ ├── status-item.ts │ │ ├── issue-item.ts │ │ └── limit-info.ts │ └── issues-explorer.ts ├── picks │ ├── back-pick.ts │ ├── unassigned-assignee-pick.ts │ └── no-working-issue-pick.ts ├── services │ ├── store.model.ts │ ├── configuration.model.ts │ ├── logger.service.ts │ ├── index.ts │ ├── notifications.service.ts │ ├── utilities.service.ts │ ├── store.service.ts │ ├── configuration.service.ts │ ├── http.model.ts │ ├── status-bar.service.ts │ ├── http.service.ts │ ├── git-integration.service.ts │ └── issue-helper.service.ts ├── extension.ts └── shared │ ├── document-link-provider.ts │ ├── jira-instance-patch.ts │ └── constants.ts ├── test ├── utils │ ├── settings.model.ts │ ├── settings.ts │ ├── settings.example.ts │ ├── setup-settings.test.ts │ └── utils.ts ├── index.ts ├── tests │ ├── utilities.test.ts │ ├── document-link-provider.test.ts │ ├── configuration.test.ts │ └── http._test.ts └── issue.mock.ts ├── tslint.json ├── tsconfig.json ├── scripts └── rename-files.js ├── LICENSE ├── .gitignore ├── CONTRIBUTING.md ├── CHANGELOG.md ├── README.md └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /images/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/icon.png -------------------------------------------------------------------------------- /images/icons/dark/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/x.png -------------------------------------------------------------------------------- /images/icons/light/x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/x.png -------------------------------------------------------------------------------- /images/readme/setup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/readme/setup.gif -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | open_collective: jira-plugin 4 | -------------------------------------------------------------------------------- /images/icons/dark/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/alert.png -------------------------------------------------------------------------------- /images/icons/dark/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/book.png -------------------------------------------------------------------------------- /images/icons/dark/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/check.png -------------------------------------------------------------------------------- /images/icons/dark/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/cloud.png -------------------------------------------------------------------------------- /images/icons/dark/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/dash.png -------------------------------------------------------------------------------- /images/icons/dark/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/eye.png -------------------------------------------------------------------------------- /images/icons/dark/flame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/flame.png -------------------------------------------------------------------------------- /images/icons/dark/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/info.png -------------------------------------------------------------------------------- /images/icons/light/book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/book.png -------------------------------------------------------------------------------- /images/icons/light/dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/dash.png -------------------------------------------------------------------------------- /images/icons/light/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/eye.png -------------------------------------------------------------------------------- /images/icons/light/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/info.png -------------------------------------------------------------------------------- /images/readme/explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/readme/explorer.png -------------------------------------------------------------------------------- /images/icons/dark/beaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/beaker.png -------------------------------------------------------------------------------- /images/icons/light/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/alert.png -------------------------------------------------------------------------------- /images/icons/light/beaker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/beaker.png -------------------------------------------------------------------------------- /images/icons/light/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/check.png -------------------------------------------------------------------------------- /images/icons/light/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/cloud.png -------------------------------------------------------------------------------- /images/icons/light/flame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/flame.png -------------------------------------------------------------------------------- /images/readme/DuMont_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/readme/DuMont_logo.jpg -------------------------------------------------------------------------------- /images/readme/status-bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/readme/status-bar.png -------------------------------------------------------------------------------- /images/icons/dark/checklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/checklist.png -------------------------------------------------------------------------------- /images/icons/dark/plus_nav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/plus_nav.png -------------------------------------------------------------------------------- /images/icons/dark/thumbsup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/thumbsup.png -------------------------------------------------------------------------------- /images/icons/light/checklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/checklist.png -------------------------------------------------------------------------------- /images/icons/light/plus_nav.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/plus_nav.png -------------------------------------------------------------------------------- /images/icons/light/thumbsup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/thumbsup.png -------------------------------------------------------------------------------- /images/readme/issue-commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/readme/issue-commands.png -------------------------------------------------------------------------------- /images/readme/working-issue.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/readme/working-issue.gif -------------------------------------------------------------------------------- /images/icons/dark/gist-secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/dark/gist-secret.png -------------------------------------------------------------------------------- /images/icons/light/gist-secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/icons/light/gist-secret.png -------------------------------------------------------------------------------- /images/readme/jira-issue-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/readme/jira-issue-link.png -------------------------------------------------------------------------------- /images/readme/set-working-project.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gioboa/jira-plugin/HEAD/images/readme/set-working-project.gif -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "bracketSpacing": true 8 | } 9 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | typings/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore 8 | tsconfig.json 9 | vsc-extension-quickstart.md 10 | scripts/** -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/--thank-you.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F495 Thank You" 3 | about: If you like this project and wanna just say Hi... 4 | title: '' 5 | labels: '' 6 | assignees: gioboa 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /src/commands/toggle-working-issue-timer.ts: -------------------------------------------------------------------------------- 1 | import { statusBar } from '../services'; 2 | 3 | export default async function toggleWorkingIssueTimer(): Promise { 4 | statusBar.toggleWorkingIssueTimer(); 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/set-working-project.ts: -------------------------------------------------------------------------------- 1 | import { selectValues, store } from '../services'; 2 | 3 | export default async function setWorkingProject(): Promise { 4 | store.changeStateProject(await selectValues.selectProject(), false); 5 | } 6 | -------------------------------------------------------------------------------- /test/utils/settings.model.ts: -------------------------------------------------------------------------------- 1 | export interface ISettings { 2 | baseUrl: string; 3 | username: string; 4 | password: string; 5 | workingProject: string; 6 | enableWorkingIssue: boolean; 7 | workingIssueStatues: string; 8 | counter: number; 9 | workingIssue: any; 10 | } 11 | -------------------------------------------------------------------------------- /src/commands/open-github-repo.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export default async function openGitHubRepo(issueId: string): Promise { 4 | const url = `https://github.com/gioboa/jira-plugin`; 5 | vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); 6 | } 7 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-string-throw": true, 4 | "no-unused-expression": true, 5 | "no-duplicate-variable": true, 6 | "curly": true, 7 | "class-name": true, 8 | "semicolon": [true, "always"], 9 | "triple-equals": true 10 | }, 11 | "defaultSeverity": "warning" 12 | } 13 | -------------------------------------------------------------------------------- /test/utils/settings.ts: -------------------------------------------------------------------------------- 1 | import { ISettings } from './settings.model'; 2 | 3 | export const settings: ISettings = { 4 | baseUrl: 'xxx', 5 | username: 'xxx', 6 | password: 'xxx', 7 | workingProject: '', 8 | enableWorkingIssue: false, 9 | workingIssueStatues: 'In progress', 10 | counter: 0, 11 | workingIssue: undefined 12 | }; 13 | -------------------------------------------------------------------------------- /test/utils/settings.example.ts: -------------------------------------------------------------------------------- 1 | import { ISettings } from './settings.model'; 2 | 3 | export const settings: ISettings = { 4 | baseUrl: 'xxx', 5 | username: 'xxx', 6 | password: 'xxx', 7 | workingProject: '', 8 | enableWorkingIssue: false, 9 | workingIssueStatues: 'In progress', 10 | counter: 0, 11 | workingIssue: undefined 12 | }; 13 | -------------------------------------------------------------------------------- /src/explorer/item/divider-item.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export class DividerItem extends vscode.TreeItem { 4 | constructor(label: string, collapsibleState?: vscode.TreeItemCollapsibleState) { 5 | super(label, vscode.TreeItemCollapsibleState.None); 6 | } 7 | 8 | get tooltip(): string { 9 | return ''; 10 | } 11 | 12 | contextValue = 'DividerItem'; 13 | } 14 | -------------------------------------------------------------------------------- /src/picks/back-pick.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickItem } from 'vscode'; 2 | import { BACK_PICK_LABEL } from '../shared/constants'; 3 | 4 | export default class BackPick implements QuickPickItem { 5 | get label(): string { 6 | return BACK_PICK_LABEL; 7 | } 8 | 9 | get description(): string { 10 | return `Previous selection`; 11 | } 12 | 13 | get pickValue(): string { 14 | return BACK_PICK_LABEL; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/picks/unassigned-assignee-pick.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickItem } from 'vscode'; 2 | import { UNASSIGNED } from '../shared/constants'; 3 | 4 | export default class UnassignedAssigneePick implements QuickPickItem { 5 | get label(): string { 6 | return UNASSIGNED; 7 | } 8 | 9 | get description(): string { 10 | return UNASSIGNED; 11 | } 12 | 13 | get pickValue(): string { 14 | return UNASSIGNED; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/open-issue.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { configuration } from '../services'; 3 | import { CONFIG } from '../shared/constants'; 4 | 5 | export default async function openIssue(issueId: string): Promise { 6 | // open the issue in the browser 7 | const url = `${configuration.get(CONFIG.BASE_URL)}/browse/${issueId}`; 8 | vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); 9 | } 10 | -------------------------------------------------------------------------------- /test/utils/setup-settings.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import ConfigurationService from '../../src/services/configuration.service'; 3 | import { settings } from './settings'; 4 | import { restoreSettings } from './utils'; 5 | 6 | suite('Setup settings', () => { 7 | test(`Restore Settings Backup`, async () => { 8 | restoreSettings(new ConfigurationService(), settings); 9 | assert.strictEqual(1, 1); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/services/store.model.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { IIssue, IJira, IProject, IStatus, IWorkingIssue } from './http.model'; 3 | 4 | export interface IState { 5 | jira: IJira; 6 | context: vscode.ExtensionContext; 7 | channel: vscode.OutputChannel; 8 | documentLinkDisposable: vscode.Disposable; 9 | statuses: IStatus[]; 10 | projects: IProject[]; 11 | issues: IIssue[]; 12 | currentSearch: { filter: string; jql: string }; 13 | workingIssue: IWorkingIssue; 14 | } 15 | -------------------------------------------------------------------------------- /src/services/configuration.model.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceConfiguration } from 'vscode'; 2 | 3 | export interface IConfiguration extends WorkspaceConfiguration { 4 | baseUrl?: string; 5 | username?: string; 6 | workingProject?: string; 7 | enableWorkingIssue?: boolean; 8 | trackingTimeMode?: string; 9 | trackingTimeModeHybridTimeout?: number; 10 | worklogMinimumTrackingTim?: number; 11 | } 12 | 13 | export interface IPickValue { 14 | pickValue: any; 15 | label: any; 16 | description: any; 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/favourites-filters.ts: -------------------------------------------------------------------------------- 1 | import { logger, selectValues } from '../services'; 2 | import { SEARCH_MODE } from '../shared/constants'; 3 | 4 | export default async function favouritesFilters(): Promise { 5 | try { 6 | const filter = await selectValues.selectFavoriteFilters(); 7 | if (filter) { 8 | selectValues.selectIssue(SEARCH_MODE.FAVOURITES_FILTERS, [filter.name, filter.jql]); 9 | } 10 | } catch (err) { 11 | logger.printErrorMessageInOutputAndShowAlert(err); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": ["dom", "es2017"], 7 | "sourceMap": true, 8 | "rootDir": ".", 9 | "strict": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "allowUnreachableCode": false, 13 | "allowUnusedLabels": false, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true 17 | }, 18 | "exclude": ["node_modules", ".vscode-test"] 19 | } 20 | -------------------------------------------------------------------------------- /src/explorer/item/loading-item.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { LOADING } from '../../shared/constants'; 3 | import { utilities } from '../../services'; 4 | 5 | export class LoadingItem extends vscode.TreeItem { 6 | constructor() { 7 | super('LOADING...', vscode.TreeItemCollapsibleState.None); 8 | } 9 | 10 | get tooltip(): string { 11 | return ''; 12 | } 13 | 14 | iconPath = { 15 | light: utilities.getIconsPath(`light/${LOADING.file}`), 16 | dark: utilities.getIconsPath(`dark/${LOADING.file}`), 17 | }; 18 | 19 | contextValue = 'LoadingItem'; 20 | } 21 | -------------------------------------------------------------------------------- /src/explorer/item/no-result-item.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { STATUS_ICONS } from '../../shared/constants'; 3 | import { utilities } from '../../services'; 4 | 5 | export class NoResultItem extends vscode.TreeItem { 6 | constructor(project: string) { 7 | super(`${project} - NO ISSUES`, vscode.TreeItemCollapsibleState.None); 8 | } 9 | 10 | get tooltip(): string { 11 | return ''; 12 | } 13 | 14 | iconPath = { 15 | light: utilities.getIconsPath(`light/${STATUS_ICONS.DEFAULT.file}`), 16 | dark: utilities.getIconsPath(`dark/${STATUS_ICONS.DEFAULT.file}`), 17 | }; 18 | 19 | contextValue = 'NoResultItem'; 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 14 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v1 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | stale-issue-message: 'This issue has been automatically marked as stale because it has not had activity in the last 30 days. Remove stale label or comment or this will be closed in 5 days. Thank you for your contributions.' 17 | days-before-stale: 30 18 | days-before-close: 5 19 | stale-issue-label: 'no-issue-activity' 20 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.codeActionsOnSave": { 4 | "source.organizeImports": true 5 | }, 6 | "files.exclude": { 7 | "out": false // set this to true to hide the "out" folder with the compiled JS files 8 | }, 9 | "search.exclude": { 10 | "out": true // set this to false to include "out" folder in search results 11 | }, 12 | "workbench.colorCustomizations": { 13 | "titleBar.activeBackground": "#e6e6e6", 14 | "titleBar.activeForeground": "#000000", 15 | "titleBar.inactiveBackground": "#e6e6e6CC", 16 | "titleBar.inactiveForeground": "#000000" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/explorer/item/filter-info-item.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { STATUS_ICONS } from '../../shared/constants'; 3 | import { utilities } from '../../services'; 4 | 5 | export class FilterInfoItem extends vscode.TreeItem { 6 | constructor(project: string, filter: string, issueCounter: number) { 7 | super(`${project} - ${filter} - COUNT: ${issueCounter}`, vscode.TreeItemCollapsibleState.None); 8 | } 9 | 10 | get tooltip(): string { 11 | return ''; 12 | } 13 | 14 | iconPath = { 15 | light: utilities.getIconsPath(`light/${STATUS_ICONS.DEFAULT.file}`), 16 | dark: utilities.getIconsPath(`dark/${STATUS_ICONS.DEFAULT.file}`), 17 | }; 18 | 19 | contextValue = 'FilterInfoItem'; 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'feat: your_title_here' 5 | labels: enhancement 6 | assignees: gioboa 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /src/picks/no-working-issue-pick.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickItem } from 'vscode'; 2 | import { IIssue } from '../services/http.model'; 3 | import { NO_WORKING_ISSUE } from '../shared/constants'; 4 | 5 | export default class NoWorkingIssuePick implements QuickPickItem { 6 | get label(): string { 7 | return `$(x) ${NO_WORKING_ISSUE.text}`; 8 | } 9 | 10 | get description(): string { 11 | return ''; 12 | } 13 | 14 | get pickValue(): IIssue { 15 | return { 16 | id: '', 17 | key: NO_WORKING_ISSUE.key, 18 | fields: { 19 | summary: '', 20 | status: { 21 | name: '', 22 | }, 23 | project: { 24 | id: '', 25 | key: '', 26 | name: '', 27 | }, 28 | }, 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import commands from './commands'; 3 | import './services'; 4 | import { gitIntegration, issuesExplorer, statusBar, store } from './services'; 5 | import { CONFIG_NAME } from './shared/constants'; 6 | 7 | export const activate = async (context: vscode.ExtensionContext): Promise => { 8 | const channel: vscode.OutputChannel = vscode.window.createOutputChannel(CONFIG_NAME.toUpperCase()); 9 | context.subscriptions.push(channel); 10 | store.state.channel = channel; 11 | store.state.context = context; 12 | vscode.window.registerTreeDataProvider('issuesExplorer', issuesExplorer); 13 | context.subscriptions.push(statusBar); 14 | context.subscriptions.push(gitIntegration); 15 | context.subscriptions.push(...commands.register()); 16 | // create Jira Instance and try to connect 17 | await store.connectToJira(); 18 | }; 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue report 3 | about: Create a report to help us improve 4 | title: 'issue: your_title_here' 5 | labels: question 6 | assignees: gioboa 7 | --- 8 | 9 | **Describe the issue** 10 | A clear and concise description of what the issue is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | Go to the help menu, click about, click copy, and paste the information in here. 29 | 30 | **Log** 31 | Please check the [output log](https://github.com/gioboa/jira-plugin/issues/34) and paste here any logs. 32 | -------------------------------------------------------------------------------- /images/icons/dark/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/icons/light/refresh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/explorer/item/status-item.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { utilities } from '../../services'; 3 | import { STATUS_ICONS } from '../../shared/constants'; 4 | 5 | export class GroupItem extends vscode.TreeItem { 6 | constructor(label: string, public fileName: string) { 7 | super(label, vscode.TreeItemCollapsibleState.None); 8 | } 9 | 10 | get tooltip(): string { 11 | return ''; 12 | } 13 | 14 | private icon(status: string): string { 15 | let icon = STATUS_ICONS.DEFAULT.file; 16 | if (!!status) { 17 | Object.values(STATUS_ICONS).forEach((value) => { 18 | if (status.toUpperCase().indexOf(value.text.toUpperCase()) !== -1) { 19 | icon = value.file; 20 | } 21 | }); 22 | } 23 | return icon; 24 | } 25 | 26 | iconPath = { 27 | light: utilities.getIconsPath(`light/${this.icon(this.fileName)}`), 28 | dark: utilities.getIconsPath(`dark/${this.icon(this.fileName)}`), 29 | }; 30 | 31 | contextValue = 'GroupItem'; 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": ["${workspaceRoot}/out/src/**/*.js"], 14 | "preLaunchTask": "npm: compile" 15 | }, 16 | { 17 | "name": "Launch Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test"], 22 | "stopOnEntry": false, 23 | "sourceMaps": true, 24 | "outFiles": ["${workspaceRoot}/out/test/**/*.js"], 25 | "preLaunchTask": "npm" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/commands/change-issue-assignee.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { IssueItem } from '../explorer/item/issue-item'; 3 | import { logger, selectValues, store } from '../services'; 4 | 5 | export default async function changeIssueAssignee(issueItem: IssueItem): Promise { 6 | try { 7 | if (issueItem && issueItem.issue && store.canExecuteJiraAPI()) { 8 | let issue = issueItem.issue; 9 | let assignee = await selectValues.selectAssignee(false, false, true, undefined); 10 | if (!!assignee) { 11 | // call Jira API 12 | const res = await store.state.jira.setAssignIssue({ issueKey: issue.key, assignee: assignee }); 13 | await vscode.commands.executeCommand('jira-plugin.refresh'); 14 | } 15 | } else { 16 | if (store.canExecuteJiraAPI()) { 17 | logger.printErrorMessageInOutputAndShowAlert('Use this command from Jira Plugin EXPLORER'); 18 | } 19 | } 20 | } catch (err) { 21 | logger.printErrorMessageInOutputAndShowAlert(err); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/explorer/item/issue-item.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { configuration } from '../../services'; 3 | import { IIssue } from '../../services/http.model'; 4 | import { CONFIG } from '../../shared/constants'; 5 | 6 | export class IssueItem extends vscode.TreeItem { 7 | private isCollapsible = (issue: IIssue): boolean => (!!issue.fields.subtasks && !!issue.fields.subtasks.length ? true : false); 8 | 9 | constructor(public readonly issue: IIssue, public readonly command?: vscode.Command) { 10 | super(`${issue.key} - ${issue.fields.summary}`, vscode.TreeItemCollapsibleState.None); 11 | if (configuration.get(CONFIG.GROUP_TASK_AND_SUBTASKS) && this.isCollapsible(issue)) { 12 | this.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 13 | this.label += ' - subtasks: ' + (issue.fields.subtasks || []).map((issue) => issue.key).join(', '); 14 | } 15 | } 16 | 17 | get tooltip(): string { 18 | return `(${this.issue.fields.status.name}) ${this.label}`; 19 | } 20 | 21 | contextValue = 'IssueItem'; 22 | } 23 | -------------------------------------------------------------------------------- /src/explorer/item/limit-info.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { STATUS_ICONS } from '../../shared/constants'; 3 | import { utilities } from '../../services'; 4 | 5 | export class LimitInfoItem extends vscode.TreeItem { 6 | constructor() { 7 | super(`Viewable rows maximum has been reached. Modify filters to narrow search`, vscode.TreeItemCollapsibleState.None); 8 | } 9 | 10 | get tooltip(): string { 11 | return this.label || ''; 12 | } 13 | 14 | private icon(status: string): string { 15 | let icon = STATUS_ICONS.DEFAULT.file; 16 | if (!!status) { 17 | Object.values(STATUS_ICONS).forEach((value) => { 18 | if (status.toUpperCase().indexOf(value.text.toUpperCase()) !== -1) { 19 | icon = value.file; 20 | } 21 | }); 22 | } 23 | return icon; 24 | } 25 | 26 | iconPath = { 27 | light: utilities.getIconsPath(`light/${STATUS_ICONS.DEFAULT.file}`), 28 | dark: utilities.getIconsPath(`dark/${STATUS_ICONS.DEFAULT.file}`), 29 | }; 30 | 31 | contextValue = 'LimitInfoItem'; 32 | } 33 | -------------------------------------------------------------------------------- /src/services/logger.service.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { store } from '.'; 3 | 4 | export default class LoggerService { 5 | public printErrorMessageInOutputAndShowAlert(err: any) { 6 | if (store.state.channel) { 7 | vscode.window.showErrorMessage(`Check logs in Jira Plugin terminal output.`); 8 | store.state.channel.append(`${err.message || err}\n`); 9 | } 10 | } 11 | 12 | public printErrorMessageInOutput(err: any) { 13 | if (store.state.channel) { 14 | store.state.channel.append(`${err.message || err}\n`); 15 | } 16 | } 17 | 18 | private debugMode() { 19 | const editor = vscode.window.activeTextEditor; 20 | if (editor && editor.document) { 21 | const text = editor.document.getText(); 22 | if (text.indexOf('JIRA_PLUGIN_DEBUG_MODE') !== -1) { 23 | return true; 24 | } 25 | } 26 | return false; 27 | } 28 | 29 | public jiraPluginDebugLog(message: string, value: any) { 30 | if (this.debugMode() && store.state.channel) { 31 | store.state.channel.append(`${message}: ${value}\n`); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /scripts/rename-files.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | var dir = ''; 4 | var regExp = ''; 5 | var replace = ''; 6 | 7 | switch (process.env.TYPE) { 8 | case '1': { 9 | dir = './out/test/utils'; 10 | regExp = '.test.js'; 11 | replace = '.test._js'; 12 | break; 13 | } 14 | case '2': { 15 | dir = './out/test/utils'; 16 | regExp = '.test._js'; 17 | replace = '.test.js'; 18 | break; 19 | } 20 | case '3': { 21 | dir = './out/test/tests'; 22 | regExp = '.test.js'; 23 | replace = '.test._js'; 24 | break; 25 | } 26 | case '4': { 27 | dir = './out/test/tests'; 28 | regExp = '.test._js'; 29 | replace = '.test.js'; 30 | break; 31 | } 32 | } 33 | 34 | const match = RegExp(regExp, 'g'); 35 | const files = fs.readdirSync(dir); 36 | 37 | files 38 | .filter(function(file) { 39 | return file.match(match); 40 | }) 41 | .forEach(function(file) { 42 | var filePath = path.join(dir, file), 43 | newFilePath = path.join(dir, file.replace(match, replace)); 44 | 45 | fs.renameSync(filePath, newFilePath); 46 | }); 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Giorgio Boa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | * 4 | * This file is providing the test runner to use when running extension tests. 5 | * By default the test runner in use is Mocha based. 6 | * 7 | * You can provide your own test runner if you want to override it by exporting 8 | * a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | * host can call to run the tests. The test runner is expected to use console.log 10 | * to report the results back to the caller. When the tests are finished, return 11 | * a possible error to the callback or null if none. 12 | */ 13 | // tslint:disable-next-line 14 | var testRunner = require('vscode/lib/testrunner'); 15 | 16 | // you can directly control Mocha options by uncommenting the following lines 17 | // see https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 18 | testRunner.configure({ 19 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 20 | useColors: true // colored output from test results 21 | }); 22 | 23 | module.exports = testRunner; 24 | -------------------------------------------------------------------------------- /src/services/index.ts: -------------------------------------------------------------------------------- 1 | import IssuesExplorer from '../explorer/issues-explorer'; 2 | import ConfigurationService from './configuration.service'; 3 | import GitIntegrationService from './git-integration.service'; 4 | import IssueHelperService from './issue-helper.service'; 5 | import LoggerService from './logger.service'; 6 | import NotificationService from './notifications.service'; 7 | import SelectValuesService from './select-values.service'; 8 | import StatusBarService from './status-bar.service'; 9 | import StoreService from './store.service'; 10 | import UtilitiesService from './utilities.service'; 11 | 12 | export const store = new StoreService(); 13 | export const configuration = new ConfigurationService(); 14 | export const issuesExplorer = new IssuesExplorer(); 15 | export const logger = new LoggerService(); 16 | export const utilities = new UtilitiesService(); 17 | export const selectValues = new SelectValuesService(); 18 | export const gitIntegration = new GitIntegrationService(); 19 | export const statusBar = new StatusBarService(); 20 | export const notifications = new NotificationService(); 21 | export const issueHelper = new IssueHelperService(); 22 | -------------------------------------------------------------------------------- /src/commands/change-issue-status.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { IssueItem } from '../explorer/item/issue-item'; 3 | import { logger, selectValues, store } from '../services'; 4 | 5 | export default async function changeIssueStatus(issueItem: IssueItem): Promise { 6 | try { 7 | if (issueItem && issueItem.issue && store.canExecuteJiraAPI()) { 8 | let issue = issueItem.issue; 9 | const newTransitionId = await selectValues.selectTransition(issue.key); 10 | if (newTransitionId) { 11 | // call Jira API 12 | const result = await store.state.jira.setTransition({ 13 | issueKey: issue.key, 14 | transition: { 15 | transition: { 16 | id: newTransitionId, 17 | }, 18 | }, 19 | }); 20 | await vscode.commands.executeCommand('jira-plugin.refresh'); 21 | } 22 | } else { 23 | if (store.canExecuteJiraAPI()) { 24 | logger.printErrorMessageInOutputAndShowAlert('Use this command from Jira Plugin EXPLORER'); 25 | } 26 | } 27 | } catch (err) { 28 | logger.printErrorMessageInOutputAndShowAlert(err); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/issue-add-worklog.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { logger, store, utilities } from '../services'; 3 | import { NO_WORKING_ISSUE } from '../shared/constants'; 4 | import openIssue from './open-issue'; 5 | 6 | export default async function issueAddWorklog(issueKey: string, timeSpentSeconds: number, comment: string): Promise { 7 | try { 8 | if (issueKey !== NO_WORKING_ISSUE.key) { 9 | if (store.canExecuteJiraAPI()) { 10 | // call Jira API 11 | const actualTimeSpentSeconds = Math.ceil(timeSpentSeconds / 60) * 60; 12 | const startedTime = new Date(Date.now() - actualTimeSpentSeconds * 1000); 13 | const response = await store.state.jira.addWorkLog({ 14 | issueKey, 15 | timeSpentSeconds: actualTimeSpentSeconds, 16 | comment, 17 | started: utilities.dateToLocalISO(startedTime), 18 | }); 19 | const action = await vscode.window.showInformationMessage(`Worklog added`, 'Open in browser'); 20 | if (action === 'Open in browser') { 21 | openIssue(issueKey); 22 | } 23 | } 24 | } 25 | } catch (err) { 26 | logger.printErrorMessageInOutputAndShowAlert(err); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/tests/utilities.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import UtilitiesService from '../../src/services/utilities.service'; 3 | 4 | suite(`Utilities`, () => { 5 | const utilities = new UtilitiesService(); 6 | 7 | test('SecondsToHHMMSS', async () => { 8 | const result = utilities.secondsToHHMMSS(60); 9 | assert.strictEqual(result, '00:01:00'); 10 | }); 11 | 12 | test('SecondsToMinutes', async () => { 13 | const result = utilities.floorSecondsToMinutes(45); 14 | assert.strictEqual(result, 0); 15 | }); 16 | 17 | test('FloorSecondsToMinutes', async () => { 18 | const result = utilities.floorSecondsToMinutes(61); 19 | assert.strictEqual(result, 1); 20 | }); 21 | 22 | test('AddStatusIcon with valid status', async () => { 23 | const result = utilities.addStatusIcon('Open', true); 24 | assert.strictEqual(result, '$(beaker) Open '); 25 | }); 26 | 27 | test('AddStatusIcon with valid status, no description', async () => { 28 | const result = utilities.addStatusIcon('Open', false); 29 | assert.strictEqual(result, '$(beaker)'); 30 | }); 31 | 32 | test('AddStatusIcon with NOT valid status', async () => { 33 | const result = utilities.addStatusIcon('ABC', true); 34 | assert.strictEqual(result, '$(info) ABC '); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # test 64 | .vscode-test/ 65 | 66 | # out 67 | out/ 68 | 69 | # settings 70 | test/utils/settings.ts 71 | -------------------------------------------------------------------------------- /src/commands/setup-credentials.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { configuration, store } from '../services'; 3 | import { CONFIG } from '../shared/constants'; 4 | 5 | export default async function setupCredentials(): Promise { 6 | const baseUrl = configuration.get(CONFIG.BASE_URL); 7 | 8 | if (baseUrl) { 9 | // ask for reset prev configuration 10 | const res = await vscode.window.showQuickPick(['Yes', 'No'], { placeHolder: 'Config already exist. Reset config?' }); 11 | if (res === 'No') { 12 | return; 13 | } 14 | } 15 | 16 | // store settings 17 | configuration.set( 18 | CONFIG.BASE_URL, 19 | await vscode.window.showInputBox({ 20 | ignoreFocusOut: true, 21 | password: false, 22 | placeHolder: 'Your Jira url', 23 | }) 24 | ); 25 | 26 | configuration.set( 27 | CONFIG.USERNAME, 28 | await vscode.window.showInputBox({ 29 | ignoreFocusOut: true, 30 | password: false, 31 | placeHolder: 'Your Jira username or full email for OAuth', 32 | }) 33 | ); 34 | 35 | configuration.setPassword( 36 | await vscode.window.showInputBox({ 37 | ignoreFocusOut: true, 38 | password: true, 39 | placeHolder: 'Your Jira password or token for OAuth', 40 | }) 41 | ); 42 | 43 | await store.connectToJira(); 44 | } 45 | -------------------------------------------------------------------------------- /src/shared/document-link-provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { IProject } from '../services/http.model'; 3 | import '../services'; 4 | import { configuration } from '../services'; 5 | import { CONFIG } from './constants'; 6 | 7 | export class IssueLinkProvider implements vscode.DocumentLinkProvider { 8 | constructor(private projects: IProject[]) {} 9 | 10 | private get baseUrl(): string | undefined { 11 | return configuration.get(CONFIG.BASE_URL); 12 | } 13 | 14 | public provideDocumentLinks(document: vscode.TextDocument): vscode.ProviderResult { 15 | const baseUrl = this.baseUrl; 16 | if (!baseUrl || !this.projects) { 17 | return null; 18 | } 19 | return document 20 | .getText() 21 | .split('\n') 22 | .reduce((matches, line, no) => this.getMatchesOnLine(baseUrl, line, no, matches), [] as vscode.DocumentLink[]); 23 | } 24 | 25 | private getMatchesOnLine(baseUrl: string, line: string, lineNo: number, matches: vscode.DocumentLink[]): vscode.DocumentLink[] { 26 | this.projects.forEach((project) => { 27 | const expr = new RegExp(`${project.key}-\\d+`, 'gi'); 28 | let match; 29 | while (true) { 30 | match = expr.exec(line); 31 | if (match === null) { 32 | break; 33 | } 34 | const range = new vscode.Range( 35 | new vscode.Position(lineNo, match.index), 36 | new vscode.Position(lineNo, match.index + match[0].length) 37 | ); 38 | matches.push({ 39 | range, 40 | target: vscode.Uri.parse(`${baseUrl}/browse/${match[0]}`), 41 | }); 42 | } 43 | }); 44 | return matches; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import ConfigurationService from '../../src/services/configuration.service'; 2 | import { CONFIG } from '../../src/shared/constants'; 3 | import { ISettings } from './settings.model'; 4 | 5 | export const backupSettings = async (configurationService: ConfigurationService, settings: ISettings) => { 6 | settings.baseUrl = await configurationService.get(CONFIG.BASE_URL); 7 | settings.username = await configurationService.get(CONFIG.USERNAME); 8 | settings.workingProject = await configurationService.get(CONFIG.WORKING_PROJECT); 9 | settings.enableWorkingIssue = await configurationService.get(CONFIG.ENABLE_WORKING_ISSUE); 10 | settings.workingIssueStatues = await configurationService.get(CONFIG.WORKING_ISSUE_STATUSES); 11 | settings.password = await configurationService.credentials.password; 12 | settings.counter = await configurationService.getGlobalCounter(); 13 | settings.workingIssue = await configurationService.getGlobalWorkingIssue(); 14 | }; 15 | 16 | export const restoreSettings = async (configurationService: ConfigurationService, settings: ISettings) => { 17 | await configurationService.set(CONFIG.BASE_URL, settings.baseUrl); 18 | await configurationService.set(CONFIG.USERNAME, settings.username); 19 | await configurationService.set(CONFIG.WORKING_PROJECT, settings.workingProject); 20 | await configurationService.set(CONFIG.ENABLE_WORKING_ISSUE, settings.enableWorkingIssue); 21 | await configurationService.set(CONFIG.WORKING_ISSUE_STATUSES, settings.workingIssueStatues); 22 | await configurationService.setPassword(settings.password); 23 | await configurationService.setGlobalCounter(settings.counter); 24 | await configurationService.setGlobalWorkingIssue(settings.workingIssue); 25 | }; 26 | -------------------------------------------------------------------------------- /test/tests/document-link-provider.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | import { IssueLinkProvider } from '../../src/shared/document-link-provider'; 4 | 5 | suite(`Issue Document Link`, () => { 6 | const linkProvider = new IssueLinkProvider([ 7 | { 8 | key: 'projectA', 9 | expand: 'projectA', 10 | self: 'projectA', 11 | id: 'projectA', 12 | name: 'projectA' 13 | }, 14 | { 15 | key: 'projectB', 16 | expand: 'projectB', 17 | self: 'projectB', 18 | id: 'projectB', 19 | name: 'projectB' 20 | }, 21 | { 22 | key: 'projectC', 23 | expand: 'projectC', 24 | self: 'projectC', 25 | id: 'projectC', 26 | name: 'projectC' 27 | } 28 | ]); 29 | 30 | const tests = [ 31 | { 32 | title: 'Right project', 33 | text: `projectA-123`, 34 | numberOfLinks: 1 35 | }, 36 | { 37 | title: 'More right project', 38 | text: `projectA-123 39 | projectB-123 40 | abc abc abc`, 41 | numberOfLinks: 2 42 | }, 43 | { 44 | title: 'Right project with other chars', 45 | text: `* projectA-123 46 | abc abc abc`, 47 | numberOfLinks: 1 48 | }, 49 | { 50 | title: 'Right project with other chars', 51 | text: `/* 52 | * projectA-123 53 | */`, 54 | numberOfLinks: 1 55 | }, 56 | { 57 | title: 'Right project with other chars', 58 | text: `// projectA-123 test test`, 59 | numberOfLinks: 1 60 | }, 61 | { 62 | title: 'Wrong project', 63 | text: `projectD-123`, 64 | numberOfLinks: 0 65 | }, 66 | { 67 | title: 'Uppercase', 68 | text: `PROJECTA-123`, 69 | numberOfLinks: 1 70 | } 71 | ]; 72 | tests.forEach(entry => { 73 | test(entry.title, async () => { 74 | const document = await vscode.workspace.openTextDocument({ 75 | language: 'text', 76 | content: entry.text 77 | }); 78 | const links: vscode.ProviderResult = await linkProvider.provideDocumentLinks(document); 79 | assert.strictEqual((links || []).length, entry.numberOfLinks); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/shared/jira-instance-patch.ts: -------------------------------------------------------------------------------- 1 | const cleanOptions = (options: any): void => { 2 | if (!!options.headers && Object.keys(options.headers).length === 0) { 3 | delete options.header; 4 | } 5 | if (!!options.body && Object.keys(options.body).length === 0) { 6 | delete options.body; 7 | } 8 | if (!!options.qs && Object.keys(options.qs).length === 0) { 9 | delete options.qs; 10 | } 11 | }; 12 | 13 | export const patchJiraInstance = (jiraInstance: any) => { 14 | // custom event 15 | // solve this issue -> https://github.com/floralvikings/jira-connector/issues/115 16 | const customGetAllProjects = (opts: any, callback: any) => { 17 | const options = jiraInstance.project.buildRequestOptions(opts, '', 'GET'); 18 | cleanOptions(options); 19 | if (!!opts && !!opts.apiVersion) { 20 | options.uri = options.uri.replace('rest/api/2/', `rest/api/${opts.apiVersion}/`); 21 | } 22 | return jiraInstance.makeRequest(options, callback); 23 | }; 24 | jiraInstance.project.getAllProjects = customGetAllProjects; 25 | 26 | const customRequest = (method: 'GET' | 'POST', uri: string, headers?: {}, body?: {}) => { 27 | const options = jiraInstance.project.buildRequestOptions({}, '', method, body || {}); 28 | for (const key of Object.keys(headers || {})) { 29 | if (!options.headers) { 30 | options.headers = {}; 31 | } 32 | options.headers[key] = (headers)[key]; 33 | } 34 | options.uri = uri; 35 | cleanOptions(options); 36 | return jiraInstance.makeRequest(options, undefined); 37 | }; 38 | jiraInstance.project.customRequest = customRequest; 39 | 40 | jiraInstance.originalRequestLib = jiraInstance.requestLib; 41 | const customRequestLib = (options: any) => { 42 | if (!!options && !!options.headers && options.headers.deleteAuth) { 43 | delete options.auth; 44 | delete options.headers.deleteAuth; 45 | } 46 | return jiraInstance.originalRequestLib(options); 47 | }; 48 | jiraInstance.requestLib = customRequestLib; 49 | 50 | jiraInstance.originalMakeRequest = jiraInstance.makeRequest; 51 | const customMakeRequest = (options: any, callback: any, successString: any) => { 52 | return jiraInstance.originalMakeRequest(options, callback, successString); 53 | }; 54 | jiraInstance.makeRequest = customMakeRequest; 55 | }; 56 | -------------------------------------------------------------------------------- /src/commands/issue-add-comment.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { IssueItem } from '../explorer/item/issue-item'; 3 | import { configuration, logger, selectValues, store } from '../services'; 4 | import { CONFIG } from '../shared/constants'; 5 | 6 | export default async function issueAddComment(issueItem: IssueItem, markAsInternal: boolean): Promise { 7 | try { 8 | if (issueItem && issueItem.issue && store.canExecuteJiraAPI()) { 9 | let issue = issueItem.issue; 10 | let text = await vscode.window.showInputBox({ 11 | ignoreFocusOut: true, 12 | placeHolder: 'Comment text...', 13 | }); 14 | if (!!text) { 15 | // ask for assignee if there is one or more [@] in the comment 16 | const num = (text.match(new RegExp('[@]', 'g')) || []).length; 17 | for (let i = 0; i < num; i++) { 18 | const assignee = await selectValues.selectAssignee(false, false, true, undefined); 19 | if (!!assignee) { 20 | text = text.replace('[@]', `[~${assignee}]`); 21 | } else { 22 | logger.printErrorMessageInOutputAndShowAlert('Abort command, wrong parameter.'); 23 | return; 24 | } 25 | } 26 | // call Jira API 27 | const comment = !markAsInternal 28 | ? { body: text } 29 | : { 30 | body: text, 31 | properties: [ 32 | { 33 | key: 'sd.public.comment', 34 | value: { 35 | internal: true, 36 | }, 37 | }, 38 | ], 39 | }; 40 | const response = await store.state.jira.addNewComment({ issueKey: issue.key, comment }); 41 | await vscode.commands.executeCommand('jira-plugin.refresh'); 42 | // modal 43 | const action = await vscode.window.showInformationMessage('Comment created', 'Open in browser'); 44 | if (action === 'Open in browser') { 45 | const baseUrl = configuration.get(CONFIG.BASE_URL); 46 | const url = 47 | `${baseUrl}/browse/${issue.key}` + 48 | `?focusedCommentId=${response.id}` + 49 | `&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel` + 50 | `#comment-${response.id}`; 51 | await vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(url)); 52 | } 53 | } 54 | } else { 55 | if (store.canExecuteJiraAPI()) { 56 | logger.printErrorMessageInOutputAndShowAlert('Use this command from Jira Plugin EXPLORER'); 57 | } 58 | } 59 | } catch (err) { 60 | logger.printErrorMessageInOutputAndShowAlert(err); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/commands/stop-working-issue.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import NoWorkingIssuePick from '../picks/no-working-issue-pick'; 3 | import { configuration, statusBar, store, utilities } from '../services'; 4 | import { IIssue, IWorkingIssue } from '../services/http.model'; 5 | import { CONFIG, NO_WORKING_ISSUE, TRACKING_TIME_MODE } from '../shared/constants'; 6 | 7 | export default async function stopWorkingIssue(storedWorkingIssue: IWorkingIssue, preloadedIssue: IIssue): Promise { 8 | const workingIssue = store.state.workingIssue || new NoWorkingIssuePick().pickValue; 9 | if (!!workingIssue.issue.key && workingIssue.issue.key !== NO_WORKING_ISSUE.key) { 10 | if ( 11 | workingIssue.issue.key !== NO_WORKING_ISSUE.key && 12 | configuration.get(CONFIG.TRACKING_TIME_MODE) !== TRACKING_TIME_MODE.NEVER && 13 | utilities.floorSecondsToMinutes(workingIssue.trackingTime) >= configuration.get(CONFIG.WORKLOG_MINIMUM_TRACKING_TIME) 14 | ) { 15 | // old working issue has trackingTime and it's equal or bigger then WORKLOG_MINIMUM_TRACKING_TIME setting 16 | statusBar.clearWorkingIssueInterval(); 17 | // modal for create Worklog 18 | // To re-implement being asked if you want to store a comment, remove the following let, uncomment until the if (!!comment) and replace it with the if (action...) 19 | let comment = await vscode.window.showInputBox({ 20 | ignoreFocusOut: true, 21 | placeHolder: 'Add worklog comment...', 22 | }); 23 | // let action = await vscode.window.showInformationMessage( 24 | // `Add worklog for the previous working issue ${workingIssue.issue.key} | timeSpent: ${utilities.secondsToHHMMSS( 25 | // workingIssue.trackingTime 26 | // )} ?`, 27 | // ACTIONS.YES_WITH_COMMENT, 28 | // ACTIONS.YES, 29 | // ACTIONS.NO 30 | // ); 31 | // menage response 32 | // let comment = 33 | // action === ACTIONS.YES_WITH_COMMENT 34 | // ? await vscode.window.showInputBox({ 35 | // ignoreFocusOut: true, 36 | // placeHolder: 'Add worklog comment...' 37 | // }) 38 | // : ''; 39 | if (!!comment) { 40 | // if (action === ACTIONS.YES || action === ACTIONS.YES_WITH_COMMENT) { 41 | await vscode.commands.executeCommand( 42 | 'jira-plugin.issueAddWorklog', 43 | store.state.workingIssue.issue.key, 44 | store.state.workingIssue.trackingTime, 45 | comment || '' 46 | ); 47 | } 48 | } 49 | // set the new working issue 50 | // store.changeStateWorkingIssue(newIssue, 0); 51 | store.changeStateWorkingIssue(new NoWorkingIssuePick().pickValue, 0); 52 | } else { 53 | vscode.window.showInformationMessage('You are not currently working on a ticket'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/issue.mock.ts: -------------------------------------------------------------------------------- 1 | export const mockIssues = [ 2 | { 3 | id: '1', 4 | key: 'ABC-1', 5 | fields: { 6 | description: 'ABC 1 Long description issue ', 7 | summary: 'ABC 1 Summary issue', 8 | status: { 9 | name: 'In Progress' 10 | } 11 | } 12 | }, 13 | { 14 | id: '2', 15 | key: 'ABC-2', 16 | fields: { 17 | description: 'ABC 2 Long description issue', 18 | summary: 'ABC 2 Summary issue', 19 | status: { 20 | name: 'Open' 21 | } 22 | } 23 | }, 24 | { 25 | id: '3', 26 | key: 'ABC-3', 27 | fields: { 28 | description: 'ABC 3 Long description issue', 29 | summary: 'ABC 3 Summary issue', 30 | status: { 31 | name: 'Reopened' 32 | } 33 | } 34 | }, 35 | { 36 | id: '4', 37 | key: 'ABC-4', 38 | fields: { 39 | description: 'ABC 4 Long description issue', 40 | summary: 'ABC 4 Summary issue', 41 | status: { 42 | name: 'Resolved' 43 | } 44 | } 45 | }, 46 | { 47 | id: '5', 48 | key: 'ABC-5', 49 | fields: { 50 | description: 'ABC 5 Long description issue', 51 | summary: 'Summary issue ABC 5', 52 | status: { 53 | name: 'Closed' 54 | } 55 | } 56 | }, 57 | { 58 | id: '6', 59 | key: 'ABC-6', 60 | fields: { 61 | description: 'ABC-6 Long description issue', 62 | summary: 'Summary issue ABC 6', 63 | status: { 64 | name: 'Detailing' 65 | } 66 | } 67 | }, 68 | { 69 | id: '7', 70 | key: 'ABC-7', 71 | fields: { 72 | description: 'ABC-7 Long description issue', 73 | summary: 'Summary issue ABC 7', 74 | status: { 75 | name: 'Estimating' 76 | } 77 | } 78 | }, 79 | { 80 | id: '8', 81 | key: 'ABC-8', 82 | fields: { 83 | description: 'ABC-8 Long description issue', 84 | summary: 'Summary issue ABC 8', 85 | status: { 86 | name: 'Remarked' 87 | } 88 | } 89 | }, 90 | { 91 | id: '9', 92 | key: 'ABC-9', 93 | fields: { 94 | description: 'ABC-9 Long description issue', 95 | summary: 'Summary issue ABC 9', 96 | status: { 97 | name: 'Approved' 98 | } 99 | } 100 | }, 101 | { 102 | id: '10', 103 | key: 'ABC-10', 104 | fields: { 105 | description: 'ABC-10 Long description issue', 106 | summary: 'Summary issue ABC 10', 107 | status: { 108 | name: 'Suspended' 109 | } 110 | } 111 | }, 112 | { 113 | id: '11', 114 | key: 'ABC-11', 115 | fields: { 116 | description: 'ABC-11 Long description issue', 117 | summary: 'Summary issue ABC 11', 118 | status: { 119 | name: 'Estimated' 120 | } 121 | } 122 | } 123 | ]; 124 | -------------------------------------------------------------------------------- /images/icons/jira-explorer-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 19 | 21 | image/svg+xml 22 | 24 | 25 | 26 | 27 | 28 | 30 | 32 | 39 | 43 | 47 | 48 | 56 | 57 | 61 | 65 | 70 | 75 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Hi there! We're thrilled that you'd like to contribute to this project.
4 | Your help is essential for keeping it great. 5 | 6 | ## Submitting a pull request 7 | 8 | 1. [Fork](https://github.com/gioboa/jira-plugin/fork) and clone the repository 9 | 1. Configure and install the dependencies: `npm install` 10 | 1. Compile code: `npm run compile` 11 | 1. Make sure the tests pass on your machine: `npm run test` 12 | 1. Create your feature branch: `git checkout -b my-new-feature` 13 | 1. Make your change, add tests, and make sure the tests still pass 14 | 1. Add your changes: `git add .` 15 | 1. Commit your changes: `git commit -am 'Add some feature'` 16 | 1. Push to the branch: `git push origin my-new-feature` 17 | 1. Submit a [pull request](https://github.com/gioboa/jira-plugin/compare) 18 | 1. Pat yourself on the back and wait for your pull request to be reviewed and merged. 19 | 20 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 21 | 22 | - Write and update tests. 23 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 24 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 25 | 26 | ## Commit Message Format 27 | 28 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 29 | format that includes a **type**, a **scope** and a **subject**: 30 | 31 | ``` 32 | (): 33 | 34 | 35 | 36 |