- multiple fixes and code refactors for last feats to work correctly
- added Jira api_version variable to field mapping context
This commit is contained in:
Alamion 2026-05-04 21:05:29 +03:00
parent 75dd9d75cd
commit a3211bcdb2
No known key found for this signature in database
GPG key ID: E5712F673595B31D
13 changed files with 129 additions and 81 deletions

View file

@ -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()`.

View file

@ -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()`.

View file

@ -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",

View file

@ -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",

View file

@ -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<string, any> | null) => any;
toJira: (value: any, api_version?: '2' | '3') => any;
fromJira: (issue: JiraIssue, api_version?: '2' | '3', data_source?: Record<string, any>) => any;
}
export const TO_JIRA_PARAMS = ['value', 'api_version'];
export const FROM_JIRA_PARAMS = ['issue', 'api_version', 'data_source'];
export const obsidianJiraFieldMappings: Record<string, FieldMapping> = {
summary: {
@ -13,8 +15,9 @@ export const obsidianJiraFieldMappings: Record<string, FieldMapping> = {
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<string, FieldMapping> = {
},
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}`))

View file

@ -9,15 +9,20 @@ import { obsidianJiraFieldMappings } from '../default/obsidianJiraFieldsMapping'
export async function updateIssueFromFile(plugin: JiraPlugin, file: TFile): Promise<string> {
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<string, any>,
): Promise<string> {
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;

View file

@ -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

View file

@ -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: Включить проверку полей

View file

@ -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);
}
/**

View file

@ -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) => {

View file

@ -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<string, any> | null) {
return function (issue: JiraIssue, api_version?: '2' | '3', data_source?: Record<string, any>) {
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<string, any> | 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<string, any>,
) => any,
};
} else {
console.warn(`Invalid function in field: ${fieldName}`);

View file

@ -8,6 +8,7 @@ import { debugLog } from './debugLogging';
export function localToJiraFields(
data_source: Record<string, any>,
customFieldMappings: Record<string, FieldMapping>,
apiVersion?: '2' | '3',
): Record<string, any> {
const jiraFields: Record<string, any> = {};
@ -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<void> {
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<string, any>,
issue: JiraIssue,
fieldMappings: Record<string, FieldMapping>,
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<string, any>,
issue: JiraIssue,
fieldMappings: Record<string, FieldMapping>,
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<st
new Notice(`Error mapping for ${key}: ${error}`);
// Create debug info about available mappings
const mappingInfo: Record<string, { hasToJira: string; hasFromJira: string }> = {};
const mappingInfo: Record<string, { toJira: string; fromJira: string }> = {};
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);
}

View file

@ -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 {