diff --git a/docs/how_to_en.md b/docs/how_to_en.md index bcbb458..5f6ff0d 100644 --- a/docs/how_to_en.md +++ b/docs/how_to_en.md @@ -96,6 +96,7 @@ Currently, the plugin provides the following commands: - `Update work log in Jira manually` - enables manual time tracking for a task. Currently, this is not reflected in the file, but it will be available in future updates. - `Update work log in Jira by batch` - enables batch time tracking. If the formatter contains `jira_worklog_batch`, a batch of data from `jira_worklog_batch` will be sent, updating each listed entity. - `Update issue status in Jira` - allows updating a task's status by selecting one of the available options. +- `Add comment to Jira` - allows adding a comment to the current task in Jira. ### Advanced Usage @@ -110,12 +111,14 @@ Similarly, you can configure the `progressPercentage` shown in the example. This Additionally, you can use some of the built-in functions: -- `jiraToMarkdown` - converts Jira markup to Markdown. -- `markdownToJira` - converts Markdown to Jira markup. +- `jiraToMarkdown` - converts Jira markup to Markdown. (for Jira API v2) +- `markdownToJira` - converts Markdown to Jira markup. (for Jira API v2) +- `markdownToAdf` - converts Markdown to Atlassian Document Format (ADF). (for Jira API v3) +- `adfToMarkdown` - converts ADF to Markdown. (for Jira API v3) - `JSON.parse` - converts a JSON string to an object. - `JSON.stringify` - converts an object to a JSON string. -and modules: (original Javascript syntax) +and modules: (original JavaScript syntax) - `Math` - provides access to mathematical functions, such as `Math.round()`. - `Date` - provides access to date functions, such as `Date.now()`. diff --git a/docs/how_to_ru.md b/docs/how_to_ru.md index fcebeb4..4ff68cb 100644 --- a/docs/how_to_ru.md +++ b/docs/how_to_ru.md @@ -95,6 +95,7 @@ Bob - `Update work log in Jira manually` - позволяет вести учёт потраченного на задачу времени вручную. В данный момент он никак не отображается в файле, это будет в ближайших обновлениях. - `Update work log in Jira by batch` - позволяет вести учёт потраченного на задачу времени батчем. Если в файле в formatter есть `jira_worklog_batch`, то вместо ручного заполнения будет послан батч данных из `jira_worklog_batch` с обновлением каждой из представленных сущностей. - `Update issue status in Jira` - позволяет обновить статус задачи, выбрав один из возможных вариантов. +- `Add comment to Jira` - позволяет добавить комментарий в выбранную задачу. ### Продвинутое использование @@ -109,12 +110,14 @@ Bob Кроме того, вы можете использовать некоторые встроенные функции: -- `jiraToMarkdown` - преобразует разметку Jira в Markdown. -- `markdownToJira` - преобразует Markdown в разметку Jira. +- `jiraToMarkdown` - преобразует разметку Jira в Markdown. (Jira API v2) +- `markdownToJira` - преобразует Markdown в разметку Jira. (Jira API v2) +- `markdownToAdf` - преобразует Markdown в ADF. (Jira API v3) +- `adfToMarkdown` - преобразует ADF в Markdown. (Jira API v3) - `JSON.parse` - преобразует строку JSON в объект. - `JSON.stringify` - преобразует объект в строку JSON. -и модули: (исходный синтаксис Javascript) +и модули: (исходный синтаксис JavaScript) - `Math` - предоставляет доступ к математическим функциям, таким как `Math.round()`. - `Date` - предоставляет доступ к функциям даты, таким как `Date.now()`. diff --git a/manifest.json b/manifest.json index 7dfeaca..0da44c7 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "jira-sync", "name": "Jira Issue Manager", - "version": "1.5.2", + "version": "1.6.0", "minAppVersion": "1.10.1", "description": "Get Jira issues, create and update them. Issue status and worklog management.", "author": "Alamion", diff --git a/package.json b/package.json index 70136fe..4799e40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-jira-sync", - "version": "1.5.2", + "version": "1.6.0", "packageManager": "yarn@1.22.22", "description": "This is a sample plugin for Obsidian (https://obsidian.md)", "main": "main.js", diff --git a/src/default/obsidianJiraFieldsMapping.ts b/src/default/obsidianJiraFieldsMapping.ts index a08219b..bcc9875 100644 --- a/src/default/obsidianJiraFieldsMapping.ts +++ b/src/default/obsidianJiraFieldsMapping.ts @@ -1,11 +1,13 @@ import { JiraIssue } from '../interfaces'; -import { jiraToMarkdown } from '../tools/markdownHtml'; -import { adfToMarkdown } from '../tools/markdownToAdf'; +import { jiraToMarkdown, markdownToJira } from '../tools/markdownHtml'; +import { adfToMarkdown, markdownToAdf } from '../tools/markdownToAdf'; export interface FieldMapping { - toJira: (value: any) => any; - fromJira: (issue: JiraIssue, data_source: Record | null) => any; + toJira: (value: any, api_version?: '2' | '3') => any; + fromJira: (issue: JiraIssue, api_version?: '2' | '3', data_source?: Record) => any; } +export const TO_JIRA_PARAMS = ['value', 'api_version']; +export const FROM_JIRA_PARAMS = ['issue', 'api_version', 'data_source']; export const obsidianJiraFieldMappings: Record = { summary: { @@ -13,8 +15,9 @@ export const obsidianJiraFieldMappings: Record = { fromJira: (issue) => issue.fields.summary, }, description: { - toJira: () => null, - fromJira: (issue) => jiraToMarkdown(issue.fields.description), + toJira: (value, api_version) => (api_version === '3' ? markdownToAdf(value) : markdownToJira(value)), + fromJira: (issue, api_version) => + api_version === '3' ? adfToMarkdown(issue.fields.description) : jiraToMarkdown(issue.fields.description), }, key: { toJira: () => null, @@ -78,14 +81,14 @@ export const obsidianJiraFieldMappings: Record = { }, comments: { toJira: () => null, - fromJira: (issue) => { + fromJira: (issue, api_version) => { const comments = issue.fields.comment?.comments; if (!comments?.length) return ''; return comments .map((c: any) => { const author = c.author?.displayName ?? 'Unknown'; const date = c.created ? c.created.replace('T', ' ').substring(0, 19) : ''; - const body = adfToMarkdown(c.body) ?? ''; + const body = api_version === '3' ? adfToMarkdown(c.body) : c.body; const calloutBody = body .split('\n') .map((l: string) => (l === '' ? '>' : `> ${l}`)) diff --git a/src/file_operations/createUpdateIssue.ts b/src/file_operations/createUpdateIssue.ts index 1968f1b..db23846 100644 --- a/src/file_operations/createUpdateIssue.ts +++ b/src/file_operations/createUpdateIssue.ts @@ -9,15 +9,20 @@ import { obsidianJiraFieldMappings } from '../default/obsidianJiraFieldsMapping' export async function updateIssueFromFile(plugin: JiraPlugin, file: TFile): Promise { let fields = await prepareJiraFieldsFromFile(plugin, file); const issueKey = fields.key; + const apiVersion = plugin.getCurrentConnection()?.apiVersion; if (!issueKey) { throw new Error('No issue key found in frontmatter'); } - fields = localToJiraFields(fields, { - ...obsidianJiraFieldMappings, - ...plugin.settings.fieldMapping.fieldMappings, - }); + fields = localToJiraFields( + fields, + { + ...obsidianJiraFieldMappings, + ...plugin.settings.fieldMapping.fieldMappings, + }, + apiVersion, + ); await updateJiraIssue(plugin, issueKey, fields); return issueKey; } @@ -27,13 +32,18 @@ export async function createIssueFromFile( file: TFile, fields?: Record, ): Promise { + const apiVersion = plugin.getCurrentConnection()?.apiVersion; if (!fields) { fields = await prepareJiraFieldsFromFile(plugin, file); } - fields = localToJiraFields(fields, { - ...obsidianJiraFieldMappings, - ...plugin.settings.fieldMapping.fieldMappings, - }); + fields = localToJiraFields( + fields, + { + ...obsidianJiraFieldMappings, + ...plugin.settings.fieldMapping.fieldMappings, + }, + apiVersion, + ); // Create the issue const issueData = await createJiraIssue(plugin, fields); const issueKey = issueData.key; diff --git a/src/localization/source/en/settings/fm.yaml b/src/localization/source/en/settings/fm.yaml index 1b0fe96..607593c 100644 --- a/src/localization/source/en/settings/fm.yaml +++ b/src/localization/source/en/settings/fm.yaml @@ -4,9 +4,11 @@ desc: | To Jira: - value - the value of field in your .md file - can be a text string from file contents or a field value from frontmatter + - api_version - Jira API version used in current connection (could be either '2' or '3') From Jira: - issue - raw data from Jira (can be seen in lower `Fetch issue settings` section) + - api_version - the same as `To Jira` - data_source - either parsed sections or frontmatter data in dictionary format, where keys are field names and values are field values fv: name: Enable field validation diff --git a/src/localization/source/ru/settings/fm.yaml b/src/localization/source/ru/settings/fm.yaml index 1ed9197..a03ee6e 100644 --- a/src/localization/source/ru/settings/fm.yaml +++ b/src/localization/source/ru/settings/fm.yaml @@ -4,9 +4,11 @@ desc: | В Jira: - value — значение поля в .md файле — это может быть текст из содержимого файла или значение поля из frontmatter + - api_version - Jira API версия используемая в текущем коннекторе (может быть '2' или '3') Из Jira: - issue — необработанные данные из Jira (можно увидеть в нижнем разделе "Настройка получаемых полей") + - api_version - То же самое что и в "В Jira" - data_source — либо спаршенные секции данных из файла, либо данные frontmatter. Показывается в формате словаря, где ключи — это названия полей, а значения — данные из файла fv: name: Включить проверку полей diff --git a/src/settings/components/FieldMappingItem.ts b/src/settings/components/FieldMappingItem.ts index 217cec7..e530818 100644 --- a/src/settings/components/FieldMappingItem.ts +++ b/src/settings/components/FieldMappingItem.ts @@ -1,5 +1,6 @@ import { validateField } from '../tools/fieldValidation'; import { useTranslations } from '../../localization/translator'; +import { FROM_JIRA_PARAMS, TO_JIRA_PARAMS } from '../../default/obsidianJiraFieldsMapping'; const t = useTranslations('settings.fm').t; /** @@ -82,7 +83,8 @@ export class FieldMappingItem { this.toJiraInput = toJiraContainer.createEl('textarea', { cls: 'to-jira-input', }); - this.toJiraInput.placeholder = '(value) => {\n // Transform Obsidian value to Jira\n return value;\n}'; + this.toJiraInput.placeholder = + '(value, api_version) => {\n // Transform Obsidian value to Jira\n return value;\n}'; this.toJiraInput.value = toJira; // Create fromJira function input @@ -98,7 +100,7 @@ export class FieldMappingItem { }); this.fromJiraInput.value = fromJira; this.fromJiraInput.placeholder = - '(issue, data_source) => {\n // Extract value from Jira issue\n return issue.fields.fieldName;\n}'; + '(issue, api_version, data_source) => {\n // Extract value from Jira issue\n return issue.fields.fieldName;\n}'; // Add remove button const removeBtn = this.fieldContainer.createEl('button', { @@ -139,8 +141,8 @@ export class FieldMappingItem { */ 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']); + await validateField(this.toJiraInput, enableValidation, 'function', TO_JIRA_PARAMS); + await validateField(this.fromJiraInput, enableValidation, 'function', FROM_JIRA_PARAMS); } /** diff --git a/src/settings/components/FieldMappingsComponent.ts b/src/settings/components/FieldMappingsComponent.ts index c05427c..11c3d2c 100644 --- a/src/settings/components/FieldMappingsComponent.ts +++ b/src/settings/components/FieldMappingsComponent.ts @@ -315,6 +315,12 @@ export class FieldMappingsComponent implements SettingsComponent { toJira: '({ key: value })', fromJira: 'issue.fields.project ? issue.fields.project.key : ""', }, + { + name: 'description', + toJira: 'api_version === "3" ? markdownToAdf(value) : markdownToJira(value)', + fromJira: + 'api_version === "3" ? adfToMarkdown(issue.fields.description) : jiraToMarkdown(issue.fields.description)', + }, ]; examples.forEach((example) => { diff --git a/src/tools/convertFunctionString.ts b/src/tools/convertFunctionString.ts index 98faf07..cff5130 100644 --- a/src/tools/convertFunctionString.ts +++ b/src/tools/convertFunctionString.ts @@ -2,7 +2,7 @@ import { Notice } from 'obsidian'; import { JiraIssue } from '../interfaces'; import { parse } from 'acorn'; import { debugLog } from './debugLogging'; -import { FieldMapping } from '../default/obsidianJiraFieldsMapping'; +import { FieldMapping, FROM_JIRA_PARAMS, TO_JIRA_PARAMS } from '../default/obsidianJiraFieldsMapping'; import { defaultIssue } from '../default/defaultIssue'; import { jiraToMarkdown, markdownToJira } from './markdownHtml'; import { markdownToAdf, adfToMarkdown } from './markdownToAdf'; @@ -57,6 +57,7 @@ export function validateFunctionStringBrowser( const varDeclarations = approved_vars .map((varName) => { if (varName === 'issue') return `${varName} = ${JSON.stringify(defaultIssue)}`; + if (varName === 'api_version') return `${varName} = "2"`; if (varName === 'value') return `${varName} = "test value"`; if (varName === 'data_source') return `${varName} = {}`; return `${varName} = {}`; @@ -91,6 +92,7 @@ export function validateFunctionStringBrowser( const testArgs = approved_vars.map((varName) => { if (varName === 'issue') return defaultIssue; + if (varName === 'api_version') return '2'; if (varName === 'value') return 'test value'; if (varName === 'data_source') return {}; return {}; @@ -180,7 +182,7 @@ export async function safeStringToFunction( if (extraValidate) { const validation = await validateFunctionString( exprString, - type === 'fromJira' ? ['issue', 'data_source'] : ['value'], + type === 'fromJira' ? FROM_JIRA_PARAMS : TO_JIRA_PARAMS, ); if (!validation.isValid) { console.warn(`Invalid function: ${validation.errorMessage}`); @@ -216,9 +218,9 @@ export async function safeStringToFunction( }; if (type === 'toJira') { - return function (value: any) { + return function (value: any, api_version?: '2' | '3') { const fn = new Function( - 'value', + ...TO_JIRA_PARAMS, 'context', ` with (context) { @@ -232,14 +234,13 @@ export async function safeStringToFunction( `, ); - return fn.call(context, value, context); + return fn.call(context, value, api_version, context); }; } else { // fromJira - return function (issue: JiraIssue, data_source: Record | null) { + return function (issue: JiraIssue, api_version?: '2' | '3', data_source?: Record) { const fn = new Function( - 'issue', - 'data_source', + ...FROM_JIRA_PARAMS, 'context', ` with (context) { @@ -253,7 +254,7 @@ export async function safeStringToFunction( `, ); - return fn.call(context, issue, data_source, context); + return fn.call(context, issue, api_version, data_source, context); }; } } catch (error) { @@ -272,29 +273,22 @@ export function jiraFunctionToString(fn: (...args: any[]) => any, isFromJira: bo const paramMatch = fnStr.match(/^\s*(?:\(?([^)]*)\)?\s*=>|\s*function\s*\(([^)]*)\))/); if (paramMatch) { - if (isFromJira) { - const params = (paramMatch[1] || paramMatch[2] || '').split(',').map((p) => p.trim()); + const params = (paramMatch[1] || paramMatch[2] || '').split(',').map((p) => p.trim()); + let resultStr = baseStr; - let resultStr = baseStr; - - if (params.length >= 1 && params[0]) { - const regex1 = new RegExp(`\\b${params[0]}\\b`, 'g'); - resultStr = resultStr.replace(regex1, 'issue'); - } - if (params.length >= 2 && params[1]) { - const regex2 = new RegExp(`\\b${params[1]}\\b`, 'g'); - resultStr = resultStr.replace(regex2, 'data_source'); - } - - return resultStr; - } else { - const param = (paramMatch[1] || paramMatch[2] || '').trim(); - - if (param) { - const regex = new RegExp(`\\b${param}\\b`, 'g'); - return baseStr.replace(regex, 'value'); - } + if (params.length >= 1 && params[0]) { + const regex1 = new RegExp(`\\b${params[0]}\\b`, 'g'); + resultStr = isFromJira ? resultStr.replace(regex1, 'issue') : resultStr.replace(regex1, 'value'); } + if (params.length >= 2 && params[1]) { + const regex2 = new RegExp(`\\b${params[1]}\\b`, 'g'); + resultStr = resultStr.replace(regex2, 'api_version'); + } + if (params.length >= 3 && params[2]) { + const regex3 = new RegExp(`\\b${params[2]}\\b`, 'g'); + resultStr = resultStr.replace(regex3, 'data_source'); + } + return resultStr; } return baseStr; @@ -351,8 +345,12 @@ export async function transform_string_to_functions_mappings( if (toJiraFn && fromJiraFn) { transformedMappings[fieldName] = { - toJira: (await toJiraFn) as (value: any) => any, - fromJira: (await fromJiraFn) as (issue: JiraIssue, data_source: Record | null) => any, + toJira: (await toJiraFn) as (value: any, api_version?: '2' | '3') => any, + fromJira: (await fromJiraFn) as ( + issue: JiraIssue, + api_version?: '2' | '3', + data_source?: Record, + ) => any, }; } else { console.warn(`Invalid function in field: ${fieldName}`); diff --git a/src/tools/mapObsidianJiraFields.ts b/src/tools/mapObsidianJiraFields.ts index d8d7934..993abbf 100644 --- a/src/tools/mapObsidianJiraFields.ts +++ b/src/tools/mapObsidianJiraFields.ts @@ -8,6 +8,7 @@ import { debugLog } from './debugLogging'; export function localToJiraFields( data_source: Record, customFieldMappings: Record, + apiVersion?: '2' | '3', ): Record { const jiraFields: Record = {}; @@ -23,10 +24,12 @@ export function localToJiraFields( const mapping = customFieldMappings[key]; try { - // Skip fields that shouldn't be sent to Jira - if (mapping.toJira(value) === null) continue; + const result = mapping.toJira(value, apiVersion); - jiraFields[key] = mapping.toJira(value); + // Skip fields that shouldn't be sent to Jira + if (result === null) continue; + + jiraFields[key] = result; } catch (e) { console.error(`Error mapping for ${key}: ${e}`); new Notice(`Error mapping for ${key}: ${e}`); @@ -42,13 +45,19 @@ export function localToJiraFields( } export async function updateJiraToLocal(plugin: JiraPlugin, file: TFile, issue: JiraIssue): Promise { + const apiVersion = plugin.getCurrentConnection()?.apiVersion; // First, update the frontmatter using processFrontMatter await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => { // Update frontmatter with Jira data - applyJiraDataToLocal(frontmatter, issue, { - ...obsidianJiraFieldMappings, - ...plugin.settings.fieldMapping.fieldMappings, - }); + applyJiraDataToLocal( + frontmatter, + issue, + { + ...obsidianJiraFieldMappings, + ...plugin.settings.fieldMapping.fieldMappings, + }, + apiVersion, + ); }); // Then, process the file content to update sync sections @@ -57,10 +66,15 @@ export async function updateJiraToLocal(plugin: JiraPlugin, file: TFile, issue: const syncSections = extractAllJiraSyncValuesFromContent(fileContent); // Update sync sections with Jira data - applyJiraDataToLocal(syncSections, issue, { - ...obsidianJiraFieldMappings, - ...plugin.settings.fieldMapping.fieldMappings, - }); + applyJiraDataToLocal( + syncSections, + issue, + { + ...obsidianJiraFieldMappings, + ...plugin.settings.fieldMapping.fieldMappings, + }, + apiVersion, + ); // Update content sections from Jira fields let updatedContent = fileContent; @@ -80,10 +94,11 @@ export function applyJiraDataToLocal( localData: Record, issue: JiraIssue, fieldMappings: Record, + apiVersion?: '2' | '3', ): void { // Process existing fields in local data for (const key of Object.keys(localData)) { - updateFieldFromJira(key, localData, issue, fieldMappings); + updateFieldFromJira(key, localData, issue, fieldMappings, apiVersion); } // Ensure required fields are always processed @@ -100,13 +115,14 @@ function updateFieldFromJira( targetObject: Record, issue: JiraIssue, fieldMappings: Record, + apiVersion?: '2' | '3', ): void { try { let value = issue.fields[key]; // Apply custom mapping if available if (key in fieldMappings) { - value = fieldMappings[key].fromJira(issue, targetObject); + value = fieldMappings[key].fromJira(issue, apiVersion, targetObject); } // debugLog(`Updating field: ${key}: ${issue.fields[key]}`); @@ -128,14 +144,14 @@ function logMappingDebugInfo(key: string, error: Error, fieldMappings: Record = {}; + const mappingInfo: Record = {}; for (const mappingKey of Object.keys(fieldMappings)) { mappingInfo[mappingKey] = { - hasToJira: typeof fieldMappings[mappingKey].toJira, - hasFromJira: typeof fieldMappings[mappingKey].fromJira, + toJira: fieldMappings[mappingKey].toJira.toString().trim(), + fromJira: fieldMappings[mappingKey].fromJira.toString().trim(), }; } - console.debug(`Available mappings: ${JSON.stringify(mappingInfo)}`); + console.debug(`Available mappings:`, mappingInfo); } diff --git a/src/tools/markdownToAdf.ts b/src/tools/markdownToAdf.ts index a042a9e..30e125b 100644 --- a/src/tools/markdownToAdf.ts +++ b/src/tools/markdownToAdf.ts @@ -74,7 +74,7 @@ interface AdfDoc { function parseInline(text: string): AdfInlineContent[] { const nodes: AdfInlineContent[] = []; - const regex = /(\*\*(.+?)\*\*|\*(.+?)\*|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)|\[\[[^\]]+\]\])/g; + const regex = /(\*\*\*(.+?)\*\*\*|\*\*(.+?)\*\*|\*(.+?)\*|`([^`]+)`|\[([^\]]+)\]\(([^)]+)\)|\[\[[^\]]+\]\])/g; let lastIndex = 0; let match: RegExpExecArray | null; @@ -83,15 +83,18 @@ function parseInline(text: string): AdfInlineContent[] { nodes.push({ type: 'text', text: text.slice(lastIndex, match.index) }); } if (match[2] !== undefined) { + // ***bold+italic*** + nodes.push({ type: 'text', text: match[2], marks: [{ type: 'strong' }, { type: 'em' }] }); + } else if (match[3] !== undefined) { // **bold** nodes.push({ type: 'text', text: match[2], marks: [{ type: 'strong' }] }); - } else if (match[3] !== undefined) { + } else if (match[4] !== undefined) { // *italic* nodes.push({ type: 'text', text: match[3], marks: [{ type: 'em' }] }); - } else if (match[4] !== undefined) { + } else if (match[5] !== undefined) { // `code` nodes.push({ type: 'text', text: match[4], marks: [{ type: 'code' }] }); - } else if (match[5] !== undefined) { + } else if (match[6] !== undefined) { // [text](url) — convert to ADF link mark nodes.push({ type: 'text', text: match[5], marks: [{ type: 'link', attrs: { href: match[6] } }] }); } else {