settings update:

- css enchantments
  - template now can be selected instead of raw manual input
  - internationalization implementation
  - work statistics now integrated in settings
This commit is contained in:
Alamion 2025-08-25 02:42:16 +03:00
parent fbf8649c1a
commit 67308c0fec
No known key found for this signature in database
GPG key ID: 0DBEAC95BA38C9EF
80 changed files with 2610 additions and 899 deletions

4
.gitignore vendored
View file

@ -11,6 +11,7 @@ node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
*.js
!/src/localization/**/*.js
# Exclude sourcemaps
*.map
@ -23,3 +24,6 @@ data.json
# Some other
tmp/
# localization compiled
src/localization/compiled

View file

@ -22,10 +22,10 @@ Not all fields are predefined, and some may need adjustments. For example, the t
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]]), instead of manual entry, a batch of data from `jira_selected_week_data` will be sent, updating each listed entity.
- `Get current issue from Jira` - allows updating the active file if its formatter contains a `key` (the Jira task ID).
- `Update issue in 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 in 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_worklog_batch` (as described in [[docs/jira_worklog_batch]]), instead of manual entry, a batch of data from `jira_worklog_batch` will be sent, updating each listed entity.
- `Update issue status in Jira` - allows updating a task's status by selecting one of the available options.
### Advanced Usage

View file

@ -25,7 +25,7 @@ formatter имеет приоритет и будет перезаписыват
- `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]]), то вместо ручного заполнения будет послан батч данных из `jira_selected_week_data` с обновлением каждой из представленных сущностей.
- `Update work log in Jira` - позволяет вести учёт потраченного на задачу времени. В данный момент он никак не отображается в файле, это будет в ближайших обновлениях. Если в файле в formatter есть `jira_worklog_batch` (как это в [[docs/jira_worklog_batch]]), то вместо ручного заполнения будет послан батч данных из `jira_worklog_batch` с обновлением каждой из представленных сущностей.
- `Update issue status in Jira` - позволяет обновить статус задачи, выбрав один из возможных вариантов.
### Продвинутое использование

View file

@ -1,7 +1,7 @@
{
"id": "jira-sync",
"name": "Jira Issue Manager",
"version": "1.1.8",
"version": "1.1.9",
"minAppVersion": "1.7.7",
"description": "Get Jira issues, create and update them. Issue status and worklog management.",
"author": "Alamion",

View file

@ -1,13 +1,16 @@
{
"name": "obsidian-sample-plugin",
"version": "1.1.8",
"version": "1.1.9",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"buildObsidian": "node buildStatistics.mjs"
"validate_locale": "node src/localization/validator.js --watch",
"compile_locale": "node src/localization/compiler.js --watch",
"validate_locale_once": "node src/localization/validator.js",
"compile_locale_once": "node src/localization/compiler.js",
"dev": "yarn run compile_locale_once && node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && yarn run compile_locale_once && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "",
@ -26,6 +29,8 @@
},
"dependencies": {
"acorn": "^8.14.1",
"chokidar": "^4.0.3",
"fs-extra": "^11.3.0",
"highlight.js": "^11.11.1",
"yaml": "^2.7.0"
}

View file

@ -3,11 +3,14 @@ import JiraPlugin from "../main";
import {addWorkLog} from "../api";
import {debugLog} from "../tools/debugLogging";
import {checkCommandCallback} from "../tools/check_command_callback";
import {useTranslations} from "../localization/translator";
const t = useTranslations("commands.add_worklog.batch").t;
export function registerUpdateWorkLogBatchCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "update-work-log-jira-batch",
name: "Update work log in Jira",
name: t("name"),
checkCallback: (checking: boolean) => {
return checkCommandCallback(plugin, checking, processFrontmatterWorkLogs, ["jira_worklog_batch"], ["jira_worklog_batch"]);
},
@ -27,7 +30,7 @@ async function processFrontmatterWorkLogs(plugin: JiraPlugin, _: TFile, jira_wor
workLogData = jira_worklog_batch;
foundData = true;
} else {
throw new TypeError(`jira_selected_week_data has an invalid type: ${typeof jira_worklog_batch}`);
throw new TypeError(`jira_worklog_batch has an invalid type: ${typeof jira_worklog_batch}`);
}
} catch (error) {
console.error("Failed to parse jira_worklog_batch:", error);
@ -43,7 +46,7 @@ async function processFrontmatterWorkLogs(plugin: JiraPlugin, _: TFile, jira_wor
}
}
async function processWorkLogBatch(plugin: JiraPlugin, workLogs: any[]): Promise<void> {
export async function processWorkLogBatch(plugin: JiraPlugin, workLogs: any[]): Promise<void> {
const results = {
success: 0,
total: workLogs.length,

View file

@ -1,14 +1,17 @@
import {TFile} from "obsidian";
import JiraPlugin from "../main";
import {WorkLogModal} from "../modals/issueWorkLogModal";
import {IssueWorkLogModal} from "../modals";
import {addWorkLog} from "../api";
import {checkCommandCallback} from "../tools/check_command_callback";
import {useTranslations} from "../localization/translator";
const t = useTranslations("commands.add_worklog.manual").t;
export function registerUpdateWorkLogManuallyCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "update-work-log-jira-manually",
name: "Update work log in Jira manually",
name: t("name"),
checkCallback: (checking: boolean) => {
return checkCommandCallback(plugin, checking, processManualWorkLog, ["key"], ["key"]);
},
@ -17,7 +20,7 @@ export function registerUpdateWorkLogManuallyCommand(plugin: JiraPlugin): void {
async function processManualWorkLog(plugin: JiraPlugin, _: TFile, issueKey: string): Promise<void> {
new WorkLogModal(plugin.app, async (timeSpent: string, startDate: string, comment: string) => {
new IssueWorkLogModal(plugin.app, async (timeSpent: string, startDate: string, comment: string) => {
await addWorkLog(plugin, issueKey, timeSpent, startDate, comment);
}).open();
}

View file

@ -4,6 +4,9 @@ import { IssueTypeModal, ProjectModal } from "../modals";
import {fetchIssueTypes, fetchProjects} from "../api";
import {createIssueFromFile} from "../file_operations/createUpdateIssue";
import {checkCommandCallback} from "../tools/check_command_callback";
import {useTranslations} from "../localization/translator";
const t = useTranslations("commands.create_issue").t;
/**
* Register the create issue command
@ -11,7 +14,7 @@ import {checkCommandCallback} from "../tools/check_command_callback";
export function registerCreateIssueCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "create-issue-jira",
name: "Create issue in Jira",
name: t("name"),
checkCallback: (checking: boolean) => {
return checkCommandCallback(plugin, checking, createIssue, ["summary"]);
},
@ -37,15 +40,15 @@ export async function createIssue(plugin: JiraPlugin, file: TFile): Promise<void
try {
// Create the issue with selected project and issue type
const issueKey = await createIssueFromFile(plugin, file, projectKey, issueType);
new Notice(`Issue ${issueKey} created successfully`);
new Notice(t('success', { issueKey }));
} catch (error) {
new Notice("Error creating issue: " + (error.message || "Unknown error"), 3000);
new Notice(t('error') + ": " + (error.message || "Unknown error"), 3000);
console.error(error);
}
}).open();
}).open();
} catch (error) {
new Notice("Error creating issue: " + (error.message || "Unknown error"));
new Notice(t('error') + ": " + (error.message || "Unknown error"));
console.error(error);
}
}

View file

@ -1,14 +1,17 @@
import {Notice, TFile} from "obsidian";
import {TFile} from "obsidian";
import JiraPlugin from "../main";
import {IssueSearchModal} from "../modals";
import {fetchIssue, validateSettings} from "../api";
import {createOrUpdateIssueNote} from "../file_operations/getIssue";
import {checkCommandCallback} from "../tools/check_command_callback";
import {useTranslations} from "../localization/translator";
const t = useTranslations("commands.get_issue").t;
export function registerGetIssueCommandWithKey(plugin: JiraPlugin): void {
plugin.addCommand({
id: "get-issue-jira-key",
name: "Get issue from Jira with custom key",
name: t("with_key"),
checkCallback: (checking: boolean) => {
const settings_are_valid = validateSettings(plugin);
if (settings_are_valid) {
@ -23,7 +26,7 @@ export function registerGetIssueCommandWithKey(plugin: JiraPlugin): void {
export function registerGetIssueCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "get-issue-jira",
name: "Get current issue from Jira",
name: t("without_key"),
checkCallback: (checking: boolean) => {
return checkCommandCallback(plugin, checking, fetchAndOpenIssue, ["key"], ["key"]);
},

View file

@ -1,6 +1,6 @@
// Re-export all commands
export * from './updateIssue';
export * from './getIssue';
export * from './addWorkLogBatch';
export * from './addWorkLogManually';
export * from './createIssue';
export * from './getIssue';
export * from './updateIssue';
export * from './updateStatus';
export {getCurrentFileMainInfo} from "../file_operations/common_prepareData";

View file

@ -2,11 +2,14 @@ import {Notice, TFile} from "obsidian";
import JiraPlugin from "../main";
import {updateIssueFromFile} from "../file_operations/createUpdateIssue";
import {checkCommandCallback} from "../tools/check_command_callback";
import {useTranslations} from "../localization/translator";
const t = useTranslations("commands.update_issue").t;
export function registerUpdateIssueCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "update-issue-jira",
name: "Update issue in Jira",
name: t("name"),
checkCallback: (checking: boolean) => {
return checkCommandCallback(plugin, checking, updateIssue, ["key"]);
},
@ -18,11 +21,11 @@ export async function updateIssue(plugin: JiraPlugin, file: TFile): Promise<void
try {
// Update the issue with all data from the file
const issueKey = await updateIssueFromFile(plugin, file);
new Notice(`Issue ${issueKey} updated successfully`);
new Notice(t('success', {issueKey}));
} catch (error) {
new Notice("Error updating issue: " + (error.message || "Unknown error"), 3000);
new Notice(t('error') + ": " + (error.message || "Unknown error"), 3000);
}
} catch (error) {
new Notice("Error updating issue: " + (error.message || "Unknown error"));
new Notice(t('error') + ": " + (error.message || "Unknown error"));
}
}

View file

@ -2,14 +2,17 @@ import {Notice, TFile} from "obsidian";
import JiraPlugin from "../main";
import {fetchIssueTransitions} from "../api";
import {updateStatusFromFile} from "../file_operations/createUpdateIssue";
import {IssueStatusModal} from "../modals/IssueStatusModal";
import {IssueStatusModal} from "../modals";
import {JiraTransitionType} from "../interfaces";
import {checkCommandCallback} from "../tools/check_command_callback";
import {useTranslations} from "../localization/translator";
const t = useTranslations("commands.update_status").t;
export function registerUpdateIssueStatusCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "update-issue-status-jira",
name: "Update issue status in Jira",
name: t('name'),
checkCallback: (checking: boolean) => {
return checkCommandCallback(plugin, checking, updateIssueStatus, ["key"],["key"]);
},
@ -23,15 +26,15 @@ export async function updateIssueStatus(plugin: JiraPlugin, file: TFile, issueKe
new IssueStatusModal(plugin.app, issueTransitions, async (transition: JiraTransitionType) => {
try {
await updateStatusFromFile(plugin, file, transition);
new Notice(`Issue ${issueKey} updated successfully`);
new Notice(t('success', {issueKey}));
} catch (error) {
new Notice("Error updating issue: " + (error.message || "Unknown error"), 3000);
new Notice(t('error') + ": " + (error.message || "Unknown error"), 3000);
console.error(error);
}
}).open();
} catch (error) {
new Notice("Error updating issue: " + (error.message || "Unknown error"));
new Notice(t('error') + ": " + (error.message || "Unknown error"));
console.error(error);
}
}

View file

@ -0,0 +1,11 @@
export const defaultTemplate =
`---
key:
summary:
status:
assignee:
tags:
description:
---
`;

View file

@ -3,7 +3,7 @@ import {Notice, TFile} from "obsidian";
import {extractAllJiraSyncValuesFromContent} from "../tools/sectionTools";
import {localToJiraFields} from "../tools/mapObsidianJiraFields";
import {debugLog} from "../tools/debugLogging";
import {obsidianJiraFieldMappings} from "../constants/obsidianJiraFieldsMapping";
import {obsidianJiraFieldMappings} from "../default/obsidianJiraFieldsMapping";
/**
* Prepares Jira fields from the content and frontmatter of a file

View file

@ -4,6 +4,7 @@ import {ensureIssuesFolder} from "../tools/filesUtils";
import {sanitizeFileName} from "../tools/sanitizers";
import {updateJiraToLocal} from "../tools/mapObsidianJiraFields";
import {Notice, TFile} from "obsidian";
import {defaultTemplate} from "../default/default_template";
export async function createOrUpdateIssueNote(plugin: JiraPlugin, issue: JiraIssue, filePath?: string): Promise<void> {
try {
@ -42,17 +43,7 @@ async function createNewIssueFile(
): Promise<TFile> {
let initialContent = "";
const hardcodedTemplate =
`---
key:
summary:
status:
assignee:
tags:
description:
---
`;
let templatePath = plugin.settings.templatePath
if (templatePath && !templatePath.endsWith(".md")) {
templatePath += ".md";
@ -66,7 +57,7 @@ description:
new Notice(`Template file not found: ${templatePath}, using default template`);
}
}
if (initialContent === "") initialContent = hardcodedTemplate
if (initialContent === "") initialContent = defaultTemplate
// Create the file with initial content
await plugin.app.vault.create(filePath, initialContent);

View file

@ -0,0 +1,123 @@
const fs = require('fs-extra');
const yaml = require('yaml');
const path = require('path');
const chokidar = require('chokidar');
const SOURCE_DIR = path.join(__dirname, 'source');
const COMPILED_DIR = path.join(__dirname, 'compiled');
// Компиляция всех языков
async function main() {
try {
const locales = fs.readdirSync(SOURCE_DIR);
for (const locale of locales) {
const localePath = path.join(SOURCE_DIR, locale);
const stat = fs.statSync(localePath);
if (stat.isDirectory()) {
await compileLocale(locale);
}
}
console.log('✅ All locales compiled');
} catch (err) {
console.error('❌ Error compiling all locales:', err.message);
}
}
async function compileLocale(locale) {
const localeDir = path.join(SOURCE_DIR, locale);
const result = {};
async function processDirectory(dirPath, parentKeys = []) {
try {
const items = fs.readdirSync(dirPath);
for (const item of items) {
// Пропускаем временные и скрытые файлы
if (item.startsWith('.')) continue;
const itemPath = path.join(dirPath, item);
let stat;
try {
stat = fs.statSync(itemPath);
} catch (err) {
if (err.code === 'ENOENT') continue; // Файл был удален
throw err;
}
if (stat.isDirectory()) {
await processDirectory(itemPath, [...parentKeys, item]);
} else if (item.endsWith('.yaml')) {
try {
const content = fs.readFileSync(itemPath, 'utf8');
const data = yaml.parse(content);
const fileNameWithoutExt = path.basename(item, '.yaml');
const targetKeys = fileNameWithoutExt === 'index'
? parentKeys
: [...parentKeys, fileNameWithoutExt];
let target = result;
for (const key of targetKeys.slice(0, -1)) {
target[key] = target[key] || {};
target = target[key];
}
const finalKey = targetKeys[targetKeys.length - 1];
target[finalKey] = deepMerge(target[finalKey] || {}, data);
} catch (err) {
console.error(`⚠️ Failed to process file ${itemPath}:`, err.message);
}
}
}
} catch (err) {
console.error(`⚠️ Failed to process directory ${dirPath}:`, err.message);
}
}
await processDirectory(localeDir);
const outputPath = path.join(COMPILED_DIR, `${locale}.json`);
await fs.ensureDir(COMPILED_DIR);
await fs.writeJson(outputPath, result, { spaces: 2 });
}
function deepMerge(target, source) {
for (const key in source) {
if (source[key] instanceof Object && target[key]) {
Object.assign(source[key], deepMerge(target[key], source[key]));
}
}
return Object.assign(target || {}, source);
}
// Watch mode
if (process.argv.includes('--watch')) {
console.log('👀 Watching for changes...');
let debounceTimer;
const debounceDelay = 100;
// Initial validation
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
main();
}, debounceDelay);
// Watch for changes in the compiled directory
chokidar.watch(SOURCE_DIR, {
ignoreInitial: true,
ignored: /.*~$/, // Игнорировать скрытые файлы
}).on('all', (event, path) => {
if (event === 'change' || event === 'add' || event === 'unlink') {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
console.log(`🔁 Detected changes in ${path} (${event}), recompiling...`);
main();
}, debounceDelay);
}
});
} else {
// Run once
main();
}

View file

@ -0,0 +1,4 @@
manual:
name: Update work log in Jira manually
batch:
name: Update work log in Jira by batch

View file

@ -0,0 +1,3 @@
name: Create issue in Jira
success: Issue {issueKey} created successfully
error: Error creating issue

View file

@ -0,0 +1,2 @@
with_key: Get issue from Jira with custom key
without_key: Get current issue from Jira

View file

@ -0,0 +1,3 @@
name: Update issue in Jira
success: Issue {issueKey} updated successfully
error: Error updating issue

View file

@ -0,0 +1,3 @@
name: Update issue status in Jira
success: Issue {issueKey} updated successfully
error: Error updating issue

View file

@ -0,0 +1 @@
placeholder: Search for a project...

View file

@ -0,0 +1,8 @@
desc: Search issue by key
key:
name: Issue key
placeholder: Issue key
desc: Enter the Jira issue key (e.g., PROJECT-123)
submit: Search

View file

@ -0,0 +1 @@
placeholder: Search for an issue status transition...

View file

@ -0,0 +1 @@
placeholder: Search for an issue type...

View file

@ -0,0 +1,26 @@
name: Add work log
spent:
name: Time spent
desc: "Format: weeks days hours, minutes"
placeholder: "3w 4d 12h 30m"
start:
name: Start datetime
desc: "Format: DD/MMM/YY HH:MM"
placeholder: "03/feb/25 15:22"
comment:
name: Work description
desc: Optional
placeholder: ""
months: ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]
submit: Submit
warns:
no_time: Time spent is required
invalid_time: Invalid time format. Use combinations of w (weeks), d (days), h (hours), m (minutes)
no_start: Start datetime is required
invalid_start: Invalid date format. Use DD/MMM/YY HH:MM AM/PM

View file

@ -0,0 +1,34 @@
title: Connection settings
url:
title: Jira URL
desc: Your Jira instance URL
def: https://yourcompany.atlassian.net
auth:
title: Authentication method
desc: Choose how to authenticate with Jira
options:
bearer: Bearer Token (PAT)
basic: Basic Auth (Username + PAT)
session: Session Cookie (Username + Password)
pat:
title: Jira PAT
desc: Personal Access Token
def: NjM5MDI5MzQ0NT...
username:
title: Jira username
desc: Your Jira username
def: admin
email:
title: Jira email
desc: Your Jira email
def: admin@gmail.com
password:
title: Jira password
desc: Your Jira password
def: qwerty

View file

@ -0,0 +1,23 @@
title: Field mappings
desc: Configure how fields are mapped between Obsidian and Jira. Each field requires both a 'To Jira' and 'From Jira' transformation function.
fv:
name: Enable field validation
desc: Check fields before saving based on the standard API response (validation does not account for custom fields)
secNote:
name: "⚠️ Note: "
desc: Field mapping uses JavaScript lambda functions.
example:
name: Example field mapping
use: Use
from_jira: From Jira
to_jira: To Jira
field_name: Field name
add_mapping_tooltip: Add new field mapping
reset_tooltip: Reset
reset_confirm_title: Reset field mapping
reset_confirm_text: Field mapping will be cleaned up. Are you sure?
reload_defaults_tooltip: Reload
reload_confirm_title: Reload default field mapping
reload_confirm_text: Current field mappings will be overwritten with the default mappings. Are you sure?

View file

@ -0,0 +1,13 @@
title: General settings
folder:
name: Issues folder
desc: Folder where Jira issues will be stored and searched for
template:
name: Template path
desc: Select a template file for creating Jira tasks
selector:
placeholder: Search templates...
empty: Template is not chosen
warning: No template plugins are currently enabled (Core Templates or Community Templater).

View file

@ -0,0 +1,4 @@
title: Raw issue viewer
key:
name: View raw issue data
desc: Enter a Jira issue key to view its raw data from API

View file

@ -0,0 +1 @@
title: Kanban board

View file

@ -0,0 +1,58 @@
title: Timekeep work log statistics
description: Manage and send work logs to Jira
settings:
max_weeks:
name: Maximum Weeks to Show
description: Number of recent weeks to display
time_range:
name: Time Range
description: Select the time range for grouping entries
options:
days: Days
weeks: Weeks
months: Months
custom: Custom Range
max_items:
name: Maximum Items to Show
description: Number of recent time periods to display
date_from:
name: From Date
description: Start date for custom range
placeholder: YYYY-MM-DD
date_to:
name: To Date
description: End date for custom range
placeholder: YYYY-MM-DD
display:
select_period: Select Period
select_period_placeholder: Select a time period...
selected_period: "Selected: {period}"
all_periods: All Recent Periods
no_entries: No timekeep entries found.
period_of: Period of {date}
table:
task: Task
block_path: Subtask
start_time: Start Time
duration: Duration
issue_key: Issue Key
actions:
send_to_jira: Send Work Log to Jira
refresh_data: Refresh Data
messages:
select_period_first: Please select a time period first.
refresh_success: Data refreshed successfully
refresh_error: "Failed to refresh data: {error}"
send_success: Work log sent to Jira successfully
send_error: "Failed to send work log to Jira: {error}"
invalid_date_range: Invalid date range. End date must be after start date.
custom_range_required: Please specify both start and end dates for custom range.

View file

@ -0,0 +1,2 @@
placeholder: Search...
empty_option_text: No selection

View file

@ -0,0 +1,11 @@
title: Test field mapping
name: Test field mapping
desc: Test your field mapping against the current issue data from Raw issue viewer. Enter a fromJira expression to see the result.
no_data: No test field mapping
invalid_exp: Invalid expression
from_jira: From Jira
value: Value
add_mapping_tooltip: Add test mapping entry
reset_tooltip: Reset

View file

@ -0,0 +1,4 @@
manual:
name: Вести журнал работы в Jira вручную
batch:
name: Вести журнал работы в Jira массово

View file

@ -0,0 +1,3 @@
name: Создать задачу в Jira
success: Задача {issueKey} успешно создана
error: Ошибка при создании задачи

View file

@ -0,0 +1,2 @@
with_key: Получить данные задачи из Jira по указанному ключу
without_key: Получить данные текущей задачи из Jira

View file

@ -0,0 +1,3 @@
name: Обновить задачу в Jira
success: Задача {issueKey} успешно обновлена
error: Ошибка при обновлении задачи

View file

@ -0,0 +1,3 @@
name: Обновить статус задачи в Jira
success: Статус задачи {issueKey} успешно обновлён
error: Ошибка при обновлении статуса задачи

View file

@ -0,0 +1 @@
placeholder: Поиск проекта...

View file

@ -0,0 +1,8 @@
desc: Поиск задачи по номеру
key:
name: Номер задачи
placeholder: Номер задачи
desc: Введите номер задачи Jira (например, PROJECT-123)
submit: Поиск

View file

@ -0,0 +1 @@
placeholder: Поиск смены статуса задачи...

View file

@ -0,0 +1 @@
placeholder: Поиск типа задачи...

View file

@ -0,0 +1,35 @@
name: Вести журнал работы
spent:
name: Затраченное время
desc: 'Формат: недели дни часы, минуты'
placeholder: 3w 4d 12h 30m
start:
name: Дата и время начала
desc: 'Формат: DD/MMM/YY HH:MM'
placeholder: 03/фев/25 15:22
comment:
name: Описание работы
desc: Необязательно
placeholder: ''
months:
- янв
- фев
- мар
- апр
- май
- июн
- июл
- авг
- сен
- окт
- ноя
- дек
submit: Отправить
warns:
no_time: Необходимо указать затраченное время
invalid_time: Неверный формат времени. Используйте комбинации w (недели), d
(дни), h (часы), m (минуты)
no_start: Необходимо указать дату начала
invalid_start: Неверный формат даты. Используйте DD/MMM/YY HH:MM AM/PM

View file

@ -0,0 +1,28 @@
title: Настройки подключения
url:
title: URL Jira
desc: URL вашей инстанции Jira
def: https://yourcompany.atlassian.net
auth:
title: Метод аутентификации
desc: Выберите способ подключения к Jira
options:
bearer: Bearer Token (PAT)
basic: Basic Auth (Имя пользователя + PAT)
session: Session Cookie (Имя пользователя + Пароль)
pat:
title: Jira PAT
desc: Персональный токен доступа
def: NjM5MDI5MzQ0NT...
username:
title: Имя пользователя Jira
desc: Ваше имя пользователя Jira
def: admin
email:
title: Email Jira
desc: Ваш email в Jira
def: admin@gmail.com
password:
title: Пароль Jira
desc: Ваш пароль от Jira
def: qwerty

View file

@ -0,0 +1,24 @@
title: Сопоставление полей
desc: Настройте, как поля будут сопоставляться между Obsidian и Jira. Для каждого
поля требуется функция преобразования 'В Jira' и 'Из Jira'.
fv:
name: Включить проверку полей
desc: Проверять поля перед сохранением базируясь на стандартном ответе API
(при проверке не учитываются пользовательские поля)
secNote:
name: "⚠️ Примечание: "
desc: Сопоставление полей использует JavaScript lambda функции.
example:
name: Примеры сопоставления полей
use: Использовать
from_jira: Из Jira
to_jira: В Jira
field_name: Название поля
add_mapping_tooltip: Добавить новое сопоставление полей
reset_tooltip: Убрать все
reset_confirm_title: Сбросить сопоставление полей
reset_confirm_text: Сопоставление полей будет очищено. Вы уверены?
reload_defaults_tooltip: Обновить
reload_confirm_title: Обновить сопоставления полей по умолчанию
reload_confirm_text: Текущие сопоставления полей будут перезаписаны сопоставлениями. Вы уверены?

View file

@ -0,0 +1,14 @@
title: Общие настройки
folder:
name: Папка для задач
desc: Папка, в которой будут создаваться и искаться задачи Jira
template:
name: Путь к шаблону
desc: Выберите файл шаблона для создания задач Jira
selector:
placeholder: Поиск шаблонов...
empty: Шаблон не выбран
warning: В данный момент не активированы плагины шаблонов (Core Templates или
Community Templater).

View file

@ -0,0 +1,4 @@
title: Просмотр необработанных данных задачи
key:
name: Просмотр необработанных данных задачи
desc: Введите номер задачи Jira, чтобы просмотреть необработанные данные, полученные из API

View file

@ -0,0 +1 @@
title: Канбан доска

View file

@ -0,0 +1,50 @@
title: Статистика ведения журнала работы через Timekeep
description: Управление и отправка журнала работы в Jira
settings:
max_weeks:
name: Максимум недель для отображения
description: Количество последних недель для показа
time_range:
name: Период времени
description: Выберите период для группировки записей
options:
days: Дни
weeks: Недели
months: Месяцы
custom: Произвольный период
max_items:
name: Максимум элементов для отображения
description: Количество последних периодов для показа
date_from:
name: Дата начала
description: Дата начала для произвольного периода
placeholder: ГГГГ-ММ-ДД
date_to:
name: Дата окончания
description: Дата окончания для произвольного периода
placeholder: ГГГГ-ММ-ДД
display:
select_period: Выбрать период
select_period_placeholder: Выберите период...
selected_period: 'Выбрано: {period}'
all_periods: Все последние периоды
no_entries: Записи рабочего лога не найдены.
period_of: Период от {date}
table:
task: Задача
block_path: Подзадача
start_time: Время начала
duration: Длительность
issue_key: Номер задачи
actions:
send_to_jira: Отправить рабочий лог в Jira
refresh_data: Обновить данные
messages:
select_period_first: Сначала выберите период.
refresh_success: Данные успешно обновлены
refresh_error: 'Ошибка при обновлении данных: {error}'
send_success: Рабочий лог успешно отправлен в Jira
send_error: 'Не удалось отправить рабочий лог в Jira: {error}'
invalid_date_range: Некорректный диапазон дат. Дата окончания должна быть позже
даты начала.
custom_range_required: Укажите дату начала и окончания для произвольного периода.

View file

@ -0,0 +1,2 @@
placeholder: Поиск...
empty_option_text: Не выбрано

View file

@ -0,0 +1,11 @@
title: Тестирование сопоставлений полей
name: Тестирование сопоставлений полей
desc: Проверьте сопоставления полей на текущих данных задачи из пункта 'Просмотр необработанных
данных задачи'. Введите выражение 'Из Jira', чтобы увидеть результат.
no_data: Не выбран номер задачи для тестирования
invalid_exp: Некорректное выражение
from_jira: Из Jira
value: Значение
add_mapping_tooltip: Добавить тестовое сопоставление полей
reset_tooltip: Сбросить

View file

@ -0,0 +1,68 @@
import * as en from './compiled/en.json';
import * as ru from './compiled/ru.json';
export const defaultLocale = 'en';
const locales: Record<string, any> = {
en,
ru,
};
const getCurrentLocale = (): string => {
const stored = window.localStorage.getItem('language');
return stored && locales[stored] ? stored : defaultLocale;
};
const getNestedValue = (obj: any, path: string): any => {
return path.split('.').reduce((current, key) => {
return current && current[key] !== undefined ? current[key] : null;
}, obj);
};
function replacePlaceholders(template: string, dict: Record<string, unknown>): string {
return template.replace(/{([^{}]+)}/g, (match, key) => {
// Trim the key in case there's whitespace inside the braces
const trimmedKey = key.trim();
return dict.hasOwnProperty(trimmedKey) ? String(dict[trimmedKey]) : match;
});
}
export const t = (key: string, prefix?: string, dict?: Record<string, unknown>): string => {
const currentLocale = getCurrentLocale();
const fullPath = prefix ? `${prefix}.${key}` : key;
const currentDict = locales[currentLocale];
const defaultDict = locales[defaultLocale];
let value = getNestedValue(currentDict, fullPath);
if (value === null && currentLocale !== defaultLocale) {
value = getNestedValue(defaultDict, fullPath);
}
if (value === null) {
console.warn(`Translation key not found: "${fullPath}" in locales: ${currentLocale}, ${defaultLocale}`);
return fullPath;
}
// Only process dictionary replacements if we have a dict and the value isn't the fallback path
if (dict && typeof value === 'string' && value !== fullPath) {
return replacePlaceholders(value, dict);
}
return value;
};
export const useTranslations = (prefix?: string) => {
const currentLocale = getCurrentLocale();
const translate = (key: string, dict?: Record<string, unknown>): string => {
return t(key, prefix, dict);
};
return {
t: translate,
locale: currentLocale,
setPrefix: (newPrefix: string) => useTranslations(newPrefix)
};
};

View file

@ -0,0 +1,244 @@
const fs = require('fs-extra');
const path = require('path');
const chokidar = require('chokidar');
const COMPILED_DIR = path.join(__dirname, 'compiled');
// Function to get all keys from an object (including nested ones)
function getAllKeys(obj, prefix = '', result = new Set()) {
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
result.add(fullKey);
if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
getAllKeys(obj[key], fullKey, result);
}
}
return result;
}
// Check if a key exists in an object
function keyExists(obj, keyPath) {
const parts = keyPath.split('.');
let current = obj;
for (const part of parts) {
if (current[part] === undefined) {
return false;
}
current = current[part];
}
return true;
}
// Get value from nested object using dot notation
function getValue(obj, keyPath) {
const parts = keyPath.split('.');
let current = obj;
for (const part of parts) {
if (current[part] === undefined) {
return undefined;
}
current = current[part];
}
return current;
}
// Function to validate translations
function validateTranslations() {
console.log('🔍 Validating translation files...');
// Read all compiled JSON files
const files = fs.readdirSync(COMPILED_DIR).filter(file => file.endsWith('.json'));
if (files.length === 0) {
console.log('⚠️ No compiled translation files found');
return;
}
// Load all translation data
const translations = {};
for (const file of files) {
const locale = path.basename(file, '.json');
const filePath = path.join(COMPILED_DIR, file);
translations[locale] = fs.readJsonSync(filePath);
}
// Get all locales
const locales = Object.keys(translations);
console.log(`📁 Found ${locales.length} locales: ${locales.join(', ')}`);
if (locales.length < 2) {
console.log('⚠️ Need at least 2 locales to compare');
return;
}
// Choose the first locale as reference
const referenceLocale = locales[0];
console.log(`🔑 Using ${referenceLocale} as reference locale`);
// Get all keys from reference locale
const referenceKeys = getAllKeys(translations[referenceLocale]);
console.log(`🔢 Reference locale has ${referenceKeys.size} unique keys`);
// Track statistics
const stats = {
missingKeys: {},
extraKeys: {},
typeErrors: {}
};
// Initialize stats for each locale
for (const locale of locales) {
if (locale !== referenceLocale) {
stats.missingKeys[locale] = [];
stats.extraKeys[locale] = [];
stats.typeErrors[locale] = [];
}
}
// Check each locale against the reference
for (const locale of locales) {
if (locale === referenceLocale) continue;
const localeKeys = getAllKeys(translations[locale]);
// Check for missing keys
for (const key of referenceKeys) {
if (!keyExists(translations[locale], key)) {
stats.missingKeys[locale].push(key);
} else {
// Check for type mismatches
const refValue = getValue(translations[referenceLocale], key);
const localeValue = getValue(translations[locale], key);
if (typeof refValue !== typeof localeValue) {
stats.typeErrors[locale].push({
key,
refType: typeof refValue,
localeType: typeof localeValue
});
}
}
}
// Check for extra keys
for (const key of localeKeys) {
if (!keyExists(translations[referenceLocale], key)) {
stats.extraKeys[locale].push(key);
}
}
}
// Print results
let hasIssues = false;
// Print missing keys
for (const locale in stats.missingKeys) {
const missing = stats.missingKeys[locale];
if (missing.length > 0) {
hasIssues = true;
console.log(`${locale} is missing ${missing.length} keys:`);
missing.forEach(key => {
console.log(` - ${key}`);
});
}
}
// Print extra keys
for (const locale in stats.extraKeys) {
const extra = stats.extraKeys[locale];
if (extra.length > 0) {
hasIssues = true;
console.log(`⚠️ ${locale} has ${extra.length} extra keys:`);
extra.forEach(key => {
console.log(` - ${key}`);
});
}
}
// Print type errors
for (const locale in stats.typeErrors) {
const typeErrors = stats.typeErrors[locale];
if (typeErrors.length > 0) {
hasIssues = true;
console.log(`⚠️ ${locale} has ${typeErrors.length} type mismatches:`);
typeErrors.forEach(err => {
console.log(` - ${err.key}: expected ${err.refType}, got ${err.localeType}`);
});
}
}
// Print empty values check if needed
console.log('\n📊 Checking for empty values...');
for (const locale of locales) {
checkEmptyValues(translations[locale], locale);
}
if (!hasIssues) {
console.log('✅ All locales have consistent structure!');
}
return hasIssues;
}
// Function to check for empty values
function checkEmptyValues(obj, locale, prefix = '') {
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
const value = obj[key];
if (value === '') {
console.log(`⚠️ ${locale} has empty string at ${fullKey}`);
} else if (value === null) {
console.log(`⚠️ ${locale} has null value at ${fullKey}`);
} else if (typeof value === 'object' && !Array.isArray(value)) {
checkEmptyValues(value, locale, fullKey);
}
}
}
// Main function
function main() {
// Create compiled directory if it doesn't exist
fs.ensureDirSync(COMPILED_DIR);
// Run validation
validateTranslations();
}
// Watch mode
if (process.argv.includes('--watch')) {
console.log('👀 Watching for changes...');
let debounceTimer;
const debounceDelay = 100;
// Initial validation
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
main();
}, debounceDelay);
// Watch for changes in the compiled directory
chokidar.watch(COMPILED_DIR, {
ignoreInitial: true,
ignored: /.*~$/, // Игнорировать скрытые файлы
}).on('all', (event, path) => {
if (event === 'change' || event === 'add' || event === 'unlink') {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
console.log(`🔁 Detected changes in ${path} (${event}), revalidating...`);
main();
}, debounceDelay);
}
});
} else {
// Run once
main();
}

View file

@ -3,13 +3,11 @@ import { JiraSettingTab } from "./settings/JiraSettingTab";
import { JiraIssueType, JiraProject } from "./interfaces";
import {DEFAULT_SETTINGS, JiraSettings} from "./settings/default";
import {
registerUpdateIssueCommand,
registerGetIssueCommand,
registerUpdateIssueCommand, registerUpdateWorkLogManuallyCommand,
registerGetIssueCommand, registerUpdateWorkLogBatchCommand,
registerCreateIssueCommand, registerGetIssueCommandWithKey, registerUpdateIssueStatusCommand
} from "./commands";
import {registerUpdateWorkLogManuallyCommand} from "./commands/addWorkLogManually";
import {transform_string_to_functions_mappings} from "./tools/convertFunctionString";
import {registerUpdateWorkLogBatchCommand} from "./commands/addWorkLogBatch";
/**
* Main plugin class

View file

@ -1,5 +1,7 @@
import {App, Modal, Setting} from "obsidian";
import {useTranslations} from "../localization/translator";
const t = useTranslations("modals.search").t;
/**
* Modal for searching issues by key
*/
@ -13,14 +15,14 @@ export class IssueSearchModal extends Modal {
}
onOpen() {
this.contentEl.createEl("h2", {text: "Search issue by key"});
this.contentEl.createEl("h2", {text: t("desc")});
new Setting(this.contentEl)
.setName("Issue key")
.setDesc("Enter the Jira issue key (e.g., PROJECT-123)")
.setName(t("key.name"))
.setDesc(t("key.desc"))
.addText((text) =>
text
.setPlaceholder("Issue key")
.setPlaceholder(t("key.placeholder"))
.onChange((value) => {
this.issueKey = value;
})
@ -29,7 +31,7 @@ export class IssueSearchModal extends Modal {
new Setting(this.contentEl)
.addButton((btn) =>
btn
.setButtonText("Search")
.setButtonText(t("submit"))
.setCta()
.onClick(() => {
this.close();

View file

@ -1,5 +1,8 @@
import {App, SuggestModal} from "obsidian";
import {JiraTransitionType} from "../interfaces";
import {useTranslations} from "../localization/translator";
const t = useTranslations("modals.status").t;
/**
* Modal for selecting an issue status
@ -12,7 +15,7 @@ export class IssueStatusModal extends SuggestModal<JiraTransitionType> {
super(app);
this.onSubmit = onSubmit;
this.issueTransitions = issueTransitions;
this.setPlaceholder("Search for an issue transition...");
this.setPlaceholder(t("placeholder"));
}
getSuggestions(query: string): JiraTransitionType[] {

View file

@ -1,5 +1,8 @@
import {App, SuggestModal} from "obsidian";
import {JiraIssueType} from "../interfaces";
import {useTranslations} from "../localization/translator";
const t = useTranslations("modals.type").t;
/**
* Modal for selecting an issue type
@ -12,7 +15,7 @@ export class IssueTypeModal extends SuggestModal<JiraIssueType> {
super(app);
this.onSubmit = onSubmit;
this.issueTypes = issueTypes;
this.setPlaceholder("Search for an issue type...");
this.setPlaceholder(t("placeholder"));
}
getSuggestions(query: string): JiraIssueType[] {

View file

@ -1,10 +1,12 @@
// modals/WorkLogModal.ts
import {App, Modal, Notice, Setting} from "obsidian";
import {useTranslations} from "../localization/translator";
const t = useTranslations("modals.worklog").t;
/**
* Modal for adding work log entries
*/
export class WorkLogModal extends Modal {
export class IssueWorkLogModal extends Modal {
private onSubmit: (timeSpent: string, startDate: string, workDescription: string) => void;
private timeSpent: string = "";
private startDate: string = "";
@ -27,7 +29,7 @@ export class WorkLogModal extends Modal {
this.startDate = date.toISOString().split('.')[0] + ".000+0000";
// Set the display format (01/Jun/21 09:00 AM)
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const months = t('months') as any;
const day = String(date.getDate()).padStart(2, '0');
const month = months[date.getMonth()];
const year = String(date.getFullYear()).slice(2);
@ -46,12 +48,16 @@ export class WorkLogModal extends Modal {
const [day, month, year] = datePart.split('/');
const [hours, minutes] = timePart.split(':');
const months = {'Jan': 0, 'Feb': 1, 'Mar': 2, 'Apr': 3, 'May': 4, 'Jun': 5,
'Jul': 6, 'Aug': 7, 'Sep': 8, 'Oct': 9, 'Nov': 10, 'Dec': 11};
const months = Object.fromEntries(
(t('months') as any).map((month: string, index: number) => [month, index])
) as Record<string, number>;
// {'Jan': 0, 'Feb': 1, 'Mar': 2, 'Apr': 3, 'May': 4, 'Jun': 5,
// 'Jul': 6, 'Aug': 7, 'Sep': 8, 'Oct': 9, 'Nov': 10, 'Dec': 11};
let hour = parseInt(hours);
if (ampm === 'PM' && hour < 12) hour += 12;
if (ampm === 'AM' && hour === 12) hour = 0;
if (ampm.toUpperCase() === 'PM' && hour < 12) hour += 12;
if (ampm.toUpperCase() === 'AM' && hour === 12) hour = 0;
const fullYear = parseInt(year) + 2000;
@ -66,14 +72,14 @@ export class WorkLogModal extends Modal {
}
onOpen() {
new Setting(this.contentEl).setName('Add work log').setHeading();
new Setting(this.contentEl).setName(t('name')).setHeading();
// Time Spent field
this.timeSpentSetting = new Setting(this.contentEl)
.setName("Time spent")
.setDesc("Format: 3w 4d 12h 30m (weeks, days, hours, minutes)")
.setName(t('spent.name'))
.setDesc(t('spent.desc'))
.addText((text) =>
text
.setPlaceholder("e.g., 4h 30m")
.setPlaceholder(t('spent.placeholder'))
.setValue(this.timeSpent)
.onChange((value) => {
this.timeSpent = value;
@ -81,11 +87,11 @@ export class WorkLogModal extends Modal {
);
this.dateSetting = new Setting(this.contentEl)
.setName("Start time")
.setDesc("Format: DD/MMM/YY HH:MM AM/PM")
.setName(t('start.name'))
.setDesc(t('start.desc'))
.addText((text) =>
text
.setPlaceholder("e.g., 01/Feb/25 10:00 AM")
.setPlaceholder(t('start.placeholder'))
.setValue(this.displayDate)
.onChange((value) => {
this.displayDate = value;
@ -158,11 +164,11 @@ export class WorkLogModal extends Modal {
// Work Description field
new Setting(this.contentEl)
.setName("Work description")
.setDesc("Optional description of the work done")
.setName(t("comment.name"))
.setDesc(t("comment.name"))
.addTextArea((text) =>
text
.setPlaceholder("Description of work done...")
.setPlaceholder(t("comment.placeholder"))
.onChange((value) => {
this.workDescription = value;
})
@ -179,7 +185,7 @@ export class WorkLogModal extends Modal {
new Setting(this.contentEl)
.addButton((btn) =>
btn
.setButtonText("Submit")
.setButtonText(t("submit"))
.setCta()
.onClick(() => {
if (!this.validateInputs()) {
@ -196,7 +202,7 @@ export class WorkLogModal extends Modal {
*/
private validateInputs(): boolean {
if (!this.timeSpent.trim()) {
new Notice("Time spent is required");
new Notice(t("warns.no_time"));
this.timeSpentSetting.setClass('invalid');
return false;
}
@ -204,20 +210,20 @@ export class WorkLogModal extends Modal {
// Validate time spent format
const timeSpentPattern = /^(\d+[wdhm]\s*)+$/;
if (!timeSpentPattern.test(this.timeSpent)) {
new Notice("Invalid time format. Use combinations of w (weeks), d (days), h (hours), m (minutes)");
new Notice(t("warns.invalid_time"));
this.timeSpentSetting.setClass('invalid');
return false;
}
if (!this.displayDate.trim()) {
new Notice("Start date is required");
new Notice(t("warns.no_start"));
this.dateSetting.setClass('invalid');
return false;
}
// Validate date format by trying to parse it
if (!this.parseDisplayDate(this.displayDate)) {
new Notice("Invalid date format. Use DD/MMM/YY HH:MM AM/PM");
new Notice(t("warns.invalid_start"));
this.dateSetting.setClass('invalid');
return false;
}

View file

@ -1,5 +1,8 @@
import {App, SuggestModal} from "obsidian";
import {JiraProject} from "../interfaces";
import {useTranslations} from "../localization/translator";
const t = useTranslations("modals.project").t;
/**
* Modal for selecting a project
@ -12,7 +15,7 @@ export class ProjectModal extends SuggestModal<JiraProject> {
super(app);
this.onSubmit = onSubmit;
this.projects = projects;
this.setPlaceholder("Search for a project...");
this.setPlaceholder(t("placeholder"));
}
getSuggestions(query: string): JiraProject[] {

View file

@ -1,3 +1,5 @@
export * from "./IssueSearchModal";
export * from "./IssueStatusModal";
export * from "./IssueTypeModal";
export * from "./IssueWorkLogModal";
export * from "./ProjectModal";

View file

@ -8,7 +8,10 @@ import { RawIssueViewerComponent } from "./components/RawIssueViewerComponent";
import { TestFieldMappingsComponent } from "./components/TestFieldMappingsComponent";
import { debugLog } from "../tools/debugLogging";
import { CollapsibleSection } from "./CollapsibleSection";
import {useTranslations} from "../localization/translator";
import {TimekeepSettingsComponent} from "./components/TimekeepSettingsComponent";
const t = useTranslations("settings").t;
/**
* Settings tab for the Jira plugin
* Orchestrates all components and manages the overall settings UI
@ -20,6 +23,7 @@ export class JiraSettingTab extends PluginSettingTab {
private fieldMappingsSettings: FieldMappingsComponent;
private rawIssueViewer: RawIssueViewerComponent;
private testFieldMappings: TestFieldMappingsComponent;
private timekeepSettings: TimekeepSettingsComponent;
constructor(app: App, plugin: JiraPlugin) {
super(app, plugin);
@ -39,6 +43,7 @@ export class JiraSettingTab extends PluginSettingTab {
this.rawIssueViewer = new RawIssueViewerComponent(componentProps);
this.testFieldMappings = new TestFieldMappingsComponent({ ...componentProps, getCurrentIssue: () => this.rawIssueViewer.getCurrentIssue() });
this.rawIssueViewer.onIssueDataChange = () => this.testFieldMappings.setCurrentIssue(this.rawIssueViewer.getCurrentIssue());
this.timekeepSettings = new TimekeepSettingsComponent(componentProps);
}
/**
@ -58,20 +63,25 @@ export class JiraSettingTab extends PluginSettingTab {
containerEl.empty();
new CollapsibleSection(this.plugin, containerEl, this.connectionSettings, "Connection settings",
// remember to add collapsable section name ("connection", "general", etc.) in settings/default.ts in order to be able to save last state
new CollapsibleSection(this.plugin, containerEl, this.connectionSettings, t("connection.title"),
this.plugin.settings.collapsedSections.connection, "connection").getContentContainer();
new CollapsibleSection(this.plugin, containerEl, this.generalSettings, "General settings",
new CollapsibleSection(this.plugin, containerEl, this.generalSettings, t("general.title"),
this.plugin.settings.collapsedSections.general, "general").getContentContainer();
new CollapsibleSection(this.plugin, containerEl, this.fieldMappingsSettings, "Field mappings",
new CollapsibleSection(this.plugin, containerEl, this.fieldMappingsSettings, t("fm.title"),
this.plugin.settings.collapsedSections.fieldMappings, "fieldMappings").getContentContainer();
new CollapsibleSection(this.plugin, containerEl, this.rawIssueViewer, "Raw issue viewer",
new CollapsibleSection(this.plugin, containerEl, this.rawIssueViewer, t("issue_view.title"),
this.plugin.settings.collapsedSections.rawIssueViewer, "rawIssueViewer").getContentContainer();
new CollapsibleSection(this.plugin, containerEl, this.testFieldMappings, "Test field mappings",
new CollapsibleSection(this.plugin, containerEl, this.testFieldMappings, t("tfm.title"),
this.plugin.settings.collapsedSections.testFieldMappings, "testFieldMappings").getContentContainer();
new CollapsibleSection(this.plugin, containerEl, this.timekeepSettings, t("statistics.title"),
this.plugin.settings.collapsedSections.statistics, "statistics").getContentContainer();
}
/**

View file

@ -1,6 +1,9 @@
import { Setting } from "obsidian";
import { useTranslations } from "src/localization/translator"
import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes";
const t = useTranslations("settings.connection").t
/**
* Component for Jira connection settings
*/
@ -19,11 +22,11 @@ export class ConnectionSettingsComponent implements SettingsComponent {
// Always show: Jira URL
new Setting(containerEl)
.setName("Jira URL")
.setDesc("Your Jira instance URL")
.setName(t("url.title"))
.setDesc(t("url.desc"))
.addText((text) =>
text
.setPlaceholder("https://yourcompany.atlassian.net")
.setPlaceholder(t("url.def"))
.setValue(plugin.settings.jiraUrl)
.onChange(async (value) => {
plugin.settings.jiraUrl = value;
@ -32,14 +35,14 @@ export class ConnectionSettingsComponent implements SettingsComponent {
);
// Auth Method Select
const authMethodSetting = new Setting(containerEl)
.setName("Authentication method")
.setDesc("Choose how to authenticate with Jira")
new Setting(containerEl)
.setName(t("auth.title"))
.setDesc(t("auth.desc"))
.addDropdown((cb) =>
cb
.addOption("bearer", "Bearer Token (PAT)")
.addOption("basic", "Basic Auth (Username + PAT)")
.addOption("session", "Session Cookie (Username + Password)")
.addOption("bearer", t("auth.options.bearer"))
.addOption("basic", t("auth.options.basic"))
.addOption("session", t("auth.options.session"))
.setValue(this.currentAuthMethod)
.onChange(async (value: "bearer" | "basic" | "session") => {
plugin.settings.authMethod = value;
@ -54,12 +57,12 @@ export class ConnectionSettingsComponent implements SettingsComponent {
// Jira PAT
const patSetting = new Setting(containerEl)
.setName("Jira PAT")
.setDesc("Personal Access Token")
.setName(t("pat.title"))
.setDesc(t("pat.desc"))
.addText((text) => {
text.inputEl.type = "password";
text
.setPlaceholder("NjM5MDI5MzQ0NT...")
.setPlaceholder(t("pat.def"))
.setValue(plugin.settings.apiToken)
.onChange(async (value) => {
plugin.settings.apiToken = value;
@ -70,11 +73,11 @@ export class ConnectionSettingsComponent implements SettingsComponent {
// Jira username
const usernameSetting = new Setting(containerEl)
.setName("Jira username")
.setDesc("Your Jira username")
.setName(t("username.title"))
.setDesc(t("username.desc"))
.addText((text) =>
text
.setPlaceholder("admin")
.setPlaceholder(t("username.def"))
.setValue(plugin.settings.username)
.onChange(async (value) => {
plugin.settings.username = value;
@ -85,11 +88,11 @@ export class ConnectionSettingsComponent implements SettingsComponent {
// Jira username
const emailSetting = new Setting(containerEl)
.setName("Jira email")
.setDesc("Your Jira email")
.setName(t("username.title"))
.setDesc(t("username.desc"))
.addText((text) =>
text
.setPlaceholder("admin@gmail.com")
.setPlaceholder(t("username.def"))
.setValue(plugin.settings.email)
.onChange(async (value) => {
plugin.settings.email = value;
@ -100,12 +103,12 @@ export class ConnectionSettingsComponent implements SettingsComponent {
// Jira password
const passwordSetting = new Setting(containerEl)
.setName("Jira password")
.setDesc("Your Jira password")
.setName(t("password.title"))
.setDesc(t("password.desc"))
.addText((text) => {
text.inputEl.type = "password";
text
.setPlaceholder("qwerty")
.setPlaceholder(t("password.def"))
.setValue(plugin.settings.password)
.onChange(async (value) => {
plugin.settings.password = value;

View file

@ -1,5 +1,7 @@
import { validateField } from "../tools/fieldValidation";
import {useTranslations} from "../../localization/translator";
const t = useTranslations("settings.fm").t;
/**
* Interface for field mapping item props
*/
@ -49,37 +51,40 @@ export class FieldMappingItem {
// Create field name input
const fieldNameContainer = this.fieldContainer.createDiv({ cls: "field-name-container" });
fieldNameContainer.createEl("span", { text: "Field Name:", cls: "field-mapping-label" });
fieldNameContainer.createEl("span", { text: t("field_name"), cls: "field-mapping-label" });
this.fieldNameInput = fieldNameContainer.createEl("input", {
type: "text",
value: fieldName,
cls: "field-name-input"
cls: "field-name-input",
placeholder: t("field_name_placeholder") || "e.g., summary, assignee, priority"
});
// Create functions container (grid layout)
const functionsContainer = this.fieldContainer.createDiv({ cls: "functions-container" });
// Create toJira function input
const toJiraContainer = this.fieldContainer.createDiv({ cls: "to-jira-container" });
toJiraContainer.createEl("span", { text: "to Jira:", cls: "field-mapping-label" });
const toJiraContainer = functionsContainer.createDiv({ cls: "to-jira-container" });
toJiraContainer.createEl("span", { text: t("to_jira"), cls: "field-mapping-label" });
this.toJiraInput = toJiraContainer.createEl("textarea", {
cls: "to-jira-input"
});
this.toJiraInput.placeholder = "(value) => null";
this.toJiraInput.placeholder = "(value) => {\n // Transform Obsidian value to Jira\n return value;\n}";
this.toJiraInput.value = toJira;
this.toJiraInput.rows = 1;
// Create fromJira function input
const fromJiraContainer = this.fieldContainer.createDiv({ cls: "from-jira-container" });
fromJiraContainer.createEl("span", { text: "from Jira:", cls: "field-mapping-label" });
const fromJiraContainer = functionsContainer.createDiv({ cls: "from-jira-container" });
fromJiraContainer.createEl("span", { text: t("from_jira"), cls: "field-mapping-label" });
this.fromJiraInput = fromJiraContainer.createEl("textarea", {
cls: "from-jira-input"
});
this.fromJiraInput.value = fromJira;
this.fromJiraInput.placeholder = "(issue, data_source) => null";
this.fromJiraInput.rows = 1;
this.fromJiraInput.placeholder = "(issue, data_source) => {\n // Extract value from Jira issue\n return issue.fields.fieldName;\n}";
// Add remove button
const removeBtn = this.fieldContainer.createEl("button", {
text: "✕",
cls: "remove-field-btn"
cls: "remove-field-btn",
attr: { 'aria-label': t("remove_field") || 'Remove field mapping' }
});
// Handle field removal
@ -88,10 +93,27 @@ export class FieldMappingItem {
this.props.onRemove();
});
// Auto-resize textareas
this.setupAutoResize(this.toJiraInput);
this.setupAutoResize(this.fromJiraInput);
// Setup validation
this.setupValidation(enableValidation);
}
private setupAutoResize(textarea: HTMLTextAreaElement) {
const resize = () => {
textarea.style.height = 'auto';
textarea.style.height = Math.max(40, textarea.scrollHeight) + 'px';
};
textarea.addEventListener('input', resize);
textarea.addEventListener('focus', resize);
// Initial resize
setTimeout(resize, 0);
}
/**
* Set up validation for inputs
*/

View file

@ -3,8 +3,11 @@ import { SettingsComponent, SettingsComponentProps } from "../../interfaces/sett
import { FieldMappingItem } from "./FieldMappingItem";
import { collectFieldMappingsFromUI, processMappings } from "../tools/mappingTransformers";
import {jiraFunctionToString, transform_string_to_functions_mappings} from "../../tools/convertFunctionString";
import { obsidianJiraFieldMappings } from "../../constants/obsidianJiraFieldsMapping";
import { obsidianJiraFieldMappings } from "../../default/obsidianJiraFieldsMapping";
import { debugLog } from "../../tools/debugLogging";
import {useTranslations} from "../../localization/translator";
const t = useTranslations("settings.fm").t;
/**
* Component for field mappings section
@ -29,13 +32,13 @@ export class FieldMappingsComponent implements SettingsComponent {
// Add explanation
mappingSection.createEl("p", {
text: "Configure how fields are mapped between Obsidian and Jira. Each field requires both a toJira and fromJira transformation function."
text: t("desc")
});
// Add validation toggle
new Setting(mappingSection)
.setName('Enable field validation')
.setDesc('Check fields before saving as functions')
.setName(t("fv.name"))
.setDesc(t("fv.desc"))
.addToggle(toggle => toggle
.setValue(plugin.settings.enableFieldValidation)
.onChange(async (value) => {
@ -56,8 +59,9 @@ export class FieldMappingsComponent implements SettingsComponent {
// Add button to add new field mapping
const addFieldBtn = buttonView.createEl("button", {
text: "",
cls: "add-field-mapping-btn"
text: t("add_mapping") || "Add Mapping",
cls: "add-field-mapping-btn",
attr: { 'data-tooltip': t("add_mapping_tooltip") || "Add new field mapping" }
});
setIcon(addFieldBtn, "circle-plus");
@ -67,40 +71,57 @@ export class FieldMappingsComponent implements SettingsComponent {
// Reset button
const resetBtn = buttonView.createEl("button", {
text: "",
cls: "reset-field-mappings-btn"
text: t("reset") || "Reset All",
cls: "reset-field-mappings-btn",
attr: { 'data-tooltip': t("reset_tooltip") || "Clear all field mappings" }
});
setIcon(resetBtn, "refresh-cw");
resetBtn.addEventListener("click", async () => {
plugin.settings.fieldMappings = {};
plugin.settings.fieldMappingsStrings = {};
await this.loadExistingMappings();
await plugin.saveSettings();
// Add confirmation dialog
const confirmed = await this.showConfirmDialog(
t("reset_confirm_title") || "Reset Field Mappings",
t("reset_confirm_text") || "Are you sure you want to clear all field mappings? This action cannot be undone."
);
if (confirmed) {
plugin.settings.fieldMappings = {};
plugin.settings.fieldMappingsStrings = {};
await this.loadExistingMappings();
await plugin.saveSettings();
}
});
// Reload default mappings button
const reloadBtn = buttonView.createEl("button", {
text: "",
cls: "reload-field-mappings-btn"
text: t("reload_defaults") || "Load Defaults",
cls: "reload-field-mappings-btn",
attr: { 'data-tooltip': t("reload_defaults_tooltip") || "Restore default field mappings" }
});
setIcon(reloadBtn, "list-restart");
reloadBtn.addEventListener("click", async () => {
plugin.settings.fieldMappings = obsidianJiraFieldMappings;
const confirmed = await this.showConfirmDialog(
t("reload_confirm_title") || "Load Default Mappings",
t("reload_confirm_text") || "This will replace all current mappings with defaults. Continue?"
);
// Also reset the string representations with default values
const defaultMappingsStrings: Record<string, { toJira: string; fromJira: string }> = {};
for (const [fieldName, mapping] of Object.entries(obsidianJiraFieldMappings)) {
defaultMappingsStrings[fieldName] = {
toJira: jiraFunctionToString(mapping.toJira, false),
fromJira: jiraFunctionToString(mapping.fromJira, true)
};
if (confirmed) {
plugin.settings.fieldMappings = obsidianJiraFieldMappings;
// Also reset the string representations with default values
const defaultMappingsStrings: Record<string, { toJira: string; fromJira: string }> = {};
for (const [fieldName, mapping] of Object.entries(obsidianJiraFieldMappings)) {
defaultMappingsStrings[fieldName] = {
toJira: jiraFunctionToString(mapping.toJira, false),
fromJira: jiraFunctionToString(mapping.fromJira, true)
};
}
plugin.settings.fieldMappingsStrings = defaultMappingsStrings;
await this.loadExistingMappings();
await plugin.saveSettings();
}
plugin.settings.fieldMappingsStrings = defaultMappingsStrings;
await this.loadExistingMappings();
await plugin.saveSettings();
});
// Load existing mappings
@ -111,9 +132,41 @@ export class FieldMappingsComponent implements SettingsComponent {
// Add security notice
const securityNote = containerEl.createEl("div", { cls: "setting-item-description" });
securityNote.createEl("strong", { text: "⚠️ Security note: " });
securityNote.createEl("strong", { text: t("secNote.name") });
securityNote.createSpan({
text: "Field mapping uses JavaScript function strings. Validate any shared mappings to prevent security issues."
text: t("secNote.desc")
});
}
private async showConfirmDialog(title: string, message: string): Promise<boolean> {
return new Promise((resolve) => {
const modal = document.createElement('div');
modal.className = 'modal-container';
modal.innerHTML = `
<div class="modal">
<div class="modal-title">${title}</div>
<div class="modal-content">${message}</div>
<div class="modal-button-container">
<button class="mod-cta" id="confirm-btn">Confirm</button>
<button id="cancel-btn">Cancel</button>
</div>
</div>
`;
document.body.appendChild(modal);
const confirmBtn = modal.querySelector('#confirm-btn') as HTMLButtonElement;
const cancelBtn = modal.querySelector('#cancel-btn') as HTMLButtonElement;
confirmBtn?.addEventListener('click', () => {
document.body.removeChild(modal);
resolve(true);
});
cancelBtn?.addEventListener('click', () => {
document.body.removeChild(modal);
resolve(false);
});
});
}
@ -214,7 +267,8 @@ export class FieldMappingsComponent implements SettingsComponent {
plugin.settings.fieldMappingsStrings = newStringMappings;
plugin.settings.fieldMappings = functionMappings;
debugLog(`To strings ${JSON.stringify(plugin.settings.fieldMappingsStrings)}\nand funcs ${JSON.stringify(plugin.settings.fieldMappings)}`);
debugLog(`To strings ${JSON.stringify(plugin.settings.fieldMappingsStrings).substring(0, 100)}\n
and funcs ${JSON.stringify(plugin.settings.fieldMappings).substring(0, 100)}`);
await plugin.saveSettings();
}
@ -223,7 +277,7 @@ export class FieldMappingsComponent implements SettingsComponent {
*/
private renderExamples(containerEl: HTMLElement): void {
const examplesSection = containerEl.createDiv({ cls: "mapping-examples" });
examplesSection.createEl("h4", { text: "Example field mappings" });
examplesSection.createEl("h4", { text: t("example.name") });
const examplesList = examplesSection.createEl("ul");
@ -251,7 +305,7 @@ export class FieldMappingsComponent implements SettingsComponent {
// Add button to use this example
const useBtn = item.createEl("button", {
text: "Use",
text: t("example.use"),
cls: "use-example-btn"
});

View file

@ -1,11 +1,20 @@
import { normalizePath, Setting } from "obsidian";
import { App, TFile, TFolder, Setting, normalizePath } from "obsidian";
import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes";
import { SuggestSelectComponent, SuggestSelectOption } from "./SuggestSelectComponent";
import {useTranslations} from "../../localization/translator";
const t = useTranslations("settings.general").t;
interface TemplatePluginInfo {
coreTemplatesEnabled: boolean;
templaterEnabled: boolean;
templateDirectory: string | null;
warningMessage: string;
}
/**
* Component for general plugin settings
*/
export class GeneralSettingsComponent implements SettingsComponent {
private props: SettingsComponentProps;
private templateSelector: SuggestSelectComponent | null = null;
constructor(props: SettingsComponentProps) {
this.props = props;
@ -16,8 +25,8 @@ export class GeneralSettingsComponent implements SettingsComponent {
// Issues folder setting
new Setting(containerEl)
.setName("Issues folder")
.setDesc("Folder where Jira issues will be stored")
.setName(t("folder.name"))
.setDesc(t("folder.desc"))
.addText((text) =>
text
.setPlaceholder("jira-issues")
@ -28,19 +37,115 @@ export class GeneralSettingsComponent implements SettingsComponent {
})
);
// Template path setting
new Setting(containerEl)
.setName("Template path")
.setDesc("The path of used template for creating Jira tasks (with or without .md extension)")
.addText((text) =>
text
.setPlaceholder("templates/template1")
.setValue(plugin.settings.templatePath)
.onChange(async (value) => {
value = normalizePath(value);
plugin.settings.templatePath = value;
await plugin.saveSettings();
})
);
// Template path setting with selector
this.createTemplateSelector(containerEl);
}
private createTemplateSelector(containerEl: HTMLElement): void {
const { plugin } = this.props;
const templateInfo = this.detectTemplatePlugins(plugin.app);
const setting = new Setting(containerEl)
.setName(t("template.name"))
.setDesc(t("template.desc"));
// Add warning if no template plugins are enabled
if (templateInfo.warningMessage) {
setting.descEl.createDiv({}, (div) => {
div.createEl("small", {
text: templateInfo.warningMessage,
cls: "mod-warning"
});
});
}
// Create template selector
this.templateSelector = new SuggestSelectComponent({
placeholder: t("template.selector.placeholder"),
emptyOptionText: t("template.selector.empty"),
searchEnabled: true,
});
// Render selector in the setting control
this.templateSelector.render(setting.controlEl);
// Load template options
const templateOptions = this.loadTemplateOptions(templateInfo);
this.templateSelector.setOptions(templateOptions);
// Set current value
if (plugin.settings.templatePath) {
this.templateSelector.setValue(plugin.settings.templatePath);
}
// Handle changes
this.templateSelector.onChange(async (selectedPath) => {
plugin.settings.templatePath = selectedPath ? normalizePath(selectedPath) : '';
await plugin.saveSettings();
});
}
private detectTemplatePlugins(app: App): TemplatePluginInfo {
const coreTemplates = (app as any).internalPlugins?.plugins?.templates;
const templaterPlugin = (app as any).plugins?.plugins?.['templater-obsidian'];
const coreTemplatesEnabled = coreTemplates?.enabled || false;
const templaterEnabled = (app as any).plugins?.enabledPlugins?.has('templater-obsidian') || false;
let templateDirectory: string | null = null;
let warningMessage = "";
if (coreTemplatesEnabled && coreTemplates.instance?.options?.folder) {
templateDirectory = coreTemplates.instance.options.folder;
} else if (templaterEnabled && templaterPlugin?.settings?.template_folder) {
templateDirectory = templaterPlugin.settings.template_folder;
}
if (!coreTemplatesEnabled && !templaterEnabled) {
warningMessage = t("template.warning");
}
return {
coreTemplatesEnabled,
templaterEnabled,
templateDirectory,
warningMessage
};
}
private loadTemplateOptions(templateInfo: TemplatePluginInfo): SuggestSelectOption[] {
const templateOptions: SuggestSelectOption[] = [];
// Get templates from specified folder or vault root
const searchFolder = templateInfo.templateDirectory
? this.props.plugin.app.vault.getAbstractFileByPath(templateInfo.templateDirectory)
: this.props.plugin.app.vault.getRoot();
if (searchFolder instanceof TFolder) {
this.scanFolderForTemplates(searchFolder, "", templateOptions);
} else {
// Fallback to root if specified folder doesn't exist
this.scanFolderForTemplates(this.props.plugin.app.vault.getRoot(), "", templateOptions);
}
// Sort templates alphabetically
templateOptions.sort((a, b) => a.label.localeCompare(b.label));
return templateOptions;
}
private scanFolderForTemplates(folder: TFolder, prefix: string, templateOptions: SuggestSelectOption[]): void {
for (const child of folder.children) {
if (child instanceof TFile && child.extension === 'md') {
const displayName = prefix ? `${prefix}/${child.basename}` : child.basename;
templateOptions.push({
value: child.path,
label: displayName
});
} else if (child instanceof TFolder) {
const newPrefix = prefix ? `${prefix}/${child.name}` : child.name;
this.scanFolderForTemplates(child, newPrefix, templateOptions);
}
}
}
}

View file

@ -3,7 +3,9 @@ import { SettingsComponent, SettingsComponentProps } from "../../interfaces/sett
import { fetchIssue } from "../../api";
import debounce from "lodash/debounce";
import hljs from "highlight.js";
import {useTranslations} from "../../localization/translator";
const t = useTranslations("settings.issue_view").t;
/**
* Component for viewing raw Jira issue data
*/
@ -22,8 +24,8 @@ export class RawIssueViewerComponent implements SettingsComponent {
render(containerEl: HTMLElement): void {
// Add input field for issue key
new Setting(containerEl)
.setName("View raw issue data")
.setDesc("Enter a Jira issue key to view its raw data from the API")
.setName(t("key.name"))
.setDesc(t("key.desc"))
.addText((text) =>
text
.setPlaceholder("PROJ-123")
@ -66,10 +68,7 @@ export class RawIssueViewerComponent implements SettingsComponent {
this.issueDataContainer.empty();
const pre = this.issueDataContainer.createEl("pre", {
cls: "jira-raw-issue-data",
attr: {
style: "user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;"
}
cls: "jira-copyable",
});
const code = pre.createEl("code", {

View file

@ -0,0 +1,127 @@
import {useTranslations} from "../../localization/translator";
export interface SuggestSelectOption {
value: string;
label: string;
isDefault?: boolean;
}
export interface SuggestSelectConfig {
placeholder?: string;
emptyOptionText?: string;
searchEnabled?: boolean;
}
const t = useTranslations("settings.suggest_select").t;
export class SuggestSelectComponent {
private selectEl: HTMLSelectElement | null = null;
private searchInputEl: HTMLInputElement | null = null;
private allOptions: SuggestSelectOption[] = [];
private filteredOptions: SuggestSelectOption[] = [];
private config: SuggestSelectConfig;
private onChangeCallback?: (value: string) => void
constructor(config: SuggestSelectConfig = {}) {
this.config = {
placeholder: t("placeholder"),
emptyOptionText: t("empty_option_text"),
searchEnabled: true,
...config
};
}
render(containerEl: HTMLElement): HTMLElement {
const selectorContainer = containerEl.createDiv({
cls: "suggest-select-container suggest"
});
if (this.config.searchEnabled) {
this.searchInputEl = selectorContainer.createEl("input", {
type: "text",
placeholder: this.config.placeholder,
cls: "suggest-select-item suggest-search-input"
});
this.searchInputEl.addEventListener('input', (e) => {
const searchTerm = (e.target as HTMLInputElement).value.toLowerCase();
this.filterOptions(searchTerm);
});
}
this.selectEl = selectorContainer.createEl("select", {
cls: "suggest-select-item suggest-select-dropdown dropdown"
});
this.selectEl.addEventListener('change', (e) => {
const selectedValue = (e.target as HTMLSelectElement).value;
this.onChangeCallback?.(selectedValue);
});
return selectorContainer;
}
setOptions(options: SuggestSelectOption[]): void {
this.allOptions = [...options];
this.filteredOptions = [...options];
this.populateSelect();
}
setValue(value: string): void {
if (!this.selectEl) return;
const options = this.selectEl.options;
for (let i = 0; i < options.length; i++) {
if (options[i].value === value) {
this.selectEl.selectedIndex = i;
break;
}
}
}
getValue(): string {
return this.selectEl?.value || "";
}
onChange(callback: (value: string) => void): void {
this.onChangeCallback = callback;
}
private populateSelect(): void {
if (!this.selectEl) return;
this.selectEl.innerHTML = "";
// Add empty option
this.selectEl.createEl("option", {
value: "",
text: this.config.emptyOptionText
});
// Add filtered options
this.filteredOptions.forEach(option => {
this.selectEl!.createEl("option", {
value: option.value,
text: option.label
});
});
}
private filterOptions(searchTerm: string): void {
if (!searchTerm) {
this.filteredOptions = [...this.allOptions];
} else {
this.filteredOptions = this.allOptions.filter(option =>
option.label.toLowerCase().includes(searchTerm) ||
option.value.toLowerCase().includes(searchTerm)
);
}
this.populateSelect();
// Maintain selection if the current value is still available
const currentValue = this.getValue();
if (currentValue) {
this.setValue(currentValue);
}
}
}

View file

@ -1,12 +1,17 @@
import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes";
import debounce from "lodash/debounce";
import { safeStringToFunction } from "../../tools/convertFunctionString";
import {useTranslations} from "../../localization/translator";
import {setIcon} from "obsidian";
const t = useTranslations("settings.tfm").t;
export class TestFieldMappingsComponent implements SettingsComponent {
private props: SettingsComponentProps;
private testMappingsContainer: HTMLElement | null = null;
private currentIssueData: any = null;
private getCurrentIssue: (() => any) | null = null;
private _testMappingItemsContainer: HTMLElement | null = null;
constructor(props: SettingsComponentProps & { getCurrentIssue?: () => any }) {
this.props = props;
@ -21,36 +26,44 @@ export class TestFieldMappingsComponent implements SettingsComponent {
cls: "jira-test-mappings-container"
});
this.testMappingsContainer.createEl("h3", {
text: "Test field mappings",
cls: "test-mappings-header"
});
this.testMappingsContainer.createEl("p", {
text: "Test your field mappings against the current issue data from Raw issue viewer. Enter a fromJira expression to see the result.",
cls: "test-mappings-desc"
});
this.testMappingsContainer.createEl("p", {
text: t("desc"),
cls: "test-mappings-desc"
});
// Container for all test mapping items
const itemsContainer = this.testMappingsContainer.createDiv({ cls: "test-mapping-items-list" });
(this as any)._testMappingItemsContainer = itemsContainer; // store for later use
this._testMappingItemsContainer = this.testMappingsContainer.createDiv({ cls: "test-mapping-items-list" });
// Add first test mapping item
this.addTestMappingItem();
const buttonView = this.testMappingsContainer.createDiv({ cls: "button-view" });
// Add new mapping button (only once, at the bottom)
const addBtn = this.testMappingsContainer.createEl("button", {
text: "+ Add test mapping",
cls: "add-test-mapping-btn"
});
const addBtn = buttonView.createEl("button", {
text: t("add_mapping") || "Add Mapping",
cls: "add-field-mapping-btn",
attr: { 'data-tooltip': t("add_mapping_tooltip") || "Add new field mapping" }
});
setIcon(addBtn, "circle-plus");
addBtn.addEventListener("click", () => {
this.addTestMappingItem();
});
const resetBtn = buttonView.createEl("button", {
text: t("reset") || "Reset All",
cls: "reset-field-mappings-btn",
attr: { 'data-tooltip': t("reset_tooltip") || "Clear all field mappings" }
});
setIcon(resetBtn, "refresh-cw");
resetBtn.addEventListener("click", () => {
this._testMappingItemsContainer?.empty();
});
}
private addTestMappingItem(): void {
// Use the dedicated items container
const itemsContainer = (this as any)._testMappingItemsContainer as HTMLElement;
const itemsContainer = this._testMappingItemsContainer as HTMLElement;
if (!itemsContainer) return;
const itemContainer = itemsContainer.createDiv({
@ -59,7 +72,7 @@ export class TestFieldMappingsComponent implements SettingsComponent {
// From Jira expression input
const fromJiraContainer = itemContainer.createDiv({ cls: "from-jira-container" });
fromJiraContainer.createEl("span", { text: "from Jira:", cls: "field-mapping-label" });
fromJiraContainer.createEl("span", { text: t("from_jira")+":", cls: "field-mapping-label" });
const fromJiraInput = fromJiraContainer.createEl("textarea", {
cls: "from-jira-input"
@ -69,7 +82,7 @@ export class TestFieldMappingsComponent implements SettingsComponent {
// Value display
const valueContainer = itemContainer.createDiv({ cls: "value-container" });
valueContainer.createEl("span", { text: "Value:", cls: "field-mapping-label" });
valueContainer.createEl("span", { text: t("value")+":", cls: "field-mapping-label" });
const valueDisplay = valueContainer.createEl("div", {
cls: "value-display",
attr: {
@ -81,7 +94,7 @@ export class TestFieldMappingsComponent implements SettingsComponent {
const debouncedEvaluate = debounce(async () => {
const issueData = this.getCurrentIssue ? this.getCurrentIssue() : this.currentIssueData;
if (!issueData) {
valueDisplay.setText("No issue data available");
valueDisplay.setText(t("no_data"));
return;
}
@ -91,7 +104,7 @@ export class TestFieldMappingsComponent implements SettingsComponent {
const result = fromJiraFn(issueData, null);
valueDisplay.setText(JSON.stringify(result, null, 2));
} else {
valueDisplay.setText("Invalid expression");
valueDisplay.setText(t("invalid_exp"));
}
} catch (error) {
valueDisplay.setText(`Error: ${error.message}`);

View file

@ -0,0 +1,522 @@
import { Setting, Notice } from 'obsidian';
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
import { processWorkLogBatch } from "../../commands";
import { SuggestSelectComponent, SuggestSelectConfig } from './SuggestSelectComponent';
import {useTranslations} from "../../localization/translator";
import {debugLog} from "../../tools/debugLogging";
type TimeRangeType = 'days' | 'weeks' | 'months' | 'custom';
interface Entry {
name: string;
startTime?: string;
endTime?: string;
subEntries?: Entry[];
}
interface TimekeepData {
entries: Entry[];
}
interface TimekeepBlock {
file: string;
path: string;
issueKey?: string;
data: TimekeepData;
}
interface ProcessedEntry {
file: string;
issueKey?: string;
blockPath: string;
startTime: string;
endTime: string;
duration: string;
timestamp: number; // For easier sorting and filtering
}
interface GroupedEntries {
[periodKey: string]: ProcessedEntry[];
}
const t = useTranslations("settings.statistics").t;
export class TimekeepSettingsComponent implements SettingsComponent {
private props: SettingsComponentProps;
// Data
private groupedByPeriod: GroupedEntries = {};
private selectedPeriodData: ProcessedEntry[] = [];
private containerEl: HTMLElement | null = null;
private periodSelector: SuggestSelectComponent | null = null;
// Elements
private maxItemsSetting: Setting | null = null;
private customDatesContainer: HTMLElement | null = null;
private sendButton: HTMLButtonElement | null = null;
constructor(props: SettingsComponentProps) {
this.props = props;
}
render(containerEl: HTMLElement): void {
this.containerEl = containerEl;
containerEl.empty();
this.renderSettings(containerEl);
this.renderPeriodSelector(containerEl);
this.toggleCustomDateInputs();
// Initialize data
this.refreshData(true);
}
private renderSettings(containerEl: HTMLElement): void {
const settingsContainer = containerEl.createDiv('timekeep-settings');
// Time range selector
new Setting(settingsContainer)
.setName(t("settings.time_range.name"))
.setDesc(t("settings.time_range.description"))
.addDropdown(dropdown => {
dropdown.addOption('days', t("settings.time_range.options.days"));
dropdown.addOption('weeks', t("settings.time_range.options.weeks"));
dropdown.addOption('months', t("settings.time_range.options.months"));
dropdown.addOption('custom', t("settings.time_range.options.custom"));
dropdown.setValue(this.props.plugin.settings.statisticsTimeType);
dropdown.onChange(async (value: TimeRangeType) => {
this.props.plugin.settings.statisticsTimeType = value;
this.toggleCustomDateInputs();
await this.refreshData();
});
});
// Max items slider (hidden for custom range)
const maxItemsSetting = new Setting(settingsContainer)
.setName(t("settings.max_items.name"))
.setDesc(t("settings.max_items.description"))
.addSlider(slider => {
slider.setLimits(1, 20, 1)
.setValue(this.props.plugin.settings.maxItemsToShow)
.setDynamicTooltip()
.onChange(async (value) => {
this.props.plugin.settings.maxItemsToShow = value;
await this.refreshData();
});
});
// Custom date inputs (initially hidden)
const customDatesContainer = settingsContainer.createDiv('custom-dates-container');
new Setting(customDatesContainer)
.setName(t("settings.date_from.name"))
.setDesc(t("settings.date_from.description"))
.addText(text => {
text.setPlaceholder(t("settings.date_from.placeholder"));
text.setValue(this.props.plugin.settings.customDateRange.start);
text.onChange(async (value) => {
this.props.plugin.settings.customDateRange.start = value;
if (this.props.plugin.settings.statisticsTimeType === 'custom' && this.props.plugin.settings.customDateRange.end) {
await this.refreshData();
}
});
});
new Setting(customDatesContainer)
.setName(t("settings.date_to.name"))
.setDesc(t("settings.date_to.description"))
.addText(text => {
text.setPlaceholder(t("settings.date_to.placeholder"));
text.setValue(this.props.plugin.settings.customDateRange.end);
text.onChange(async (value) => {
this.props.plugin.settings.customDateRange.end = value;
if (this.props.plugin.settings.statisticsTimeType === 'custom' && this.props.plugin.settings.customDateRange.start) {
await this.refreshData();
}
});
});
// Store references for toggling
this.maxItemsSetting = maxItemsSetting;
this.customDatesContainer = customDatesContainer;
}
private toggleCustomDateInputs(): void {
if (!this.maxItemsSetting || !this.customDatesContainer) return;
const isCustom = this.props.plugin.settings.statisticsTimeType === 'custom';
if (isCustom) {
this.maxItemsSetting.settingEl.hide()
this.customDatesContainer.show();
} else {
this.maxItemsSetting.settingEl.show();
this.customDatesContainer.hide();
}
}
private async refreshData(disableNotification = false): Promise<void> {
if (!this.containerEl) return;
// Показываем индикатор загрузки
this.containerEl.addClass('timekeep-loading');
try {
await this.processFiles();
this.updateDisplay();
if (!disableNotification) new Notice(t("messages.refresh_success"));
} catch (error) {
console.error('Error refreshing timekeep data:', error);
new Notice(t("messages.refresh_error", { error: error.message }));
} finally {
// Убираем индикатор загрузки
this.containerEl.removeClass('timekeep-loading');
}
}
private updateDisplay(): void {
if (!this.containerEl) return;
// Remove existing display elements
this.containerEl.querySelector('.all-periods-display')?.remove();
this.containerEl.querySelector('.timekeep-no-data')?.remove();
if (Object.keys(this.groupedByPeriod).length === 0) {
this.containerEl.createEl('div', {
text: t("display.no_entries"),
cls: 'timekeep-no-data'
});
return;
}
this.renderAllPeriodsDisplay(this.containerEl as HTMLElement);
}
private renderPeriodSelector(container: HTMLElement): void {
const setting = new Setting(container).setClass('period-selector')
.setName(t("display.select_period") + ': ');
const config: SuggestSelectConfig = {
placeholder: t("display.select_period_placeholder"),
searchEnabled: false
};
this.periodSelector = new SuggestSelectComponent(config);
this.periodSelector.render(setting.controlEl);
this.sendButton = setting.controlEl.createEl('button', {
text: t("actions.send_to_jira"),
cls: 'timekeep-btn'
});
this.sendButton.addEventListener('click', () => this.sendWorkLogToJira());
this.periodSelector.onChange((selectedPeriod) => {
if (selectedPeriod) {
this.selectedPeriodData = this.groupedByPeriod[selectedPeriod] || [];
this.sendButton? this.sendButton.disabled = false : null;
}
else {
this.selectedPeriodData = [];
this.sendButton? this.sendButton.disabled = true : null;
}
});
}
private renderAllPeriodsDisplay(container: HTMLElement): void {
const display = container.createDiv('all-periods-display jira-copyable timekeep-fade-in');
display.createEl('h3', {
text: t("display.all_periods"),
cls: 'timekeep-section-title'
});
const sortedPeriods = Object.keys(this.groupedByPeriod).sort().reverse();
sortedPeriods.forEach((period, index) => {
const periodDiv = display.createDiv('period-section');
// Добавляем небольшую задержку для каскадной анимации
periodDiv.style.animationDelay = `${index * 50}ms`;
periodDiv.createEl('h4', {
text: this.formatPeriodLabel(period),
cls: 'timekeep-period-title'
});
const entries = this.groupedByPeriod[period];
this.createEntriesTable(periodDiv, entries);
});
}
private createEntriesTable(container: HTMLElement, entries: ProcessedEntry[]): void {
const tableContainer = container.createDiv('timekeep-table-container');
const table = tableContainer.createEl('table', { cls: 'timekeep-entries-table' });
const thead = table.createEl('thead');
const headerRow = thead.createEl('tr');
headerRow.createEl('th', { text: t("display.table.task") });
headerRow.createEl('th', { text: t("display.table.issue_key") });
headerRow.createEl('th', { text: t("display.table.block_path") });
headerRow.createEl('th', { text: t("display.table.start_time") });
headerRow.createEl('th', { text: t("display.table.duration") });
const tbody = table.createEl('tbody');
entries.forEach((entry, index) => {
const row = tbody.createEl('tr');
if (index % 2 === 1) row.addClass('timekeep-table-row-alt');
// Добавляем title для длинных текстов
const taskCell = row.createEl('td', { text: entry.file });
taskCell.title = entry.file;
const issueCell = row.createEl('td', { text: entry.issueKey || '-' });
if (entry.issueKey) {
issueCell.title = `Issue: ${entry.issueKey}`;
}
const pathCell = row.createEl('td', { text: entry.blockPath });
pathCell.title = entry.blockPath; // Полный путь в тултипе
const timeCell = row.createEl('td', { text: entry.startTime });
timeCell.title = `Started: ${entry.startTime}`;
const durationCell = row.createEl('td', { text: entry.duration });
durationCell.title = `Duration: ${entry.duration}`;
});
}
private async sendWorkLogToJira(): Promise<void> {
if (this.selectedPeriodData.length === 0) {
new Notice(t("messages.select_period_first"));
return;
}
try {
await processWorkLogBatch(this.props.plugin, this.selectedPeriodData);
new Notice(t("messages.send_success"));
} catch (error) {
console.error('Error sending work log to Jira:', error);
new Notice(t("messages.send_error", { error: error.message }));
}
}
private formatPeriodLabel(periodKey: string): string {
return t("display.period_of", { date: this.formatPeriodDate(periodKey) });
}
private formatPeriodDate(periodKey: string): string {
const date = new Date(periodKey);
switch (this.props.plugin.settings.statisticsTimeType) {
case 'days':
return date.toLocaleDateString();
case 'weeks':
const weekEnd = new Date(date);
weekEnd.setDate(weekEnd.getDate() + 6);
return `${date.toLocaleDateString()} - ${weekEnd.toLocaleDateString()}`;
case 'months':
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
case 'custom':
return `${this.props.plugin.settings.customDateRange.start} - ${this.props.plugin.settings.customDateRange.end}`;
default:
return date.toLocaleDateString();
}
}
private getPeriodKey(date: Date): string {
switch (this.props.plugin.settings.statisticsTimeType) {
case 'days':
return date.toISOString().split('T')[0];
case 'weeks':
return this.getWeekStartDate(date).toISOString().split('T')[0];
case 'months':
return new Date(date.getFullYear(), date.getMonth(), 1).toISOString().split('T')[0];
case 'custom':
return `${this.props.plugin.settings.customDateRange.start}_${this.props.plugin.settings.customDateRange.end}`;
default:
return date.toISOString().split('T')[0];
}
}
private isDateInRange(date: Date): boolean {
if (this.props.plugin.settings.statisticsTimeType !== 'custom') return true;
if (!this.props.plugin.settings.customDateRange.start || !this.props.plugin.settings.customDateRange.end) return false;
const from = new Date(this.props.plugin.settings.customDateRange.start);
const to = new Date(this.props.plugin.settings.customDateRange.end);
return date >= from && date <= to;
}
private validateCustomRange(): boolean {
if (this.props.plugin.settings.statisticsTimeType !== 'custom') return true;
if (!this.props.plugin.settings.customDateRange.start || !this.props.plugin.settings.customDateRange.end) {
new Notice(t("messages.custom_range_required"));
return false;
}
const from = new Date(this.props.plugin.settings.customDateRange.start);
const to = new Date(this.props.plugin.settings.customDateRange.end);
if (from >= to) {
new Notice(t("messages.invalid_date_range"));
return false;
}
return true;
}
// Utility methods
private getWeekStartDate(date: Date): Date {
const result = new Date(date);
const day = result.getDay();
const diff = result.getDate() - day + (day === 0 ? -6 : 1);
result.setDate(diff);
return new Date(result.setHours(0, 0, 0, 0));
}
private formatDuration(ms: number): string {
const units = [
{ label: "w", ms: 1000 * 60 * 60 * 24 * 7 },
{ label: "d", ms: 1000 * 60 * 60 * 24 },
{ label: "h", ms: 1000 * 60 * 60 },
{ label: "m", ms: 1000 * 60 },
{ label: "s", ms: 1000 }
];
let remainingMs = ms;
const result: string[] = [];
for (const unit of units) {
const value = Math.floor(remainingMs / unit.ms);
if (value > 0) {
result.push(`${value}${unit.label}`);
remainingMs %= unit.ms;
}
}
return result.length > 0 ? result.join(" ") : "0s";
}
private trimOldEntries(groupedEntries: GroupedEntries): void {
if (this.props.plugin.settings.statisticsTimeType === 'custom') return;
const periods = Object.keys(groupedEntries).sort();
while (periods.length > this.props.plugin.settings.maxItemsToShow) {
const oldestPeriod = periods.shift();
if (oldestPeriod) {
delete groupedEntries[oldestPeriod];
}
}
}
private processEntries(
entries: Entry[] | undefined,
fileName: string,
issueKey: string | undefined,
parentPath: string,
groupedEntries: GroupedEntries
): void {
if (!entries || !Array.isArray(entries)) return;
for (const entry of entries) {
const currentPath = parentPath ? `${parentPath} > ${entry.name}` : entry.name;
if (entry.startTime) {
const startTime = new Date(entry.startTime);
// Check if date is in range for custom range
if (!this.isDateInRange(startTime)) continue;
const periodKey = this.getPeriodKey(startTime);
if (!groupedEntries[periodKey]) groupedEntries[periodKey] = [];
let duration: string;
let endTime: Date;
if (entry.endTime) {
endTime = new Date(entry.endTime);
duration = this.formatDuration(endTime.getTime() - startTime.getTime());
} else {
endTime = new Date();
duration = "ongoing";
}
groupedEntries[periodKey].push({
file: fileName.replace(".md", ""),
issueKey: issueKey,
blockPath: currentPath,
startTime: startTime.toLocaleString(),
endTime: entry.endTime ? endTime.toLocaleString() : "ongoing",
duration: duration,
timestamp: startTime.getTime()
});
}
// Process sub-entries recursively
if (entry.subEntries && Array.isArray(entry.subEntries)) {
this.processEntries(entry.subEntries, fileName, issueKey, currentPath, groupedEntries);
}
}
}
private async processFiles(): Promise<void> {
if (!this.validateCustomRange()) return;
const timekeepBlocks: TimekeepBlock[] = [];
const files = this.props.app.vault.getMarkdownFiles().filter(
file => file.path.startsWith(`${this.props.plugin.settings.issuesFolder}/`)
);
await Promise.all(files.map(async file => {
const content = await this.props.app.vault.read(file);
if (!content.includes("```timekeep")) return;
const metadata = this.props.app.metadataCache.getFileCache(file);
const issueKey = metadata?.frontmatter?.key;
const timekeepMatches = content.match(/```timekeep([\s\S]*?)```/g);
timekeepMatches?.forEach(block => {
try {
const jsonContent = block.slice(11, -3).trim();
const data = JSON.parse(jsonContent);
timekeepBlocks.push({
file: file.name,
path: file.path,
issueKey,
data
});
} catch (error) {
console.error(`Error parsing timekeep block in ${file.name}:`, error);
}
});
}));
this.groupedByPeriod = {};
timekeepBlocks.forEach(block => {
this.processEntries(block.data.entries, block.file, block.issueKey, "", this.groupedByPeriod);
});
debugLog("grouped entries", this.groupedByPeriod);
this.trimOldEntries(this.groupedByPeriod);
const sortedPeriods = Object.keys(this.groupedByPeriod).sort().reverse(); // Most recent first
const options = sortedPeriods.map(period => ({
value: period,
label: this.formatPeriodLabel(period)
}));
this.periodSelector?.setOptions(options);
this.periodSelector?.setValue("");
this.sendButton ? this.sendButton.disabled = true : null;
}
hide(): void {
this.containerEl = null;
this.periodSelector = null;
}
}

View file

@ -1,4 +1,4 @@
import {FieldMapping} from "../constants/obsidianJiraFieldsMapping";
import {FieldMapping} from "../default/obsidianJiraFieldsMapping";
export interface JiraSettings {
collapsedSections: Record<string, boolean>;
@ -16,13 +16,20 @@ export interface JiraSettings {
fieldMappings: Record<string, FieldMapping>;
fieldMappingsStrings: Record<string, { toJira: string; fromJira: string }>;
enableFieldValidation: boolean;
statisticsTimeType: string;
maxItemsToShow: number;
customDateRange: { start: string; end: string };
}
export const DEFAULT_SETTINGS: JiraSettings = {
collapsedSections: {
connection: true,
general: true,
fieldMappings: false
fieldMappings: false,
rawIssueViewer: false,
testFieldMappings: false,
statistics: false,
},
authMethod: "bearer",
apiToken: "",
@ -36,6 +43,10 @@ export const DEFAULT_SETTINGS: JiraSettings = {
templatePath: "",
fieldMappings: {},
fieldMappingsStrings: {},
enableFieldValidation: true
enableFieldValidation: true,
statisticsTimeType: "weeks",
maxItemsToShow: 10,
customDateRange: { start: "", end: "" }
};

View file

@ -2,8 +2,8 @@ import { Notice } from "obsidian";
import { JiraIssue } from "../interfaces";
import { parse } from "acorn";
import {debugLog} from "./debugLogging";
import {FieldMapping} from "../constants/obsidianJiraFieldsMapping";
import {defaultIssue} from "../constants/defaultIssue";
import {FieldMapping} from "../default/obsidianJiraFieldsMapping";
import {defaultIssue} from "../default/defaultIssue";
// Constants for validation and error messages
const FORBIDDEN_PATTERNS = ["document", "window", "eval", "Function", "fetch", "setTimeout"]; // Prevent unsafe code execution
@ -26,7 +26,7 @@ export function validateFunctionStringBrowser(fnString: string, approved_vars: s
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}`);
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

View file

@ -3,7 +3,7 @@ import {jiraToMarkdown} from "./markdown_html";
import {Notice, TFile} from "obsidian";
import JiraPlugin from "../main";
import {extractAllJiraSyncValuesFromContent, updateJiraSyncContent} from "./sectionTools";
import {FieldMapping, obsidianJiraFieldMappings} from "../constants/obsidianJiraFieldsMapping";
import {FieldMapping, obsidianJiraFieldMappings} from "../default/obsidianJiraFieldsMapping";
export function localToJiraFields(

View file

@ -1,2 +0,0 @@
// I want to create something like template choice from Kanban plugin here,
// but it's GNU licensed... Probably will implement something similar later

View file

@ -18,11 +18,49 @@
padding-left: 20px;
}
.jira-test-mappings-container {
margin-top: 2em;
padding: 1em 0;
/* Template Selector Styles */
.suggest-select-container {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.suggest-select-container .icon-open {
position: absolute;
bottom: 1em;
right: 1em;
}
.suggest-search-input {
padding: 6px 12px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background: var(--background-primary);
color: var(--text-normal);
font-size: var(--font-ui-small);
}
.suggest-search-input:focus {
outline: none;
border-color: var(--interactive-accent);
}
.suggest-select-item {
max-width: 400px;
width: 100%;
align-self: flex-end;
}
.mod-warning {
color: var(--text-warning);
font-style: italic;
margin-top: 4px;
}
.test-mappings-header {
margin-bottom: 0.2em;
}
@ -51,11 +89,12 @@
}
.value-display {
background: var(--background-primary-alt);
background: var(--background-secondary);
border-radius: 4px;
padding: 0.5em 0.8em;
font-family: var(--font-monospace);
/*font-family: var(--font-monospace);*/
font-size: 0.98em;
min-height: 2em;
margin-top: 0.2em;
white-space: pre-wrap;
word-break: break-all;
@ -69,34 +108,17 @@
.test-mapping-item .from-jira-input {
width: 100%;
min-height: 2.2em;
font-family: var(--font-monospace);
/*font-family: var(--font-monospace);*/
font-size: 1em;
margin-top: 0.2em;
border-radius: 4px;
border: 1px solid var(--background-modifier-border);
background: var(--background-primary-alt);
background: var(--background-secondary);
color: var(--text-normal);
padding: 0.3em 0.5em;
resize: vertical;
}
.remove-field-btn {
position: absolute;
top: 0.5em;
right: 0.7em;
background: var(--background-modifier-error);
color: var(--text-on-accent);
border: none;
border-radius: 4px;
font-size: 1.1em;
padding: 0.1em 0.5em;
cursor: pointer;
transition: background 0.2s;
}
.remove-field-btn:hover {
background: var(--background-modifier-error-hover);
}
.add-test-mapping-btn {
display: block;
margin: 1.5em auto 0 auto;
@ -109,26 +131,31 @@
cursor: pointer;
transition: background 0.2s;
}
.add-test-mapping-btn:hover {
background: var(--interactive-accent-hover);
}
.jira-field-mappings {
margin-top: 10px;
}
.field-mappings-list {
margin: 10px 0;
margin: 20px 0;
display: flex;
flex-direction: column;
gap: 16px;
background: var(--background-primary-alt);
padding: 20px;
border-radius: 8px;
border: 1px solid var(--background-modifier-border);
}
.field-mapping-item {
display: grid;
grid-template-columns: 1fr 2fr 2fr auto;
gap: 8px;
margin-bottom: 8px;
padding: 8px;
background: var(--background-primary);
border: 1px solid var(--background-modifier-border);
align-items: center;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: relative;
transition: all 0.2s ease;
}
.field-mapping-item:hover {
border-color: var(--interactive-accent);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.work-description-container textarea {
@ -136,63 +163,111 @@
}
.field-name-container {
background-color: var(--background-primary);
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 2px solid var(--background-modifier-border);
}
.field-mapping-label {
font-weight: 500;
font-weight: 600;
color: var(--text-normal);
font-size: 0.9em;
/*text-transform: uppercase;*/
letter-spacing: 0.5px;
display: block;
margin-bottom: 8px;
}
.field-name-input, .to-jira-input, .from-jira-input {
.field-name-input {
width: 100%;
border: 1px solid var(--background-modifier-border);
padding: 10px 12px;
border: 2px solid var(--background-modifier-border);
border-radius: 6px;
background: var(--background-secondary);
font-size: 1em;
font-weight: 500;
transition: border-color 0.2s ease;
}
.field-name-input:focus {
border-color: var(--interactive-accent);
outline: none;
box-shadow: 0 0 0 3px rgba(var(--interactive-accent-rgb), 0.1);
}
.functions-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 16px;
}
.to-jira-container,
.from-jira-container {
background: var(--background-secondary-alt);
padding: 16px;
border-radius: 6px;
border: 1px solid var(--background-modifier-border-hover);
}
.test-mapping-item .from-jira-container {
background: var(--transparent);
padding: 0;
border: 0;
}
.field-mapping-item .invalid,
.setting-item.invalid input {
background-color: rgba(var(--color-red-rgb), 0.2);
.to-jira-input,
.from-jira-input {
width: 100%;
min-height: 40px;
padding: 12px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background: var(--background-primary);
font-family: var(--font-monospace);
font-size: 0.9em;
line-height: 1.4;
resize: vertical;
transition: border-color 0.2s ease;
}
.to-jira-input:focus,
.from-jira-input:focus {
border-color: var(--interactive-accent);
outline: none;
box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.1);
}
.field-mapping-item .invalid {
border-color: var(--color-red) !important;
background-color: rgba(var(--color-red-rgb), 0.1) !important;
box-shadow: 0 0 0 2px rgba(var(--color-red-rgb), 0.2) !important;
}
.remove-field-btn {
background-color: var(--background-modifier-error);
position: absolute;
top: 8px;
right: 16px;
background: var(--background-modifier-error);
color: var(--text-on-accent);
border: none;
border-radius: 50%;
width: 28px;
height: 28px;
font-size: 14px;
cursor: pointer;
font-weight: bold;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.add-field-mapping-btn {
padding: 6px 12px;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border-radius: 4px;
cursor: pointer;
}
.mapping-examples {
margin-top: 20px;
padding: 10px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
}
.mapping-examples ul {
padding-left: 20px;
}
.mapping-examples li {
margin-bottom: 10px;
}
.use-example-btn {
margin-left: 8px;
padding: 2px 6px;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
.remove-field-btn:hover {
background: var(--background-modifier-error-hover);
transform: scale(1.1);
}
.button-view {
@ -207,6 +282,121 @@
width: fit-content;
}
.button-view {
display: flex;
justify-content: flex-start;
gap: 12px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--background-modifier-border);
}
.button-view button {
padding: 10px 16px;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
position: relative;
}
.add-field-mapping-btn {
background: var(--interactive-accent);
color: var(--text-on-accent);
}
.add-field-mapping-btn:hover {
background: var(--interactive-accent-hover);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.reset-field-mappings-btn,
.reload-field-mappings-btn {
background: var(--background-secondary);
color: var(--text-normal);
border: 1px solid var(--background-modifier-border);
}
.reset-field-mappings-btn:hover,
.reload-field-mappings-btn:hover {
background: var(--background-secondary-alt);
border-color: var(--interactive-accent);
transform: translateY(-1px);
}
.button-view button::after {
content: attr(data-tooltip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--background-tooltip);
color: var(--text-on-accent);
padding: 6px 10px;
border-radius: 4px;
font-size: 0.8em;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
margin-bottom: 8px;
}
.button-view button:hover::after {
opacity: 1;
}
.mapping-examples {
margin-top: 24px;
padding: 20px;
background: var(--background-secondary-alt);
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
}
.mapping-examples h4 {
color: var(--text-normal);
margin-bottom: 16px;
font-size: 1.1em;
}
.mapping-examples ul {
list-style: none;
padding: 0;
display: grid;
gap: 12px;
}
.mapping-examples li {
background: var(--background-primary);
padding: 16px;
border-radius: 6px;
border: 1px solid var(--background-modifier-border-hover);
position: relative;
}
.use-example-btn {
position: absolute;
top: 12px;
right: 12px;
padding: 6px 12px;
background: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: 4px;
font-size: 0.8em;
cursor: pointer;
transition: background 0.2s ease;
}
.use-example-btn:hover {
background: var(--interactive-accent-hover);
}
/*hljs.css*/
pre code.hljs {
@ -331,3 +521,236 @@ code.hljs {
/* purposely ignored */
}
/* Period selector improvements */
.period-selector .setting-item-control {
flex-direction: column;
align-items: flex-end;
gap: 0.75rem;
}
.period-selector .timekeep-btn {
background: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: 6px;
padding: 0.5rem 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.period-selector .timekeep-btn:hover:not(:disabled) {
background: var(--interactive-accent-hover);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
}
.period-selector .timekeep-btn:disabled {
background: var(--background-modifier-border);
color: var(--text-muted);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Custom dates container */
.custom-dates-container {
margin-top: 1rem;
padding: 1.25rem;
background: var(--background-primary-alt);
border-radius: 8px;
border: 1px solid var(--background-modifier-border-hover);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
/* Main display container */
.all-periods-display {
margin-top: 1.5rem;
user-select: text;
-webkit-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
}
.timekeep-section-title {
color: var(--text-normal);
margin-bottom: 1.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--interactive-accent);
font-size: 1.1em;
font-weight: 600;
}
/* Period sections */
.period-section {
margin-bottom: 2rem;
background: var(--background-primary);
border-radius: 8px;
border: 1px solid var(--background-modifier-border);
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.timekeep-period-title {
background: var(--background-secondary);
padding: 0.875rem 1.25rem;
margin: 0;
color: var(--text-normal);
font-weight: 600;
font-size: 0.95em;
border-bottom: 1px solid var(--background-modifier-border);
letter-spacing: 0.025em;
}
/* Table container and styling */
.timekeep-table-container {
overflow-x: auto;
background: var(--background-primary);
}
.timekeep-entries-table {
width: 100%;
border-collapse: collapse;
font-size: 0.875em;
}
.timekeep-entries-table th {
background: var(--background-secondary-alt);
color: var(--text-normal);
font-weight: 600;
text-align: left;
padding: 0.75rem 1rem;
border-bottom: 2px solid var(--background-modifier-border);
font-size: 0.8em;
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
}
.timekeep-entries-table td {
padding: 0.875rem 1rem;
border-bottom: 1px solid var(--background-modifier-border-hover);
vertical-align: top;
line-height: 1.4;
}
.timekeep-entries-table tbody tr {
transition: background-color 0.2s ease;
}
.timekeep-entries-table tbody tr:hover {
background: var(--background-secondary-alt);
}
.timekeep-table-row-alt {
background: var(--background-primary-alt);
}
.timekeep-table-row-alt:hover {
background: var(--background-secondary-alt);
}
/* Column-specific styling */
.timekeep-entries-table td:nth-child(1) {
font-weight: 500;
color: var(--text-normal);
}
.timekeep-entries-table td:nth-child(2) {
font-family: var(--font-monospace);
font-size: 0.85em;
color: var(--interactive-accent);
font-weight: 500;
}
.timekeep-entries-table td:nth-child(3) {
color: var(--text-muted);
font-size: 0.85em;
max-width: 200px;
word-wrap: break-word;
hyphens: auto;
}
.timekeep-entries-table td:nth-child(4) {
font-family: var(--font-monospace);
font-size: 0.8em;
color: var(--text-muted);
white-space: nowrap;
}
.timekeep-entries-table td:nth-child(5) {
font-family: var(--font-monospace);
font-weight: 600;
color: var(--text-accent);
white-space: nowrap;
}
/* No data state */
.timekeep-no-data {
text-align: center;
padding: 3rem 1rem;
color: var(--text-muted);
font-style: italic;
background: var(--background-secondary-alt);
border-radius: 8px;
border: 1px dashed var(--background-modifier-border);
margin-top: 1.5rem;
}
/* Responsive design */
@media (max-width: 768px) {
.functions-container {
grid-template-columns: 1fr;
gap: 12px;
}
.button-view {
flex-direction: column;
align-items: stretch;
}
.button-view button {
justify-content: center;
}
.timekeep-entries-table {
font-size: 0.8em;
}
.timekeep-entries-table td {
padding: 0.625rem 0.75rem;
}
.timekeep-entries-table th {
padding: 0.625rem 0.75rem;
font-size: 0.75em;
}
.timekeep-entries-table td:nth-child(3) {
max-width: 150px;
}
}
/* Loading and transition states */
.timekeep-loading {
opacity: 0.6;
pointer-events: none;
transition: opacity 0.3s ease;
}
.timekeep-fade-in {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View file

@ -6,6 +6,8 @@
"module": "node16",
"target": "ES2018",
"allowJs": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"noImplicitAny": true,
"moduleResolution": "node16",
"importHelpers": true,

776
yarn.lock
View file

@ -2,83 +2,131 @@
# yarn lockfile v1
"@codemirror/state@^6.0.0", "@codemirror/state@^6.5.0":
version "6.5.2"
resolved "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz"
integrity sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==
dependencies:
"@marijn/find-cluster-break" "^1.0.0"
"@esbuild/aix-ppc64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz#499600c5e1757a524990d5d92601f0ac3ce87f64"
integrity sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==
"@codemirror/view@^6.0.0":
version "6.36.4"
resolved "https://registry.npmjs.org/@codemirror/view/-/view-6.36.4.tgz"
integrity sha512-ZQ0V5ovw/miKEXTvjgzRyjnrk9TwriUB1k4R5p7uNnHR9Hus+D1SXHGdJshijEzPFjU25xea/7nhIeSqYFKdbA==
dependencies:
"@codemirror/state" "^6.5.0"
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
"@esbuild/android-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz#b9b8231561a1dfb94eb31f4ee056b92a985c324f"
integrity sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==
"@esbuild/android-arm@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.0.tgz#ca6e7888942505f13e88ac9f5f7d2a72f9facd2b"
integrity sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==
"@esbuild/android-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.0.tgz#e765ea753bac442dfc9cb53652ce8bd39d33e163"
integrity sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==
"@esbuild/darwin-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz#fa394164b0d89d4fdc3a8a21989af70ef579fa2c"
integrity sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==
"@esbuild/darwin-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz#91979d98d30ba6e7d69b22c617cc82bdad60e47a"
integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==
"@esbuild/freebsd-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz#b97e97073310736b430a07b099d837084b85e9ce"
integrity sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==
"@esbuild/freebsd-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz#f3b694d0da61d9910ec7deff794d444cfbf3b6e7"
integrity sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==
"@esbuild/linux-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz#f921f699f162f332036d5657cad9036f7a993f73"
integrity sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==
"@esbuild/linux-arm@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz#cc49305b3c6da317c900688995a4050e6cc91ca3"
integrity sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==
"@esbuild/linux-ia32@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz#3e0736fcfab16cff042dec806247e2c76e109e19"
integrity sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==
"@esbuild/linux-loong64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz#ea2bf730883cddb9dfb85124232b5a875b8020c7"
integrity sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==
"@esbuild/linux-mips64el@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz#4cababb14eede09248980a2d2d8b966464294ff1"
integrity sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==
"@esbuild/linux-ppc64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz#8860a4609914c065373a77242e985179658e1951"
integrity sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==
"@esbuild/linux-riscv64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz#baf26e20bb2d38cfb86ee282dff840c04f4ed987"
integrity sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==
"@esbuild/linux-s390x@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz#8323afc0d6cb1b6dc6e9fd21efd9e1542c3640a4"
integrity sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==
"@esbuild/linux-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz#08fcf60cb400ed2382e9f8e0f5590bac8810469a"
integrity sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==
"@esbuild/netbsd-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz#935c6c74e20f7224918fbe2e6c6fe865b6c6ea5b"
integrity sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==
"@esbuild/netbsd-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz#414677cef66d16c5a4d210751eb2881bb9c1b62b"
integrity sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==
"@esbuild/openbsd-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz#8fd55a4d08d25cdc572844f13c88d678c84d13f7"
integrity sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==
"@esbuild/openbsd-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz#0c48ddb1494bbc2d6bcbaa1429a7f465fa1dedde"
integrity sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==
"@esbuild/sunos-x64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz#86ff9075d77962b60dd26203d7352f92684c8c92"
integrity sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==
"@esbuild/win32-arm64@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz#849c62327c3229467f5b5cd681bf50588442e96c"
integrity sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==
"@esbuild/win32-ia32@0.25.0":
version "0.25.0"
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz#f62eb480cd7cca088cb65bb46a6db25b725dc079"
integrity sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==
"@esbuild/win32-x64@0.25.0":
version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz"
integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==
"@eslint-community/eslint-utils@^4.2.0":
version "4.5.1"
resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz"
integrity sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==
dependencies:
eslint-visitor-keys "^3.4.3"
"@eslint-community/regexpp@^4.6.1":
version "4.12.1"
resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz"
integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
"@eslint/eslintrc@^2.1.4":
version "2.1.4"
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz"
integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
espree "^9.6.0"
globals "^13.19.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.0"
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@8.57.1":
version "8.57.1"
resolved "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz"
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
"@humanwhocodes/config-array@^0.13.0":
version "0.13.0"
resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz"
integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==
dependencies:
"@humanwhocodes/object-schema" "^2.0.3"
debug "^4.3.1"
minimatch "^3.0.5"
"@humanwhocodes/module-importer@^1.0.1":
version "1.0.1"
resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz"
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
"@humanwhocodes/object-schema@^2.0.3":
version "2.0.3"
resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@marijn/find-cluster-break@^1.0.0":
version "1.0.2"
resolved "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz"
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@ -87,12 +135,12 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
@ -149,7 +197,7 @@
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/parser@^5.0.0", "@typescript-eslint/parser@5.29.0":
"@typescript-eslint/parser@5.29.0":
version "5.29.0"
resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.29.0.tgz"
integrity sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==
@ -214,66 +262,16 @@
"@typescript-eslint/types" "5.29.0"
eslint-visitor-keys "^3.3.0"
"@ungap/structured-clone@^1.2.0":
version "1.3.0"
resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz"
integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.14.1, acorn@^8.9.0:
acorn@^8.14.1:
version "8.14.1"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz"
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-styles@^4.1.0:
version "4.3.0"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz"
@ -286,57 +284,20 @@ builtin-modules@3.3.0:
resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz"
integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
chalk@^4.0.0:
version "4.1.2"
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
chokidar@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
readdirp "^4.0.1"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz"
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
cross-spawn@^7.0.2:
version "7.0.6"
resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
debug@^4.3.4:
version "4.3.7"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz"
integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==
dependencies:
ms "^2.1.3"
deep-is@^0.1.3:
version "0.1.4"
resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz"
@ -344,13 +305,6 @@ dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
doctrine@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz"
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
dependencies:
esutils "^2.0.2"
esbuild@0.25.0:
version "0.25.0"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz"
@ -382,11 +336,6 @@ esbuild@0.25.0:
"@esbuild/win32-ia32" "0.25.0"
"@esbuild/win32-x64" "0.25.0"
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz"
@ -395,14 +344,6 @@ eslint-scope@^5.1.1:
esrecurse "^4.3.0"
estraverse "^4.1.1"
eslint-scope@^7.2.2:
version "7.2.2"
resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz"
integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
dependencies:
esrecurse "^4.3.0"
estraverse "^5.2.0"
eslint-utils@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz"
@ -420,76 +361,6 @@ eslint-visitor-keys@^3.3.0:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint-visitor-keys@^3.4.1:
version "3.4.3"
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint-visitor-keys@^3.4.3:
version "3.4.3"
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint@*, "eslint@^6.0.0 || ^7.0.0 || ^8.0.0", "eslint@^6.0.0 || ^7.0.0 || >=8.0.0", eslint@>=5:
version "8.57.1"
resolved "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz"
integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.6.1"
"@eslint/eslintrc" "^2.1.4"
"@eslint/js" "8.57.1"
"@humanwhocodes/config-array" "^0.13.0"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
"@ungap/structured-clone" "^1.2.0"
ajv "^6.12.4"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
eslint-scope "^7.2.2"
eslint-visitor-keys "^3.4.3"
espree "^9.6.1"
esquery "^1.4.2"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
find-up "^5.0.0"
glob-parent "^6.0.2"
globals "^13.19.0"
graphemer "^1.4.0"
ignore "^5.2.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
is-path-inside "^3.0.3"
js-yaml "^4.1.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash.merge "^4.6.2"
minimatch "^3.1.2"
natural-compare "^1.4.0"
optionator "^0.9.3"
strip-ansi "^6.0.1"
text-table "^0.2.0"
espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz"
integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
dependencies:
acorn "^8.9.0"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.4.1"
esquery@^1.4.2:
version "1.6.0"
resolved "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz"
integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
dependencies:
estraverse "^5.1.0"
esrecurse@^4.3.0:
version "4.3.0"
resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz"
@ -502,26 +373,11 @@ estraverse@^4.1.1:
resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.1.0:
version "5.3.0"
resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estraverse@^5.2.0:
version "5.3.0"
resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.2.9:
version "3.3.2"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz"
@ -533,16 +389,6 @@ fast-glob@^3.2.9:
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
fast-levenshtein@^2.0.6:
version "2.0.6"
resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
fastq@^1.6.0:
version "1.17.1"
resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz"
@ -550,13 +396,6 @@ fastq@^1.6.0:
dependencies:
reusify "^1.0.4"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz"
integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
dependencies:
flat-cache "^3.0.4"
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz"
@ -564,32 +403,14 @@ fill-range@^7.1.1:
dependencies:
to-regex-range "^5.0.1"
find-up@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz"
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
fs-extra@^11.3.0:
version "11.3.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d"
integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==
dependencies:
locate-path "^6.0.0"
path-exists "^4.0.0"
flat-cache@^3.0.4:
version "3.2.0"
resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz"
integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==
dependencies:
flatted "^3.2.9"
keyv "^4.5.3"
rimraf "^3.0.2"
flatted@^3.2.9:
version "3.3.3"
resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz"
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
functional-red-black-tree@^1.0.1:
version "1.0.1"
@ -603,32 +424,6 @@ glob-parent@^5.1.2:
dependencies:
is-glob "^4.0.1"
glob-parent@^6.0.2:
version "6.0.2"
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz"
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
dependencies:
is-glob "^4.0.3"
glob@^7.1.3:
version "7.2.3"
resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz"
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.1.1"
once "^1.3.0"
path-is-absolute "^1.0.0"
globals@^13.19.0:
version "13.24.0"
resolved "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz"
integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==
dependencies:
type-fest "^0.20.2"
globby@^11.1.0:
version "11.1.0"
resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz"
@ -641,15 +436,10 @@ globby@^11.1.0:
merge2 "^1.4.1"
slash "^3.0.0"
graphemer@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
highlight.js@^11.11.1:
version "11.11.1"
@ -661,38 +451,12 @@ ignore@^5.2.0:
resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
import-fresh@^3.2.1:
version "3.3.1"
resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz"
integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==
dependencies:
parent-module "^1.0.0"
resolve-from "^4.0.0"
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"
integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz"
integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.4"
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3:
is-glob@^4.0.1, is-glob@^4.0.3:
version "4.0.3"
resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
@ -704,64 +468,14 @@ is-number@^7.0.0:
resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-path-inside@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
js-yaml@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz"
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==
dependencies:
argparse "^2.0.1"
json-buffer@3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz"
integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
keyv@^4.5.3:
version "4.5.4"
resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz"
integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
dependencies:
json-buffer "3.0.1"
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz"
integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
dependencies:
prelude-ls "^1.2.1"
type-check "~0.4.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz"
integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
dependencies:
p-locate "^5.0.0"
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
lodash@^4.17.21:
version "4.17.21"
@ -781,13 +495,6 @@ micromatch@^4.0.4:
braces "^3.0.3"
picomatch "^2.3.1"
minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
moment@2.29.4:
version "2.29.4"
resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz"
@ -798,11 +505,6 @@ ms@^2.1.3:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
obsidian@latest:
version "1.8.7"
resolved "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz"
@ -811,61 +513,6 @@ obsidian@latest:
"@types/codemirror" "5.60.8"
moment "2.29.4"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
dependencies:
wrappy "1"
optionator@^0.9.3:
version "0.9.4"
resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz"
integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==
dependencies:
deep-is "^0.1.3"
fast-levenshtein "^2.0.6"
levn "^0.4.1"
prelude-ls "^1.2.1"
type-check "^0.4.0"
word-wrap "^1.2.5"
p-limit@^3.0.2:
version "3.1.0"
resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
dependencies:
yocto-queue "^0.1.0"
p-locate@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz"
integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
dependencies:
p-limit "^3.0.2"
parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz"
integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
dependencies:
callsites "^3.0.0"
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz"
integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==
path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
@ -876,43 +523,26 @@ picomatch@^2.3.1:
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
punycode@^2.1.0:
version "2.3.1"
resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz"
integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
readdirp@^4.0.1:
version "4.1.2"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
regexpp@^3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
resolve-from@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rimraf@^3.0.2:
version "3.0.2"
resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
dependencies:
glob "^7.1.3"
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
@ -925,52 +555,11 @@ semver@^7.3.7:
resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz"
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
dependencies:
shebang-regex "^3.0.0"
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-mod@^4.1.0:
version "4.1.2"
resolved "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz"
integrity sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==
supports-color@^7.1.0:
version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"
integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
dependencies:
has-flag "^4.0.0"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
@ -978,16 +567,16 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@2.4.0:
version "2.4.0"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
tslib@^1.8.1:
version "1.14.1"
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz"
@ -995,58 +584,17 @@ tsutils@^3.21.0:
dependencies:
tslib "^1.8.1"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
dependencies:
prelude-ls "^1.2.1"
type-fest@^0.20.2:
version "0.20.2"
resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
"typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta", typescript@4.7.4:
typescript@4.7.4:
version "4.7.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz"
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
dependencies:
punycode "^2.1.0"
w3c-keyname@^2.2.4:
version "2.2.8"
resolved "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz"
integrity sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==
which@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
word-wrap@^1.2.5:
version "1.2.5"
resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
wrappy@1:
version "1.0.2"
resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz"
integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
yaml@^2.7.0:
version "2.7.0"
resolved "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz"
integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==