inital commit

This commit is contained in:
2025-05-30 22:21:59 +03:00
commit 67374bf430
19 changed files with 3265 additions and 0 deletions

47
src/jiraApi.ts Executable file
View 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
View 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
View 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);
}
}