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 0000000..f937737 Binary files /dev/null and b/docs/images/progressPercentageExample.png differ diff --git a/docs/images/statisticsExample.png b/docs/images/statisticsExample.png new file mode 100644 index 0000000..266edd5 Binary files /dev/null and b/docs/images/statisticsExample.png differ diff --git a/docs/statistics.md b/docs/statistics.md index 1b6d7a9..50945bc 100644 --- a/docs/statistics.md +++ b/docs/statistics.md @@ -98,7 +98,7 @@ id: "" style: primary actions: - type: command - command: obsidian-jira-sync:update-work-log-jira + command: jira-sync:update-work-log-jira \`\`\``); } else { dv.paragraph("No entries found."); diff --git a/docs/template_example.md b/docs/template_example.md index 5eb4347..244bb9d 100644 --- a/docs/template_example.md +++ b/docs/template_example.md @@ -1,17 +1,20 @@ --- key: assignee: +summary: +status: deadline: -tags: - - task lastViewed: -status: +tags: +- task --- +`BUTTON[get]` `BUTTON[update]` +`BUTTON[status]` `BUTTON[worklogs]` ### Time spent -```timekeep -{"entries":[]} -``` +```timekeep +{"entries":[]} +``` ### Deadline @@ -29,3 +32,47 @@ status: `jira-sync-line-reporter` `jira-sync-line-creator` + + + + + +```meta-bind-button +label: Create worklog +hidden: true +id: worklogs +style: default +actions: + - type: command + command: jira-sync:update-work-log-jira +``` + +```meta-bind-button +label: Update status +hidden: true +id: status +style: default +actions: + - type: command + command: jira-sync:update-issue-status-jira +``` + +```meta-bind-button +label: Update issue +hidden: true +id: update +style: default +actions: + - type: command + command: jira-sync:update-issue-jira +``` + +```meta-bind-button +label: Get issue +hidden: true +id: get +style: default +actions: + - type: command + command: jira-sync:get-issue-jira +``` diff --git a/src/commands/addWorkLog.ts b/src/commands/addWorkLog.ts index ee3cf9c..988035b 100644 --- a/src/commands/addWorkLog.ts +++ b/src/commands/addWorkLog.ts @@ -4,6 +4,7 @@ import JiraPlugin from "../main"; import {getCurrentFileMainInfo} from "../file_operations/common_prepareData"; import {WorkLogModal} from "../modals/issueWorkLogModal"; import {addWorkLog, authenticate} from "../api"; +import {debugLog} from "../tools/debugLogging"; /** * Register the update work log command @@ -43,7 +44,15 @@ async function processFrontmatterWorkLogs(plugin: JiraPlugin, file: TFile): Prom await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => { 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