mirror of
https://github.com/alamion/obsidian-jira-sync.git
synced 2026-07-22 05:43:04 +00:00
Optimized and fixed several bugs with field mapping. Added how_to detailed instruction about plugin. Updated examples. Many minor bug fixes.
This commit is contained in:
parent
befbffed44
commit
e9553967f4
16 changed files with 392 additions and 91 deletions
|
|
@ -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.
|
||||
|
|
|
|||
107
docs/how_to_en.md
Normal file
107
docs/how_to_en.md
Normal file
|
|
@ -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:
|
||||
|
||||

|
||||
|
||||
#### 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:
|
||||
|
||||

|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
```
|
||||
106
docs/how_to_ru.md
Normal file
106
docs/how_to_ru.md
Normal file
|
|
@ -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
|
||||
|
||||
Будет это выглядеть примерно так:
|
||||
|
||||

|
||||
|
||||
#### Статистика
|
||||
Это предзаготовленный файл [[docs/statistics.md]]. Для работы с ним потребуется несколько дополнительных плагинов:
|
||||
- **Timekeep** для ведения времени работы с задачами - в каждой задаче можно запускать, останавливать и редактировать многоуровневый таймер;
|
||||
- **Dataview** с включенным в настройках JavaScript queries для создания динамичной таблички всего потраченного времени за последние пару недель;
|
||||
- **Meta Bind** для выбора нужной недели и отправки данных о работе за неделю одной кнопкой.
|
||||
|
||||
Как было написано, страница статистики позволяет отправить сразу батч данных о работе за целевую неделю, сокращая время для ручного труда.
|
||||
|
||||
Выглядит табличка с выключенными темами Обсидиана примерно так:
|
||||
|
||||

|
||||
|
||||
Формат батча данных для отправки данных если захочется сделать альтернативный [[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"
|
||||
}
|
||||
]
|
||||
```
|
||||
BIN
docs/images/progressPercentageExample.png
Normal file
BIN
docs/images/progressPercentageExample.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
BIN
docs/images/statisticsExample.png
Normal file
BIN
docs/images/statisticsExample.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string, { toJira: string; fromJira: string }> = {};
|
||||
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<string, FieldMapping> = {};
|
||||
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<string, any>) => 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<string, { toJira: string; fromJira: string }> = {};
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -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<string, { toJira: string; fromJira: string }>) => {
|
||||
// Also convert to functions for runtime use
|
||||
const transformedMappings: Record<string, FieldMapping> = {};
|
||||
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<string, any>) => any,
|
||||
};
|
||||
} else {
|
||||
console.warn(`Invalid function in field: ${fieldName}`);
|
||||
}
|
||||
}
|
||||
return transformedMappings
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, FieldMapping> = {
|
|||
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<string, {
|
||||
hasToJira: string,
|
||||
hasFromJira: string
|
||||
}> = {};
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string> {
|
||||
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<string, string> {
|
||||
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<string, string> = {};
|
||||
|
||||
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<string,
|
|||
|
||||
let match;
|
||||
while ((match = syncLineRegex.exec(fileContent)) !== null) {
|
||||
// debugLog(`Found match ${JSON.stringify(match)} in line`)
|
||||
const lineName = match[1];
|
||||
lines[lineName] = match[2].trim();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue