mirror of
https://github.com/alamion/obsidian-jira-sync.git
synced 2026-07-22 05:43:04 +00:00
Field Mapping update
- Now you have a few built-in functions and JS modules to use in Field Mapping section, validation supported - Readmes updated, a few images replaced
This commit is contained in:
parent
925c827f49
commit
a4ec70fecd
8 changed files with 128 additions and 74 deletions
|
|
@ -49,33 +49,28 @@ In the settings, you can configure custom mapping for any additional fields rece
|
|||
|
||||
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:
|
||||
Additionally, you can use some of the built-in functions:
|
||||
- `jiraToMarkdown` - converts Jira markup to Markdown.
|
||||
- `markdownToJira` - converts Markdown to Jira markup.
|
||||
- `JSON.parse` - converts a JSON string to an object.
|
||||
- `JSON.stringify` - converts an object to a JSON string.
|
||||
|
||||

|
||||
and modules: (original Javascript syntax)
|
||||
- `Math` - provides access to mathematical functions, such as `Math.round()`.
|
||||
- `Date` - provides access to date functions, such as `Date.now()`.
|
||||
- `String` - provides access to string functions, such as `String.fromCharCode()`.
|
||||
- `Number` - provides access to number functions, such as `Number.isNaN()`.
|
||||
- `Boolean` - provides access to boolean functions, such as `Boolean(true)`.
|
||||
- `Array` - provides access to array functions, such as `Array.isArray()`.
|
||||
- `Object` - provides access to object functions, such as `Object.keys()`.
|
||||
|
||||
Field mapping can like this:
|
||||
|
||||

|
||||
|
||||
#### Statistics
|
||||
Statistics are now integrated into the plugin and can be accessed through the plugin settings in the "Timekeep work log statistics" section. This feature provides a dynamically generated table (or series of tables) showing work statistics and allowing you to send work logs to Jira. You can conveniently select time periods for calculation and transfer work log information to Jira directly from the settings interface.
|
||||
|
||||
With Obsidian themes disabled, the table looks like this:
|
||||
The table looks like this:
|
||||
|
||||

|
||||
|
||||
### Plugin Settings
|
||||
|
||||
#### Connection Settings
|
||||
This section allows you to select the authentication method and enter corresponding credentials. The plugin supports three authentication methods: Bearer Token (PAT), Basic Auth (Username + PAT), and Session Cookie (Username + Password). If using Personal Access Token authentication, ensure the `write:jira-work` and `read:jira-work` scopes are enabled for proper functionality.
|
||||
|
||||
#### General Settings
|
||||
This section allows you to select the folder where new issues created using commands will be stored. This folder is also used for searching issues in the Timekeep work log statistics section.
|
||||
|
||||
#### Field Mapping
|
||||
This section provides non-standard mappings (in JavaScript format) of information from Jira to Obsidian and vice versa. You can configure custom field transformations and mappings for any additional fields received from Jira.
|
||||
|
||||
#### Raw Issue Viewer
|
||||
This section displays raw API output for entered issues, which should help with writing field mappings. This data is not saved and is provided for debugging and development purposes.
|
||||
|
||||
#### Test Field Mapping
|
||||
This section provides a preview of mapping results for issues from the raw issue viewer. This feature helps with testing and refining field mappings. The data is not saved and is provided for convenience when working with mappings.
|
||||
|
||||
#### Timekeep Work Log Statistics
|
||||
This section provides a dynamically generated table (or series of tables, see image) showing work statistics and providing the ability to send work logs to Jira. You can select time periods for calculation and transfer work log information to Jira directly from this interface.
|
||||
|
|
|
|||
|
|
@ -49,33 +49,28 @@ formatter имеет приоритет и будет перезаписыват
|
|||
|
||||
Таким же образом можно настроить показанный в примере `progressPercentage` - такого поля в запросе не существует, но его можно 'собрать' из существующего `progress`: `issue.fields.progress.total ? 100 * issue.fields.progress.progress / issue.fields.progress.total : 0`. Как понятно из синтаксиса, для маппинга используется обрезанный TypeScript
|
||||
|
||||
Будет это выглядеть примерно так:
|
||||
Кроме того, вы можете использовать некоторые встроенные функции:
|
||||
- `jiraToMarkdown` - преобразует разметку Jira в Markdown.
|
||||
- `markdownToJira` - преобразует Markdown в разметку Jira.
|
||||
- `JSON.parse` - преобразует строку JSON в объект.
|
||||
- `JSON.stringify` - преобразует объект в строку JSON.
|
||||
|
||||

|
||||
и модули: (исходный синтаксис Javascript)
|
||||
- `Math` - предоставляет доступ к математическим функциям, таким как `Math.round()`.
|
||||
- `Date` - предоставляет доступ к функциям даты, таким как `Date.now()`.
|
||||
- `String` - предоставляет доступ к функциям строки, таким как `String.fromCharCode()`.
|
||||
- `Number` - предоставляет доступ к числовым функциям, таким как `Number.isNaN()`.
|
||||
- `Boolean` - предоставляет доступ к булевым функциям, таким как `Boolean(true)`.
|
||||
- `Array` - предоставляет доступ к массивным функциям, таким как `Array.isArray()`.
|
||||
- `Object` - предоставляет доступ к объектным функциям, таким как `Object.keys()`.
|
||||
|
||||
Секция маппинга может выглядеть так:
|
||||
|
||||

|
||||
|
||||
#### Статистика
|
||||
Статистика теперь интегрирована в плагин и доступна через настройки плагина в секции "Timekeep work log statistics" ("Статистика ведения журнала работы через Timekeep"). Эта функция предоставляет динамически генерируемую таблицу (или ряд таблиц), показывающую статистику работы и позволяющую отправлять журналы работ в Jira. Там же можно удобно выбирать временные промежутки для которых проводится расчёт и передавать информацию ведения журнала работы в Jira.
|
||||
|
||||
Выглядит табличка с выключенными темами Обсидиана примерно так:
|
||||
Выглядит табличка так:
|
||||
|
||||

|
||||
|
||||
### Настройки плагина
|
||||
|
||||
#### Connection Settings
|
||||
Эта секция позволяет выбрать метод аутентификации и ввести соответствующие креды. Плагин поддерживает три метода аутентификации: Bearer Token (PAT), Basic Auth (Username + PAT) и Session Cookie (Username + Password). При использовании аутентификации по персональному токену доступа убедитесь, что области `write:jira-work` и `read:jira-work` включены для корректной работы.
|
||||
|
||||
#### General Settings
|
||||
Эта секция позволяет выбрать папку, где будут храниться новые issues созданные с помощью команд. Эта папка также используется для поиска issues в разделе Timekeep work log statistics.
|
||||
|
||||
#### Field Mapping
|
||||
Эта секция предоставляет нестандартные маппинги (в JavaScript формате) информации из Jira в Obsidian и обратно. Вы можете настроить кастомные трансформации полей и маппинги для любых дополнительных полей, получаемых из Jira.
|
||||
|
||||
#### Raw Issue Viewer
|
||||
Эта секция отображает сырой вывод API для введённых issues, что должно помочь с написанием маппингов полей. Эти данные не сохраняются и предоставляются для отладки и разработки.
|
||||
|
||||
#### Test Field Mapping
|
||||
Эта секция предоставляет предпоказ результатов маппинга для issues из raw issue viewer. Эта функция помогает с тестированием и улучшением маппингов полей. Данные не сохраняются и предоставляются для удобства при работе с маппингами.
|
||||
|
||||
#### Timekeep Work Log Statistics
|
||||
Эта секция предоставляет динамически генерируемую таблицу (или ряд таблиц, см. изображение), показывающую статистику работы и дающую возможность отправить журнал работ в Jira. Вы можете выбирать временные промежутки для расчёта и передавать информацию ведения журнала работы в Jira прямо из этого интерфейса.
|
||||
|
|
|
|||
BIN
docs/images/fieldMappingExample.png
Normal file
BIN
docs/images/fieldMappingExample.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 117 KiB |
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "jira-sync",
|
||||
"name": "Jira Issue Manager",
|
||||
"version": "1.3.8",
|
||||
"version": "1.4.0",
|
||||
"minAppVersion": "1.10.1",
|
||||
"description": "Get Jira issues, create and update them. Issue status and worklog management.",
|
||||
"author": "Alamion",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-sample-plugin",
|
||||
"version": "1.3.8",
|
||||
"version": "1.4.0",
|
||||
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,26 @@ import { parse } from "acorn";
|
|||
import {debugLog} from "./debugLogging";
|
||||
import {FieldMapping} from "../default/obsidianJiraFieldsMapping";
|
||||
import {defaultIssue} from "../default/defaultIssue";
|
||||
import {jiraToMarkdown, markdownToJira} from "./markdownHtml";
|
||||
|
||||
// Constants for validation and error messages
|
||||
const FORBIDDEN_PATTERNS = ["document", "window", "eval", "Function", "fetch", "setTimeout"]; // Prevent unsafe code execution
|
||||
const FORBIDDEN_PATTERNS = ["document", "window", "eval", "Function", "fetch", "setTimeout", "globalThis"];
|
||||
const SYNTAX_KEYWORDS = ["return", "if", "else", "for", "while", "switch", "try", "catch"];
|
||||
const SAFE_GLOBALS = {
|
||||
jiraToMarkdown,
|
||||
markdownToJira,
|
||||
JSON: {
|
||||
parse: JSON.parse,
|
||||
stringify: JSON.stringify
|
||||
},
|
||||
Math: Math,
|
||||
Date: Date,
|
||||
String: String,
|
||||
Number: Number,
|
||||
Boolean: Boolean,
|
||||
Array: Array,
|
||||
Object: Object
|
||||
};
|
||||
|
||||
export function validateFunctionStringBrowser(fnString: string, approved_vars: string[] = []): Promise<{ isValid: boolean; errorMessage?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
|
|
@ -23,14 +39,49 @@ export function validateFunctionStringBrowser(fnString: string, approved_vars: s
|
|||
const iframeWindow = iframe.contentWindow as Window;
|
||||
|
||||
try {
|
||||
let testFnString = fnString;
|
||||
|
||||
if (isSimpleExpression(fnString) || (fnString.startsWith("{") && fnString.endsWith("}") && !fnString.includes("return"))) {
|
||||
fnString = `(${approved_vars.map(varName => varName==='issue'?`${varName} = ${JSON.stringify(defaultIssue)}`:`${varName} = {}`).join(', ')}) => ${fnString}`;
|
||||
}
|
||||
debugLog(`checking function: ${fnString.substring(0, 100)}`);
|
||||
let func = (iframeWindow as any).eval(fnString);
|
||||
if (typeof func === 'function') {
|
||||
func(); // Execute the function to catch runtime errors
|
||||
const varDeclarations = approved_vars.map(varName => {
|
||||
if (varName === 'issue') return `${varName} = ${JSON.stringify(defaultIssue)}`;
|
||||
if (varName === 'value') return `${varName} = "test value"`;
|
||||
if (varName === 'data_source') return `${varName} = {}`;
|
||||
return `${varName} = {}`;
|
||||
}).join(', ');
|
||||
|
||||
testFnString = `(${varDeclarations}) => ${fnString}`;
|
||||
}
|
||||
debugLog(`checking function: ${testFnString.substring(0, 100)}`);
|
||||
|
||||
const context: Record<string, any> = {};
|
||||
Object.keys(SAFE_GLOBALS).forEach(key => {
|
||||
context[key] = (SAFE_GLOBALS as any)[key];
|
||||
});
|
||||
|
||||
const safeBuiltIns = ['Infinity', 'NaN', 'undefined', 'isFinite', 'isNaN',
|
||||
'parseFloat', 'parseInt', 'decodeURI', 'decodeURIComponent',
|
||||
'encodeURI', 'encodeURIComponent'];
|
||||
safeBuiltIns.forEach(key => {
|
||||
context[key] = (iframeWindow as any)[key];
|
||||
});
|
||||
|
||||
const testArgs = approved_vars.map(varName => {
|
||||
if (varName === 'issue') return defaultIssue;
|
||||
if (varName === 'value') return "test value";
|
||||
if (varName === 'data_source') return {};
|
||||
return {};
|
||||
});
|
||||
|
||||
const contextKeys = Object.keys(context);
|
||||
const contextValues = contextKeys.map(key => context[key]);
|
||||
|
||||
const func = new Function(
|
||||
...approved_vars,
|
||||
...contextKeys,
|
||||
`return (${fnString});`
|
||||
);
|
||||
|
||||
func(...testArgs, ...contextValues);
|
||||
resolve({ isValid: true });
|
||||
} catch (error) {
|
||||
debugLog('Not valid')
|
||||
|
|
@ -121,37 +172,50 @@ export async function safeStringToFunction(
|
|||
body = `{ return ${body.slice(1, -1)} }`;
|
||||
}
|
||||
|
||||
const context = {
|
||||
jiraToMarkdown,
|
||||
markdownToJira,
|
||||
JSON,
|
||||
Math,
|
||||
Date,
|
||||
String,
|
||||
Number,
|
||||
Boolean,
|
||||
Array,
|
||||
Object
|
||||
};
|
||||
|
||||
if (type === 'toJira') {
|
||||
return function(value: any) {
|
||||
const context = {
|
||||
};
|
||||
|
||||
const fn = new Function('value', `
|
||||
try {
|
||||
${body}
|
||||
} catch (e) {
|
||||
console.error("Error in toJira function:", e);
|
||||
return null;
|
||||
}
|
||||
const fn = new Function('value', 'context', `
|
||||
with (context) {
|
||||
try {
|
||||
${body}
|
||||
} catch (e) {
|
||||
console.error("Error in toJira function:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return fn.apply(context, [value]);
|
||||
return fn.call(context, value, context);
|
||||
};
|
||||
} else { // fromJira
|
||||
return function(issue: JiraIssue, data_source: Record<string, any> | null) {
|
||||
const context = {
|
||||
};
|
||||
|
||||
const fn = new Function('issue', 'data_source', `
|
||||
try {
|
||||
${body}
|
||||
} catch (e) {
|
||||
console.error("Error in fromJira function:", e);
|
||||
return null;
|
||||
const fn = new Function('issue', 'data_source', 'context', `
|
||||
with (context) {
|
||||
try {
|
||||
${body}
|
||||
} catch (e) {
|
||||
console.error("Error in fromJira function:", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
return fn.apply(context, [issue, data_source]);
|
||||
return fn.call(context, issue, data_source, context);
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue