From e9553967f46b58a639cd1d95ff06af0c5df39eae Mon Sep 17 00:00:00 2001 From: Alamion Date: Mon, 17 Mar 2025 16:02:03 +0300 Subject: [PATCH] Optimized and fixed several bugs with field mapping. Added how_to detailed instruction about plugin. Updated examples. Many minor bug fixes. --- README.md | 2 +- docs/how_to.md | 0 docs/how_to_en.md | 107 ++++++++++++++++++++++ docs/how_to_ru.md | 106 +++++++++++++++++++++ docs/images/progressPercentageExample.png | Bin 0 -> 6573 bytes docs/images/statisticsExample.png | Bin 0 -> 54401 bytes docs/statistics.md | 2 +- docs/template_example.md | 59 ++++++++++-- src/commands/addWorkLog.ts | 13 ++- src/file_operations/common_prepareData.ts | 7 +- src/main.ts | 2 + src/markdown_html/index.ts | 10 +- src/settings/JiraSettingTab.ts | 52 +++++------ src/tools/convertFunctionString.ts | 80 +++++++++------- src/tools/mappingObsidianJiraFields.ts | 26 ++++-- src/tools/sectionTools.ts | 17 ++-- 16 files changed, 392 insertions(+), 91 deletions(-) delete mode 100644 docs/how_to.md create mode 100644 docs/how_to_en.md create mode 100644 docs/how_to_ru.md create mode 100644 docs/images/progressPercentageExample.png create mode 100644 docs/images/statisticsExample.png diff --git a/README.md b/README.md index 4e7927c..aad4443 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,4 @@ Until it's stopped by '# ' or other jira field ## Learn More -Visit the [GitHub repository](https://github.com/your-username/obsidian-jira-plugin) for detailed docs, stats, and updates. +Visit the [GitHub repository](https://github.com/your-username/obsidian-jira-plugin/docs) for more detailed docs. diff --git a/docs/how_to.md b/docs/how_to.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/how_to_en.md b/docs/how_to_en.md new file mode 100644 index 0000000..6044a7e --- /dev/null +++ b/docs/how_to_en.md @@ -0,0 +1,107 @@ +The functionality of the plugin can be divided into several parts. + +### Basic Functionality +For basic functionality, it is enough to specify Jira credentials (username, password, URL) and the folder for created tasks. + +When running `Get issues from Jira with custom key`, the command will download the latest tasks from Jira and save them in the specified folder. + +Without a template, such a page will be completely empty except for its title, so using a template is highly recommended. The template is used only when creating a new page. + +#### Template +The template can consist of several different parts: +1. **formatter** - meta-information at the top of the screen. When specifying keys for it and using any `Get issues from Jira` variant, they will be populated with corresponding fields from the response data. +2. **body** - the main content of the page. When using indicators like `jira-sync-section-*` or `jira-sync-line-*`, they will be filled with the corresponding fields from the response data. The difference between these two options is as follows: `line` reads and writes values from the current line, separating the indicator and value with a space. `section` reads values from multiple lines after the indicator, stopping only at another indicator or a heading. An example can be found in [[docs/template_example.md]]. + +It is highly recommended to specify basic values in the template's formatter: `key` - the Jira task ID used for updates, `summary` - the Jira task title, and `status` - the current status of the task in Jira. + +The body takes priority and will overwrite formatter values when updating a task in Jira if a field's value is present in both sections. + +Not all fields are predefined, and some may need adjustments. For example, the template provided in [[docs/template_example.md]] will not correctly retrieve `progressPercent` and `creator` from Jira, even though these fields exist. To fix this, refer to the advanced usage section below. + +### Commands + +Currently, the plugin provides the following commands: +- `Get issue from Jira with custom key` - allows creating a file in the configured folder that imports information from Jira using a manually specified ID. +- `Get issue from Jira` - allows updating the active file if its formatter contains a `key` (the Jira task ID). +- `Update issue from Jira` - allows updating the information from the file in Jira using the key specified in the formatter. Some system fields (e.g., `status`) cannot be changed this way and have dedicated commands. +- `Create issue from Jira` - allows creating a new task in Jira. The formatter must include `summary` (task title) and optionally `project` and `issuetype` (the latter two can be selected from existing options during creation). +- `Update work log in Jira` - enables tracking time spent on a task. Currently, this is not reflected in the file, but it will be available in future updates. If the formatter contains `jira_selected_week_data` (as described in [[docs/jira_selected_week_data.md]]), instead of manual entry, a batch of data from `jira_selected_week_data` 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. + +### Advanced Usage + +#### Field Mapping +In the settings, you can configure custom mapping for any additional fields received from Jira. To do this: +- Configure how information is sent to Jira (e.g., with the `null` function, the field will be ignored). +- Define how it is received from Jira (e.g., `issue.fields.creator.name` will retrieve the creator's name instead of the entire object with related data). + +Similarly, you can configure the `progressPercentage` shown in the example. This field does not exist in the response, but it can be "assembled" from the existing `progress` field: `issue.fields.progress.total ? 100 * issue.fields.progress.progress / issue.fields.progress.total : 0`. As seen in the syntax, mapping uses a simplified TypeScript format. + +It will look something like this: + +![](images/progressPercentageExample.png) + +#### Statistics +This is a pre-configured file [[docs/statistics.md]]. To work with it, several additional plugins are required: +- **Timekeep** for tracking time spent on tasks - each task can have a multi-level timer started, stopped, and edited. +- **Dataview** with JavaScript queries enabled in settings to create a dynamic table of total time spent over the last few weeks. +- **Meta Bind** for selecting the desired week and submitting work data for the week with a single button. + +As mentioned, the statistics page allows sending a batch of work data for the target week, reducing manual input time. + +With Obsidian themes disabled, the table looks like this: + +![](images/statisticsExample.png) + +The data batch format for submission, if you want to create an alternative [[docs/statistics.md]] variant: +```json +{ + "type": "array", + "items": { + "type": "object", + "properties": { + "issueKey": { + "type": "string", + "pattern": "^[A-Z]+-\d+$", + "description": "Task key in the Jira system" + }, + "startTime": { + "type": "string", + "format": "date-time", + "description": "Start time in DD-MM-YYYY HH:MM format" + }, + "duration": { + "type": "string", + "description": "Task duration in hours" + }, + "comment": { + "type": "string", + "description": "Job description" + } + }, + "required": ["issueKey", "startTime", "duration"] + } +} +``` +Example: + +```json +[ + { + "file": "Develop new Obsidian plugin", + "issueKey": "JIR-2", + "blockPath": "Deployment & Documentation > Publishing on Obsidian Marketplace", + "startTime": "17-03-2025 17:00", + "endTime": "17-03-2025 21:00", + "duration": "4h" + }, + { + "file": "Develop new Obsidian plugin", + "issueKey": "JIR-2", + "blockPath": "Deployment & Documentation > Writing Documentation", + "startTime": "18-03-2025 12:00", + "endTime": "18-03-2025 21:00", + "duration": "9h" + } +] +``` diff --git a/docs/how_to_ru.md b/docs/how_to_ru.md new file mode 100644 index 0000000..3315758 --- /dev/null +++ b/docs/how_to_ru.md @@ -0,0 +1,106 @@ +Функционал плагина можно разделить на несколько частей. + +### Базовый функционал +Для него достаточно указать креды Jira (username, password, URL) и папку с создаваемыми задачами. + +При запуске `Get issues from Jira with custom key` команда будет скачивать актуальные задачи из Jira и сохранять их в указанную папку. + +Без шаблона такая страничка будет полностью пустой за исключением её названия, так что шаблон настоятельно рекомендуется к использованию. Шаблон используется только при создании новой страницы. + +#### Шаблон +Шаблон можно составить из нескольких разных частей: +1. **formatter** - он же метаинформация сверху экрана. При указании ключей для него и любом варианте `Get issues from Jira` они будут заполняться соответствующими полями из ответных данных. +2. **body** - основное содержимое страницы. При написании указателя вида `jira-sync-section-*` или `jira-sync-line-*` они будут заполняться соответствующими полями из ответных данных. Разница между этими двумя вариантами в следующем: `line` читает и пишет значение из текущей строки, разделяя указатель и значение пробелом. `section` читает значение из ряда строк после указателя, останавливаясь только при другом указателе или заголовке. Пример можно посмотреть в [[docs/template_example.md]] + +Настоятельно рекомендуется указать в formatter шаблоона базовые значения: `key` - id задачи в Jira, по которому производится обновление, `summary` - название задачи в Jira, `status` - текущий статус задачи в Jira. + +body имеет приоритет и будет перезаписывать значения formatter при одновлении задачи в Jira, если значения для одного поля имеются в обоих местах. + +Не все поля прописаны заранее и некоторые могут нуждаться в доработке. Пример предоставленный в [[docs/template_example.md]] например, не сможет подтянуть корректно `progressPercent` и `creator` из Jira, хотя такие поля существуют. Чтобы это исправить, нужно отбратиться к разделу продвинутого использования ниже. + +### Команды + +На текущий момент плагин предоставляет следующие команды: +- `Get issue from Jira with custom key` - позволяет создать в папке, указанной в настройках, файл, импортирующий информацию из Jira по указанному вручную id. +- `Get issue from Jira` - позволяет обновить активный файл, если в его formatter указан key - id задачи Jira. +- `Update issue from Jira` - позволяет обновить информацию из файла в Jira по указанному в formatter ключу. Ряд системных полей (например, `status`, таким образом изменить нельзя. Для них созданы отдельные команды) +- `Create issue from Jira` - позволяет создать в Jira новую задачу. В formatter обязательно нужно указать summary - название задачи и, опционально, `project` и `issuetype` (последние два можно выбрать из существующих при создании) +- `Update work log in Jira` - позволяет вести учёт потраченного на задачу времени. В данный момент он никак не отображается в файле, это будет в ближайших обновлениях. Если в файле в formatter есть `jira_selected_week_data` (как это в [[docs/jira_selected_week_data.md]]), то вместо ручного заполнения будет послан батч данных из `jira_selected_week_data` с обновлением каждой из представленных сущностей. +- `Update issue status in Jira` - позволяет обновить статус задачи, выбрав один из возможных вариантов. + +### Продвинутое использование + +#### Маппинг полей +В настройках можно задать кастомный маппинг дл любых дополнительных полей, приходящих из запроса. Для этого нужно: +- Настроить в каком виде информация отправляется в Jira (например при функции `null` поле будет игнорироваться) +- В каком виде получается из Jira (например `issue.fields.creator.name` позволит получать соответственное имя создателя запроса, а не весь объект с информацией о нём в целом.) + +Таким же образом можно настроить показанный в примере `progressPercentage` - такого поля в запросе не существует, но его можно 'собрать' из существующего `progress`: `issue.fields.progress.total ? 100 * issue.fields.progress.progress / issue.fields.progress.total : 0`. Как понятно из синтаксиса, для маппинга используется обрезанный TypeScript + +Будет это выглядеть примерно так: + +![](images/progressPercentageExample.png ) + +#### Статистика +Это предзаготовленный файл [[docs/statistics.md]]. Для работы с ним потребуется несколько дополнительных плагинов: +- **Timekeep** для ведения времени работы с задачами - в каждой задаче можно запускать, останавливать и редактировать многоуровневый таймер; +- **Dataview** с включенным в настройках JavaScript queries для создания динамичной таблички всего потраченного времени за последние пару недель; +- **Meta Bind** для выбора нужной недели и отправки данных о работе за неделю одной кнопкой. + +Как было написано, страница статистики позволяет отправить сразу батч данных о работе за целевую неделю, сокращая время для ручного труда. + +Выглядит табличка с выключенными темами Обсидиана примерно так: + +![](images/statisticsExample.png) + +Формат батча данных для отправки данных если захочется сделать альтернативный [[docs/statistics.md]] вариант: +```json +{ + "type": "array", + "items": { + "type": "object", + "properties": { + "issueKey": { + "type": "string", + "pattern": "^[A-Z]+-\\d+$", + "description": "Ключ задачи в системе Jira" + }, + "startTime": { + "type": "string", + "format": "date-time", + "description": "Время начала в формате DD-MM-YYYY HH:MM" + }, + "duration": { + "type": "string", + "description": "Продолжительность задачи в часах" + }, + "comment": { + "type": "string", + "description": "описание проделанной работы" + } + }, + "required": ["issueKey", "startTime", "duration"] + } +} +``` +Пример: +```json +[ + { + "file": "Develop new Obsidian plugin", + "issueKey": "JIR-2", + "blockPath": "Deployment & Documentation > Publishing on Obsidian Marketplace", + "startTime": "17-03-2025 17:00", + "endTime": "17-03-2025 21:00", + "duration": "4h" + }, + { + "file": "Develop new Obsidian plugin", + "issueKey": "JIR-2", + "blockPath": "Deployment & Documentation > Writing Documentation", + "startTime": "18-03-2025 12:00", + "endTime": "18-03-2025 21:00", + "duration": "9h" + } +] +``` diff --git a/docs/images/progressPercentageExample.png b/docs/images/progressPercentageExample.png new file mode 100644 index 0000000000000000000000000000000000000000..f937737ac9041729da28191e2e910cedd909a8bc GIT binary patch literal 6573 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 literal 0 HcmV?d00001 diff --git a/docs/images/statisticsExample.png b/docs/images/statisticsExample.png new file mode 100644 index 0000000000000000000000000000000000000000..266edd509f6eb1121b0c48fddd1449808083ce7a GIT binary patch literal 54401 zcmdSBc|4T=-}kMRQV}6pMo~maWXUp=CE1dSvP?*_@4GQ6vYU#cEMu4K+1DY<)CgI} zHU?wtW-`N!F=pl--`{!O=Y3xHwVc;=|8@Ik#_@1`=5u_OW1g?q>wSp3Z)$M*^svV>=Saah!F;4o)d%V|&GBsH^iZ*lwfEBth2TQ&9$7BBJB-%XeKo zO6LsI8jtAs@W%6>I~r@cqdn7^n`^FB765JRQTX zLps_&BBOQ{Bf5aU(eC9at#HKbG$omSN|ZyTVbH1XQyb%KJH*G&Z{!fG18g0E`#-K6 zykYBb(MdgW@W%aAp76ozGe>S#Pgy@gZ=~1{UMJ4$svW#`^ok+>Ia&F*`I&9e3<$L(Y?m|&MNrT|5E1Vrxz zvu~m}in)#*d%c^o`x-~_YL<`KF{cbw+67_y=_|)P7lCGZ`}9EyTEz^^=mU|1Xu#F> zT?C;8ruF>bN>OMEmj#sz7!U-ZJfUpK1Mylk1b{yb5kXoj#m6+Y#o!tJ0$sbnFuF+) zU@c_7gw|^}dN2@5f36_Cv^Lx6juIM-EH$Pp3T@_qyT~ywsqnIAig+RMD|XK2ixjhr zwiJZ%hqZlQHM>M^NQk9x!DR&$K@7i_UGc+EnA`_`YX}Wxu?GxCftj!#^o(83ThxA%>!&f*+EL-B%thfNKoGZH`v zc%`WiW8mav#|XL^p}2Kuc+Wu)0s^q8VM^33nFjhzNGlw@hVXDRY& zZyetK0b5V=#FL=t3~!2Rb65pl6*CiM87~@R8MjZ3hBAFZe4l~f?*s(rPjsd&JW4-l z1pcf(zjf^H5}slJ-zy8FPvdN9JIs})34KsiFowDiG$bRt6p*$^_y6ZUywQ2mT$HKk zEb(`RI4$<=>Tct!wScphwq(%0P55_uPEjEGP&=hn37q<}#*Q@68jw zJhk7E4!xO2Hxe`g_Not??)M1rla6&p>QYP1T2zKC@3EE&?!+D2*7`W&{Oc>N$47x` z*KbcUj(8zs@Wo8|HH#x->c}Ra68S2P{RuF9Q1Wbk^FpyQ=XlWY4JgEpF+>_+)~>fh z8Rp9Ba;#Z+by!BXDI&=sLgZqo3JOl-j?1J1wDT=Fx`ENXm7r4ub3~<^)I~nD9)R}g zWGQMWY|#y0Hc+|r9P~bR)-3txWQ`tG9-i2)Pn-bnwo#3rnYIp$vbu{OfyCO0?2%}A z^{vIZe9-60^8Mn-aM-PCh(BeY^8RlI1_-lz6nB(N{H7;yLFUwttCGm0`RNH!V=-KPZt{>| zEF`n2Lqm%KE#>ShJsoovSIOv78FEuF6FAGb3Bnn0$1&reiGN2+l~~IC7A2Y z673|A#fQokU9eU5st~`(vGJR!a!txytDdk{ej=iq{ekawq6%X zg5$A+*4r}H2c0S}JBM2W>@c)u&NfsA>IVIR=^|0{eD4-393>JLS~k=e^^Lbr&+ayiA^U__|V^pgpA%ss;#EQCJM&dHjOosh2 z9sp|s?S0_azQq!!y)dZQ=uNPV!SV&1&bV@)U#vjdLjER-6yC| znpQlqKZ)zHk-NMxq`~eOKZrt`%0As!fQ}-ml+jRf_-5APBJvUd^tA+hsTT`Lm{(`& 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 { if (frontmatter["jira_selected_week_data"]) { try { - workLogData = JSON.parse(frontmatter["jira_selected_week_data"]); + let jira_selected_week_data = frontmatter["jira_selected_week_data"]; + + if (typeof jira_selected_week_data === 'string') { + workLogData = JSON.parse(jira_selected_week_data); + } else if (typeof jira_selected_week_data === 'object') { + workLogData = jira_selected_week_data; + } else { + throw new TypeError(`jira_selected_week_data has an invalid type: ${typeof jira_selected_week_data}`); + } foundData = true; } catch (error) { console.error("Failed to parse jira_selected_week_data:", error); @@ -105,7 +114,7 @@ async function processWorkLogBatch(plugin: JiraPlugin, workLogs: any[]): Promise continue; } const parsed_duration = parseDuration(duration); - console.log(`Duration: ${duration}, Parsed duration: ${parsed_duration}`); + debugLog(`Duration: ${duration}, Parsed duration: ${parsed_duration}`); if (parsed_duration === '') { addFailure(results, "Duration must be at least 1 minute", issueKey); continue; diff --git a/src/file_operations/common_prepareData.ts b/src/file_operations/common_prepareData.ts index b924e8a..4ea0207 100644 --- a/src/file_operations/common_prepareData.ts +++ b/src/file_operations/common_prepareData.ts @@ -2,6 +2,7 @@ import JiraPlugin from "../main"; import {Notice, TFile} from "obsidian"; import {extractAllJiraSyncValuesFromContent} from "../tools/sectionTools"; import {fieldMappings, localToJiraFields} from "../tools/mappingObsidianJiraFields"; +import {debugLog} from "../tools/debugLogging"; /** * Prepares Jira fields from the content and frontmatter of a file @@ -25,6 +26,7 @@ export async function prepareJiraFieldsFromFile( // Get frontmatter and prepare fields await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => { + debugLog(`Received frontmatter info: ${JSON.stringify(frontmatter)}`) // Get issue key if exists issueKey = frontmatter["key"]; @@ -43,6 +45,9 @@ export async function getCurrentFileMainInfo(plugin: JiraPlugin): Promise<{issue new Notice("No active file"); return {}; } - const {issueKey} = await prepareJiraFieldsFromFile(plugin, file); + let issueKey: string | undefined + await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => { + issueKey = frontmatter["key"]; + }); return {issueKey, filePath: file.path}; } diff --git a/src/main.ts b/src/main.ts index b798ea4..d7e6b5d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,6 +8,7 @@ import { registerCreateIssueCommand, registerGetIssueCommandWithKey, registerUpdateIssueStatusCommand } from "./commands"; import {registerUpdateWorkLogCommand} from "./commands/addWorkLog"; +import {transform_string_to_functions_mappings} from "./tools/convertFunctionString"; /** * Main plugin class @@ -66,6 +67,7 @@ export default class JiraPlugin extends Plugin { */ async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + this.settings.fieldMappings = transform_string_to_functions_mappings(this.settings.fieldMappingsStrings); } /** diff --git a/src/markdown_html/index.ts b/src/markdown_html/index.ts index cfd1c43..b147d16 100644 --- a/src/markdown_html/index.ts +++ b/src/markdown_html/index.ts @@ -3,8 +3,14 @@ * @param str The Jira markup string * @returns The Markdown string */ -export function jiraToMarkdown(str: string): string { - if (!str) return ''; +export function jiraToMarkdown(str: any): string { + if (str === null || str === undefined) return ''; + else if (typeof str === "number") { + str = str.toString() + } + else if (typeof str === "object") { + str = JSON.stringify(str) + } return ( str // Un-Ordered Lists diff --git a/src/settings/JiraSettingTab.ts b/src/settings/JiraSettingTab.ts index 6a73fc6..9a31e25 100644 --- a/src/settings/JiraSettingTab.ts +++ b/src/settings/JiraSettingTab.ts @@ -1,8 +1,11 @@ import {App, normalizePath, PluginSettingTab, setIcon, Setting} from "obsidian"; import JiraPlugin from "../main"; -import {FieldMapping, fieldMappings} from "../tools/mappingObsidianJiraFields"; -import {functionToArrowString, safeStringToFunction} from "../tools/convertFunctionString"; -import {JiraIssue} from "../interfaces"; +import {fieldMappings} from "../tools/mappingObsidianJiraFields"; +import { + functionToExpressionString, + transform_string_to_functions_mappings +} from "../tools/convertFunctionString"; +import {debugLog} from "../tools/debugLogging"; /** * Settings tab for the plugin @@ -173,6 +176,7 @@ export class JiraSettingTab extends PluginSettingTab { // Function to save all field mappings const saveFieldMappings = async () => { + debugLog(`From strings ${JSON.stringify(this.plugin.settings.fieldMappingsStrings)}\n\n and funcs ${JSON.stringify(this.plugin.settings.fieldMappings)}`) const mappings: Record = {}; const fieldItems = fieldsList.querySelectorAll(".field-mapping-item"); @@ -203,24 +207,8 @@ export class JiraSettingTab extends PluginSettingTab { // Store the string representations directly in a separate property this.plugin.settings.fieldMappingsStrings = mappings; - // Also convert to functions for runtime use - const transformedMappings: Record = {}; - for (const [fieldName, { toJira, fromJira }] of Object.entries(mappings)) { - const toJiraFn = safeStringToFunction(toJira); - const fromJiraFn = safeStringToFunction(fromJira); - - if (toJiraFn && fromJiraFn) { - transformedMappings[fieldName] = { - toJira: toJiraFn as (value: any) => any, - fromJira: fromJiraFn as (issue: JiraIssue, data_source: Record) => any, - }; - } else { - console.warn(`Invalid function in field: ${fieldName}`); - } - } - // Save the functional mappings - this.plugin.settings.fieldMappings = transformedMappings; + debugLog(`To strings ${JSON.stringify(this.plugin.settings.fieldMappingsStrings)}\n\nand funcs ${JSON.stringify(this.plugin.settings.fieldMappings)}`) await this.plugin.saveSettings(); }; @@ -261,8 +249,8 @@ export class JiraSettingTab extends PluginSettingTab { const defaultMappingsStrings: Record = {}; for (const [fieldName, mapping] of Object.entries(fieldMappings)) { defaultMappingsStrings[fieldName] = { - toJira: functionToArrowString(mapping.toJira), - fromJira: functionToArrowString(mapping.fromJira) + toJira: functionToExpressionString(mapping.toJira), + fromJira: functionToExpressionString(mapping.fromJira) }; } this.plugin.settings.fieldMappingsStrings = defaultMappingsStrings; @@ -273,6 +261,7 @@ export class JiraSettingTab extends PluginSettingTab { // Load existing mappings if available const loadExistingMappings = () => { + debugLog(`Loading mapping settings`) // Clear existing field list fieldsList.empty(); @@ -290,6 +279,7 @@ export class JiraSettingTab extends PluginSettingTab { ); } } + this.plugin.settings.fieldMappings = transform_string_to_functions_mappings(this.plugin.settings.fieldMappingsStrings); } // Otherwise, try to use the function mappings and convert them to strings else if (this.plugin.settings.fieldMappings && @@ -300,8 +290,8 @@ export class JiraSettingTab extends PluginSettingTab { if (mapping && typeof mapping === 'object' && 'toJira' in mapping && 'fromJira' in mapping) { addFieldMapping( fieldName, - functionToArrowString(mapping.toJira), - functionToArrowString(mapping.fromJira) + functionToExpressionString(mapping.toJira), + functionToExpressionString(mapping.fromJira) ); } } @@ -323,18 +313,18 @@ export class JiraSettingTab extends PluginSettingTab { [ { name: "summary", - toJira: "(value) => value", - fromJira: "(issue) => issue.fields.summary" + toJira: "value", + fromJira: "issue.fields.summary" }, { - name: "description", - toJira: "(value) => markdownToJira(value)", - fromJira: "(issue) => jiraToMarkdown(issue.fields.description)" + name: "reporter", + toJira: "null", + fromJira: "issue.fields.reporter.name" }, { name: "project", - toJira: "(value) => ({ key: value })", - fromJira: "(issue) => issue.fields.project ? issue.fields.project.key : \"\"" + toJira: "({ key: value })", + fromJira: "issue.fields.project ? issue.fields.project.key : \"\"" } ].forEach(example => { const item = examplesList.createEl("li"); diff --git a/src/tools/convertFunctionString.ts b/src/tools/convertFunctionString.ts index b24928a..8f78a2e 100644 --- a/src/tools/convertFunctionString.ts +++ b/src/tools/convertFunctionString.ts @@ -1,91 +1,103 @@ import {Notice} from "obsidian"; const forbiddenPatterns = ["document", "window", "eval", "Function", "fetch"]; // Prevent unsafe code execution -import { jiraToMarkdown, markdownToJira } from "../markdown_html"; +import {FieldMapping} from "./mappingObsidianJiraFields"; +import {JiraIssue} from "../interfaces"; function isValidFunctionBody(fnString: string): boolean { return !forbiddenPatterns.some(forbidden => fnString.includes(forbidden)); } -export function safeStringToFunction(fnString: string): Function | null { +export function safeStringToFunction(exprString: string): Function | null { try { - // Extract function body from an arrow function string - const functionBodyMatch = fnString.match(/\((.*?)\)\s*=>\s*(.*)/); - if (!functionBodyMatch) return null; + // Check if the input is a full arrow function or just an expression + const arrowFnMatch = exprString.match(/^\s*\((.*?)\)\s*=>\s*(.*)$/s); - const params = functionBodyMatch[1].trim(); - const body = functionBodyMatch[2].trim(); + // If it's an arrow function, extract just the body + const body = arrowFnMatch ? arrowFnMatch[2].trim() : exprString.trim(); // Ensure function body does not contain unsafe references if (!isValidFunctionBody(body)) { - console.warn(`Unsafe function detected: ${fnString}`); - new Notice(`Unsafe function detected: ${fnString}`); + console.warn(`Unsafe expression detected: ${exprString}`); + new Notice(`Unsafe expression detected: ${exprString}`); return null; } // Create a function that has access to our utility functions - return function(...args: any[]) { + return function(issue: any) { // Create a context with our utility functions const context = { - jiraToMarkdown, - markdownToJira + // jiraToMarkdown, + // markdownToJira }; // Create a new function with the utility functions in scope const fn = new Function( - ...params.split(',').map(p => p.trim()), + 'issue', ` - const jiraToMarkdown = this.jiraToMarkdown; - const markdownToJira = this.markdownToJira; + // const jiraToMarkdown = this.jiraToMarkdown; + // const markdownToJira = this.markdownToJira; return ${body}; ` ); // Execute the function with our context - return fn.apply(context, args); + return fn.apply(context, [issue]); }; } catch (error) { - console.error("Failed to parse function string:", error); + console.error("Failed to parse expression:", error); return null; } } -export function functionToArrowString(fn: Function): string { +export function functionToExpressionString(fn: Function): string { try { const fnStr = fn.toString().trim(); - // Handle arrow functions (both single-line and multi-line) + // Handle arrow functions const arrowMatch = fnStr.match(/^\s*\(?([^)]*)\)?\s*=>\s*(.+)$/s); if (arrowMatch) { - const params = arrowMatch[1].trim(); - const body = arrowMatch[2].trim(); - return `(${params}) => ${body}`; - } else { - console.error("Failed to parse arrow function:", fnStr); + return arrowMatch[2].trim(); } - // Handle regular named and anonymous functions + // Handle regular functions const functionMatch = fnStr.match(/^(?:function\s+[\w$]*\s*)?\(([^)]*)\)\s*\{([\s\S]*)\}$/); if (functionMatch) { - const params = functionMatch[1].trim(); let body = functionMatch[2].trim(); // If body contains just a return statement, simplify it const returnMatch = body.match(/^\s*return\s+([\s\S]*?)\s*;\s*$/); if (returnMatch) { - body = returnMatch[1]; + return returnMatch[1].trim(); } - - return `(${params}) => ${body}`; - } else { - console.error("Failed to parse function:", fnStr); } - // Return original string if no patterns match - return fnStr; + // If we can't parse it properly, return empty string + console.error("Failed to extract expression from function:", fnStr); + return ""; } catch (error) { - console.error("Error converting function to string:", error); + console.error("Error converting function to expression string:", error); return ""; } } + +export const transform_string_to_functions_mappings = ( + mappings: Record) => { + // Also convert to functions for runtime use + const transformedMappings: Record = {}; + for (const [fieldName, { toJira, fromJira }] of Object.entries(mappings)) { + const toJiraFn = safeStringToFunction(toJira); + const fromJiraFn = safeStringToFunction(fromJira); + + if (toJiraFn && fromJiraFn) { + transformedMappings[fieldName] = { + toJira: toJiraFn as (value: any) => any, + fromJira: fromJiraFn as (issue: JiraIssue, data_source: Record) => any, + }; + } else { + console.warn(`Invalid function in field: ${fieldName}`); + } + } + return transformedMappings +} diff --git a/src/tools/mappingObsidianJiraFields.ts b/src/tools/mappingObsidianJiraFields.ts index 36a2959..c9f1365 100644 --- a/src/tools/mappingObsidianJiraFields.ts +++ b/src/tools/mappingObsidianJiraFields.ts @@ -3,6 +3,7 @@ import {jiraToMarkdown, markdownToJira} from "../markdown_html"; import {Notice, TFile} from "obsidian"; import JiraPlugin from "../main"; import {extractAllJiraSyncValuesFromContent, updateJiraSyncContent} from "./sectionTools"; +import {debugLog} from "./debugLogging"; /** * Field mapping configurations @@ -27,8 +28,8 @@ export const fieldMappings: Record = { fromJira: (issue) => issue.fields.summary, }, "description": { - toJira: (value) => markdownToJira(value), // markdownToJira is applied separately - fromJira: (issue) => jiraToMarkdown(issue.fields.description as string), + toJira: (value) => value, // markdownToJira is applied separately + fromJira: (issue) => issue.fields.description, }, "key": { toJira: () => null, // Not sent to Jira @@ -155,10 +156,10 @@ export async function updateLocalFromJira( // Update sections from Jira fields if they exist for (const [fieldName, fieldValue] of Object.entries(syncSections)) { // Only update fields that can be in sections and only existing sections - if (syncSections[fieldName]) { - const markdownValue = jiraToMarkdown(fieldValue); - fileContent = updateJiraSyncContent(fileContent, fieldName, markdownValue); - } + debugLog(`Updating ${fieldName} = ${fieldValue} in content`) + const markdownValue = jiraToMarkdown(fieldValue); + debugLog(`Converted from jira variant ${fieldValue} to md variant ${markdownValue}`) + fileContent = updateJiraSyncContent(fileContent, fieldName, markdownValue); } // Save the updated content @@ -207,7 +208,18 @@ export function updateLocalRecordsFromJira( try { value = customFieldMappings[key].fromJira(issue, {...frontmatter, ...sections}); } catch (e) { - console.error(`Error mapping for ${key}: ${e}`); + const result: Record = {}; + + for (const key of Object.keys(customFieldMappings)) { + result[key] = { + hasToJira: typeof customFieldMappings[key].toJira, + hasFromJira: typeof customFieldMappings[key].fromJira + }; + } + console.error(`Error mapping for ${key}: ${e}\n${JSON.stringify(result)}`); new Notice(`Error mapping for ${key}: ${e}`); continue; } diff --git a/src/tools/sectionTools.ts b/src/tools/sectionTools.ts index e51c439..9bd2d0e 100644 --- a/src/tools/sectionTools.ts +++ b/src/tools/sectionTools.ts @@ -23,15 +23,17 @@ export function updateJiraSyncContent(fileContent: string, sectionName: string, * @returns The updated file content */ function updateJiraSyncSection(fileContent: string, sectionName: string, markdownContent: string, force: boolean = false): string { - const sectionRegex = new RegExp(`\`jira-sync-section-${sectionName}\`\\n([\\s\\S]*?)(?=\\n##|\\n\`jira-sync-|$)`, 'g'); + const sectionRegex = new RegExp(`(\`jira-sync-section-${sectionName}\` [\\s\\S]*?)\\n([\\s\\S]*?)(?=\\n##|\`jira-sync-|$)`, 'g'); // If section exists, update it, otherwise append it + // debugLog(`Trying to find field ${sectionName} in section`) if (sectionRegex.test(fileContent)) { // Reset regex lastIndex + // debugLog(`Found a field ${sectionName} in section`) sectionRegex.lastIndex = 0; return fileContent.replace( sectionRegex, - `\`jira-sync-section-${sectionName}\`\n${markdownContent}\n` + (match, group1) => `${group1}\n${markdownContent}\n` ); } else if (force) { // No section found, append it @@ -54,8 +56,10 @@ function updateJiraSyncLine(fileContent: string, lineName: string, lineContent: const newLine = `\`jira-sync-line-${lineName}\` ${lineContent.split('\n')[0]}`; // If line exists, update it, otherwise append it + // debugLog(`Trying to find field ${lineName} in line`) if (lineRegex.test(fileContent)) { // Reset regex lastIndex + // debugLog(`Found a field ${lineName} in line`) lineRegex.lastIndex = 0; return fileContent.replace(lineRegex, newLine); } else if (force) { @@ -74,7 +78,7 @@ function updateJiraSyncLine(fileContent: string, lineName: string, lineContent: export function extractAllJiraSyncValuesFromContent(fileContent: string): Record { const sections = extractAllJiraSyncValuesFromSections(fileContent); const lines = extractAllJiraSyncValuesFromLines(fileContent); - debugLog(`extracted from file: ${JSON.stringify(sections)}`); + debugLog(`extracted content from file: ${JSON.stringify({ ...sections, ...lines })}`); return { ...sections, ...lines }; } @@ -84,13 +88,13 @@ export function extractAllJiraSyncValuesFromContent(fileContent: string): Record * @returns Object mapping section names to their content */ function extractAllJiraSyncValuesFromSections(fileContent: string): Record { - const syncSectionRegex = /`jira-sync-section-([\w-]+)`\n([\s\S]*?)(?=\n##|\n`jira-sync-|$)/g; + const syncSectionRegex = /`jira-sync-section-([\w-]+)` ([\s\S]*?)\n([\s\S]*?)(?=\n##|\n`jira-sync-|$)/g; const sections: Record = {}; - let match; while ((match = syncSectionRegex.exec(fileContent)) !== null) { + // debugLog(`Found match ${JSON.stringify(match)} in section`) const sectionName = match[1]; - sections[sectionName] = match[2].trim(); + sections[sectionName] = match[3].trim(); } return sections; @@ -107,6 +111,7 @@ function extractAllJiraSyncValuesFromLines(fileContent: string): Record