diff --git a/manifest.json b/manifest.json index 2a5565b..78088f8 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "jira-sync", "name": "Jira Issue Manager", - "version": "1.1.7", + "version": "1.1.8", "minAppVersion": "1.7.7", "description": "Get Jira issues, create and update them. Issue status and worklog management.", "author": "Alamion", diff --git a/package-lock.json b/package-lock.json index a6ed4a3..ff0376f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,26 @@ { "name": "obsidian-sample-plugin", - "version": "1.0.0", + "version": "1.1.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-sample-plugin", - "version": "1.0.0", + "version": "1.1.8", "license": "MIT", "dependencies": { - "@types/turndown": "^5.0.5", "acorn": "^8.14.1", - "turndown": "^7.2.0", + "highlight.js": "^11.11.1", "yaml": "^2.7.0" }, "devDependencies": { + "@types/lodash": "^4.17.16", "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", "builtin-modules": "3.3.0", "esbuild": "0.25.0", + "lodash": "^4.17.21", "obsidian": "latest", "tslib": "2.4.0", "typescript": "4.7.4" @@ -185,11 +186,6 @@ "dev": true, "peer": true }, - "node_modules/@mixmark-io/domino": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", - "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==" - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -252,6 +248,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.16", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz", + "integrity": "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==", + "dev": true + }, "node_modules/@types/node": { "version": "16.18.121", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.121.tgz", @@ -269,11 +271,6 @@ "@types/estree": "*" } }, - "node_modules/@types/turndown": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz", - "integrity": "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.29.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz", @@ -1251,6 +1248,14 @@ "node": ">=8" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1431,6 +1436,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -1897,14 +1908,6 @@ "dev": true, "license": "0BSD" }, - "node_modules/turndown": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz", - "integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==", - "dependencies": { - "@mixmark-io/domino": "^2.2.0" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index b83db6f..60cfb67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-sample-plugin", - "version": "1.1.7", + "version": "1.1.8", "description": "This is a sample plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { @@ -13,17 +13,20 @@ "author": "", "license": "MIT", "devDependencies": { + "@types/lodash": "^4.17.16", "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/parser": "5.29.0", "builtin-modules": "3.3.0", "esbuild": "0.25.0", + "lodash": "^4.17.21", "obsidian": "latest", "tslib": "2.4.0", "typescript": "4.7.4" }, "dependencies": { "acorn": "^8.14.1", + "highlight.js": "^11.11.1", "yaml": "^2.7.0" } } diff --git a/src/api/auth.ts b/src/api/auth.ts index b052473..da18175 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -49,24 +49,56 @@ export function validateSettings(plugin: JiraPlugin): boolean { return true; } +// Helper function to create Basic Auth header +function createBasicAuthHeader(email: string, token: string): string { + const credentials = `${email}:${token}`; + // Convert to base64 + const base64Credentials = btoa(credentials); + return `Basic ${base64Credentials}`; +} export async function getAuthHeaders(plugin: JiraPlugin): Promise> { - const pat = plugin.settings.apiToken; - if (pat) { - return { - Authorization: `Bearer ${pat}`, - "Content-type": "application/json", - Origin: plugin.settings.jiraUrl, - }; - } - let cookie = localStorage.getItem(getSessionCookieKey(plugin)); - if (!cookie) { - await authenticate(plugin); - cookie = localStorage.getItem(getSessionCookieKey(plugin)); - } - return { - Cookie: `${plugin.settings.sessionCookieName}=${cookie}`, + // Common headers + const commonHeaders = { "Content-type": "application/json", Origin: plugin.settings.jiraUrl, }; + + // Personal Access Token + const pat = plugin.settings.apiToken?.trim() || ""; + + // Option 1: Bearer token (PAT) + if (plugin.settings.authMethod === "bearer" || !plugin.settings.authMethod) { + return { + ...commonHeaders, + Authorization: `Bearer ${pat}`, + }; + } + + // Option 2: Basic Auth with API token + else if (plugin.settings.authMethod === "basic") { + return { + ...commonHeaders, + Authorization: createBasicAuthHeader(plugin.settings.email || "", pat), + }; + } + + else if (plugin.settings.authMethod === "session") { + // Option 3: Session cookie + let cookie = localStorage.getItem(getSessionCookieKey(plugin)); + if (!cookie) { + await authenticate(plugin); + cookie = localStorage.getItem(getSessionCookieKey(plugin)); + } + + if (cookie) { + return { + ...commonHeaders, + Cookie: `${plugin.settings.sessionCookieName}=${cookie}`, + }; + } + } + + // No valid auth method found + throw new Error("No valid authentication method available"); } diff --git a/src/api/base.ts b/src/api/base.ts index 6239756..bf0620f 100644 --- a/src/api/base.ts +++ b/src/api/base.ts @@ -1,32 +1,38 @@ import JiraPlugin from "../main"; import {requestUrl} from "obsidian"; -import {getAuthHeaders} from "./auth"; +import {authenticate, getAuthHeaders, getSessionCookieKey} from "./auth"; import {debugLog} from "../tools/debugLogging"; export async function baseRequest( plugin: JiraPlugin, method: string, additional_url_path: string, - body?: string + body?: string, + retries: number = 1 ): Promise { - const response = await requestUrl({ + const requestParams = { url: `${plugin.settings.jiraUrl}/rest/api/2${additional_url_path}`, method: method, headers: await getAuthHeaders(plugin), contentType: "application/json", throw: false, body - }); + } + debugLog(requestParams) + const response = await requestUrl(requestParams); + debugLog(response); if (response.status < 200 || response.status >= 300) { - const error = new Error(`Error updating issue: + if (response.status === 401 && retries > 0) { + await authenticate(plugin); + return await baseRequest(plugin, method, additional_url_path, body, retries-1); + } + // console.error(error); + throw new Error(` ${response.text || "Unknown error"} ${body && '\nbody:' + body || ""}`); - console.error(error); - throw error; } debugLog({"url": `${plugin.settings.jiraUrl}/rest/api/2${additional_url_path}`, "method": method, "body": body}); - debugLog(response); return response.status === 204 ? null : response.json; } diff --git a/src/commands/getIssue.ts b/src/commands/getIssue.ts index 3a2addf..be267f9 100644 --- a/src/commands/getIssue.ts +++ b/src/commands/getIssue.ts @@ -37,11 +37,6 @@ function openIssueModal(plugin: JiraPlugin): void { } export async function fetchAndOpenIssue(plugin: JiraPlugin, file: TFile | null, issueKey: string): Promise { - try { - const issue = await fetchIssue(plugin, issueKey); - await createOrUpdateIssueNote(plugin, issue, file?.path); - } catch (error) { - new Notice("Error retrieving issue: " + (error.message || "Unknown error")); - console.error(error); - } + const issue = await fetchIssue(plugin, issueKey); + await createOrUpdateIssueNote(plugin, issue, file?.path); } diff --git a/src/constants/defaultIssue.ts b/src/constants/defaultIssue.ts new file mode 100644 index 0000000..34dc632 --- /dev/null +++ b/src/constants/defaultIssue.ts @@ -0,0 +1,405 @@ +export const defaultIssue = { + "expand": "", + "id": "", + "self": "", + "key": "", + "fields": { + "issuetype": { + "self": "", + "id": "", + "description": "", + "iconUrl": "", + "name": "", + "subtask": 0, + "avatarId": 0 + }, + "timespent": 0, + "project": { + "self": "", + "id": "", + "key": "", + "name": "", + "projectTypeKey": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + } + }, + "fixVersions": [], + "aggregatetimespent": 0, + "resolution": null, + "resolutiondate": null, + "workratio": 0, + "lastViewed": "", + "watches": { + "self": "", + "watchCount": 0, + "isWatching": 0 + }, + "created": "", + "priority": { + "self": "", + "iconUrl": "", + "name": "", + "id": "" + }, + "labels": [], + "timeestimate": 0, + "aggregatetimeoriginalestimate": null, + "versions": [], + "issuelinks": [], + "assignee": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "updated": "", + "status": { + "self": "", + "description": "", + "iconUrl": "", + "name": "", + "id": "", + "statusCategory": { + "self": "", + "id": 0, + "key": "", + "colorName": "", + "name": "" + } + }, + "components": [], + "timeoriginalestimate": null, + "description": "", + "archiveddate": null, + "timetracking": { + "remainingEstimate": "", + "timeSpent": "", + "remainingEstimateSeconds": 0, + "timeSpentSeconds": 0 + }, + "attachment": [], + "aggregatetimeestimate": 0, + "summary": "", + "creator": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "subtasks": [], + "reporter": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "aggregateprogress": { + "progress": 0, + "total": 0, + "percent": 0 + }, + "environment": null, + "duedate": null, + "progress": { + "progress": 0, + "total": 0, + "percent": 0 + }, + "comment": { + "comments": [], + "maxResults": 0, + "total": 0, + "startAt": 0 + }, + "votes": { + "self": "", + "votes": 0, + "hasVoted": 0 + }, + "worklog": { + "startAt": 0, + "maxResults": 0, + "total": 0, + "worklogs": [ + { + "self": "", + "author": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "updateAuthor": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "comment": "", + "created": "", + "updated": "", + "started": "", + "timeSpent": "", + "timeSpentSeconds": 0, + "id": "", + "issueId": "" + }, + { + "self": "", + "author": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "updateAuthor": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "comment": "", + "created": "", + "updated": "", + "started": "", + "timeSpent": "", + "timeSpentSeconds": 0, + "id": "", + "issueId": "" + }, + { + "self": "", + "author": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "updateAuthor": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "comment": "", + "created": "", + "updated": "", + "started": "", + "timeSpent": "", + "timeSpentSeconds": 0, + "id": "", + "issueId": "" + }, + { + "self": "", + "author": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "updateAuthor": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "comment": "", + "created": "", + "updated": "", + "started": "", + "timeSpent": "", + "timeSpentSeconds": 0, + "id": "", + "issueId": "" + }, + { + "self": "", + "author": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "updateAuthor": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "comment": "", + "created": "", + "updated": "", + "started": "", + "timeSpent": "", + "timeSpentSeconds": 0, + "id": "", + "issueId": "" + }, + { + "self": "", + "author": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "updateAuthor": { + "self": "", + "name": "", + "key": "", + "emailAddress": "", + "avatarUrls": { + "48x48": "", + "24x24": "", + "16x16": "", + "32x32": "" + }, + "displayName": "", + "active": 0, + "timeZone": "" + }, + "comment": "", + "created": "", + "updated": "", + "started": "", + "timeSpent": "", + "timeSpentSeconds": 0, + "id": "", + "issueId": "" + } + ] + }, + "archivedby": null + } +} diff --git a/src/file_operations/getIssue.ts b/src/file_operations/getIssue.ts index 529a6e2..833a6e7 100644 --- a/src/file_operations/getIssue.ts +++ b/src/file_operations/getIssue.ts @@ -32,6 +32,7 @@ export async function createOrUpdateIssueNote(plugin: JiraPlugin, issue: JiraIss new Notice(`Issue ${issue.key} imported successfully`); } catch (error) { new Notice("Error creating issue note: " + (error.message || "Unknown error")); + console.error(error); } } diff --git a/src/interfaces/settingsTypes.ts b/src/interfaces/settingsTypes.ts new file mode 100644 index 0000000..7988240 --- /dev/null +++ b/src/interfaces/settingsTypes.ts @@ -0,0 +1,59 @@ +import { App } from "obsidian"; +import JiraPlugin from "../main"; + +/** + * Function type for field mappings + */ +export type JiraFieldMappingFunction = (...args: any[]) => any; + +/** + * Interface for field mappings between Obsidian and Jira + */ +export interface JiraFieldMapping { + toJira: JiraFieldMappingFunction; + fromJira: JiraFieldMappingFunction; +} + +/** + * Interface for string representation of field mappings + */ +export interface JiraFieldMappingString { + toJira: string; + fromJira: string; +} + +/** + * Base interface for settings components + */ +export interface SettingsComponent { + render(containerEl: HTMLElement): void; + hide?(): void; +} + +/** + * Props for settings components + */ +export interface SettingsComponentProps { + app: App; + plugin: JiraPlugin; + onSettingsChange?: () => Promise; +} + +/** + * Validation result interface + */ +export interface ValidationResult { + isValid: boolean; + errorMessage?: string; +} + +/** + * Interface for collapsed sections + */ +export interface CollapsedSections { + connection: boolean; + general: boolean; + fieldMappings: boolean; + rawIssueViewer: boolean; + testFieldMappings: boolean; +} diff --git a/src/settings/CollapsibleSection.ts b/src/settings/CollapsibleSection.ts new file mode 100644 index 0000000..94ce516 --- /dev/null +++ b/src/settings/CollapsibleSection.ts @@ -0,0 +1,98 @@ +import { ButtonComponent } from "obsidian"; +import JiraPlugin from "../main"; +import { SettingsComponent } from "../interfaces/settingsTypes"; +import { debugLog } from "../tools/debugLogging"; + +export class CollapsibleSection { + private containerEl: HTMLDivElement; + private contentEl: HTMLElement; + private child: SettingsComponent; + private isOpen = false; + private headerText: string; + private saveStateKey?: string; + private plugin: JiraPlugin; + private toggleBtn: ButtonComponent; + + constructor( + plugin: JiraPlugin, + parentEl: HTMLElement, + child: SettingsComponent, + headerText: string, + isOpenByDefault = false, + saveStateKey?: string + ) { + this.plugin = plugin; + this.child = child; + this.headerText = headerText; + this.isOpen = isOpenByDefault; + this.saveStateKey = saveStateKey; + + // Create the container div for the entire collapsible section + this.containerEl = parentEl.createDiv({ + cls: "jira-collapsible-section", + }); + + // Create the header element + const headerEl = this.containerEl.createDiv({ + cls: "jira-collapsible-header", + text: headerText, + }); + headerEl.style.cursor = "pointer"; + headerEl.addEventListener("click", async () => { + await this.toggle(); + }); + + // Add toggle button to the header + this.toggleBtn = new ButtonComponent(headerEl as any); + this.toggleBtn.setIcon(this.isOpen ? "chevron-down" : "chevron-right") + .setTooltip(this.isOpen ? "Collapse" : "Expand") + + // Content wrapper + this.contentEl = this.containerEl.createDiv({ + cls: "jira-collapsible-content", + }); + + this.child.render(this.contentEl); + + if (!this.isOpen) { + this.contentEl.hide(); + } + + // Optional: restore saved state + // if (this.saveStateKey && this.plugin.settings.collapsedSections[this.saveStateKey] !== undefined) { + // this.isOpen = !this.plugin.settings.collapsedSections[this.saveStateKey]; + // if (!this.isOpen) { + // this.contentEl.hide(); + // } + // } + + debugLog("Rendered", this.saveStateKey || this.headerText, "as", this.isOpen); + } + + public getContentContainer(): HTMLElement { + return this.contentEl; + } + + private async toggle() { + this.isOpen = !this.isOpen; + debugLog("Toggled", this.saveStateKey || this.headerText, "to", this.isOpen); + + const icon = this.isOpen ? "chevron-down" : "chevron-right"; + const tooltip = this.isOpen ? "Collapse" : "Expand"; + + // Rebuild or update the toggle button if needed + // Or store a reference to the button and just update its properties + + if (this.isOpen) { + this.contentEl.show(); + } else { + this.contentEl.hide(); + } + this.toggleBtn.setIcon(icon).setTooltip(tooltip); + + if (this.saveStateKey && this.plugin) { + this.plugin.settings.collapsedSections[this.saveStateKey] = this.isOpen; + await this.plugin.saveSettings(); + } + } +} diff --git a/src/settings/JiraSettingTab.ts b/src/settings/JiraSettingTab.ts index 7a57616..f97eae1 100644 --- a/src/settings/JiraSettingTab.ts +++ b/src/settings/JiraSettingTab.ts @@ -1,446 +1,94 @@ -import {App, normalizePath, Notice, PluginSettingTab, setIcon, Setting} from "obsidian"; +import { App, PluginSettingTab } from "obsidian"; import JiraPlugin from "../main"; -import {obsidianJiraFieldMappings} from "../constants/obsidianJiraFieldsMapping"; -import { - jiraFunctionToString, transform_string_to_functions_mappings, validateFunctionString -} from "../tools/convertFunctionString"; -import {debugLog} from "../tools/debugLogging"; +import { SettingsComponentProps } from "../interfaces/settingsTypes"; +import { ConnectionSettingsComponent } from "./components/ConnectionSettingsComponent"; +import { GeneralSettingsComponent } from "./components/GeneralSettingsComponent"; +import { FieldMappingsComponent } from "./components/FieldMappingsComponent"; +import { RawIssueViewerComponent } from "./components/RawIssueViewerComponent"; +import { TestFieldMappingsComponent } from "./components/TestFieldMappingsComponent"; +import { debugLog } from "../tools/debugLogging"; +import { CollapsibleSection } from "./CollapsibleSection"; /** - * Settings tab for the plugin + * Settings tab for the Jira plugin + * Orchestrates all components and manages the overall settings UI */ export class JiraSettingTab extends PluginSettingTab { private plugin: JiraPlugin; + private connectionSettings: ConnectionSettingsComponent; + private generalSettings: GeneralSettingsComponent; + private fieldMappingsSettings: FieldMappingsComponent; + private rawIssueViewer: RawIssueViewerComponent; + private testFieldMappings: TestFieldMappingsComponent; constructor(app: App, plugin: JiraPlugin) { super(app, plugin); this.plugin = plugin; + + // Create shared props for all components + const componentProps: SettingsComponentProps = { + app, + plugin, + onSettingsChange: this.handleSettingsChange.bind(this) + }; + + // Initialize all settings components + this.connectionSettings = new ConnectionSettingsComponent(componentProps); + this.generalSettings = new GeneralSettingsComponent(componentProps); + this.fieldMappingsSettings = new FieldMappingsComponent(componentProps); + this.rawIssueViewer = new RawIssueViewerComponent(componentProps); + this.testFieldMappings = new TestFieldMappingsComponent({ ...componentProps, getCurrentIssue: () => this.rawIssueViewer.getCurrentIssue() }); + this.rawIssueViewer.onIssueDataChange = () => this.testFieldMappings.setCurrentIssue(this.rawIssueViewer.getCurrentIssue()); } - // Function to save all field mappings - async saveStringFieldMappings (element: HTMLDivElement) { - debugLog(`From strings ${JSON.stringify(this.plugin.settings.fieldMappingsStrings)}\nand funcs ${JSON.stringify(this.plugin.settings.fieldMappings)}`) - const mappings: Record = {}; - const fieldItems = element.querySelectorAll(".field-mapping-item"); - - fieldItems.forEach(item => { - const fieldNameInput = item.querySelector(".field-name-input"); - const toJiraInput = item.querySelector(".to-jira-input"); - const fromJiraInput = item.querySelector(".from-jira-input"); - if (!fieldNameInput || !toJiraInput || !fromJiraInput) { - return; - } - - const fieldName = (fieldNameInput as any).value.trim(); - const toJira = (toJiraInput as any).value.trim(); - const fromJira = (fromJiraInput as any).value.trim(); - - // Only save valid mappings with all fields filled - if (fieldName) { - mappings[fieldName] = { - toJira: toJira, - fromJira: fromJira - }; - } - }); - - debugLog(`Mappings: ${JSON.stringify(mappings)}`) - - this.plugin.settings.fieldMappingsStrings = mappings; - this.plugin.settings.fieldMappings = await transform_string_to_functions_mappings(mappings, - this.plugin.settings.enableFieldValidation); - - // Save the functional mappings - debugLog(`To strings ${JSON.stringify(this.plugin.settings.fieldMappingsStrings)}\nand funcs ${JSON.stringify(this.plugin.settings.fieldMappings)}`) + /** + * Handle settings changes from any component + * This can be used for cross-component updates or validation + */ + private async handleSettingsChange(): Promise { + debugLog("Settings changed, handling updates"); await this.plugin.saveSettings(); - }; - + } + /** + * Display the settings UI + */ display(): void { const { containerEl } = this; containerEl.empty(); - new Setting(containerEl) - .setName("Jira URL") - .setDesc("Your Jira instance URL (e.g., https://yourcompany.atlassian.net)") - .addText((text) => - text - .setPlaceholder("URL") - .setValue(this.plugin.settings.jiraUrl) - .onChange(async (value) => { - this.plugin.settings.jiraUrl = value; - await this.plugin.saveSettings(); - }) - ); + new CollapsibleSection(this.plugin, containerEl, this.connectionSettings, "Connection settings", + this.plugin.settings.collapsedSections.connection, "connection").getContentContainer(); - new Setting(containerEl) - .setName("Jira PAT") - .setDesc("Personal Access Token (preferred over username+password authentication)") - .addText((text) => { - text.inputEl.type = "password"; - text - .setPlaceholder("NjM5MDI5MzQ0NT...") - .setValue(this.plugin.settings.apiToken) - .onChange(async (value) => { - this.plugin.settings.apiToken = value; - await this.plugin.saveSettings(); - }) - }); + new CollapsibleSection(this.plugin, containerEl, this.generalSettings, "General settings", + this.plugin.settings.collapsedSections.general, "general").getContentContainer(); - new Setting(containerEl) - .setName("Jira username") - .setDesc("Your Jira username or email") - .addText((text) => - text - .setPlaceholder("admin") - .setValue(this.plugin.settings.username) - .onChange(async (value) => { - this.plugin.settings.username = value; - await this.plugin.saveSettings(); - }) - ); + new CollapsibleSection(this.plugin, containerEl, this.fieldMappingsSettings, "Field mappings", + this.plugin.settings.collapsedSections.fieldMappings, "fieldMappings").getContentContainer(); - new Setting(containerEl) - .setName("Jira password") - .setDesc("Your Jira password or API token") - .addText((text) => { - text.inputEl.type = "password"; - text - .setPlaceholder("qwerty") - .setValue(this.plugin.settings.password) - .onChange(async (value) => { - this.plugin.settings.password = value; - await this.plugin.saveSettings(); - }); - }); + new CollapsibleSection(this.plugin, containerEl, this.rawIssueViewer, "Raw issue viewer", + this.plugin.settings.collapsedSections.rawIssueViewer, "rawIssueViewer").getContentContainer(); - - new Setting(containerEl) - .setName("Session cookie name") - .setDesc("Only needed for username+password authentication") - .addText((text) => - text - .setPlaceholder("JSESSIONID") - .setValue(this.plugin.settings.sessionCookieName) - .onChange(async (value) => { - this.plugin.settings.sessionCookieName = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerEl) - .setName("Issues folder") - .setDesc("Folder where Jira issues will be stored") - .addText((text) => - text - .setPlaceholder("jira-issues") - .setValue(this.plugin.settings.issuesFolder) - .onChange(async (value) => { - this.plugin.settings.issuesFolder = value; - await this.plugin.saveSettings(); - }) - ); - - new Setting(containerEl) - .setName("Template path") - .setDesc("The path of used template for creating Jira tasks (with or without .md extension)") - .addText((text) => - text - .setPlaceholder("templates/template1") - .setValue(this.plugin.settings.templatePath) - .onChange(async (value) => { - value = normalizePath(value); - this.plugin.settings.templatePath = value; - await this.plugin.saveSettings(); - }) - ); - - // Add Jira-Obsidian Mapping setting - new Setting(containerEl).setName("Jira-Obsidian field mapping").setHeading(); - - // Create Field Mappings UI - const mappingSection = containerEl.createDiv({ cls: "jira-field-mappings" }); - - // Add field mapping explanation - mappingSection.createEl("p", { - text: "Configure how fields are mapped between Obsidian and Jira. Each field requires both a toJira and fromJira transformation function." - }); - - new Setting(mappingSection) - .setName('Enable Field validation') - .setDesc('Check fields before saving as functions') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableFieldValidation) - .onChange(async (value) => { - this.plugin.settings.enableFieldValidation = value; - await this.plugin.saveSettings(); - const fieldItems = fieldsList.querySelectorAll(".field-mapping-item"); - - fieldItems.forEach(async item => { - const fieldNameInput = item.querySelector(".field-name-input"); - const toJiraInput = item.querySelector(".to-jira-input"); - const fromJiraInput = item.querySelector(".from-jira-input"); - if (fieldNameInput) await fieldValidation(fieldNameInput as HTMLInputElement, value, 'string'); - if (fieldNameInput) await fieldValidation(toJiraInput as HTMLTextAreaElement, value, 'function', ['value']); - if (fieldNameInput) await fieldValidation(fromJiraInput as HTMLTextAreaElement, value, 'function', ['issue', 'data_source']); - }); - })); - - // Add change listeners to update settings - const pointInvalidField = (validation: { isValid: boolean; errorMessage?: string }, element: HTMLInputElement | HTMLTextAreaElement, console_output: boolean) => { - if (!validation.isValid) { - element.classList.add("invalid"); - if (console_output) { - console.warn(`${validation.errorMessage}`); - new Notice(`${validation.errorMessage}`); - } - } else { - element.classList.remove("invalid"); - } - } - - async function fieldValidation(input: HTMLInputElement | HTMLTextAreaElement, requireValidating: boolean = true, type: string = 'string', validateParams: Array = []) { - const validatorFunction = async (event?: Event)=> { - const console_output = !!event; - let validation; - switch(type) { - case 'string': - validation = input.value.length === 0? { isValid: false, errorMessage: "Field name cannot be empty" } : { isValid: true }; - break; - case 'function': - validation = await validateFunctionString(input.value, validateParams); - break; - default: - validation = { isValid: true }; - break; - } - pointInvalidField(validation, input, console_output); - } - if (!input.hasOwnProperty('_validatorFunction')) { - Object.defineProperty(input, '_validatorFunction', { - value: validatorFunction, - writable: true, - configurable: true - }); - } else { - input.removeEventListener("change", (input as any)._validatorFunction); - (input as any)._validatorFunction = validatorFunction; - } - - if (requireValidating) { - input.addEventListener("change", (input as any)._validatorFunction); - await validatorFunction(); // Run initial validation - } else { - input.removeEventListener("change", (input as any)._validatorFunction); - pointInvalidField({ isValid: true}, input, false); // Clear any validation errors - } - } - - - - // Create a list container for field mappings - const fieldsList = mappingSection.createDiv({ cls: "field-mappings-list" }); - - // Function to add a new field mapping UI - const addFieldMapping = async (fieldName = "", toJira = "", fromJira = "") => { - const fieldContainer = fieldsList.createDiv({ cls: "field-mapping-item" }); - - // Field name input - const fieldNameContainer = fieldContainer.createDiv({ cls: "field-name-container" }); - fieldNameContainer.createEl("span", { text: "Field Name:", cls: "field-mapping-label" }); - const fieldNameInput = fieldNameContainer.createEl("input", { - type: "text", - value: fieldName, - cls: "field-name-input" - }); - - // toJira function input - const toJiraContainer = fieldContainer.createDiv({ cls: "to-jira-container" }); - toJiraContainer.createEl("span", { text: "to Jira:", cls: "field-mapping-label" }); - const toJiraInput = toJiraContainer.createEl("textarea", { - cls: "to-jira-input" - }); - toJiraInput.placeholder = "(value) => null"; - toJiraInput.value = toJira; - toJiraInput.rows = 1; - - // fromJira function input - const fromJiraContainer = fieldContainer.createDiv({ cls: "from-jira-container" }); - fromJiraContainer.createEl("span", { text: "from Jira:", cls: "field-mapping-label" }); - const fromJiraInput = fromJiraContainer.createEl("textarea", { - cls: "from-jira-input" - }); - fromJiraInput.value = fromJira; - fromJiraInput.placeholder = "(issue, data_source) => null"; - fromJiraInput.rows = 1; - - // Add remove button - const removeBtn = fieldContainer.createEl("button", { - text: "✕", - cls: "remove-field-btn" - }); - - // Handle field removal - removeBtn.addEventListener("click", () => { - fieldContainer.remove(); - // saveFieldMappings(); - }); - - await fieldValidation(fieldNameInput, this.plugin.settings.enableFieldValidation, 'string'); - await fieldValidation(toJiraInput, this.plugin.settings.enableFieldValidation, 'function', ['value']); - await fieldValidation(fromJiraInput, this.plugin.settings.enableFieldValidation, 'function', ['issue', 'data_source']); - - return { fieldNameInput, toJiraInput, fromJiraInput, container: fieldContainer }; - }; - - const buttonView = mappingSection.createDiv({ cls: "button-view" }); - - // Add button to add new field mapping - const addFieldBtn = buttonView.createEl("button", { - text: "", - cls: "add-field-mapping-btn" - }); - setIcon(addFieldBtn, "circle-plus") - - addFieldBtn.addEventListener("click", async () => { - await addFieldMapping(); - }); - - const resetBtn = buttonView.createEl("button", { - text: "", - cls: "reset-field-mappings-btn" - }) - setIcon(resetBtn, "refresh-cw") - resetBtn.addEventListener("click", async () => { - this.plugin.settings.fieldMappings = {}; - this.plugin.settings.fieldMappingsStrings = {}; - await loadExistingMappings(); - await this.plugin.saveSettings(); - }); - - const reloadBtn = buttonView.createEl("button", { - text: "", - cls: "reload-field-mappings-btn" - }) - setIcon(reloadBtn, "list-restart") - reloadBtn.addEventListener("click", async () => { - this.plugin.settings.fieldMappings = obsidianJiraFieldMappings; - - // Also reset the string representations with default values - const defaultMappingsStrings: Record = {}; - for (const [fieldName, mapping] of Object.entries(obsidianJiraFieldMappings)) { - defaultMappingsStrings[fieldName] = { - toJira: jiraFunctionToString(mapping.toJira, false), - fromJira: jiraFunctionToString(mapping.fromJira, true) - }; - } - this.plugin.settings.fieldMappingsStrings = defaultMappingsStrings; - - await loadExistingMappings(); - await this.plugin.saveSettings(); - }); - - // Load existing mappings if available - const loadExistingMappings = async () => { - debugLog(`Loading mapping settings`) - // Clear existing field list - fieldsList.empty(); - - // If we have string representations stored, use those - if (this.plugin.settings.fieldMappingsStrings && - Object.keys(this.plugin.settings.fieldMappingsStrings).length > 0) { - debugLog('Current mapping (string) is: ',this.plugin.settings.fieldMappingsStrings) - - const savedMappings = this.plugin.settings.fieldMappingsStrings; - for (const [fieldName, mapping] of Object.entries(savedMappings)) { - if (mapping && typeof mapping === 'object' && 'toJira' in mapping && 'fromJira' in mapping) { - await addFieldMapping( - fieldName, - mapping.toJira, - mapping.fromJira - ); - } - } - this.plugin.settings.fieldMappings = await transform_string_to_functions_mappings( - this.plugin.settings.fieldMappingsStrings, this.plugin.settings.enableFieldValidation); - } - // Otherwise, try to use the function mappings and convert them to strings - else if (this.plugin.settings.fieldMappings && - Object.keys(this.plugin.settings.fieldMappings).length > 0) { - debugLog('Current mapping is: ',this.plugin.settings.fieldMappings) - - const existingMappings = this.plugin.settings.fieldMappings; - for (const [fieldName, mapping] of Object.entries(existingMappings)) { - if (mapping && typeof mapping === 'object' && 'toJira' in mapping && 'fromJira' in mapping) { - await addFieldMapping( - fieldName, - jiraFunctionToString(mapping.toJira, false), - jiraFunctionToString(mapping.fromJira, true) - ); - } - } - - // Create the string representations for future use - await this.saveStringFieldMappings(fieldsList); - } - }; - - // Make sure to call this function after it's defined - loadExistingMappings(); - - // Add example mappings section - const examplesSection = mappingSection.createDiv({ cls: "mapping-examples" }); - examplesSection.createEl("h4", { text: "Example field mappings" }); - - const examplesList = examplesSection.createEl("ul"); - - [ - { - name: "summary", - toJira: "value", - fromJira: "issue.fields.summary" - }, - { - name: "reporter", - toJira: "null", - fromJira: "issue.fields.reporter.name" - }, - { - name: "project", - toJira: "({ key: value })", - fromJira: "issue.fields.project ? issue.fields.project.key : \"\"" - } - ].forEach(example => { - const item = examplesList.createEl("li"); - item.createEl("strong", { text: example.name }); - // Add button to use this example - const useBtn = item.createEl("button", { - text: "Use", - cls: "use-example-btn" - }); - item.createEl("br"); - item.createSpan({ text: `toJira: ${example.toJira}` }); - item.createEl("br"); - item.createSpan({ text: `fromJira: ${example.fromJira}` }); - - - useBtn.addEventListener("click", async () => { - await addFieldMapping(example.name, example.toJira, example.fromJira); - }); - }); - - // Add security notice - const securityNote = containerEl.createEl("div", { cls: "setting-item-description" }); - securityNote.createEl("strong", { text: "⚠️ Security Note: " }); - securityNote.createSpan({ text: "Field mapping uses JavaScript function strings. Validate any shared mappings to prevent security issues." }); + new CollapsibleSection(this.plugin, containerEl, this.testFieldMappings, "Test field mappings", + this.plugin.settings.collapsedSections.testFieldMappings, "testFieldMappings").getContentContainer(); } - hide() { - const fieldsList = this.containerEl.querySelector(".field-mappings-list"); - if (fieldsList) { - this.saveStringFieldMappings(fieldsList as HTMLDivElement); + /** + * Called when the settings tab is hidden + */ + hide(): void { + // Call hide on components that need cleanup + if (this.fieldMappingsSettings.hide) { + this.fieldMappingsSettings.hide(); } + if (this.rawIssueViewer.hide) { + this.rawIssueViewer.hide(); + } + if (this.testFieldMappings.hide) { + this.testFieldMappings.hide(); + } + super.hide(); } } diff --git a/src/settings/components/ConnectionSettingsComponent.ts b/src/settings/components/ConnectionSettingsComponent.ts new file mode 100644 index 0000000..b52c168 --- /dev/null +++ b/src/settings/components/ConnectionSettingsComponent.ts @@ -0,0 +1,187 @@ +import { Setting } from "obsidian"; +import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes"; + +/** + * Component for Jira connection settings + */ +export class ConnectionSettingsComponent implements SettingsComponent { + private props: SettingsComponentProps; + private currentAuthMethod: "bearer" | "basic" | "session"; + private settingElements: Map = new Map(); + + constructor(props: SettingsComponentProps) { + this.props = props; + this.currentAuthMethod = props.plugin.settings.authMethod || "bearer"; + } + + render(containerEl: HTMLElement): void { + const { plugin } = this.props; + + // Always show: Jira URL + new Setting(containerEl) + .setName("Jira URL") + .setDesc("Your Jira instance URL") + .addText((text) => + text + .setPlaceholder("https://yourcompany.atlassian.net") + .setValue(plugin.settings.jiraUrl) + .onChange(async (value) => { + plugin.settings.jiraUrl = value; + await plugin.saveSettings(); + }) + ); + + // Auth Method Select + const authMethodSetting = new Setting(containerEl) + .setName("Authentication method") + .setDesc("Choose how to authenticate with Jira") + .addDropdown((cb) => + cb + .addOption("bearer", "Bearer Token (PAT)") + .addOption("basic", "Basic Auth (Username + PAT)") + .addOption("session", "Session Cookie (Username + Password)") + .setValue(this.currentAuthMethod) + .onChange(async (value: "bearer" | "basic" | "session") => { + plugin.settings.authMethod = value; + await plugin.saveSettings(); + this.currentAuthMethod = value; + this.updateVisibility(); + }) + ); + + // // Store reference for dynamic visibility control + // this.settingElements.set("authMethod", authMethodSetting.settingEl); + + // Jira PAT + const patSetting = new Setting(containerEl) + .setName("Jira PAT") + .setDesc("Personal Access Token") + .addText((text) => { + text.inputEl.type = "password"; + text + .setPlaceholder("NjM5MDI5MzQ0NT...") + .setValue(plugin.settings.apiToken) + .onChange(async (value) => { + plugin.settings.apiToken = value; + await plugin.saveSettings(); + }); + }); + this.settingElements.set("pat", patSetting.settingEl); + + // Jira username + const usernameSetting = new Setting(containerEl) + .setName("Jira username") + .setDesc("Your Jira username") + .addText((text) => + text + .setPlaceholder("admin") + .setValue(plugin.settings.username) + .onChange(async (value) => { + plugin.settings.username = value; + await plugin.saveSettings(); + }) + ); + this.settingElements.set("username", usernameSetting.settingEl); + + // Jira username + const emailSetting = new Setting(containerEl) + .setName("Jira email") + .setDesc("Your Jira email") + .addText((text) => + text + .setPlaceholder("admin@gmail.com") + .setValue(plugin.settings.email) + .onChange(async (value) => { + plugin.settings.email = value; + await plugin.saveSettings(); + }) + ); + this.settingElements.set("email", emailSetting.settingEl); + + // Jira password + const passwordSetting = new Setting(containerEl) + .setName("Jira password") + .setDesc("Your Jira password") + .addText((text) => { + text.inputEl.type = "password"; + text + .setPlaceholder("qwerty") + .setValue(plugin.settings.password) + .onChange(async (value) => { + plugin.settings.password = value; + await plugin.saveSettings(); + }); + }); + this.settingElements.set("password", passwordSetting.settingEl); + + // // Session cookie name + // const sessionCookieSetting = new Setting(containerEl) + // .setName("Session cookie name") + // .setDesc("Only needed for username+password authentication") + // .addText((text) => + // text + // .setPlaceholder("JSESSIONID") + // .setValue(plugin.settings.sessionCookieName) + // .onChange(async (value) => { + // plugin.settings.sessionCookieName = value; + // await plugin.saveSettings(); + // }) + // ); + // this.settingElements.set("sessionCookie", sessionCookieSetting.settingEl); + + // Initial visibility + this.updateVisibility(); + } + + private updateVisibility(): void { + const method = this.currentAuthMethod; + + // // Show always + // this.showElement("authMethod"); + + // Show PAT if bearer or basic + if (method === "bearer" || method === "basic") { + this.showElement("pat"); + } else { + this.hideElement("pat"); + } + + // Show username if basic or session + if (method === "session") { + this.showElement("username"); + } else { + this.hideElement("username"); + } + + // Show email if basic or session + if (method === "basic") { + this.showElement("email"); + } else { + this.hideElement("email"); + } + + // Show password only if session + if (method === "session") { + this.showElement("password"); + } else { + this.hideElement("password"); + } + + // // Show session cookie only if session + // if (method === "session") { + // this.showElement("sessionCookie"); + // } else { + // this.hideElement("sessionCookie"); + // } + } + + private showElement(key: string): void { + const el = this.settingElements.get(key); + if (el) el.style.display = ""; + } + + private hideElement(key: string): void { + const el = this.settingElements.get(key); + if (el) el.style.display = "none"; + } +} diff --git a/src/settings/components/FieldMappingItem.ts b/src/settings/components/FieldMappingItem.ts new file mode 100644 index 0000000..3bad76f --- /dev/null +++ b/src/settings/components/FieldMappingItem.ts @@ -0,0 +1,110 @@ +import { validateField } from "../tools/fieldValidation"; + +/** + * Interface for field mapping item props + */ +export interface FieldMappingItemProps { + container: HTMLElement; + fieldName?: string; + toJira?: string; + fromJira?: string; + enableValidation: boolean; + onRemove: () => void; +} + +/** + * Class representing a single field mapping row + */ +export class FieldMappingItem { + private props: FieldMappingItemProps; + private fieldContainer: HTMLDivElement; + private fieldNameInput: HTMLInputElement; + private toJiraInput: HTMLTextAreaElement; + private fromJiraInput: HTMLTextAreaElement; + + constructor(props: FieldMappingItemProps) { + this.props = props; + this.render(); + } + + /** + * Get the current field values + */ + getValues() { + return { + fieldName: this.fieldNameInput.value.trim(), + toJira: this.toJiraInput.value.trim(), + fromJira: this.fromJiraInput.value.trim() + }; + } + + /** + * Render the field mapping item + */ + private render() { + const { container, fieldName = "", toJira = "", fromJira = "", enableValidation } = this.props; + + // Create the field container + this.fieldContainer = container.createDiv({ cls: "field-mapping-item" }); + + // Create field name input + const fieldNameContainer = this.fieldContainer.createDiv({ cls: "field-name-container" }); + fieldNameContainer.createEl("span", { text: "Field Name:", cls: "field-mapping-label" }); + this.fieldNameInput = fieldNameContainer.createEl("input", { + type: "text", + value: fieldName, + cls: "field-name-input" + }); + + // Create toJira function input + const toJiraContainer = this.fieldContainer.createDiv({ cls: "to-jira-container" }); + toJiraContainer.createEl("span", { text: "to Jira:", cls: "field-mapping-label" }); + this.toJiraInput = toJiraContainer.createEl("textarea", { + cls: "to-jira-input" + }); + this.toJiraInput.placeholder = "(value) => null"; + this.toJiraInput.value = toJira; + this.toJiraInput.rows = 1; + + // Create fromJira function input + const fromJiraContainer = this.fieldContainer.createDiv({ cls: "from-jira-container" }); + fromJiraContainer.createEl("span", { text: "from Jira:", cls: "field-mapping-label" }); + this.fromJiraInput = fromJiraContainer.createEl("textarea", { + cls: "from-jira-input" + }); + this.fromJiraInput.value = fromJira; + this.fromJiraInput.placeholder = "(issue, data_source) => null"; + this.fromJiraInput.rows = 1; + + // Add remove button + const removeBtn = this.fieldContainer.createEl("button", { + text: "✕", + cls: "remove-field-btn" + }); + + // Handle field removal + removeBtn.addEventListener("click", () => { + this.fieldContainer.remove(); + this.props.onRemove(); + }); + + // Setup validation + this.setupValidation(enableValidation); + } + + /** + * Set up validation for inputs + */ + async setupValidation(enableValidation: boolean) { + await validateField(this.fieldNameInput, enableValidation, 'string'); + await validateField(this.toJiraInput, enableValidation, 'function', ['value']); + await validateField(this.fromJiraInput, enableValidation, 'function', ['issue', 'data_source']); + } + + /** + * Update validation state + */ + async updateValidation(enableValidation: boolean) { + await this.setupValidation(enableValidation); + } +} diff --git a/src/settings/components/FieldMappingsComponent.ts b/src/settings/components/FieldMappingsComponent.ts new file mode 100644 index 0000000..7243e60 --- /dev/null +++ b/src/settings/components/FieldMappingsComponent.ts @@ -0,0 +1,275 @@ +import { Setting, setIcon } from "obsidian"; +import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes"; +import { FieldMappingItem } from "./FieldMappingItem"; +import { collectFieldMappingsFromUI, processMappings } from "../tools/mappingTransformers"; +import {jiraFunctionToString, transform_string_to_functions_mappings} from "../../tools/convertFunctionString"; +import { obsidianJiraFieldMappings } from "../../constants/obsidianJiraFieldsMapping"; +import { debugLog } from "../../tools/debugLogging"; + +/** + * Component for field mappings section + */ +export class FieldMappingsComponent implements SettingsComponent { + private props: SettingsComponentProps; + private fieldsList: HTMLDivElement; + private mappingItems: FieldMappingItem[] = []; + + constructor(props: SettingsComponentProps) { + this.props = props; + } + + /** + * Render the field mappings component + */ + render(containerEl: HTMLElement): void { + const { plugin } = this.props; + + // Create the mapping section + const mappingSection = containerEl.createDiv({ cls: "jira-field-mappings" }); + + // Add explanation + mappingSection.createEl("p", { + text: "Configure how fields are mapped between Obsidian and Jira. Each field requires both a toJira and fromJira transformation function." + }); + + // Add validation toggle + new Setting(mappingSection) + .setName('Enable field validation') + .setDesc('Check fields before saving as functions') + .addToggle(toggle => toggle + .setValue(plugin.settings.enableFieldValidation) + .onChange(async (value) => { + plugin.settings.enableFieldValidation = value; + await plugin.saveSettings(); + + // Update validation for all mapping items + this.mappingItems.forEach(item => { + item.updateValidation(value); + }); + })); + + // Create list container for field mappings + this.fieldsList = mappingSection.createDiv({ cls: "field-mappings-list" }); + + // Create button container + const buttonView = mappingSection.createDiv({ cls: "button-view" }); + + // Add button to add new field mapping + const addFieldBtn = buttonView.createEl("button", { + text: "", + cls: "add-field-mapping-btn" + }); + setIcon(addFieldBtn, "circle-plus"); + + addFieldBtn.addEventListener("click", () => { + this.addFieldMapping(); + }); + + // Reset button + const resetBtn = buttonView.createEl("button", { + text: "", + cls: "reset-field-mappings-btn" + }); + setIcon(resetBtn, "refresh-cw"); + + resetBtn.addEventListener("click", async () => { + plugin.settings.fieldMappings = {}; + plugin.settings.fieldMappingsStrings = {}; + await this.loadExistingMappings(); + await plugin.saveSettings(); + }); + + // Reload default mappings button + const reloadBtn = buttonView.createEl("button", { + text: "", + cls: "reload-field-mappings-btn" + }); + setIcon(reloadBtn, "list-restart"); + + reloadBtn.addEventListener("click", async () => { + plugin.settings.fieldMappings = obsidianJiraFieldMappings; + + // Also reset the string representations with default values + const defaultMappingsStrings: Record = {}; + for (const [fieldName, mapping] of Object.entries(obsidianJiraFieldMappings)) { + defaultMappingsStrings[fieldName] = { + toJira: jiraFunctionToString(mapping.toJira, false), + fromJira: jiraFunctionToString(mapping.fromJira, true) + }; + } + plugin.settings.fieldMappingsStrings = defaultMappingsStrings; + + await this.loadExistingMappings(); + await plugin.saveSettings(); + }); + + // Load existing mappings + this.loadExistingMappings(); + + // Add examples section + this.renderExamples(mappingSection); + + // Add security notice + const securityNote = containerEl.createEl("div", { cls: "setting-item-description" }); + securityNote.createEl("strong", { text: "⚠️ Security note: " }); + securityNote.createSpan({ + text: "Field mapping uses JavaScript function strings. Validate any shared mappings to prevent security issues." + }); + } + + /** + * Add a new field mapping item + */ + addFieldMapping(fieldName = "", toJira = "", fromJira = ""): FieldMappingItem { + const item = new FieldMappingItem({ + container: this.fieldsList, + fieldName, + toJira, + fromJira, + enableValidation: this.props.plugin.settings.enableFieldValidation, + onRemove: () => { + // Remove from array when item is removed + const index = this.mappingItems.indexOf(item); + if (index !== -1) { + this.mappingItems.splice(index, 1); + } + } + }); + + this.mappingItems.push(item); + return item; + } + + /** + * Load existing mappings + */ + async loadExistingMappings(): Promise { + const { plugin } = this.props; + + // Clear existing field list and items array + this.fieldsList.empty(); + this.mappingItems = []; + + debugLog(`Loading mapping settings`); + + // If we have string representations stored, use those + if (plugin.settings.fieldMappingsStrings && + Object.keys(plugin.settings.fieldMappingsStrings).length > 0) { + debugLog('Current mapping (string) is: ', plugin.settings.fieldMappingsStrings); + + const savedMappings = plugin.settings.fieldMappingsStrings; + for (const [fieldName, mapping] of Object.entries(savedMappings)) { + if (mapping && typeof mapping === 'object' && 'toJira' in mapping && 'fromJira' in mapping) { + this.addFieldMapping( + fieldName, + mapping.toJira, + mapping.fromJira + ); + } + } + plugin.settings.fieldMappings = await transform_string_to_functions_mappings( + plugin.settings.fieldMappingsStrings, + plugin.settings.enableFieldValidation + ); + } + // Otherwise, try to use the function mappings and convert them to strings + else if (plugin.settings.fieldMappings && + Object.keys(plugin.settings.fieldMappings).length > 0) { + debugLog('Current mapping is: ', plugin.settings.fieldMappings); + + const existingMappings = plugin.settings.fieldMappings; + for (const [fieldName, mapping] of Object.entries(existingMappings)) { + if (mapping && typeof mapping === 'object' && 'toJira' in mapping && 'fromJira' in mapping) { + this.addFieldMapping( + fieldName, + jiraFunctionToString(mapping.toJira, false), + jiraFunctionToString(mapping.fromJira, true) + ); + } + } + + // Create the string representations for future use + await this.saveFieldMappings(); + } + } + + /** + * Save current field mappings + */ + async saveFieldMappings(): Promise { + const { plugin } = this.props; + + debugLog(`From strings ${JSON.stringify(plugin.settings.fieldMappingsStrings)}\nand funcs ${JSON.stringify(plugin.settings.fieldMappings)}`); + + // Collect mappings from UI + const stringMappings = await collectFieldMappingsFromUI(this.fieldsList); + + // Process mappings + const { stringMappings: newStringMappings, functionMappings } = await processMappings( + stringMappings, + plugin.settings.enableFieldValidation + ); + + // Update settings + plugin.settings.fieldMappingsStrings = newStringMappings; + plugin.settings.fieldMappings = functionMappings; + + debugLog(`To strings ${JSON.stringify(plugin.settings.fieldMappingsStrings)}\nand funcs ${JSON.stringify(plugin.settings.fieldMappings)}`); + await plugin.saveSettings(); + } + + /** + * Render example mappings + */ + private renderExamples(containerEl: HTMLElement): void { + const examplesSection = containerEl.createDiv({ cls: "mapping-examples" }); + examplesSection.createEl("h4", { text: "Example field mappings" }); + + const examplesList = examplesSection.createEl("ul"); + + const examples = [ + { + name: "summary", + toJira: "value", + fromJira: "issue.fields.summary" + }, + { + name: "reporter", + toJira: "null", + fromJira: "issue.fields.reporter.name" + }, + { + name: "project", + toJira: "({ key: value })", + fromJira: "issue.fields.project ? issue.fields.project.key : \"\"" + } + ]; + + examples.forEach(example => { + const item = examplesList.createEl("li"); + item.createEl("strong", { text: example.name }); + + // Add button to use this example + const useBtn = item.createEl("button", { + text: "Use", + cls: "use-example-btn" + }); + + item.createEl("br"); + item.createSpan({ text: `toJira: ${example.toJira}` }); + item.createEl("br"); + item.createSpan({ text: `fromJira: ${example.fromJira}` }); + + useBtn.addEventListener("click", () => { + this.addFieldMapping(example.name, example.toJira, example.fromJira); + }); + }); + } + + /** + * Called when the component is hidden + */ + hide(): void { + this.saveFieldMappings(); + } +} diff --git a/src/settings/components/GeneralSettingsComponent.ts b/src/settings/components/GeneralSettingsComponent.ts new file mode 100644 index 0000000..cc69f3f --- /dev/null +++ b/src/settings/components/GeneralSettingsComponent.ts @@ -0,0 +1,46 @@ +import { normalizePath, Setting } from "obsidian"; +import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes"; + +/** + * Component for general plugin settings + */ +export class GeneralSettingsComponent implements SettingsComponent { + private props: SettingsComponentProps; + + constructor(props: SettingsComponentProps) { + this.props = props; + } + + render(containerEl: HTMLElement): void { + const { plugin } = this.props; + + // Issues folder setting + new Setting(containerEl) + .setName("Issues folder") + .setDesc("Folder where Jira issues will be stored") + .addText((text) => + text + .setPlaceholder("jira-issues") + .setValue(plugin.settings.issuesFolder) + .onChange(async (value) => { + plugin.settings.issuesFolder = value; + await plugin.saveSettings(); + }) + ); + + // Template path setting + new Setting(containerEl) + .setName("Template path") + .setDesc("The path of used template for creating Jira tasks (with or without .md extension)") + .addText((text) => + text + .setPlaceholder("templates/template1") + .setValue(plugin.settings.templatePath) + .onChange(async (value) => { + value = normalizePath(value); + plugin.settings.templatePath = value; + await plugin.saveSettings(); + }) + ); + } +} diff --git a/src/settings/components/RawIssueViewerComponent.ts b/src/settings/components/RawIssueViewerComponent.ts new file mode 100644 index 0000000..63a6485 --- /dev/null +++ b/src/settings/components/RawIssueViewerComponent.ts @@ -0,0 +1,95 @@ +import { Setting, Notice } from "obsidian"; +import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes"; +import { fetchIssue } from "../../api"; +import debounce from "lodash/debounce"; +import hljs from "highlight.js"; + +/** + * Component for viewing raw Jira issue data + */ +export class RawIssueViewerComponent implements SettingsComponent { + private props: SettingsComponentProps; + private issueDataContainer: HTMLElement | null = null; + private debouncedFetch: ReturnType; + private currentIssueData: any = null; + public onIssueDataChange?: () => void; + + constructor(props: SettingsComponentProps) { + this.props = props; + this.debouncedFetch = debounce(this.fetchIssueData.bind(this), 500); + } + + render(containerEl: HTMLElement): void { + // Add input field for issue key + new Setting(containerEl) + .setName("View raw issue data") + .setDesc("Enter a Jira issue key to view its raw data from the API") + .addText((text) => + text + .setPlaceholder("PROJ-123") + .onChange((value) => { + if (value) { + this.debouncedFetch(value); + } else { + this.clearIssueData(); + } + }) + ); + + // Create container for issue data + this.issueDataContainer = containerEl.createDiv({ + cls: "jira-raw-issue-container" + }); + } + + public getCurrentIssue(): any { + return this.currentIssueData; + } + + private async fetchIssueData(value: string): Promise { + try { + const issueData = await fetchIssue(this.props.plugin, value); + this.currentIssueData = issueData; + this.displayIssueData(issueData); + } catch (error) { + new Notice(`Failed to fetch issue: ${error.message}`); + this.clearIssueData(); + } + if (this.onIssueDataChange) { + this.onIssueDataChange(); + } + } + + private displayIssueData(issueData: any): void { + if (!this.issueDataContainer) return; + + this.issueDataContainer.empty(); + + const pre = this.issueDataContainer.createEl("pre", { + cls: "jira-raw-issue-data", + attr: { + style: "user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;" + } + }); + + const code = pre.createEl("code", { + cls: "language-json" + }); + + const jsonString = JSON.stringify(issueData, null, 2); + code.setText(jsonString); + hljs.highlightElement(code); + } + + private clearIssueData(): void { + if (this.issueDataContainer) { + this.issueDataContainer.empty(); + } + this.currentIssueData = null; + } + + hide(): void { + this.clearIssueData(); + this.debouncedFetch.cancel(); + } +} diff --git a/src/settings/components/TestFieldMappingsComponent.ts b/src/settings/components/TestFieldMappingsComponent.ts new file mode 100644 index 0000000..e37a28d --- /dev/null +++ b/src/settings/components/TestFieldMappingsComponent.ts @@ -0,0 +1,126 @@ +import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes"; +import debounce from "lodash/debounce"; +import { safeStringToFunction } from "../../tools/convertFunctionString"; + +export class TestFieldMappingsComponent implements SettingsComponent { + private props: SettingsComponentProps; + private testMappingsContainer: HTMLElement | null = null; + private currentIssueData: any = null; + private getCurrentIssue: (() => any) | null = null; + + constructor(props: SettingsComponentProps & { getCurrentIssue?: () => any }) { + this.props = props; + if (props.getCurrentIssue) { + this.getCurrentIssue = props.getCurrentIssue; + } + } + + render(containerEl: HTMLElement): void { + // Add section header + this.testMappingsContainer = containerEl.createDiv({ + cls: "jira-test-mappings-container" + }); + + this.testMappingsContainer.createEl("h3", { + text: "Test field mappings", + cls: "test-mappings-header" + }); + + this.testMappingsContainer.createEl("p", { + text: "Test your field mappings against the current issue data from Raw issue viewer. Enter a fromJira expression to see the result.", + cls: "test-mappings-desc" + }); + + // Container for all test mapping items + const itemsContainer = this.testMappingsContainer.createDiv({ cls: "test-mapping-items-list" }); + (this as any)._testMappingItemsContainer = itemsContainer; // store for later use + + // Add first test mapping item + this.addTestMappingItem(); + + // Add new mapping button (only once, at the bottom) + const addBtn = this.testMappingsContainer.createEl("button", { + text: "+ Add test mapping", + cls: "add-test-mapping-btn" + }); + addBtn.addEventListener("click", () => { + this.addTestMappingItem(); + }); + } + + private addTestMappingItem(): void { + // Use the dedicated items container + const itemsContainer = (this as any)._testMappingItemsContainer as HTMLElement; + if (!itemsContainer) return; + + const itemContainer = itemsContainer.createDiv({ + cls: "test-mapping-item" + }); + + // From Jira expression input + const fromJiraContainer = itemContainer.createDiv({ cls: "from-jira-container" }); + fromJiraContainer.createEl("span", { text: "from Jira:", cls: "field-mapping-label" }); + + const fromJiraInput = fromJiraContainer.createEl("textarea", { + cls: "from-jira-input" + }); + fromJiraInput.rows = 1; + fromJiraInput.placeholder = "(issue) => null"; + + // Value display + const valueContainer = itemContainer.createDiv({ cls: "value-container" }); + valueContainer.createEl("span", { text: "Value:", cls: "field-mapping-label" }); + const valueDisplay = valueContainer.createEl("div", { + cls: "value-display", + attr: { + style: "user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;" + } + }); + + // Debounced evaluation function + const debouncedEvaluate = debounce(async () => { + const issueData = this.getCurrentIssue ? this.getCurrentIssue() : this.currentIssueData; + if (!issueData) { + valueDisplay.setText("No issue data available"); + return; + } + + try { + const fromJiraFn = await safeStringToFunction(fromJiraInput.value, 'fromJira', false); + if (fromJiraFn) { + const result = fromJiraFn(issueData, null); + valueDisplay.setText(JSON.stringify(result, null, 2)); + } else { + valueDisplay.setText("Invalid expression"); + } + } catch (error) { + valueDisplay.setText(`Error: ${error.message}`); + } + }, 500); + + // Add event listeners + fromJiraInput.addEventListener("input", debouncedEvaluate); + + // Add remove button + const removeBtn = itemContainer.createEl("button", { + text: "✕", + cls: "remove-field-btn" + }); + removeBtn.addEventListener("click", () => { + itemContainer.remove(); + }); + } + + // Optionally, allow setting the current issue externally + setCurrentIssue(issue: any) { + this.currentIssueData = issue; + // Trigger re-evaluation of all test mappings + this.testMappingsContainer?.querySelectorAll(".from-jira-input").forEach((input: HTMLTextAreaElement) => { + input.dispatchEvent(new Event("input")); + }); + } + + hide(): void { + // No persistent state to clear + } +} diff --git a/src/settings/default.ts b/src/settings/default.ts index 7cfecce..c9a300e 100644 --- a/src/settings/default.ts +++ b/src/settings/default.ts @@ -1,10 +1,15 @@ import {FieldMapping} from "../constants/obsidianJiraFieldsMapping"; export interface JiraSettings { + collapsedSections: Record; + + authMethod: "bearer" | "basic" | "session"; apiToken: string; username: string; + email: string; password: string; jiraUrl: string; + issuesFolder: string; sessionCookieName: string; templatePath: string; @@ -14,10 +19,18 @@ export interface JiraSettings { } export const DEFAULT_SETTINGS: JiraSettings = { + collapsedSections: { + connection: true, + general: true, + fieldMappings: false + }, + authMethod: "bearer", apiToken: "", username: "", + email: "", password: "", jiraUrl: "", + issuesFolder: "jira-issues", sessionCookieName: "JSESSIONID", templatePath: "", diff --git a/src/settings/tools/fieldValidation.ts b/src/settings/tools/fieldValidation.ts new file mode 100644 index 0000000..54c6e37 --- /dev/null +++ b/src/settings/tools/fieldValidation.ts @@ -0,0 +1,89 @@ +import { Notice } from "obsidian"; +import { ValidationResult } from "../../interfaces/settingsTypes"; +import { validateFunctionString } from "../../tools/convertFunctionString"; +import { debugLog } from "../../tools/debugLogging"; + +/** + * Validate a field and update its visual state + */ +export async function validateField( + input: HTMLInputElement | HTMLTextAreaElement, + requireValidating: boolean = true, + type: string = 'string', + validateParams: Array = [] +): Promise { + const validatorFunction = async (event?: Event) => { + const consoleOutput = !!event; + const validation = await getValidationResult(input.value, type, validateParams); + updateFieldAppearance(validation, input, consoleOutput); + }; + + // Store the validator function on the element to allow for removal + setupValidatorProperty(input, validatorFunction); + + if (requireValidating) { + input.addEventListener("change", (input as any)._validatorFunction); + await validatorFunction(); // Run initial validation + } else { + input.removeEventListener("change", (input as any)._validatorFunction); + updateFieldAppearance({ isValid: true }, input, false); // Clear any validation errors + } +} + +/** + * Get validation result based on type and value + */ +async function getValidationResult( + value: string, + type: string = 'string', + validateParams: Array = [] +): Promise { + switch(type) { + case 'string': + return value.length === 0 + ? { isValid: false, errorMessage: "Field name cannot be empty" } + : { isValid: true }; + case 'function': + return await validateFunctionString(value, validateParams); + default: + return { isValid: true }; + } +} + +/** + * Update the field appearance based on validation result + */ +export function updateFieldAppearance( + validation: ValidationResult, + element: HTMLInputElement | HTMLTextAreaElement, + consoleOutput: boolean +): void { + if (!validation.isValid) { + element.classList.add("invalid"); + if (consoleOutput && validation.errorMessage) { + debugLog(validation.errorMessage); + new Notice(validation.errorMessage); + } + } else { + element.classList.remove("invalid"); + } +} + +/** + * Setup validator property on input element + */ +function setupValidatorProperty( + input: HTMLInputElement | HTMLTextAreaElement, + validatorFunction: (event?: Event) => Promise +): void { + if (!input.hasOwnProperty('_validatorFunction')) { + Object.defineProperty(input, '_validatorFunction', { + value: validatorFunction, + writable: true, + configurable: true + }); + } else { + input.removeEventListener("change", (input as any)._validatorFunction); + (input as any)._validatorFunction = validatorFunction; + } +} diff --git a/src/settings/tools/mappingTransformers.ts b/src/settings/tools/mappingTransformers.ts new file mode 100644 index 0000000..82473ab --- /dev/null +++ b/src/settings/tools/mappingTransformers.ts @@ -0,0 +1,85 @@ +import { JiraFieldMapping, JiraFieldMappingString } from "../../interfaces/settingsTypes"; +import { + jiraFunctionToString, + transform_string_to_functions_mappings, + // validateFunctionString +} from "../../tools/convertFunctionString"; +import { debugLog } from "../../tools/debugLogging"; + +/** + * Convert function mappings to their string representation + */ +export function convertFunctionMappingsToStrings( + mappings: Record +): Record { + const result: Record = {}; + + for (const [fieldName, mapping] of Object.entries(mappings)) { + if (mapping && typeof mapping === 'object' && 'toJira' in mapping && 'fromJira' in mapping) { + result[fieldName] = { + toJira: jiraFunctionToString(mapping.toJira, false), + fromJira: jiraFunctionToString(mapping.fromJira, true) + }; + } + } + + return result; +} + +/** + * Collect field mappings from the UI + */ +export async function collectFieldMappingsFromUI( + element: HTMLElement, + // enableValidation: boolean +): Promise> { + const mappings: Record = {}; + const fieldItems = element.querySelectorAll(".field-mapping-item"); + + fieldItems.forEach(item => { + const fieldNameInput = item.querySelector(".field-name-input"); + const toJiraInput = item.querySelector(".to-jira-input"); + const fromJiraInput = item.querySelector(".from-jira-input"); + + if (!fieldNameInput || !toJiraInput || !fromJiraInput) { + return; + } + + const fieldName = (fieldNameInput as HTMLInputElement).value.trim(); + const toJira = (toJiraInput as HTMLTextAreaElement).value.trim(); + const fromJira = (fromJiraInput as HTMLTextAreaElement).value.trim(); + + // Only save valid mappings with field name filled + if (fieldName) { + mappings[fieldName] = { + toJira: toJira, + fromJira: fromJira + }; + } + }); + + debugLog(`Collected mappings: ${JSON.stringify(mappings)}`); + return mappings; +} + +/** + * Process string mappings to function mappings + */ +export async function processMappings( + stringMappings: Record, + enableValidation: boolean +): Promise<{ + stringMappings: Record, + functionMappings: Record +}> { + // Convert string mappings to function mappings + const functionMappings = await transform_string_to_functions_mappings( + stringMappings, + enableValidation + ); + + return { + stringMappings, + functionMappings + }; +} diff --git a/src/tools/convertFunctionString.ts b/src/tools/convertFunctionString.ts index 79bfdcc..30d85c4 100644 --- a/src/tools/convertFunctionString.ts +++ b/src/tools/convertFunctionString.ts @@ -3,6 +3,7 @@ import { JiraIssue } from "../interfaces"; import { parse } from "acorn"; import {debugLog} from "./debugLogging"; import {FieldMapping} from "../constants/obsidianJiraFieldsMapping"; +import {defaultIssue} from "../constants/defaultIssue"; // Constants for validation and error messages const FORBIDDEN_PATTERNS = ["document", "window", "eval", "Function", "fetch", "setTimeout"]; // Prevent unsafe code execution @@ -23,7 +24,7 @@ export function validateFunctionStringBrowser(fnString: string, approved_vars: s try { if (isSimpleExpression(fnString) || (fnString.startsWith("{") && fnString.endsWith("}") && !fnString.includes("return"))) { - fnString = `(${approved_vars.map(varName => varName==='issue'?`${varName} = {fields: {}}`:`${varName} = {}`).join(', ')}) => ${fnString}`; + fnString = `(${approved_vars.map(varName => varName==='issue'?`${varName} = ${JSON.stringify(defaultIssue)}`:`${varName} = {}`).join(', ')}) => ${fnString}`; } debugLog(`checking function: ${fnString}`); let func = (iframeWindow as any).eval(fnString); diff --git a/styles.css b/styles.css index 78707c4..428f244 100644 --- a/styles.css +++ b/styles.css @@ -1,3 +1,118 @@ +.jira-collapsible-section { + padding: 8px; + border-left: 2px solid var(--background-modifier-border); + margin-bottom: 4px; +} + +.jira-collapsible-header { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + font-weight: bold; + padding: 4px 0; +} + +.jira-collapsible-content { + margin-top: 20px; + padding-left: 20px; +} + +.jira-test-mappings-container { + margin-top: 2em; + padding: 1em 0; +} + +.test-mappings-header { + margin-bottom: 0.2em; +} + +.test-mappings-desc { + margin-bottom: 1em; + color: var(--text-muted); +} + +.test-mapping-items-list { + display: flex; + flex-direction: column; + gap: 1.2em; +} + +.test-mapping-item { + background: var(--background-secondary-alt); + border-radius: 8px; + padding: 1em 1em 1em 1.2em; + position: relative; + box-shadow: 0 1px 4px rgba(0,0,0,0.04); +} + +.value-container { + margin-bottom: 0.5em; +} + +.value-display { + background: var(--background-primary-alt); + border-radius: 4px; + padding: 0.5em 0.8em; + font-family: var(--font-monospace); + font-size: 0.98em; + margin-top: 0.2em; + white-space: pre-wrap; + word-break: break-all; + border: 1px solid var(--background-modifier-border); +} + +.test-mapping-item .from-jira-container { + margin-bottom: 0.5em; +} + +.test-mapping-item .from-jira-input { + width: 100%; + min-height: 2.2em; + font-family: var(--font-monospace); + font-size: 1em; + margin-top: 0.2em; + border-radius: 4px; + border: 1px solid var(--background-modifier-border); + background: var(--background-primary-alt); + color: var(--text-normal); + padding: 0.3em 0.5em; + resize: vertical; +} + +.remove-field-btn { + position: absolute; + top: 0.5em; + right: 0.7em; + background: var(--background-modifier-error); + color: var(--text-on-accent); + border: none; + border-radius: 4px; + font-size: 1.1em; + padding: 0.1em 0.5em; + cursor: pointer; + transition: background 0.2s; +} +.remove-field-btn:hover { + background: var(--background-modifier-error-hover); +} + +.add-test-mapping-btn { + display: block; + margin: 1.5em auto 0 auto; + padding: 0.5em 1.5em; + background: var(--interactive-accent); + color: var(--text-on-accent); + border: none; + border-radius: 6px; + font-size: 1em; + cursor: pointer; + transition: background 0.2s; +} +.add-test-mapping-btn:hover { + background: var(--interactive-accent-hover); +} + .jira-field-mappings { margin-top: 10px; } @@ -91,3 +206,128 @@ .button-view button { width: fit-content; } + + +/*hljs.css*/ +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em +} +code.hljs { + padding: 3px 5px +} +/*! + Theme: GitHub + Description: Light theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-light + Current colors taken from GitHub's CSS +*/ +.hljs { + color: #24292e; + background: #ffffff +} +.hljs-doctag, +.hljs-keyword, +.hljs-meta .hljs-keyword, +.hljs-template-tag, +.hljs-template-variable, +.hljs-type, +.hljs-variable.language_ { + /* prettylights-syntax-keyword */ + color: #f6cc42 +} +.hljs-title, +.hljs-title.class_, +.hljs-title.class_.inherited__, +.hljs-title.function_ { + /* prettylights-syntax-entity */ + color: #6f42c1 +} +.hljs-attr, +.hljs-attribute, +.hljs-literal, +.hljs-meta, + +.hljs-operator, +.hljs-variable, +.hljs-selector-attr, +.hljs-selector-class, +.hljs-selector-id { + /* prettylights-syntax-constant */ + color: #6ecbfa +} +.hljs-number { + /* prettylights-syntax-numeric-literal */ + color: #93cda7 +} +.hljs-regexp, +.hljs-string, +.hljs-meta .hljs-string { + /* prettylights-syntax-string */ + color: #cd905c +} +.hljs-built_in, +.hljs-symbol { + /* prettylights-syntax-variable */ + color: #e36209 +} +.hljs-comment, +.hljs-code, +.hljs-formula { + /* prettylights-syntax-comment */ + color: #6a737d +} +.hljs-name, +.hljs-quote, +.hljs-selector-tag, +.hljs-selector-pseudo { + /* prettylights-syntax-entity-tag */ + color: #22863a +} +.hljs-subst { + /* prettylights-syntax-storage-modifier-import */ + color: #24292e +} +.hljs-section { + /* prettylights-syntax-markup-heading */ + color: #005cc5; + font-weight: bold +} +.hljs-bullet { + /* prettylights-syntax-markup-list */ + color: #735c0f +} +.hljs-emphasis { + /* prettylights-syntax-markup-italic */ + color: #24292e; + font-style: italic +} +.hljs-strong { + /* prettylights-syntax-markup-bold */ + color: #24292e; + font-weight: bold +} +.hljs-addition { + /* prettylights-syntax-markup-inserted */ + color: #22863a; + background-color: #f0fff4 +} +.hljs-deletion { + /* prettylights-syntax-markup-deleted */ + color: #b31d28; + background-color: #ffeef0 +} +.hljs-char.escape_, +.hljs-link, +.hljs-params, +.hljs-property, +.hljs-punctuation, +.hljs-tag { + /* purposely ignored */ + +} diff --git a/yarn.lock b/yarn.lock index b9db08c..f9dc860 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,131 +2,83 @@ # yarn lockfile v1 -"@esbuild/aix-ppc64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64" - integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ== +"@codemirror/state@^6.0.0", "@codemirror/state@^6.5.0": + version "6.5.2" + resolved "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz" + integrity sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA== + dependencies: + "@marijn/find-cluster-break" "^1.0.0" -"@esbuild/android-arm64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f" - integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g== - -"@esbuild/android-arm@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b" - integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g== - -"@esbuild/android-x64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163" - integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg== - -"@esbuild/darwin-arm64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c" - integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw== - -"@esbuild/darwin-x64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a" - integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg== - -"@esbuild/freebsd-arm64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce" - integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w== - -"@esbuild/freebsd-x64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7" - integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A== - -"@esbuild/linux-arm64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73" - integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg== - -"@esbuild/linux-arm@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3" - integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg== - -"@esbuild/linux-ia32@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19" - integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg== - -"@esbuild/linux-loong64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7" - integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw== - -"@esbuild/linux-mips64el@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1" - integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ== - -"@esbuild/linux-ppc64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951" - integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw== - -"@esbuild/linux-riscv64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987" - integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA== - -"@esbuild/linux-s390x@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4" - integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA== - -"@esbuild/linux-x64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a" - integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw== - -"@esbuild/netbsd-arm64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b" - integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw== - -"@esbuild/netbsd-x64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b" - integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA== - -"@esbuild/openbsd-arm64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7" - integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw== - -"@esbuild/openbsd-x64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde" - integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg== - -"@esbuild/sunos-x64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92" - integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg== - -"@esbuild/win32-arm64@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c" - integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw== - -"@esbuild/win32-ia32@0.25.0": - version "0.25.0" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079" - integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA== +"@codemirror/view@^6.0.0": + version "6.36.4" + resolved "https://registry.npmjs.org/@codemirror/view/-/view-6.36.4.tgz" + integrity sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA== + dependencies: + "@codemirror/state" "^6.5.0" + style-mod "^4.1.0" + w3c-keyname "^2.2.4" "@esbuild/win32-x64@0.25.0": version "0.25.0" resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz" integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ== +"@eslint-community/eslint-utils@^4.2.0": + version "4.5.1" + resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz" + integrity sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w== + dependencies: + eslint-visitor-keys "^3.4.3" + +"@eslint-community/regexpp@^4.6.1": + version "4.12.1" + resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz" + integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.1": + version "8.57.1" + resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz" + integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== + +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== + dependencies: + "@humanwhocodes/object-schema" "^2.0.3" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.3": + version "2.0.3" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@marijn/find-cluster-break@^1.0.0": + version "1.0.2" + resolved "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz" + integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" @@ -135,12 +87,12 @@ "@nodelib/fs.stat" "2.0.5" run-parallel "^1.1.9" -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": +"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5": version "2.0.5" resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3": +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -165,6 +117,11 @@ resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== +"@types/lodash@^4.17.16": + version "4.17.16" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz" + integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g== + "@types/node@^16.11.6": version "16.18.121" resolved "https://registry.npmjs.org/@types/node/-/node-16.18.121.tgz" @@ -192,7 +149,7 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@5.29.0": +"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@5.29.0": version "5.29.0" resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.29.0.tgz" integrity sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw== @@ -257,16 +214,66 @@ "@typescript-eslint/types" "5.29.0" eslint-visitor-keys "^3.3.0" -acorn@^8.14.1: +"@ungap/structured-clone@^1.2.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.14.1, acorn@^8.9.0: version "8.14.1" resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz" integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + array-union@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + braces@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" @@ -279,13 +286,57 @@ builtin-modules@3.3.0: resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== -debug@^4.3.4: +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cross-spawn@^7.0.2: + version "7.0.6" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.3.7" resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz" integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: ms "^2.1.3" +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" @@ -293,6 +344,13 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + esbuild@0.25.0: version "0.25.0" resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz" @@ -324,6 +382,11 @@ esbuild@0.25.0: "@esbuild/win32-ia32" "0.25.0" "@esbuild/win32-x64" "0.25.0" +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" @@ -332,6 +395,14 @@ eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + eslint-utils@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" @@ -349,6 +420,76 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@*, "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@>=5: + version "8.57.1" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz" + integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.1" + "@humanwhocodes/config-array" "^0.13.0" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2: + version "1.6.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" @@ -361,11 +502,26 @@ estraverse@^4.1.1: resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== +estraverse@^5.1.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + estraverse@^5.2.0: version "5.3.0" resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + fast-glob@^3.2.9: version "3.3.2" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" @@ -377,6 +533,16 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + fastq@^1.6.0: version "1.17.1" resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz" @@ -384,6 +550,13 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + fill-range@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz" @@ -391,6 +564,33 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.3.3" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz" + integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" @@ -403,6 +603,32 @@ glob-parent@^5.1.2: dependencies: is-glob "^4.0.1" +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + globby@^11.1.0: version "11.1.0" resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" @@ -415,17 +641,58 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +highlight.js@^11.11.1: + version "11.11.1" + resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz" + integrity sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w== + ignore@^5.2.0: version "5.3.2" resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== +import-fresh@^3.2.1: + version "3.3.1" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-glob@^4.0.1, is-glob@^4.0.3: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -437,6 +704,70 @@ is-number@^7.0.0: resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" @@ -450,6 +781,13 @@ micromatch@^4.0.4: braces "^3.0.3" picomatch "^2.3.1" +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + moment@2.29.4: version "2.29.4" resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz" @@ -460,6 +798,11 @@ ms@^2.1.3: resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + obsidian@latest: version "1.8.7" resolved "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz" @@ -468,6 +811,61 @@ obsidian@latest: "@types/codemirror" "5.60.8" moment "2.29.4" +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -478,6 +876,16 @@ picomatch@^2.3.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -488,11 +896,23 @@ regexpp@^3.2.0: resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" @@ -505,11 +925,52 @@ semver@^7.3.7: resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + slash@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +style-mod@^4.1.0: + version "4.1.2" + resolved "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz" + integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" @@ -517,16 +978,16 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -tslib@2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz" - integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== - tslib@^1.8.1: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz" + integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" @@ -534,12 +995,58 @@ tsutils@^3.21.0: dependencies: tslib "^1.8.1" -typescript@4.7.4: +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +"typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@4.7.4: version "4.7.4" resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +w3c-keyname@^2.2.4: + version "2.2.8" + resolved "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz" + integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + yaml@^2.7.0: version "2.7.0" resolved "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz" integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==