inital commit
This commit is contained in:
10
.editorconfig
Normal file
10
.editorconfig
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
tab_width = 4
|
||||||
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
|
||||||
|
main.js
|
||||||
24
.eslintrc
Normal file
24
.eslintrc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"env": { "node": true },
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"no-prototype-builtins": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
.github/workflows/release.yml
vendored
Normal file
34
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
name: Release Obsidian plugin
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "18.x"
|
||||||
|
|
||||||
|
- name: Build plugin
|
||||||
|
run: |
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
tag="${GITHUB_REF#refs/tags/}"
|
||||||
|
|
||||||
|
gh release create "$tag" \
|
||||||
|
--title="$tag" \
|
||||||
|
main.js manifest.json styles.css
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# vscode
|
||||||
|
.vscode
|
||||||
|
|
||||||
|
# Intellij
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# npm
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Don't include the compiled main.js file in the repo.
|
||||||
|
# They should be uploaded to GitHub releases instead.
|
||||||
|
main.js
|
||||||
|
|
||||||
|
# Exclude sourcemaps
|
||||||
|
*.map
|
||||||
|
|
||||||
|
# obsidian
|
||||||
|
data.json
|
||||||
|
|
||||||
|
# Exclude macOS Finder (System Explorer) View States
|
||||||
|
.DS_Store
|
||||||
|
.continue
|
||||||
|
examples
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Maksim Syomochkin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
80
README.md
Normal file
80
README.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Obsidian Jira Plugin
|
||||||
|
|
||||||
|
A plugin for [Obsidian](https://obsidian.md/) that enables you to create Markdown notes from Jira issues and keep them up to date automatically. Easily generate notes from your Jira tickets and ensure your vault always reflects the latest issue status.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Fetch Jira issues and save them as Markdown notes using a customizable template.
|
||||||
|
- Update individual or all tracked issues with a single click.
|
||||||
|
- Create new issue notes from selected text in the editor.
|
||||||
|
- Manage your Jira connection and tracked issues from the settings tab.
|
||||||
|
- Status bar button for quick updates of the current issue note.
|
||||||
|
- Supports ignoring TLS certificate errors (for self-signed Jira servers).
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. **Manual Installation**
|
||||||
|
- Download the plugin files (`main.ts`, `settings.ts`, `styles.css`) and build them using your preferred TypeScript build process.
|
||||||
|
- Place the compiled files in your Obsidian vault's `.obsidian/plugins/your-plugin-folder/` directory.
|
||||||
|
|
||||||
|
2. **Via Community Plugins**
|
||||||
|
- (Not yet available in the community plugins list.)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. **Configure Settings**
|
||||||
|
- Go to `Settings → Jira Plugin`.
|
||||||
|
- Enter your Jira API URL, username, and password.
|
||||||
|
- Set the path where issue notes will be saved (e.g., `jira`).
|
||||||
|
- Optionally, customize the Markdown template for issue notes.
|
||||||
|
- Toggle "Ignore TLS certificate errors" if your Jira server uses a self-signed certificate.
|
||||||
|
|
||||||
|
2. **Creating an Issue Note**
|
||||||
|
- Select an issue key (e.g., `PROJ-123`) in the editor.
|
||||||
|
- Run the command palette and select `Jira: Create issue`.
|
||||||
|
- The plugin will fetch the issue from Jira, create a note using your template, and link it.
|
||||||
|
|
||||||
|
3. **Updating Issues**
|
||||||
|
- Open an issue note and click the status bar button to update it.
|
||||||
|
- Or, use the command palette:
|
||||||
|
- `Jira: Update issue` updates the current issue note.
|
||||||
|
- `Jira: Update all issues` updates all tracked issues.
|
||||||
|
|
||||||
|
4. **Managing Issues**
|
||||||
|
- In the settings tab, view, add, or remove tracked issues.
|
||||||
|
|
||||||
|
## Template
|
||||||
|
|
||||||
|
The template is filled with data received from Jira.
|
||||||
|
You can explore the available data fields by performing a `curl` request to your Jira API, for example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -u your-username:password https://jira.example.com/rest/api/2/issue/PROJ-123
|
||||||
|
```
|
||||||
|
|
||||||
|
The template uses [Nunjucks](https://mozilla.github.io/nunjucks/) syntax. Example:
|
||||||
|
|
||||||
|
### Example template
|
||||||
|
```nunjucks
|
||||||
|
---
|
||||||
|
jira_assignee: "[[{{ fields.assignee.displayName }}]]"
|
||||||
|
jira_reporter: "[[{{ fields.reporter.displayName }}]]"
|
||||||
|
jira_type: {{ fields.issuetype.name }}
|
||||||
|
jira_project: {{fields.project.key}}
|
||||||
|
jira_status: {{fields.status.name}}
|
||||||
|
jira_summary: {{ fields.summary }}
|
||||||
|
jira_priority: {{fields.priority.name}}
|
||||||
|
jira_link: https://jira.example.com.com/browse/{{key}}
|
||||||
|
jira_components: [{% for comp in fields.components %}"{{ comp.name }}"{% if not loop.last %}, {% endif %}{% endfor %}]
|
||||||
|
---
|
||||||
|
# [[{{ key }}]]
|
||||||
|
|
||||||
|
{{ fields.description }}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT. See LICENSE for details.
|
||||||
|
|
||||||
|
## Support & Feedback
|
||||||
|
For questions, suggestions, or bug reports, please open an issue on the GitHub repository.
|
||||||
50
esbuild.config.mjs
Normal file
50
esbuild.config.mjs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import esbuild from "esbuild";
|
||||||
|
import process from "process";
|
||||||
|
import builtins from "builtin-modules";
|
||||||
|
|
||||||
|
const banner =
|
||||||
|
`/*
|
||||||
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
|
*/
|
||||||
|
`;
|
||||||
|
|
||||||
|
const prod = (process.argv[2] === "production");
|
||||||
|
|
||||||
|
const context = await esbuild.context({
|
||||||
|
banner: {
|
||||||
|
js: banner,
|
||||||
|
},
|
||||||
|
entryPoints: ["src/main.ts"],
|
||||||
|
bundle: true,
|
||||||
|
platform: "node",
|
||||||
|
external: [
|
||||||
|
"obsidian",
|
||||||
|
"electron",
|
||||||
|
"@codemirror/autocomplete",
|
||||||
|
"@codemirror/collab",
|
||||||
|
"@codemirror/commands",
|
||||||
|
"@codemirror/language",
|
||||||
|
"@codemirror/lint",
|
||||||
|
"@codemirror/search",
|
||||||
|
"@codemirror/state",
|
||||||
|
"@codemirror/view",
|
||||||
|
"@lezer/common",
|
||||||
|
"@lezer/highlight",
|
||||||
|
"@lezer/lr",
|
||||||
|
...builtins],
|
||||||
|
format: "cjs",
|
||||||
|
target: "es2018",
|
||||||
|
logLevel: "info",
|
||||||
|
sourcemap: prod ? false : "inline",
|
||||||
|
treeShaking: true,
|
||||||
|
outfile: "main.js",
|
||||||
|
minify: prod,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prod) {
|
||||||
|
await context.rebuild();
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
await context.watch();
|
||||||
|
}
|
||||||
10
manifest.json
Normal file
10
manifest.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"id": "jira-issue-notes",
|
||||||
|
"name": "Jira Issue Notes",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"minAppVersion": "0.15.0",
|
||||||
|
"description": "Plugin for fetching and managing Jira issues directly from your notes",
|
||||||
|
"author": "Maxim Syomochkin",
|
||||||
|
"authorUrl": "https://mak-sim.ru",
|
||||||
|
"isDesktopOnly": true
|
||||||
|
}
|
||||||
2546
package-lock.json
generated
Normal file
2546
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "jira-issue-notes",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Plugin for fetching and managing Jira issues directly from your notes.",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "node esbuild.config.mjs",
|
||||||
|
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
|
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^16.11.6",
|
||||||
|
"@types/nunjucks": "^3.2.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||||
|
"@typescript-eslint/parser": "5.29.0",
|
||||||
|
"builtin-modules": "3.3.0",
|
||||||
|
"esbuild": "0.17.3",
|
||||||
|
"obsidian": "latest",
|
||||||
|
"punycode": "^2.3.1",
|
||||||
|
"tslib": "2.4.0",
|
||||||
|
"typescript": "4.7.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"node-fetch": "^3.3.2",
|
||||||
|
"nunjucks": "^3.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/jiraApi.ts
Executable file
47
src/jiraApi.ts
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
import fetch from "node-fetch";
|
||||||
|
import https from "https";
|
||||||
|
import { Notice } from "obsidian";
|
||||||
|
|
||||||
|
export class JiraApi {
|
||||||
|
constructor(
|
||||||
|
url: string,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
ignoreTLS = true
|
||||||
|
) {
|
||||||
|
this.headers = {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization:
|
||||||
|
"Basic " +
|
||||||
|
Buffer.from(`${username}:${password}`).toString("base64"),
|
||||||
|
};
|
||||||
|
this.url = `${url}/rest/api/2/issue/`;
|
||||||
|
this.ignoreTLS = ignoreTLS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private url: string;
|
||||||
|
private headers: any;
|
||||||
|
private ignoreTLS: boolean;
|
||||||
|
|
||||||
|
public async fetchIssue(issueKey: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const options: any = { headers: this.headers };
|
||||||
|
if (this.ignoreTLS) {
|
||||||
|
options.agent = new https.Agent({ rejectUnauthorized: false });
|
||||||
|
}
|
||||||
|
const response = await fetch(this.url + issueKey, options);
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorDetail = await response.text();
|
||||||
|
throw new Error(
|
||||||
|
`Error fetching issue: ${response.statusText}. Details: ${errorDetail}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const data: any = await response.json();
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching issue:", error);
|
||||||
|
new Notice("Error fetching issue: " + error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
src/main.ts
Normal file
136
src/main.ts
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { Editor, Notice, Plugin } from "obsidian";
|
||||||
|
|
||||||
|
import { renderString } from "nunjucks";
|
||||||
|
|
||||||
|
import { JiraSettings, JiraSettingTab, DEFAULT_SETTINGS } from "./settings";
|
||||||
|
import { JiraApi } from "./jiraApi";
|
||||||
|
|
||||||
|
export default class Jira extends Plugin {
|
||||||
|
settings: JiraSettings;
|
||||||
|
statusBarItemEl: HTMLElement;
|
||||||
|
jiraApi: JiraApi;
|
||||||
|
|
||||||
|
async onload() {
|
||||||
|
await this.loadSettings();
|
||||||
|
|
||||||
|
this.createJiraApi();
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "create-issue",
|
||||||
|
name: "Create issue",
|
||||||
|
editorCallback: this.createCallback.bind(this),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "update-issue",
|
||||||
|
name: "Update issue",
|
||||||
|
editorCallback: this.UpdateCallback.bind(this),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: "update-all-issues",
|
||||||
|
name: "Update all issues",
|
||||||
|
editorCallback: this.UpdateAllCallback.bind(this),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.statusBarItemEl = this.addStatusBarItem();
|
||||||
|
this.statusBarItemEl.addClass("jira-statusbar-btn");
|
||||||
|
|
||||||
|
this.statusBarItemEl.addEventListener("click", () => {
|
||||||
|
const file = this.app.workspace.getActiveFile();
|
||||||
|
|
||||||
|
if (file?.basename) {
|
||||||
|
this.UpdateIssue(file.basename);
|
||||||
|
} else {
|
||||||
|
new Notice("No active file to update.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.registerEvent(
|
||||||
|
this.app.workspace.on("active-leaf-change", () => {
|
||||||
|
const file = this.app.workspace.getActiveFile();
|
||||||
|
|
||||||
|
if (file && this.settings.issues.includes(file.basename)) {
|
||||||
|
this.statusBarItemEl.show();
|
||||||
|
this.statusBarItemEl.setText(`Update: ${file.basename}`);
|
||||||
|
} else {
|
||||||
|
this.statusBarItemEl.hide();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.addSettingTab(new JiraSettingTab(this.app, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
private createJiraApi() {
|
||||||
|
this.jiraApi = new JiraApi(
|
||||||
|
this.settings.apiUrl,
|
||||||
|
this.settings.username,
|
||||||
|
this.settings.password,
|
||||||
|
this.settings.ignoreTLS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveIssueToFile(issueKey: string, data: any) {
|
||||||
|
const filePath = `${this.settings.path}/${issueKey}.md`;
|
||||||
|
const content = renderString(this.settings.template, data);
|
||||||
|
await this.app.vault.adapter.write(filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UpdateIssue(issueKey: string) {
|
||||||
|
console.log(`Updating ${issueKey}`);
|
||||||
|
new Notice(`Updating ${issueKey}`);
|
||||||
|
const data = await this.jiraApi.fetchIssue(issueKey);
|
||||||
|
await this.saveIssueToFile(issueKey, data);
|
||||||
|
new Notice(`Issue ${issueKey} updated`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UpdateCallback() {
|
||||||
|
const file = this.app.workspace.getActiveFile();
|
||||||
|
if (file && this.settings.issues.includes(file.basename)) {
|
||||||
|
const data = await this.jiraApi.fetchIssue(file.basename);
|
||||||
|
|
||||||
|
await this.saveIssueToFile(file.basename, data);
|
||||||
|
|
||||||
|
new Notice(`Issue ${file.basename} updated`);
|
||||||
|
} else {
|
||||||
|
new Notice("This note is not linked to a Jira issue.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UpdateAllCallback() {
|
||||||
|
for (const issue of this.settings.issues) {
|
||||||
|
this.UpdateIssue(issue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createCallback(editor: Editor) {
|
||||||
|
const issueKey = editor.getSelection();
|
||||||
|
const data = await this.jiraApi.fetchIssue(issueKey);
|
||||||
|
|
||||||
|
await this.saveIssueToFile(issueKey, data);
|
||||||
|
|
||||||
|
editor.replaceSelection(`[[${issueKey}]]`);
|
||||||
|
if (!this.settings.issues.includes(issueKey)) {
|
||||||
|
this.settings.issues.push(issueKey);
|
||||||
|
this.saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
new Notice(`Note ${issueKey} created`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onunload() {}
|
||||||
|
|
||||||
|
async loadSettings() {
|
||||||
|
this.settings = Object.assign(
|
||||||
|
{},
|
||||||
|
DEFAULT_SETTINGS,
|
||||||
|
await this.loadData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSettings() {
|
||||||
|
await this.saveData(this.settings);
|
||||||
|
this.createJiraApi();
|
||||||
|
}
|
||||||
|
}
|
||||||
190
src/settings.ts
Normal file
190
src/settings.ts
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||||
|
|
||||||
|
import Jira from "./main";
|
||||||
|
|
||||||
|
export interface JiraSettings {
|
||||||
|
apiUrl: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
path: string;
|
||||||
|
template: string;
|
||||||
|
issues: string[];
|
||||||
|
ignoreTLS: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_SETTINGS: JiraSettings = {
|
||||||
|
apiUrl: "https://jira.example.com",
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
path: "jira",
|
||||||
|
template: "",
|
||||||
|
issues: [],
|
||||||
|
ignoreTLS: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export class JiraSettingTab extends PluginSettingTab {
|
||||||
|
plugin: Jira;
|
||||||
|
|
||||||
|
constructor(app: App, plugin: Jira) {
|
||||||
|
super(app, plugin);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
display(): void {
|
||||||
|
const { containerEl } = this;
|
||||||
|
|
||||||
|
containerEl.empty();
|
||||||
|
|
||||||
|
new Setting(containerEl).setName("apiUrl").addText((text) =>
|
||||||
|
text
|
||||||
|
.setValue(this.plugin.settings.apiUrl)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.apiUrl = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
new Setting(containerEl).setName("username").addText((text) =>
|
||||||
|
text
|
||||||
|
.setValue(this.plugin.settings.username)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.username = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
new Setting(containerEl).setName("password").addText((text) => {
|
||||||
|
text.setValue(this.plugin.settings.password).onChange(
|
||||||
|
async (value) => {
|
||||||
|
this.plugin.settings.password = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
text.inputEl.type = "password";
|
||||||
|
|
||||||
|
const toggleBtn = document.createElement("button");
|
||||||
|
toggleBtn.type = "button";
|
||||||
|
toggleBtn.style.marginLeft = "8px";
|
||||||
|
toggleBtn.style.background = "none";
|
||||||
|
toggleBtn.style.border = "none";
|
||||||
|
toggleBtn.style.cursor = "pointer";
|
||||||
|
let visible = false;
|
||||||
|
|
||||||
|
const eyeIcon = `
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7z"/>
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
const eyeOffIcon = `
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||||
|
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M17.94 17.94A10.94 10.94 0 0 1 12 19c-7 0-11-7-11-7a21.81 21.81 0 0 1 5.06-6.06"/>
|
||||||
|
<path d="M1 1l22 22"/>
|
||||||
|
<path d="M9.53 9.53A3 3 0 0 0 12 15a3 3 0 0 0 2.47-5.47"/>
|
||||||
|
<path d="M14.47 14.47A3 3 0 0 1 12 9a3 3 0 0 1-2.47 5.47"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
toggleBtn.innerHTML = eyeIcon;
|
||||||
|
|
||||||
|
toggleBtn.onclick = () => {
|
||||||
|
visible = !visible;
|
||||||
|
text.inputEl.type = visible ? "text" : "password";
|
||||||
|
toggleBtn.innerHTML = visible ? eyeOffIcon : eyeIcon;
|
||||||
|
};
|
||||||
|
|
||||||
|
text.inputEl.parentElement?.appendChild(toggleBtn);
|
||||||
|
});
|
||||||
|
new Setting(containerEl).setName("path").addText((text) =>
|
||||||
|
text.setValue(this.plugin.settings.path).onChange(async (value) => {
|
||||||
|
this.plugin.settings.path = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
new Setting(containerEl)
|
||||||
|
.setName("Ignore TLS certificate errors")
|
||||||
|
.setDesc("Disable TLS certificate validation (not secure)")
|
||||||
|
.addToggle((toggle) =>
|
||||||
|
toggle
|
||||||
|
.setValue(this.plugin.settings.ignoreTLS)
|
||||||
|
.onChange(async (value) => {
|
||||||
|
this.plugin.settings.ignoreTLS = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
new Setting(containerEl).setName("template").addTextArea((text) => {
|
||||||
|
text.setValue(this.plugin.settings.template).onChange(
|
||||||
|
async (value) => {
|
||||||
|
this.plugin.settings.template = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
text.inputEl.setAttr("rows", 25);
|
||||||
|
text.inputEl.setAttr("cols", 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
const details = document.createElement("details");
|
||||||
|
const summary = document.createElement("summary");
|
||||||
|
summary.textContent = "Issues (expand to view/edit)";
|
||||||
|
details.appendChild(summary);
|
||||||
|
|
||||||
|
const issuesList = document.createElement("ul");
|
||||||
|
this.plugin.settings.issues.forEach((issue, idx) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.textContent = issue;
|
||||||
|
|
||||||
|
const removeBtn = document.createElement("button");
|
||||||
|
removeBtn.type = "button";
|
||||||
|
removeBtn.innerHTML = `
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="red" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="3 6 5 6 21 6"></polyline>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path>
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17"></line>
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17"></line>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
removeBtn.style.background = "none";
|
||||||
|
removeBtn.style.border = "none";
|
||||||
|
removeBtn.style.cursor = "pointer";
|
||||||
|
removeBtn.style.marginLeft = "8px";
|
||||||
|
removeBtn.title = "Remove";
|
||||||
|
|
||||||
|
removeBtn.onclick = async () => {
|
||||||
|
this.plugin.settings.issues.splice(idx, 1);
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.display();
|
||||||
|
};
|
||||||
|
li.appendChild(removeBtn);
|
||||||
|
|
||||||
|
issuesList.appendChild(li);
|
||||||
|
});
|
||||||
|
details.appendChild(issuesList);
|
||||||
|
|
||||||
|
// Форма для добавления нового issue
|
||||||
|
const addDiv = document.createElement("div");
|
||||||
|
const input = document.createElement("input");
|
||||||
|
input.type = "text";
|
||||||
|
input.placeholder = "Add issue...";
|
||||||
|
addDiv.appendChild(input);
|
||||||
|
|
||||||
|
const addBtn = document.createElement("button");
|
||||||
|
addBtn.textContent = "Add";
|
||||||
|
addBtn.onclick = async () => {
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (value && !this.plugin.settings.issues.includes(value)) {
|
||||||
|
this.plugin.settings.issues.push(value);
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.display();
|
||||||
|
}
|
||||||
|
input.value = "";
|
||||||
|
};
|
||||||
|
addDiv.appendChild(addBtn);
|
||||||
|
|
||||||
|
details.appendChild(addDiv);
|
||||||
|
|
||||||
|
containerEl.appendChild(details);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
styles.css
Normal file
17
styles.css
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.jira-statusbar-btn {
|
||||||
|
padding: 2px 10px;
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
color: var(--text-normal);
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 2px 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jira-statusbar-btn:hover {
|
||||||
|
background: var(--background-modifier-hover);
|
||||||
|
border-color: var(--interactive-accent);
|
||||||
|
}
|
||||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"inlineSourceMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES6",
|
||||||
|
"allowJs": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"importHelpers": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"lib": [
|
||||||
|
"DOM",
|
||||||
|
"ES5",
|
||||||
|
"ES6",
|
||||||
|
"ES7"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
14
version-bump.mjs
Normal file
14
version-bump.mjs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { readFileSync, writeFileSync } from "fs";
|
||||||
|
|
||||||
|
const targetVersion = process.env.npm_package_version;
|
||||||
|
|
||||||
|
// read minAppVersion from manifest.json and bump version to target version
|
||||||
|
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
|
||||||
|
const { minAppVersion } = manifest;
|
||||||
|
manifest.version = targetVersion;
|
||||||
|
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
|
||||||
|
|
||||||
|
// update versions.json with target version and minAppVersion from manifest.json
|
||||||
|
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
|
||||||
|
versions[targetVersion] = minAppVersion;
|
||||||
|
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
||||||
3
versions.json
Normal file
3
versions.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"1.0.0": "0.15.0"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user