- `Super Simple Time Tracker` plugin format support
- Support of multiple Jira connections.
- `AGENTS.md` file from Obidian plugin template and filled with info about project
- Updated eslint configs to `eslint.config.js` format + linted all the code; added prettier with according config
- localization validator improvement; multiple localization fixes
- many minor fixes
This commit is contained in:
Alamoin 2026-04-29 17:41:24 +03:00
parent e603b2545d
commit 0786db1535
No known key found for this signature in database
GPG key ID: 0DBEAC95BA38C9EF
100 changed files with 3479 additions and 6038 deletions

View file

@ -1,3 +0,0 @@
node_modules/
main.js

View file

@ -1,23 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

View file

@ -3,7 +3,7 @@ name: Release Obsidian plugin
on:
push:
tags:
- "*"
- '*'
jobs:
build:
@ -15,7 +15,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
node-version: '18.x'
- name: Build plugin
run: |

4
.gitignore vendored
View file

@ -7,11 +7,15 @@
# npm
node_modules
yarn.lock
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
*.js
*.cjs
!eslint.config.js
!/src/localization/**/*.js
/src/localization/translator.js
!check-version.js
# Exclude sourcemaps

View file

@ -1 +1,5 @@
node check-version.js
yarn run format
yarn run lint:fix
yarn run build
yarn run validate_locale_once

7
.prettierrc Normal file
View file

@ -0,0 +1,7 @@
{
"tabWidth": 4,
"useTabs": true,
"singleQuote": true,
"printWidth": 120,
"semi": true
}

1
.yarnrc.yml Normal file
View file

@ -0,0 +1 @@
nodeLinker: node-modules

265
AGENTS.md Normal file
View file

@ -0,0 +1,265 @@
# Obsidian Jira Sync - Community plugin for Obsidian
## Project overview
- **Plugin ID**: `jira-sync`
- **Name**: Jira Issue Manager
- **Target**: Obsidian Community Plugin (TypeScript → bundled JavaScript)
- **Entry point**: `main.ts` compiled to `main.js` and loaded by Obsidian
- **Required release artifacts**: `main.js`, `manifest.json`, and optional `styles.css`
- **Min App Version**: 1.10.1
- **Author**: Alamion
- **Mobile compatible**: Yes (`isDesktopOnly: false`)
## Environment & tooling
- Node.js: use current LTS (Node 18+ recommended)
- **Package manager: yarn** (required - `package.json` defines yarn scripts)
- **Bundler: esbuild** (required - `esbuild.config.mjs` and build scripts depend on it)
- Types: `obsidian` type definitions
### Install
```bash
yarn install
```
### Dev (watch)
```bash
yarn run dev
```
### Production build
```bash
yarn run build
```
### Localization
```bash
yarn run validate_locale_once # Validate YAML source files
yarn run compile_locale_once # Compile YAML to JSON
```
## Linting
All three commands must pass without errors:
1. `yarn run format` - No Prettier errors
2. `yarn run lint:fix` - No ESLint errors
3. `yarn run build` - No TypeScript/build errors
4. `yarn run validate_locale_once` - No localization errors
## File & folder conventions
- **Source lives in `src/`**. Keep `main.ts` small and focused on plugin lifecycle.
- **Do not commit build artifacts**: Never commit `node_modules/`, `main.js`, or other generated files.
- Generated output should be placed at the plugin root. Release artifacts must end up at the top level of the plugin folder in the vault.
### Directory structure (`src/`)
| Directory | Purpose | Key Files |
| ------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `commands/` | User commands | `getIssue.ts`, `createIssue.ts`, `updateIssue.ts`, `updateStatus.ts`, `addWorkLogBatch.ts`, `addWorkLogManually.ts`, `batchFetchIssues.ts` |
| `api/` | Jira REST API | `base.ts` (core HTTP), `auth.ts`, `issues.ts`, `projects.ts`, `self.ts` |
| `settings/` | Settings tab & components | `default.ts` (defaults), `JiraSettingTab.ts`, `components/*` |
| `modals/` | UI dialogs | `IssueSearchModal.ts`, `JQLSearchModal.ts`, `IssueWorkLogModal.ts`, `ProjectModal.ts`, `IssueTypeModal.ts`, `IssueStatusModal.ts` |
| `file_operations/` | File read/write | `getIssue.ts`, `createUpdateIssue.ts`, `commonPrepareData.ts` |
| `tools/` | Utilities | `debugLogging.ts`, `cacheUtils.ts`, `filesUtils.ts`, `convertFunctionString.ts`, `sanitizers.ts`, `asyncLimiter.ts` |
| `postprocessing/` | Live Preview/Reading | `livePreview.ts`, `reading.ts` |
| `default/` | Defaults | `defaultTemplate.ts`, `defaultIssue.ts`, `obsidianJiraFieldsMapping.ts` |
| `interfaces/` | TypeScript types | `settingsTypes.ts`, `index.ts` |
| `localization/` | i18n | `translator.ts`, `compiled/*.json`, `source/*.yaml` |
## Packages used
| Package | Purpose | Usage |
| -------------- | ----------------- | ------------------------------------------------------ |
| `obsidian` | Obsidian API | Core plugin, `requestUrl`, `TFile`, `Plugin` etc. |
| `esbuild` | Bundler | `esbuild.config.mjs` - bundles all deps into `main.js` |
| `typescript` | Type safety | TypeScript 4.7.4 |
| `fs-extra` | File operations | `src/tools/filesUtils.ts` |
| `yaml` | YAML parsing | Settings validation, field mapping config |
| `highlight.js` | Code highlighting | `src/tools/markdownHtml.ts` |
| `chokidar` | File watching | Localization compiler watch mode |
## Logging
- Location: `src/tools/debugLogging.ts:1-19`
- **IMPORTANT BUG**: Uses `process.env.NODE_ENV` which is always `undefined` in Obsidian (no Node.js runtime) - logging is **always on** in both dev and production
- Exports: `debugLog`, `debugWarn`, `debugError`
- Used in: `src/api/base.ts` for API request/response logging
- Prefix format: `[DEBUG]`, `[DEBUG WARNING]`, `[DEBUG ERROR]`
```typescript
import { debugLog, debugWarn, debugError } from './tools/debugLogging';
debugLog('Request:', { url, method });
debugWarn('Missing field:', fieldName);
debugError('Failed to fetch:', error);
```
## Cache system
- Maps Jira issue keys to local file paths
- In-memory cache: `issueKeyToFilePathCache` (Map in plugin instance)
- Persisted in settings: `settings.issueKeyToFilePathCache`
- Vault event listeners for maintenance (`main.ts:105-130`):
- `rename`: updates cache when file is moved
- `delete`: removes from cache when file is deleted
## Localization (i18n)
- Source files: `src/localization/source/{lang}/*.yaml`
- Compiled to: `src/localization/compiled/{lang}.json`
- Auto-detects locale from `window.localStorage.getItem('language')`
- Falls back to `en` if not found
- Build-time compilation: `src/localization/compiler.js`
## Pre-commit hooks
- Uses husky (`.husky/pre-commit`)
- Current check: `node check-version.js`
- `version-bump.mjs` - syncs version from `package.json` to `manifest.json` and `versions.json`
```bash
# Runs on every commit:
node check-version.js
```
- **Note**: Future pre-commit upgrades may include linting and type checking
## Manifest rules
- Must include:
- `id` (stable - never change after release)
- `name`
- `version` (Semantic Versioning x.y.z)
- `minAppVersion`
- `description`
- `isDesktopOnly` (boolean)
- Keep `minAppVersion` accurate when using newer APIs
## Testing
- Manual install for testing: copy `main.js`, `manifest.json`, `styles.css` to:
```
<Vault>/.obsidian/plugins/<plugin-id>/
```
- Reload Obsidian and enable in **Settings → Community plugins**
## Commands & settings
- Commands added via `this.addCommand(...)` in `src/commands/`
- Settings persisted with `this.loadData()` / `this.saveData()`
- Use stable command IDs - avoid renaming after release
- All commands wrapped in `register*Command` functions
## Versioning & releases
- Bump version in `manifest.json` (SemVer) and update `versions.json`
- Run `yarn version` to auto-bump via `version-bump.mjs`
- Create GitHub release with tag matching `manifest.json` version (no leading `v`)
- Attach: `manifest.json`, `main.js`, `styles.css` (if present)
## Security, privacy, and compliance
- Default to local/offline operation. Only make network requests when essential.
- No hidden telemetry.
- Never execute remote code, fetch and eval scripts.
- Minimize scope: read/write only what's necessary inside the vault.
- Use `register*` helpers for all listeners to ensure proper cleanup on unload.
## Performance
- Keep startup light - defer heavy work until needed
- Batch disk access, avoid excessive vault scans
- Debounce/throttle operations in response to file system events
## Coding conventions
- TypeScript with `"strict": true` preferred
- Keep `main.ts` minimal - delegate feature logic to separate modules
- Split large files (~200-300+ lines) into smaller focused modules
- Bundle everything into `main.js` (no unbundled runtime deps)
- Prefer `async/await` over promise chains
- Handle errors gracefully
- **Do not add comments unless explicitly requested**
## Agent do/don't
**Do**
- Add commands with stable IDs (don't rename after release)
- Provide defaults and validation in settings
- Write idempotent code paths
- Use `this.register*` helpers for cleanup
**Don't**
- Introduce network calls without user-facing reason and documentation
- Ship features requiring cloud services without opt-in
- Store or transmit vault contents unless essential
## Common tasks
### Add a command
```typescript
export function registerMyCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: 'my-command',
name: t('name'),
checkCallback: (checking: boolean) => {
const valid = validateSettings(plugin);
if (!checking && valid) doSomething(plugin);
return valid;
},
});
}
```
### Make API request
```typescript
import { baseRequest } from './api/base';
const issue = await baseRequest(plugin, 'GET', `/issue/${issueKey}`);
```
### Persist settings
```typescript
await this.saveData(this.settings);
```
### Register listeners safely
```typescript
this.registerEvent(
this.app.workspace.on('file-open', (f) => {
/* ... */
}),
);
this.registerDomEvent(window, 'resize', () => {
/* ... */
});
this.registerInterval(
window.setInterval(() => {
/* ... */
}, 1000),
);
```
## Troubleshooting
- Plugin doesn't load: ensure `main.js` and `manifest.json` at top level of plugin folder
- Build issues: run `yarn run dev` or `yarn run build` to compile
- Commands not appearing: verify `addCommand` runs after `onload` and IDs are unique
- Settings not persisting: ensure `loadData`/`saveData` are awaited
## References
- Obsidian plugin docs: https://docs.obsidian.md
- Plugin guidelines: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines
- Style guide: https://help.obsidian.md/style-guide

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Alamion
Copyright (c) 2025-2026 Alamion
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -7,6 +7,7 @@ Originally forked from [obsidian-to-jira](https://github.com/angelperezasenjo/ob
## Why this plugin exists
Other plugins treat Jira issues as read-only reference material. This one lets you:
- **Build full-featured issue templates** that mirror your Jira workflow
- **Sync ANY field**—even custom ones from plugins like ScriptRunner or Insight
- **Create new fields** with dynamic field mapping (e.g. auto-generate browse URLs from `issue.self + issue.key` from API response)
@ -18,7 +19,9 @@ Other plugins treat Jira issues as read-only reference material. This one lets y
## Killer features
### 🧩 Your Jira, your template
Create Obsidian notes that look exactly like your team's Jira workflow. Pull in:
- Standard fields (status, assignee, priority)
- Custom fields (progress bars, sprint IDs, epic links)
- Plugin fields (ScriptRunner outputs, Insight assets)
@ -27,6 +30,7 @@ Create Obsidian notes that look exactly like your team's Jira workflow. Pull in:
### Brief example:
#### Here is how template can look like:
```markdown
---
key: ""
@ -39,14 +43,15 @@ link: "" <!-- Built-in auto-generated link -->
### Customer Impact `jira-sync-section-customfield_10842` <!-- From your CRM plugin -->
### Other
User `jira-sync-inline-start-assignee``jira-sync-inline-end` should be working on this. <!-- Inline indicator, built-in -->
Expected time spent on the task: `jira-sync-line-originalEstimate` <!-- Line indicator, custom -->
```
#### Here is how it will look after syncing:
```markdown
---
key: JIR-1234
@ -58,15 +63,18 @@ link: http://jira.local:8000/browse/JIR-1234
---
### Customer Impact `jira-sync-section-customfield_10842`
The current API structure is too fragmented and requires standardization.
### Other
User `jira-sync-inline-start-assignee`Jack A.M.`jira-sync-inline-end` should be working on this.
Expected time spent on the task: `jira-sync-line-originalEstimate`1w 3d
```
#### Here is how the user will see it most cases (all indicators are hidden from view):
```markdown
---
key: JIR-1234
@ -78,16 +86,20 @@ link: http://jira.local:8000/browse/JIR-1234
---
### Customer Impact
The current API structure is too fragmented and requires standardization.
### Other
User Jack A.M. should be working on this.
Expected time spent on the task: 1w 3d
```
### 🔐 Multiple authentication methods
Choose the authentication that fits your security requirements:
- **Bearer Token (PAT)** - Personal Access Token for secure API access
- **Basic Auth (Username + PAT)** - Username with Personal Access Token
- **Session Cookie (Username + Password)** - Traditional authentication
@ -95,20 +107,25 @@ Choose the authentication that fits your security requirements:
> **Security Note**: When using PAT authentication, ensure `write:jira-work` and `read:jira-work` scopes are enabled.
### 🔄 Two-way sync that doesn't fight you
- **Smart conflict resolution** when notes change locally while syncing
- **Partial updates**—edit just the fields you care about
- **Worklog batching** push a week's worth of time entries at once
- **Status management** update issue status directly from Obsidian
### ⚙️ Field mapping kitchen
We include mappings for:
- Basic fields (like status, priority, assignee)
- Temporal fields (created, updated, due dates)
- Calculated fields (progress %, time estimates, custom formulas)
- **Bring your own** for custom integrations and complex transformations
### 📊 Integrated work log statistics
- **No external plugins required** - everything is built-in
- **Support for multiple formats** - works with Timekeep and Super Simple Time Tracker time tracking formats
- **Dynamic time period selection** - view stats for days, weeks, or months
- **Batch work log submission** - send multiple time entries at once
- **Visual progress tracking** - see your work patterns at a glance
@ -133,6 +150,7 @@ We include mappings for:
## Documentation
For detailed setup, configuration, and advanced usage:
- [English Guide](docs/how_to_en.md)
- [Russian Guide](docs/how_to_ru.md)
- [Template Examples](docs/template_example.md)

View file

@ -1,41 +0,0 @@
import esbuild from "esbuild";
import fs from "fs";
async function build() {
try {
const result = await esbuild.build({
entryPoints: ['docs/statistics.ts'],
bundle: true,
minify: true,
format: 'iife',
globalName: 'exports',
outfile: 'docs/statistics.min.js',
write: false,
target: 'es2018',
});
const minifiedCode = new TextDecoder().decode(result.outputFiles[0].contents);
const dataviewBlock =`---
jira_worklog_batch: '[]'
---
To set folder searching issue files in, redact in one of last lines \`t="Jira Issues"\`
To set number of weeks shown, redact in one of last lines \`e=3\`
\`\`\`dataviewjs
${minifiedCode}
exports.runTimekeep();
\`\`\`
Select a week to sent work log to Jira. Always reselect before pressing the button`;
fs.writeFileSync('docs/statistics.md', dataviewBlock);
console.log('Build completed successfully!');
console.log(`Minified size: ${minifiedCode.length} bytes`);
} catch (error) {
console.error('Build failed:', error);
process.exit(1);
}
}
build();

View file

@ -2,15 +2,14 @@
const { execSync } = require('child_process');
const stagedFiles = execSync('git diff --cached --name-only', { encoding: 'utf-8' })
const stagedFiles = execSync('git diff --cached --name-only', {
encoding: 'utf-8',
})
.trim()
.split('\n')
.filter(Boolean);
const hasCodeChanges = stagedFiles.some(file =>
file.endsWith('.ts') ||
file.endsWith('.tsx')
);
const hasCodeChanges = stagedFiles.some((file) => file.endsWith('.ts') || file.endsWith('.tsx'));
const manifestChanged = stagedFiles.includes('manifest.json');
@ -26,7 +25,9 @@ if (hasCodeChanges && !manifestChanged) {
if (manifestChanged && hasCodeChanges) {
try {
const manifestDiff = execSync('git diff --cached manifest.json', { encoding: 'utf-8' });
const manifestDiff = execSync('git diff --cached manifest.json', {
encoding: 'utf-8',
});
if (!manifestDiff.includes('"version"')) {
console.error('\x1b[33m⚠ manifest.json has been modified, but the version has not been updated!\x1b[0m');
@ -36,8 +37,7 @@ if (manifestChanged && hasCodeChanges) {
console.error('');
process.exit(1);
}
} catch (error) {
}
} catch (error) {}
}
console.log('\x1b[32m✓ Version check passed\x1b[0m');

View file

@ -1,10 +1,12 @@
The functionality of the plugin can be divided into several parts.
### Basic Functionality
For basic functionality, it is enough to specify Jira credentials and the folder for created tasks.
**Authentication Methods**
The plugin supports three authentication methods:
- **Bearer Token (PAT)** - Personal Access Token authentication
- **Basic Auth (Username + PAT)** - Username with Personal Access Token
- **Session Cookie (Username + Password)** - Traditional username and password authentication
@ -16,12 +18,15 @@ When running `Get issue from Jira with custom key`, the command will download th
Without a template, such a page will be completely empty except for its title, so using a template is highly recommended. The template is used only when creating a new page.
#### Template
The template can consist of several different parts:
##### Frontmatter
Meta-information at the top of the screen. When specifying keys for it and using any `Get issues from Jira` variant, they will be populated with corresponding fields from the response data.
##### Body
The main content of the page. When using indicators like `jira-sync-"type"-*`, they will be filled with the corresponding fields from the response data.
The difference between these options is as follows:
@ -29,30 +34,40 @@ The difference between these options is as follows:
- `line` reads and writes values from the current line.
Example:
```md
The responsible person for this task is `jira-sync-line-assignee` Bob
The description is `jira-sync-line-description` Some description
```
- `section` reads values from multiple lines after the indicator, stopping only at any other indicator or a heading.
Example:
```md
### Responsible `jira-sync-section-assignee`
Bob
### Description `jira-sync-section-description`
Some description
`jira-sync-section-customfield_10842`
...
```
- `inline` allows indicator of start and end to be placed anywhere in the text. Must have an ending part of indicator (`jira-sync-end`).
Example:
```md
The responsible person for this task is `jira-sync-inline-start-assignee`Bob`jira-sync-end`, and the description is `jira-sync-inline-start-description`Some description`jira-sync-end`.
```
- `block` is basically the same as `inline`, but with line break before and after indicators.
Example:
```md
The responsible person for this task is `jira-sync-block-assignee`
Bob
@ -72,6 +87,7 @@ Not all fields are predefined, and some may need adjustments. To add any custom
### Commands
Currently, the plugin provides the following commands:
- `Get issue from Jira with custom key` - allows creating a file in the configured folder that imports information from Jira using a manually specified ID.
- `Batch Fetch Issues by JQL` - allows mass issue import from Jira using a JQL query.
- `Get current issue from Jira` - allows updating the active file if its formatter contains a `key` (the Jira task ID).
@ -84,19 +100,23 @@ Currently, the plugin provides the following commands:
### Advanced Usage
#### Field Mapping
In the settings, you can configure custom mapping for any additional fields received from Jira. To do this:
- Configure how information is sent to Jira (e.g., with the `null` function, the field will be ignored).
- Define how it is received from Jira (e.g., `issue.fields.creator.name` will retrieve the creator's name instead of the entire object with related data).
Similarly, you can configure the `progressPercentage` shown in the example. This field does not exist in the response, but it can be "assembled" from the existing `progress` field: `issue.fields.progress.total ? 100 * issue.fields.progress.progress / issue.fields.progress.total : 0`. As seen in the syntax, mapping uses a simplified TypeScript format.
Additionally, you can use some of the built-in functions:
- `jiraToMarkdown` - converts Jira markup to Markdown.
- `markdownToJira` - converts Markdown to Jira markup.
- `JSON.parse` - converts a JSON string to an object.
- `JSON.stringify` - converts an object to a JSON string.
and modules: (original Javascript syntax)
- `Math` - provides access to mathematical functions, such as `Math.round()`.
- `Date` - provides access to date functions, such as `Date.now()`.
- `String` - provides access to string functions, such as `String.fromCharCode()`.
@ -109,8 +129,10 @@ Field mapping can like this:
![](images/fieldMappingExample.png)
#### Statistics
Statistics are now integrated into the plugin and can be accessed through the plugin settings in the "Timekeep work log statistics" section. This feature provides a dynamically generated table (or series of tables) showing work statistics and allowing you to send work logs to Jira. You can conveniently select time periods for calculation and transfer work log information to Jira directly from the settings interface.
#### Time Tracking Statistics
Statistics can be accessed through the plugin settings in the "Timekeep work log statistics" section. This feature provides a dynamically generated table (or series of tables) showing work statistics and allowing you to send work logs to Jira. You can conveniently select time periods for calculation and transfer work log information to Jira directly from the settings interface.
The statistics feature supports timekeep and simple-time-tracker time tracking formats.
The table looks like this:

View file

@ -1,10 +1,12 @@
Функционал плагина можно разделить на несколько частей.
### Базовый функционал
Для него достаточно указать креды Jira и папку с создаваемыми задачами.
**Методы аутентификации**
Плагин поддерживает три метода аутентификации:
- **Bearer Token (PAT)** - аутентификация по персональному токену доступа
- **Basic Auth (Username + PAT)** - имя пользователя с персональным токеном доступа
- **Session Cookie (Username + Password)** - традиционная аутентификация по имени пользователя и паролю
@ -16,12 +18,15 @@
Без шаблона такая страничка будет полностью пустой за исключением её названия, так что шаблон настоятельно рекомендуется к использованию. Шаблон используется только при создании новой страницы.
#### Шаблон
Шаблон можно составить из нескольких разных частей:
##### Frontmatter
Оно же метаинформация сверху экрана. При указании ключей для него и любом варианте `Get issues from Jira` они будут заполняться соответствующими полями из ответных данных.
##### Body
Основное содержание страницы. При использовании индикаторов типа `jira-sync-"type"-*` они будут заполняться соответствующими полями из сырых данных задачи.
Разница между этими параметрами заключается в следующем:
@ -29,30 +34,39 @@
- `line` считывает и записывает значения из текущей строки.
Пример:
```md
Ответственным за эту задачу является `jira-sync-line-assignee` Боб.
Описание: `jira-sync-line-description` Некоторое описание.
```
- `section` считывает значения из нескольких строк после индикатора, останавливаясь только на любом другом индикаторе или заголовке.
Пример:
```md
### Ответственный `jira-sync-section-assignee`
Боб
### Описание `jira-sync-section-description`
Некоторое описание
`jira-sync-section-customfield_10842`...
```
- `inline` позволяет размещать индикатор начала и окончания в любом месте текста. Должна быть конечная часть индикатора (`jira-sync-end`).
Пример:
```md
Ответственным за эту задачу является `jira-sync-inline-start-assignee`Боб`jira-sync-end`, а описание — `jira-sync-inline-start-description`Некоторое описание`jira-sync-end`.
```
- `block` в основном аналогичен `inline`, но с разрывом строки перед и после индикаторов.
Пример:
```md
Ответственным за эту задачу является `jira-sync-block-assignee`
Bob
@ -72,6 +86,7 @@ Bob
### Команды
На текущий момент плагин предоставляет следующие команды:
- `Get issue from Jira with custom key` - позволяет создать в папке, указанной в настройках, файл, импортирующий информацию из Jira по указанному вручную id.
- `Batch Fetch Issues by JQL` - позволяет получить задачи по JQL и создать/обновить все соответствующие заметки.
- `Get current issue from Jira` - позволяет обновить активный файл, если в его formatter указан key - id задачи Jira.
@ -84,19 +99,23 @@ Bob
### Продвинутое использование
#### Маппинг полей
В настройках можно задать кастомный маппинг дл любых дополнительных полей, приходящих из запроса. Для этого нужно:
- Настроить в каком виде информация отправляется в Jira (например при функции `null` поле будет игнорироваться)
- В каком виде получается из Jira (например `issue.fields.creator.name` позволит получать соответственное имя создателя запроса, а не весь объект с информацией о нём в целом.)
Таким же образом можно настроить показанный в примере `progressPercentage` - такого поля в запросе не существует, но его можно 'собрать' из существующего `progress`: `issue.fields.progress.total ? 100 * issue.fields.progress.progress / issue.fields.progress.total : 0`. Как понятно из синтаксиса, для маппинга используется обрезанный TypeScript
Кроме того, вы можете использовать некоторые встроенные функции:
- `jiraToMarkdown` - преобразует разметку Jira в Markdown.
- `markdownToJira` - преобразует Markdown в разметку Jira.
- `JSON.parse` - преобразует строку JSON в объект.
- `JSON.stringify` - преобразует объект в строку JSON.
и модули: (исходный синтаксис Javascript)
- `Math` - предоставляет доступ к математическим функциям, таким как `Math.round()`.
- `Date` - предоставляет доступ к функциям даты, таким как `Date.now()`.
- `String` - предоставляет доступ к функциям строки, таким как `String.fromCharCode()`.
@ -107,10 +126,13 @@ Bob
Секция маппинга может выглядеть так:
![](images/fieldMappingExample.png )
![](images/fieldMappingExample.png)
#### Статистика
Статистика теперь интегрирована в плагин и доступна через настройки плагина в секции "Timekeep work log statistics" ("Статистика ведения журнала работы через Timekeep"). Эта функция предоставляет динамически генерируемую таблицу (или ряд таблиц), показывающую статистику работы и позволяющую отправлять журналы работ в Jira. Там же можно удобно выбирать временные промежутки для которых проводится расчёт и передавать информацию ведения журнала работы в Jira.
#### Статистика ведения журнала работы
Статистика доступна через настройки плагина в секции "Timekeep work log statistics" ("Статистика ведения журнала работы через Timekeep"). Эта функция предоставляет динамически генерируемую таблицу (или ряд таблиц), показывающую статистику работы и позволяющую отправлять журналы работ в Jira. Там же можно удобно выбирать временные промежутки для которых проводится расчёт и передавать информацию ведения журнала работы в Jira.
Поддерживаются форматы временных записей: timekeep и simple-time-tracker.
Выглядит табличка так:

View file

@ -12,6 +12,7 @@ link: http://jira.local:8000/browse/JIR-1234
This is a comprehensive test file for the jira-sync parser.
## Summary `jira-sync-section-summary`
Complete API standardization and consolidation across all microservices to improve developer experience and reduce integration complexity.
This section continues here with more details.
@ -20,7 +21,9 @@ Multiple paragraphs are supported.
## Details Section
### Customer Impact `jira-sync-section-customfield_10842`
The current API structure is too fragmented and requires standardization. Customers are experiencing difficulties integrating with our services due to:
- Inconsistent authentication methods
- Different response formats
- Lack of versioning strategy
@ -28,6 +31,7 @@ The current API structure is too fragmented and requires standardization. Custom
This impacts approximately 2,300 enterprise customers.
### Next Heading Should Stop Section
This content should NOT be part of customfield_10842.
## Assignment Info
@ -45,6 +49,7 @@ Actual time logged so far: `jira-sync-line-timeSpent`2d 4h
Remaining estimate: `jira-sync-line-remainingEstimate`5d
## Acceptance Criteria `jira-sync-section-customfield_10100`
1. All endpoints follow RESTful conventions
2. API versioning implemented via headers
3. Consistent error handling across services
@ -54,6 +59,7 @@ Remaining estimate: `jira-sync-line-remainingEstimate`5d
All criteria must be met before moving to Done.
### Technical Requirements
This is a separate section that shouldn't be included above.
## Code Block Examples
@ -80,20 +86,21 @@ ssl: true
## Edge Cases Testing
### Empty Inline
Status indicator: `jira-sync-inline-start-status_indicator``jira-sync-end`
### Whitespace Heavy Section `jira-sync-section-whitespace_test`
This section has leading and trailing whitespace
Should be trimmed properly before sending to Jira.
### Single Line Section `jira-sync-section-oneliner`
Just one line here.
## Multi-line Inline Test
Complex description: `jira-sync-inline-start-description` This is a description
that spans multiple lines
with various formatting `jira-sync-inline-end` and continues here.

View file

@ -1,45 +1,45 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import esbuild from 'esbuild';
import process from 'process';
import builtins from 'builtin-modules';
const banner =
`/*
const banner = `/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const prod = process.argv[2] === 'production';
console.log("Esbuild context initialized:", prod);
console.log('Esbuild context initialized:', prod);
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["src/main.ts"],
entryPoints: ['src/main.ts'],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: prod ? "info" : "debug",
sourcemap: prod ? false : "inline",
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins,
],
format: 'cjs',
target: 'es2018',
logLevel: prod ? 'info' : 'debug',
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: "main.js",
outfile: 'main.js',
minify: prod,
});

34
eslint.config.js Normal file
View file

@ -0,0 +1,34 @@
import js from '@eslint/js';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
export default defineConfig([
{
ignores: ['main.js', 'node_modules/**', 'src/**/*.js'],
},
{
files: ['**/*.ts'],
languageOptions: {
parser: tsParser,
parserOptions: {
sourceType: 'module',
project: './tsconfig.json',
ecmaVersion: 'latest',
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
'@typescript-eslint': tsPlugin,
},
rules: {
...js.configs.recommended.rules,
...tsPlugin.configs.recommended.rules,
'@typescript-eslint/no-explicit-any': 'warn',
},
},
]);

View file

@ -1,9 +1,10 @@
{
"id": "jira-sync",
"name": "Jira Issue Manager",
"version": "1.4.4",
"version": "1.5.0",
"minAppVersion": "1.10.1",
"description": "Get Jira issues, create and update them. Issue status and worklog management.",
"author": "Alamion",
"authorUrl": "https://github.com/Alamion",
"isDesktopOnly": false
}

2418
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
{
"name": "obsidian-sample-plugin",
"version": "1.4.4",
"version": "1.5.0",
"packageManager": "yarn@1.22.22",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js",
"scripts": {
@ -11,7 +12,10 @@
"dev": "concurrently --kill-others-on-fail \"node esbuild.config.mjs\" \"yarn run compile_locale\"",
"build": "yarn run compile_locale_once && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json",
"prepare": "husky"
"prepare": "husky",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write ."
},
"keywords": [],
"author": "",
@ -21,18 +25,22 @@
"@codemirror/language": "^6.11.3",
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.38.1",
"@eslint/js": "^10.0.1",
"@types/lodash": "^4.17.16",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"@typescript-eslint/eslint-plugin": "^8.59.1",
"@typescript-eslint/parser": "^8.59.1",
"builtin-modules": "3.3.0",
"concurrently": "^9.2.1",
"esbuild": "0.25.0",
"eslint": "^10.2.1",
"globals": "^17.5.0",
"husky": "^9.1.7",
"lodash": "^4.17.23",
"obsidian": "latest",
"prettier": "^3.8.3",
"tslib": "2.4.0",
"typescript": "4.7.4"
"typescript": "^6.0.3"
},
"dependencies": {
"acorn": "^8.14.1",

View file

@ -1,8 +1,8 @@
import { Notice, requestUrl } from "obsidian";
import JiraPlugin from "../main";
import { Notice, requestUrl } from 'obsidian';
import JiraPlugin from '../main';
export function getSessionCookieKey(plugin: JiraPlugin): string {
return "jira-issue-managing-session-cookie-" + (plugin.app as any).appId;
return 'jira-issue-managing-session-cookie-' + (plugin.app as any).appId;
}
// Session cookie only
@ -12,38 +12,39 @@ export async function authenticate(plugin: JiraPlugin): Promise<boolean> {
return false;
}
const conn = plugin.getCurrentConnection();
if (!conn) return false;
const response = await requestUrl({
url: `${plugin.settings.connection.jiraUrl}/rest/auth/1/session`,
method: "post",
url: `${conn.jiraUrl}/rest/auth/1/session`,
method: 'post',
body: JSON.stringify({
username: plugin.settings.connection.username,
password: plugin.settings.connection.password,
username: conn.username,
password: conn.password,
}),
contentType: "application/json",
contentType: 'application/json',
headers: {
"Content-type": "application/json",
Origin: plugin.settings.connection.jiraUrl,
'Content-type': 'application/json',
Origin: conn.jiraUrl,
},
});
localStorage.setItem(
getSessionCookieKey(plugin),
response.json.session.value
);
localStorage.setItem(getSessionCookieKey(plugin), response.json.session.value);
return true;
} catch (error) {
new Notice("Authentication failed: " + (error.message || "Unknown error"));
} catch (error: unknown) {
new Notice('Authentication failed: ' + ((error as Error).message || 'Unknown error'));
return false;
}
}
export function validateSettings(plugin: JiraPlugin): boolean {
if (!plugin.settings.connection.jiraUrl) {
new Notice("Please configure Jira URL in plugin settings");
const conn = plugin.getCurrentConnection();
if (!conn || !conn.jiraUrl) {
new Notice('Please configure Jira URL in plugin settings');
return false;
}
if ((!plugin.settings.connection.username || !plugin.settings.connection.password) && !plugin.settings.connection.apiToken) {
new Notice("Please configure Jira username and password or PAT API token in plugin settings");
if ((!conn.username || !conn.password) && !conn.apiToken) {
new Notice('Please configure Jira username and password or PAT API token in plugin settings');
return false;
}
return true;
@ -58,17 +59,20 @@ function createBasicAuthHeader(email: string, token: string): string {
}
export async function getAuthHeaders(plugin: JiraPlugin): Promise<Record<string, string>> {
const conn = plugin.getCurrentConnection();
if (!conn) throw new Error('No connection configured');
// Common headers
const commonHeaders = {
"Content-type": "application/json",
Origin: plugin.settings.connection.jiraUrl,
'Content-type': 'application/json',
Origin: conn.jiraUrl,
};
// Personal Access Token
const pat = plugin.settings.connection.apiToken?.trim() || "";
const pat = conn.apiToken?.trim() || '';
// Option 1: Bearer token (PAT)
if (plugin.settings.connection.authMethod === "bearer" || !plugin.settings.connection.authMethod) {
if (conn.authMethod === 'bearer' || !conn.authMethod) {
return {
...commonHeaders,
Authorization: `Bearer ${pat}`,
@ -76,14 +80,12 @@ export async function getAuthHeaders(plugin: JiraPlugin): Promise<Record<string,
}
// Option 2: Basic Auth with API token
else if (plugin.settings.connection.authMethod === "basic") {
else if (conn.authMethod === 'basic') {
return {
...commonHeaders,
Authorization: createBasicAuthHeader(plugin.settings.connection.email || "", pat),
Authorization: createBasicAuthHeader(conn.email || '', pat),
};
}
else if (plugin.settings.connection.authMethod === "session") {
} else if (conn.authMethod === 'session') {
// Option 3: Session cookie
let cookie = localStorage.getItem(getSessionCookieKey(plugin));
if (!cookie) {
@ -100,5 +102,5 @@ export async function getAuthHeaders(plugin: JiraPlugin): Promise<Record<string,
}
// No valid auth method found
throw new Error("No valid authentication method available");
throw new Error('No valid authentication method available');
}

View file

@ -1,7 +1,7 @@
import JiraPlugin from "../main";
import {requestUrl} from "obsidian";
import {authenticate, getAuthHeaders} from "./auth";
import {debugLog} from "../tools/debugLogging";
import JiraPlugin from '../main';
import { requestUrl } from 'obsidian';
import { authenticate, getAuthHeaders } from './auth';
import { debugLog } from '../tools/debugLogging';
export async function baseRequest(
plugin: JiraPlugin,
@ -9,53 +9,59 @@ export async function baseRequest(
additional_url_path: string,
body?: string,
params?: Record<string, any>,
retries: number = 1
retries: number = 1,
): Promise<any> {
const queryString = params && Object.keys(params).length > 0 ? '?' + new URLSearchParams(params).toString() : '';
const queryString = params && Object.keys(params).length > 0
? "?" + new URLSearchParams(params).toString()
: "";
const conn = plugin.getCurrentConnection();
if (!conn) throw new Error('No connection configured');
const url = `${plugin.settings.connection.jiraUrl}/rest/api/${plugin.settings.connection.apiVersion}${additional_url_path}${queryString}`;
const url = `${conn.jiraUrl}/rest/api/${conn.apiVersion}${additional_url_path}${queryString}`;
const requestParams = {
url,
method: method,
headers: await getAuthHeaders(plugin),
contentType: "application/json",
accept: "application/json",
contentType: 'application/json',
accept: 'application/json',
throw: false,
body
}
debugLog("Request:\n", requestParams)
body,
};
debugLog('Request:\n', requestParams);
const response = await requestUrl(requestParams);
debugLog("Response:\n", response);
debugLog('Response:\n', response);
if (response.status < 200 || response.status >= 300) {
if (response.status === 401 && retries > 0) {
await authenticate(plugin);
return await baseRequest(plugin, method, additional_url_path, body, params, retries-1);
return await baseRequest(plugin, method, additional_url_path, body, params, retries - 1);
}
// console.error(error);
throw new Error(`
${response.text || "Unknown error"}
${body && '\nbody:' + body || ""}`);
${response.text || 'Unknown error'}
${(body && '\nbody:' + body) || ''}`);
}
debugLog("Additional request info:\n", {"url": url,
"method": method, "body": body, params: params});
debugLog('Additional request info:\n', {
url: url,
method: method,
body: body,
params: params,
});
return response.status === 204 ? null : response.json;
}
export function sanitizeObject(obj: any): any {
if (Array.isArray(obj)) {
obj = obj.map((item: any) => sanitizeObject(item));
}
else if (typeof obj === "object") {
obj = Object.entries(obj).filter(([, v]) => v !== null && v !== undefined).reduce((acc, [k, v]) => {
acc[k] = sanitizeObject(v)
return acc
}, {} as Record<string, any>);
} else if (typeof obj === 'object') {
obj = Object.entries(obj)
.filter(([, v]) => v !== null && v !== undefined)
.reduce(
(acc, [k, v]) => {
acc[k] = sanitizeObject(v);
return acc;
},
{} as Record<string, any>,
);
}
return obj;
}

View file

@ -1,9 +1,8 @@
import JiraPlugin from "../main";
import {JiraIssue, JiraTransitionType} from "../interfaces";
import {baseRequest, sanitizeObject} from "./base";
import {Notice} from "obsidian";
import {chunkArray, createLimiter} from "../tools/asyncLimiter";
import JiraPlugin from '../main';
import { JiraIssue, JiraTransitionType } from '../interfaces';
import { baseRequest, sanitizeObject } from './base';
import { Notice } from 'obsidian';
import { chunkArray, createLimiter } from '../tools/asyncLimiter';
/**
* Fetch an issue from Jira by its key
@ -12,10 +11,10 @@ import {chunkArray, createLimiter} from "../tools/asyncLimiter";
* @returns The issue data
*/
export async function fetchIssue(plugin: JiraPlugin, issueKey: string): Promise<JiraIssue> {
return await baseRequest(plugin, 'get', `/issue/${issueKey}`, undefined, {
fields: plugin.settings.fetchIssue.fields || ["*all"],
expand: plugin.settings.fetchIssue.expand.join(",") || "",
}) as Promise<JiraIssue>;
return (await baseRequest(plugin, 'get', `/issue/${issueKey}`, undefined, {
fields: plugin.settings.fetchIssue.fields || ['*all'],
expand: plugin.settings.fetchIssue.expand.join(',') || '',
})) as Promise<JiraIssue>;
}
/**
@ -26,21 +25,22 @@ export async function fetchIssuesByJQL(
plugin: JiraPlugin,
jql: string,
limit?: number,
fields?: string[]
fields?: string[],
): Promise<JiraIssue[]> {
let totalAvailable = 0;
let useNextPageToken = false;
switch (plugin.settings.connection.apiVersion) {
case "2":
switch (plugin.getCurrentConnection()?.apiVersion) {
case '2': {
const result = await fetchIssuesByJQLRaw(plugin, jql, 1, fields);
totalAvailable = result.total;
break;
case "3":
}
case '3':
totalAvailable = await fetchCountIssuesByJQL(plugin, jql);
useNextPageToken = true;
if (!fields) {
fields = ["*all"];
fields = ['*all'];
}
break;
}
@ -101,7 +101,6 @@ export async function fetchIssuesByJQL(
// return results.flat();
// }
/**
* Fetch issues by JQL and return the raw search response (includes total, startAt, etc.).
* Useful for previewing results and counts.
@ -121,23 +120,27 @@ export async function fetchIssuesByJQLRaw(
nextPageToken,
fields: fields && fields.length > 0 ? fields : plugin.settings.fetchIssue.fields || [],
};
if (plugin.settings.connection.apiVersion === "3") {
if (plugin.getCurrentConnection()?.apiVersion === '3') {
requestBody.nextPageToken = nextPageToken;
requestBody.expand = plugin.settings.fetchIssue.expand.join(",") || "";
requestBody.expand = plugin.settings.fetchIssue.expand.join(',') || '';
} else {
requestBody.startAt = startAt;
requestBody.expand = plugin.settings.fetchIssue.expand || [];
}
const body = JSON.stringify(sanitizeObject(requestBody));
return await baseRequest(plugin, 'post', `/search${plugin.settings.connection.apiVersion === "3" ? "/jql" : ""}`, body);
return await baseRequest(
plugin,
'post',
`/search${plugin.getCurrentConnection()?.apiVersion === '3' ? '/jql' : ''}`,
body,
);
}
export async function fetchCountIssuesByJQL(plugin: JiraPlugin, jql: string): Promise<number> {
const body = JSON.stringify(sanitizeObject({ jql }));
return (await baseRequest(plugin, 'post', "/search/approximate-count", body)).count;
return (await baseRequest(plugin, 'post', '/search/approximate-count', body)).count;
}
/**
* Fetch an issue from Jira by its key
* @param plugin The plugin instance
@ -146,10 +149,10 @@ export async function fetchCountIssuesByJQL(plugin: JiraPlugin, jql: string): Pr
*/
export async function fetchIssueTransitions(plugin: JiraPlugin, issueKey: string): Promise<JiraTransitionType[]> {
const result = await baseRequest(plugin, 'get', `/issue/${issueKey}/transitions`);
return result.transitions.map(({ id, name, to }: { id: string; name: string; to: { name: string }}) => ({
return result.transitions.map(({ id, name, to }: { id: string; name: string; to: { name: string } }) => ({
id,
action: name,
status: to.name
status: to.name,
})) as JiraTransitionType[];
}
@ -162,48 +165,37 @@ export async function fetchIssueTransitions(plugin: JiraPlugin, issueKey: string
export async function updateJiraIssue(
plugin: JiraPlugin,
issueKey: string,
fields: Record<string, any>
fields: Record<string, any>,
): Promise<JiraIssue> {
const body = JSON.stringify({ fields })
return await baseRequest(plugin, 'put', `/issue/${issueKey}`, body) as Promise<JiraIssue>;
const body = JSON.stringify({ fields });
return (await baseRequest(plugin, 'put', `/issue/${issueKey}`, body)) as Promise<JiraIssue>;
}
export async function bulkUpdateJiraIssues(
plugin: JiraPlugin,
updates: { issueKey: string, fields: Record<string, any> }[]
updates: { issueKey: string; fields: Record<string, any> }[],
): Promise<any[]> {
const limit = createLimiter(5);
const promises = updates.map(update =>
limit(() => updateJiraIssue(plugin, update.issueKey, update.fields))
);
const promises = updates.map((update) => limit(() => updateJiraIssue(plugin, update.issueKey, update.fields)));
return await Promise.allSettled(promises);
}
/**
* Create a new issue in Jira
* @param plugin The plugin instance
* @param fields The fields for the new issue
* @returns The created issue key
*/
export async function createJiraIssue(
plugin: JiraPlugin,
fields: Record<string, any>
): Promise<JiraIssue> {
export async function createJiraIssue(plugin: JiraPlugin, fields: Record<string, any>): Promise<JiraIssue> {
// Ensure required fields are present
if (!fields.project || !fields.issuetype || !fields.summary) {
throw new Error("Missing required fields: project, issuetype, and summary are required");
throw new Error('Missing required fields: project, issuetype, and summary are required');
}
const body = JSON.stringify({ fields })
return await baseRequest(plugin, 'post', `/issue/`, body) as Promise<JiraIssue>;
const body = JSON.stringify({ fields });
return (await baseRequest(plugin, 'post', `/issue/`, body)) as Promise<JiraIssue>;
}
export async function bulkCreateJiraIssues(
plugin: JiraPlugin,
issues: Record<string, any>[]
): Promise<any[]> {
export async function bulkCreateJiraIssues(plugin: JiraPlugin, issues: Record<string, any>[]): Promise<any[]> {
const chunks = chunkArray(issues, 50);
let results: any[] = [];
for (const chunk of chunks) {
@ -214,23 +206,17 @@ export async function bulkCreateJiraIssues(
return results;
}
/**
* Update an issue status in Jira
* @param plugin The plugin instance
* @param issueKey The issue key to update
* @param status The status to update the issue to
*/
export async function updateJiraStatus(
plugin: JiraPlugin,
issueKey: string,
status: string
): Promise<JiraIssue> {
const body = JSON.stringify({ transition: { id: status } })
return await baseRequest(plugin, 'post', `/issue/${issueKey}/transitions`, body) as Promise<JiraIssue>;
export async function updateJiraStatus(plugin: JiraPlugin, issueKey: string, status: string): Promise<JiraIssue> {
const body = JSON.stringify({ transition: { id: status } });
return (await baseRequest(plugin, 'post', `/issue/${issueKey}/transitions`, body)) as Promise<JiraIssue>;
}
/**
* Add a work log entry to a Jira issue
*/
@ -239,13 +225,13 @@ export async function addWorkLog(
issueKey: string,
timeSpent: string,
startedAt: string,
comment: string = "",
showNotice: boolean = true
comment: string = '',
showNotice: boolean = true,
): Promise<any> {
const payload = {
timeSpent,
started: startedAt,
comment
comment,
};
const response = await baseRequest(plugin, 'post', `/issue/${issueKey}/worklog`, JSON.stringify(payload));
@ -256,15 +242,21 @@ export async function addWorkLog(
return response;
}
export async function bulkAddWorkLog(
plugin: JiraPlugin,
worklogs: { issueKey: string, timeSpent: string, startedAt: string, comment: string }[],
showNotice: boolean = true
worklogs: {
issueKey: string;
timeSpent: string;
startedAt: string;
comment: string;
}[],
showNotice: boolean = true,
): Promise<any[]> {
const limit = createLimiter(5);
const promises = worklogs.map(worklog =>
limit(() => addWorkLog(plugin, worklog.issueKey, worklog.timeSpent, worklog.startedAt, worklog.comment, showNotice))
const promises = worklogs.map((worklog) =>
limit(() =>
addWorkLog(plugin, worklog.issueKey, worklog.timeSpent, worklog.startedAt, worklog.comment, showNotice),
),
);
return await Promise.allSettled(promises);
}

View file

@ -1,9 +1,8 @@
import JiraPlugin from "../main";
import {baseRequest} from "./base";
import JiraPlugin from '../main';
import { baseRequest } from './base';
export async function fetchProjects(plugin: JiraPlugin): Promise<Record<string, any>[]> {
return await baseRequest(plugin, 'get', '/project');
}
export async function fetchIssueTypes(plugin: JiraPlugin, projectKey: string): Promise<Record<string, any>> {

View file

@ -1,7 +1,6 @@
import JiraPlugin from "../main";
import {baseRequest} from "./base";
import JiraPlugin from '../main';
import { baseRequest } from './base';
export async function fetchSelf(plugin: JiraPlugin): Promise<Record<string, any>[]> {
return await baseRequest(plugin, 'get', '/myself');
}

View file

@ -1,18 +1,24 @@
import {Notice, TFile} from "obsidian";
import JiraPlugin from "../main";
import {addWorkLog} from "../api";
import {debugLog} from "../tools/debugLogging";
import {checkCommandCallback} from "../tools/checkCommandCallback";
import {useTranslations} from "../localization/translator";
import { Notice, TFile } from 'obsidian';
import JiraPlugin from '../main';
import { addWorkLog } from '../api';
import { debugLog } from '../tools/debugLogging';
import { checkCommandCallback } from '../tools/checkCommandCallback';
import { useTranslations } from '../localization/translator';
const t = useTranslations("commands.add_worklog.batch").t;
const t = useTranslations('commands.add_worklog.batch').t;
export function registerUpdateWorkLogBatchCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "update-work-log-jira-batch",
name: t("name"),
id: 'update-work-log-jira-batch',
name: t('name'),
checkCallback: (checking: boolean) => {
return checkCommandCallback(plugin, checking, processFrontmatterWorkLogs, ["jira_worklog_batch"], ["jira_worklog_batch"]);
return checkCommandCallback(
plugin,
checking,
processFrontmatterWorkLogs,
['jira_worklog_batch'],
['jira_worklog_batch'],
);
},
});
}
@ -32,20 +38,20 @@ async function processFrontmatterWorkLogs(plugin: JiraPlugin, _: TFile, jira_wor
} else {
throw new TypeError(`jira_worklog_batch has an invalid type: ${typeof jira_worklog_batch}`);
}
} catch (error) {
new Notice("Failed to parse jira_worklog_batch");
console.error("Failed to parse jira_worklog_batch:", error);
} catch (error: unknown) {
new Notice('Failed to parse jira_worklog_batch');
console.error('Failed to parse jira_worklog_batch:', error);
}
if (!foundData || workLogData.length === 0) {
new Notice("No work log data to process");
console.warn("No work log data to process");
new Notice('No work log data to process');
console.warn('No work log data to process');
return;
}
await processWorkLogBatch(plugin, workLogData);
} catch (error) {
console.error("Error processing frontmatter work logs:", error);
new Notice("Failed to process work log data from frontmatter");
} catch (error: unknown) {
console.error('Error processing frontmatter work logs:', error);
new Notice('Failed to process work log data from frontmatter');
}
}
@ -53,7 +59,7 @@ export async function processWorkLogBatch(plugin: JiraPlugin, workLogs: any[]):
const results = {
success: 0,
total: workLogs.length,
failures: [] as { reason: string; issueKeys: string[] }[]
failures: [] as { reason: string; issueKeys: string[] }[],
};
for (const workLog of workLogs) {
@ -61,48 +67,56 @@ export async function processWorkLogBatch(plugin: JiraPlugin, workLogs: any[]):
const { issueKey, startTime, duration, comment } = workLog;
if (!issueKey || !startTime || !duration) {
addFailure(results, "Missing required fields", issueKey || "unknown");
addFailure(results, 'Missing required fields', issueKey || 'unknown');
continue;
}
const startDate = convertToStandardDate(startTime);
if (!startDate) {
addFailure(results, "Invalid start time format", issueKey);
addFailure(results, 'Invalid start time format', issueKey);
continue;
}
const parsed_duration = parseDuration(duration);
debugLog(`Duration: ${duration}, Parsed duration: ${parsed_duration}`);
if (parsed_duration === '') {
addFailure(results, "Duration must be at least 1 minute", issueKey);
addFailure(results, 'Duration must be at least 1 minute', issueKey);
continue;
}
await addWorkLog(plugin, issueKey, parsed_duration, startDate, comment || "", false);
await addWorkLog(plugin, issueKey, parsed_duration, startDate, comment || '', false);
results.success++;
} catch (error) {
addFailure(results, error.message || "Unknown error", workLog.issueKey || "unknown");
} catch (error: unknown) {
addFailure(results, (error as Error).message || 'Unknown error', workLog.issueKey || 'unknown');
}
}
showResults(results);
}
function showResults(results: { success: number, total: number, failures: { reason: string; issueKeys: string[] }[] }): void {
function showResults(results: {
success: number;
total: number;
failures: { reason: string; issueKeys: string[] }[];
}): void {
new Notice(`Work logs updated: ${results.success}/${results.total} succeeded`);
if (results.failures.length > 0) {
console.error("Work log update failures:", results.failures);
console.error('Work log update failures:', results.failures);
results.failures.forEach(failure => {
results.failures.forEach((failure) => {
if (failure.issueKeys.length > 0) {
new Notice(`Failed: ${failure.reason} for issues: ${failure.issueKeys.join(", ")}`);
new Notice(`Failed: ${failure.reason} for issues: ${failure.issueKeys.join(', ')}`);
}
});
}
}
function addFailure(results: { failures: { reason: string; issueKeys: string[] }[] }, reason: string, issueKey: string): void {
const existingFailure = results.failures.find(f => f.reason === reason);
function addFailure(
results: { failures: { reason: string; issueKeys: string[] }[] },
reason: string,
issueKey: string,
): void {
const existingFailure = results.failures.find((f) => f.reason === reason);
if (existingFailure) {
existingFailure.issueKeys.push(issueKey);
} else {
@ -116,20 +130,18 @@ function convertToStandardDate(dateString: string): string | null {
const date = new Date(dateString);
if (isNaN(date.getTime())) {
throw new Error("Invalid date format");
throw new Error('Invalid date format');
}
// Format to ISO string and adjust to desired format
const isoString = date.toISOString();
return isoString.replace('Z', '+0000');
} catch (error) {
console.error("Error converting date:", error);
console.error('Error converting date:', error);
return null;
}
}
function parseDuration(input: string): string {
const regex = /(\d+)([wdhms])/g;
let match;

View file

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

View file

@ -1,19 +1,19 @@
import JiraPlugin from "../main";
import {JQLSearchModal} from "../modals";
import {fetchIssuesByJQL, validateSettings} from "../api";
import {createOrUpdateIssueNote} from "../file_operations/getIssue";
import {useTranslations} from "../localization/translator";
import {Notice} from "obsidian";
import JiraPlugin from '../main';
import { JQLSearchModal } from '../modals';
import { fetchIssuesByJQL, validateSettings } from '../api';
import { createOrUpdateIssueNote } from '../file_operations/getIssue';
import { useTranslations } from '../localization/translator';
import { Notice } from 'obsidian';
const t = useTranslations("commands.batch_fetch_issues").t;
const t = useTranslations('commands.batch_fetch_issues').t;
/**
* Register the batch fetch issues command
*/
export function registerBatchFetchIssuesCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "batch-fetch-issues-jira",
name: t("name"),
id: 'batch-fetch-issues-jira',
name: t('name'),
checkCallback: (checking: boolean) => {
const settings_are_valid = validateSettings(plugin);
if (settings_are_valid) {
@ -33,16 +33,16 @@ function openJQLModal(plugin: JiraPlugin): void {
export async function batchFetchAndCreateIssues(plugin: JiraPlugin, jql: string): Promise<void> {
try {
new Notice(t("fetching_started"));
new Notice(t('fetching_started'));
const issues = await fetchIssuesByJQL(plugin, jql);
if (issues.length === 0) {
new Notice(t("no_issues_found"));
new Notice(t('no_issues_found'));
return;
}
new Notice(t("fetching_completed", { count: issues.length.toString() }));
new Notice(t('fetching_completed', { count: issues.length.toString() }));
// Process each issue
let successCount = 0;
@ -53,26 +53,27 @@ export async function batchFetchAndCreateIssues(plugin: JiraPlugin, jql: string)
try {
await createOrUpdateIssueNote(plugin, issue);
successCount++;
} catch (error) {
} catch (error: unknown) {
errorCount++;
errors.push(`${issue.key}: ${error.message || "Unknown error"}`);
errors.push(`${issue.key}: ${(error as Error).message || 'Unknown error'}`);
console.error(`Error processing issue ${issue.key}:`, error);
}
}
// Show results
if (errorCount === 0) {
new Notice(t("all_success", { count: successCount.toString() }));
new Notice(t('all_success', { count: successCount.toString() }));
} else {
new Notice(t("partial_success", {
new Notice(
t('partial_success', {
success: successCount.toString(),
errors: errorCount.toString()
}));
console.error("Batch fetch errors:", errors);
errors: errorCount.toString(),
}),
);
console.error('Batch fetch errors:', errors);
}
} catch (error) {
new Notice(t("fetch_error") + ": " + (error.message || "Unknown error"));
console.error("Batch fetch error:", error);
} catch (error: unknown) {
new Notice(t('fetch_error') + ': ' + ((error as Error).message || 'Unknown error'));
console.error('Batch fetch error:', error);
}
}

View file

@ -1,21 +1,20 @@
import { Notice, TFile } from "obsidian";
import JiraPlugin from "../main";
import { IssueTypeModal, ProjectModal } from "../modals";
import {fetchIssueTypes, fetchProjects} from "../api";
import {createIssueFromFile} from "../file_operations/createUpdateIssue";
import {checkCommandCallback} from "../tools/checkCommandCallback";
import {useTranslations} from "../localization/translator";
import {readJiraFieldsFromFile} from "../file_operations/commonPrepareData";
import {JiraIssueType, JiraProject} from "../interfaces";
import {IssueAddSummaryModal} from "../modals/IssueAddSummaryModal";
const t = useTranslations("commands.create_issue").t;
import { Notice, TFile } from 'obsidian';
import JiraPlugin from '../main';
import { IssueTypeModal, ProjectModal } from '../modals';
import { fetchIssueTypes, fetchProjects } from '../api';
import { createIssueFromFile } from '../file_operations/createUpdateIssue';
import { checkCommandCallback } from '../tools/checkCommandCallback';
import { useTranslations } from '../localization/translator';
import { readJiraFieldsFromFile } from '../file_operations/commonPrepareData';
import { JiraIssueType, JiraProject } from '../interfaces';
import { IssueAddSummaryModal } from '../modals/IssueAddSummaryModal';
const t = useTranslations('commands.create_issue').t;
export function registerCreateIssueCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "create-issue-jira",
name: t("name"),
id: 'create-issue-jira',
name: t('name'),
checkCallback: (checking: boolean) => {
return checkCommandCallback(plugin, checking, createIssue, []);
},
@ -29,9 +28,9 @@ export async function createIssue(plugin: JiraPlugin, file: TFile): Promise<void
await checkIssueTypes(plugin, fields);
await checkSummary(plugin, fields);
const issueKey = await createIssueFromFile(plugin, file, fields);
new Notice(t('success', {issueKey}));
} catch (error) {
new Notice(t('error') + ": " + (error.message || "Unknown error"), 3000);
new Notice(t('success', { issueKey }));
} catch (error: unknown) {
new Notice(t('error') + ': ' + ((error as Error).message || 'Unknown error'), 3000);
console.error(error);
}
}
@ -40,7 +39,8 @@ export async function checkProjects(plugin: JiraPlugin, fields: any): Promise<vo
const projects = await fetchProjects(plugin);
if (!fields.project || !projects.map((project: any) => project.key).includes(fields.project)) {
await new Promise<void>((resolve) => {
new ProjectModal(plugin.app,
new ProjectModal(
plugin.app,
projects.map((project: any) => ({
id: project.key,
name: project.name,
@ -48,34 +48,36 @@ export async function checkProjects(plugin: JiraPlugin, fields: any): Promise<vo
async (projectKey: string) => {
fields.project = projectKey;
resolve();
}).open();
},
).open();
});
}
}
async function checkIssueTypes(plugin: JiraPlugin, fields: any): Promise<void> {
const issueTypesResponse = await fetchIssueTypes(plugin, fields.project);
const issueTypes = plugin.settings.connection.apiVersion === "3" ? issueTypesResponse.issueTypes : issueTypesResponse.values;
const issueTypes =
plugin.getCurrentConnection()?.apiVersion === '3' ? issueTypesResponse.issueTypes : issueTypesResponse.values;
if (!fields.issuetype || !issueTypes.map((issueType: any) => issueType.name).includes(fields.issuetype)) {
await new Promise<void>((resolve) => {
new IssueTypeModal(plugin.app,
new IssueTypeModal(
plugin.app,
issueTypes.map((type: any) => ({
name: type.name,
})) as JiraIssueType[],
async (issueType: string) => {
fields.issuetype = issueType;
resolve();
}).open();
},
).open();
});
}
}
async function checkSummary(plugin: JiraPlugin, fields: any): Promise<void> {
if (!fields.summary) {
await new Promise<void>((resolve) => {
new IssueAddSummaryModal(plugin.app,
async (summary: string) => {
new IssueAddSummaryModal(plugin.app, async (summary: string) => {
fields.summary = summary;
resolve();
}).open();

View file

@ -1,17 +1,17 @@
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/checkCommandCallback";
import {useTranslations} from "../localization/translator";
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/checkCommandCallback';
import { useTranslations } from '../localization/translator';
const t = useTranslations("commands.get_issue").t;
const t = useTranslations('commands.get_issue').t;
export function registerGetIssueCommandWithCustomKey(plugin: JiraPlugin): void {
plugin.addCommand({
id: "get-issue-jira-key",
name: t("with_key"),
id: 'get-issue-jira-key',
name: t('with_key'),
checkCallback: (checking: boolean) => {
const settings_are_valid = validateSettings(plugin);
if (settings_are_valid) {
@ -25,10 +25,10 @@ export function registerGetIssueCommandWithCustomKey(plugin: JiraPlugin): void {
export function registerGetCurrentIssueCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "get-issue-jira",
name: t("without_key"),
id: 'get-issue-jira',
name: t('without_key'),
checkCallback: (checking: boolean) => {
return checkCommandCallback(plugin, checking, fetchAndOpenIssue, ["key"], ["key"]);
return checkCommandCallback(plugin, checking, fetchAndOpenIssue, ['key'], ['key']);
},
});
}

View file

@ -1,17 +1,17 @@
import {Notice, TFile} from "obsidian";
import JiraPlugin from "../main";
import {updateIssueFromFile} from "../file_operations/createUpdateIssue";
import {checkCommandCallback} from "../tools/checkCommandCallback";
import {useTranslations} from "../localization/translator";
import { Notice, TFile } from 'obsidian';
import JiraPlugin from '../main';
import { updateIssueFromFile } from '../file_operations/createUpdateIssue';
import { checkCommandCallback } from '../tools/checkCommandCallback';
import { useTranslations } from '../localization/translator';
const t = useTranslations("commands.update_issue").t;
const t = useTranslations('commands.update_issue').t;
export function registerUpdateIssueCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "update-issue-jira",
name: t("name"),
id: 'update-issue-jira',
name: t('name'),
checkCallback: (checking: boolean) => {
return checkCommandCallback(plugin, checking, updateIssue, ["key"]);
return checkCommandCallback(plugin, checking, updateIssue, ['key']);
},
});
}
@ -21,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(t('success', {issueKey}));
} catch (error) {
new Notice(t('error') + ": " + (error.message || "Unknown error"), 3000);
new Notice(t('success', { issueKey }));
} catch (error: unknown) {
new Notice(t('error') + ': ' + ((error as Error).message || 'Unknown error'), 3000);
}
} catch (error) {
new Notice(t('error') + ": " + (error.message || "Unknown error"));
} catch (error: unknown) {
new Notice(t('error') + ': ' + ((error as Error).message || 'Unknown error'));
}
}

View file

@ -1,20 +1,20 @@
import {Notice, TFile} from "obsidian";
import JiraPlugin from "../main";
import {fetchIssueTransitions} from "../api";
import {updateStatusFromFile} from "../file_operations/createUpdateIssue";
import {IssueStatusModal} from "../modals";
import {JiraTransitionType} from "../interfaces";
import {checkCommandCallback} from "../tools/checkCommandCallback";
import {useTranslations} from "../localization/translator";
import { Notice, TFile } from 'obsidian';
import JiraPlugin from '../main';
import { fetchIssueTransitions } from '../api';
import { updateStatusFromFile } from '../file_operations/createUpdateIssue';
import { IssueStatusModal } from '../modals';
import { JiraTransitionType } from '../interfaces';
import { checkCommandCallback } from '../tools/checkCommandCallback';
import { useTranslations } from '../localization/translator';
const t = useTranslations("commands.update_status").t;
const t = useTranslations('commands.update_status').t;
export function registerUpdateIssueStatusCommand(plugin: JiraPlugin): void {
plugin.addCommand({
id: "update-issue-status-jira",
id: 'update-issue-status-jira',
name: t('name'),
checkCallback: (checking: boolean) => {
return checkCommandCallback(plugin, checking, updateIssueStatus, ["key"],["key"]);
return checkCommandCallback(plugin, checking, updateIssueStatus, ['key'], ['key']);
},
});
}
@ -26,15 +26,14 @@ 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(t('success', {issueKey}));
} catch (error) {
new Notice(t('error') + ": " + (error.message || "Unknown error"), 3000);
new Notice(t('success', { issueKey }));
} catch (error: unknown) {
new Notice(t('error') + ': ' + ((error as Error).message || 'Unknown error'), 3000);
console.error(error);
}
}).open();
} catch (error) {
new Notice(t('error') + ": " + (error.message || "Unknown error"));
} catch (error: unknown) {
new Notice(t('error') + ': ' + ((error as Error).message || 'Unknown error'));
console.error(error);
}
}

View file

@ -1,405 +1,405 @@
export const defaultIssue = {
"expand": "",
"id": "",
"self": "",
"key": "",
"fields": {
"issuetype": {
"self": "",
"id": "",
"description": "",
"iconUrl": "",
"name": "",
"subtask": 0,
"avatarId": 0
expand: '',
id: '',
self: '',
key: '',
fields: {
issuetype: {
self: '',
id: '',
description: '',
iconUrl: '',
name: '',
subtask: 0,
avatarId: 0,
},
"timespent": 0,
"project": {
"self": "",
"id": "",
"key": "",
"name": "",
"projectTypeKey": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
}
timespent: 0,
project: {
self: '',
id: '',
key: '',
name: '',
projectTypeKey: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"fixVersions": [],
"aggregatetimespent": 0,
"resolution": null,
"resolutiondate": null,
"workratio": 0,
"lastViewed": "",
"watches": {
"self": "",
"watchCount": 0,
"isWatching": 0
},
"created": "",
"priority": {
"self": "",
"iconUrl": "",
"name": "",
"id": ""
fixVersions: [],
aggregatetimespent: 0,
resolution: null,
resolutiondate: null,
workratio: 0,
lastViewed: '',
watches: {
self: '',
watchCount: 0,
isWatching: 0,
},
"labels": [],
"timeestimate": 0,
"aggregatetimeoriginalestimate": null,
"versions": [],
"issuelinks": [],
"assignee": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
created: '',
priority: {
self: '',
iconUrl: '',
name: '',
id: '',
},
"displayName": "",
"active": 0,
"timeZone": ""
labels: [],
timeestimate: 0,
aggregatetimeoriginalestimate: null,
versions: [],
issuelinks: [],
assignee: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"updated": "",
"status": {
"self": "",
"description": "",
"iconUrl": "",
"name": "",
"id": "",
"statusCategory": {
"self": "",
"id": 0,
"key": "",
"colorName": "",
"name": ""
}
displayName: '',
active: 0,
timeZone: '',
},
"components": [],
"timeoriginalestimate": null,
"description": "",
"archiveddate": null,
"timetracking": {
"remainingEstimate": "",
"timeSpent": "",
"remainingEstimateSeconds": 0,
"timeSpentSeconds": 0
updated: '',
status: {
self: '',
description: '',
iconUrl: '',
name: '',
id: '',
statusCategory: {
self: '',
id: 0,
key: '',
colorName: '',
name: '',
},
"attachment": [],
"aggregatetimeestimate": 0,
"summary": "",
"creator": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
},
"displayName": "",
"active": 0,
"timeZone": ""
components: [],
timeoriginalestimate: null,
description: '',
archiveddate: null,
timetracking: {
remainingEstimate: '',
timeSpent: '',
remainingEstimateSeconds: 0,
timeSpentSeconds: 0,
},
"subtasks": [],
"reporter": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
attachment: [],
aggregatetimeestimate: 0,
summary: '',
creator: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"aggregateprogress": {
"progress": 0,
"total": 0,
"percent": 0
subtasks: [],
reporter: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"environment": null,
"duedate": null,
"progress": {
"progress": 0,
"total": 0,
"percent": 0
displayName: '',
active: 0,
timeZone: '',
},
"comment": {
"comments": [],
"maxResults": 0,
"total": 0,
"startAt": 0
aggregateprogress: {
progress: 0,
total: 0,
percent: 0,
},
"votes": {
"self": "",
"votes": 0,
"hasVoted": 0
environment: null,
duedate: null,
progress: {
progress: 0,
total: 0,
percent: 0,
},
"worklog": {
"startAt": 0,
"maxResults": 0,
"total": 0,
"worklogs": [
comment: {
comments: [],
maxResults: 0,
total: 0,
startAt: 0,
},
votes: {
self: '',
votes: 0,
hasVoted: 0,
},
worklog: {
startAt: 0,
maxResults: 0,
total: 0,
worklogs: [
{
"self": "",
"author": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
self: '',
author: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"updateAuthor": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
updateAuthor: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"comment": "",
"created": "",
"updated": "",
"started": "",
"timeSpent": "",
"timeSpentSeconds": 0,
"id": "",
"issueId": ""
comment: '',
created: '',
updated: '',
started: '',
timeSpent: '',
timeSpentSeconds: 0,
id: '',
issueId: '',
},
{
"self": "",
"author": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
self: '',
author: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"updateAuthor": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
updateAuthor: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"comment": "",
"created": "",
"updated": "",
"started": "",
"timeSpent": "",
"timeSpentSeconds": 0,
"id": "",
"issueId": ""
comment: '',
created: '',
updated: '',
started: '',
timeSpent: '',
timeSpentSeconds: 0,
id: '',
issueId: '',
},
{
"self": "",
"author": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
self: '',
author: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"updateAuthor": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
updateAuthor: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"comment": "",
"created": "",
"updated": "",
"started": "",
"timeSpent": "",
"timeSpentSeconds": 0,
"id": "",
"issueId": ""
comment: '',
created: '',
updated: '',
started: '',
timeSpent: '',
timeSpentSeconds: 0,
id: '',
issueId: '',
},
{
"self": "",
"author": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
self: '',
author: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"updateAuthor": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
updateAuthor: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"comment": "",
"created": "",
"updated": "",
"started": "",
"timeSpent": "",
"timeSpentSeconds": 0,
"id": "",
"issueId": ""
comment: '',
created: '',
updated: '',
started: '',
timeSpent: '',
timeSpentSeconds: 0,
id: '',
issueId: '',
},
{
"self": "",
"author": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
self: '',
author: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"updateAuthor": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
updateAuthor: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"comment": "",
"created": "",
"updated": "",
"started": "",
"timeSpent": "",
"timeSpentSeconds": 0,
"id": "",
"issueId": ""
comment: '',
created: '',
updated: '',
started: '',
timeSpent: '',
timeSpentSeconds: 0,
id: '',
issueId: '',
},
{
"self": "",
"author": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
self: '',
author: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"updateAuthor": {
"self": "",
"name": "",
"key": "",
"emailAddress": "",
"avatarUrls": {
"48x48": "",
"24x24": "",
"16x16": "",
"32x32": ""
updateAuthor: {
self: '',
name: '',
key: '',
emailAddress: '',
avatarUrls: {
'48x48': '',
'24x24': '',
'16x16': '',
'32x32': '',
},
"displayName": "",
"active": 0,
"timeZone": ""
displayName: '',
active: 0,
timeZone: '',
},
"comment": "",
"created": "",
"updated": "",
"started": "",
"timeSpent": "",
"timeSpentSeconds": 0,
"id": "",
"issueId": ""
}
]
comment: '',
created: '',
updated: '',
started: '',
timeSpent: '',
timeSpentSeconds: 0,
id: '',
issueId: '',
},
"archivedby": null
}
}
],
},
archivedby: null,
},
};

View file

@ -1,5 +1,4 @@
export const defaultTemplate =
`---
export const defaultTemplate = `---
key:
summary:
status:

View file

@ -1,4 +1,4 @@
import {JiraIssue} from "../interfaces";
import { JiraIssue } from '../interfaces';
export interface FieldMapping {
toJira: (value: any) => any;
@ -6,74 +6,72 @@ export interface FieldMapping {
}
export const obsidianJiraFieldMappings: Record<string, FieldMapping> = {
"summary": {
summary: {
toJira: (value) => value,
fromJira: (issue) => issue.fields.summary,
},
"description": {
description: {
toJira: () => null,
fromJira: (issue) => issue.fields.description,
},
"key": {
key: {
toJira: () => null,
fromJira: (issue) => issue.key,
},
"self": {
self: {
toJira: () => null,
fromJira: (issue) => issue.self,
},
"project": {
project: {
toJira: (value) => ({ key: value }),
fromJira: (issue) =>
issue.fields.project ?issue.fields.project.key :"",
fromJira: (issue) => (issue.fields.project ? issue.fields.project.key : ''),
},
"issuetype": {
issuetype: {
toJira: (value) => ({ name: value }),
fromJira: (issue) =>
issue.fields.issuetype ?issue.fields.issuetype.name :"",
fromJira: (issue) => (issue.fields.issuetype ? issue.fields.issuetype.name : ''),
},
"priority": {
priority: {
toJira: (value) => ({ name: value }),
fromJira: (issue) =>issue.fields.priority ?issue.fields.priority.name :"",
fromJira: (issue) => (issue.fields.priority ? issue.fields.priority.name : ''),
},
"status": {
status: {
toJira: () => null,
fromJira: (issue) =>issue.fields.status ?issue.fields.status.name :"",
fromJira: (issue) => (issue.fields.status ? issue.fields.status.name : ''),
},
"assignee": {
assignee: {
toJira: (value) => ({ name: value }),
fromJira: (issue) =>issue.fields.assignee ?issue.fields.assignee.name :"",
fromJira: (issue) => (issue.fields.assignee ? issue.fields.assignee.name : ''),
},
"reporter": {
reporter: {
toJira: (value) => ({ name: value }),
fromJira: (issue) =>issue.fields.reporter ?issue.fields.reporter.name :"",
fromJira: (issue) => (issue.fields.reporter ? issue.fields.reporter.name : ''),
},
"creator": {
creator: {
toJira: () => null,
fromJira: (issue) =>issue.fields.creator ?issue.fields.creator.name :"",
fromJira: (issue) => (issue.fields.creator ? issue.fields.creator.name : ''),
},
"lastViewed": {
lastViewed: {
toJira: () => null,
fromJira: (issue) => issue.fields.lastViewed,
},
"updated": {
updated: {
toJira: () => null,
fromJira: (issue) => issue.fields.updated,
},
"created": {
created: {
toJira: () => null,
fromJira: (issue) => issue.fields.created,
},
"link": {
link: {
toJira: () => null,
fromJira: (issue) => issue.self.replace(/(\w+:\/\/\S+?)\/.*/, `$1/browse/${issue.key}`),
},
"openLink": {
openLink: {
toJira: () => null,
fromJira: (issue) => issue.self.replace(/(\w+:\/\/\S+?)\/.*/, `[Open in Jira]($1/browse/${issue.key})`),
fromJira: (issue) => issue.self.replace(/(\w+:\/\/\S+?)\/.*/, `[${issue.key}]($1/browse/${issue.key})`),
},
"progress": {
progress: {
toJira: () => null,
fromJira: (issue) => issue.fields.aggregateprogress.percent+'%',
fromJira: (issue) => issue.fields.aggregateprogress.percent + '%',
},
};

View file

@ -1,11 +1,8 @@
import JiraPlugin from "../main";
import {TFile} from "obsidian";
import {extractAllJiraSyncValuesFromContent} from "../tools/sectionTools";
import JiraPlugin from '../main';
import { TFile } from 'obsidian';
import { extractAllJiraSyncValuesFromContent } from '../tools/sectionTools';
export async function prepareJiraFieldsFromFile(
plugin: JiraPlugin,
file: TFile
): Promise<Record<string, any>> {
export async function prepareJiraFieldsFromFile(plugin: JiraPlugin, file: TFile): Promise<Record<string, any>> {
const fileContent = await plugin.app.vault.read(file);
const syncSections = extractAllJiraSyncValuesFromContent(fileContent);
@ -13,16 +10,13 @@ export async function prepareJiraFieldsFromFile(
let fields: Record<string, any> = {};
await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
fields = {...syncSections, ...frontmatter};
fields = { ...syncSections, ...frontmatter };
});
return fields;
}
export async function readJiraFieldsFromFile(
plugin: JiraPlugin,
file: TFile
): Promise<Record<string, any>> {
export async function readJiraFieldsFromFile(plugin: JiraPlugin, file: TFile): Promise<Record<string, any>> {
const fileContent = await plugin.app.vault.cachedRead(file);
const syncSections = extractAllJiraSyncValuesFromContent(fileContent);
@ -30,5 +24,5 @@ export async function readJiraFieldsFromFile(
const cachedMetadata = plugin.app.metadataCache.getFileCache(file);
const frontmatter = cachedMetadata?.frontmatter || {};
return {...syncSections, ...frontmatter};
return { ...syncSections, ...frontmatter };
}

View file

@ -1,20 +1,23 @@
import JiraPlugin from "../main";
import {TFile} from "obsidian";
import {createJiraIssue, updateJiraIssue, updateJiraStatus} from "../api";
import {prepareJiraFieldsFromFile} from "./commonPrepareData";
import {localToJiraFields, updateJiraToLocal} from "../tools/mapObsidianJiraFields";
import {JiraIssue, JiraTransitionType} from "../interfaces";
import {obsidianJiraFieldMappings} from "../default/obsidianJiraFieldsMapping";
import JiraPlugin from '../main';
import { TFile } from 'obsidian';
import { createJiraIssue, updateJiraIssue, updateJiraStatus } from '../api';
import { prepareJiraFieldsFromFile } from './commonPrepareData';
import { localToJiraFields, updateJiraToLocal } from '../tools/mapObsidianJiraFields';
import { JiraIssue, JiraTransitionType } from '../interfaces';
import { obsidianJiraFieldMappings } from '../default/obsidianJiraFieldsMapping';
export async function updateIssueFromFile(plugin: JiraPlugin, file: TFile): Promise<string> {
let fields = await prepareJiraFieldsFromFile(plugin, file);
const issueKey = fields.key;
if (!issueKey) {
throw new Error("No issue key found in frontmatter");
throw new Error('No issue key found in frontmatter');
}
fields = localToJiraFields(fields, {...obsidianJiraFieldMappings, ...plugin.settings.fieldMapping.fieldMappings});
fields = localToJiraFields(fields, {
...obsidianJiraFieldMappings,
...plugin.settings.fieldMapping.fieldMappings,
});
await updateJiraIssue(plugin, issueKey, fields);
return issueKey;
}
@ -27,27 +30,36 @@ export async function createIssueFromFile(
if (!fields) {
fields = await prepareJiraFieldsFromFile(plugin, file);
}
fields = localToJiraFields(fields, {...obsidianJiraFieldMappings, ...plugin.settings.fieldMapping.fieldMappings});
fields = localToJiraFields(fields, {
...obsidianJiraFieldMappings,
...plugin.settings.fieldMapping.fieldMappings,
});
// Create the issue
const issueData = await createJiraIssue(plugin, fields);
const issueKey = issueData.key;
// Update frontmatter with the new issue key
await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
frontmatter["key"] = issueKey;
frontmatter['key'] = issueKey;
});
return issueKey;
}
export async function updateStatusFromFile(plugin: JiraPlugin, file: TFile, transition: JiraTransitionType): Promise<string> {
export async function updateStatusFromFile(
plugin: JiraPlugin,
file: TFile,
transition: JiraTransitionType,
): Promise<string> {
const fields = await prepareJiraFieldsFromFile(plugin, file);
if (!fields.key) {
throw new Error("No issue key found in frontmatter");
throw new Error('No issue key found in frontmatter');
}
await updateJiraStatus(plugin, fields.key, transition.id);
await updateJiraToLocal(plugin, file, {fields: {status: {name: transition.status}}} as JiraIssue);
await updateJiraToLocal(plugin, file, {
fields: { status: { name: transition.status } },
} as JiraIssue);
return fields.key;
}

View file

@ -1,36 +1,36 @@
import JiraPlugin from "../main";
import {JiraIssue} from "../interfaces";
import {ensureIssuesFolder} from "../tools/filesUtils";
import {sanitizeFileName} from "../tools/sanitizers";
import {updateJiraToLocal} from "../tools/mapObsidianJiraFields";
import {Notice, TFile, TFolder} from "obsidian";
import {defaultTemplate} from "../default/defaultTemplate";
import { debugLog } from "src/tools/debugLogging";
import JiraPlugin from '../main';
import { JiraIssue } from '../interfaces';
import { ensureIssuesFolder } from '../tools/filesUtils';
import { sanitizeFileName } from '../tools/sanitizers';
import { updateJiraToLocal } from '../tools/mapObsidianJiraFields';
import { Notice, TFile, TFolder } from 'obsidian';
import { defaultTemplate } from '../default/defaultTemplate';
import { debugLog } from '../tools/debugLogging';
function generateFilenameFromTemplate(template: string, issue: JiraIssue): string {
let filename = template;
// Replace {summary} with sanitized summary
const summary = issue.fields?.summary || "";
const summary = issue.fields?.summary || '';
const sanitizedSummary = sanitizeFileName(summary);
filename = filename.replace(/\{summary\}/g, sanitizedSummary);
// Replace {key} with issue key
const key = issue.key || "";
const key = issue.key || '';
filename = filename.replace(/\{key\}/g, key);
// Sanitize the entire filename to remove any prohibited characters
filename = sanitizeFileName(filename);
// Fallback if the result is empty or only whitespace
if (!filename || filename.trim() === "") {
if (!filename || filename.trim() === '') {
// Use key if available, otherwise use a default
if (key) {
filename = key;
} else if (sanitizedSummary) {
filename = sanitizedSummary;
} else {
filename = "jira-issue";
filename = 'jira-issue';
}
}
@ -42,7 +42,7 @@ export async function createOrUpdateIssueNote(plugin: JiraPlugin, issue: JiraIss
await ensureIssuesFolder(plugin);
let targetFile: TFile | null = null;
let targetPath: string = "";
let targetPath: string = '';
if (filePath) {
targetPath = filePath;
@ -74,7 +74,7 @@ export async function createOrUpdateIssueNote(plugin: JiraPlugin, issue: JiraIss
// If still not found, create new file
if (!targetFile) {
const template = plugin.settings.fetchIssue.filenameTemplate || "{summary} ({key})";
const template = plugin.settings.fetchIssue.filenameTemplate || '{summary} ({key})';
const filename = generateFilenameFromTemplate(template, issue);
targetPath = `${plugin.settings.global.issuesFolder}/${filename}.md`;
debugLog(`Issue ${issue.key} not found in cache or filesystem, creating new file: ${targetPath}`);
@ -82,35 +82,30 @@ export async function createOrUpdateIssueNote(plugin: JiraPlugin, issue: JiraIss
}
if (targetFile) {
await updateJiraToLocal(plugin, targetFile, issue)
await plugin.app.workspace.openLinkText(targetFile.path, "");
await updateJiraToLocal(plugin, targetFile, issue);
await plugin.app.workspace.openLinkText(targetFile.path, '');
} else {
const newFile = await createNewIssueFile(plugin, targetPath);
await updateJiraToLocal(plugin, newFile, issue)
await plugin.app.workspace.openLinkText(newFile.path, "");
await updateJiraToLocal(plugin, newFile, issue);
await plugin.app.workspace.openLinkText(newFile.path, '');
// Add new file to cache
plugin.setFilePathForIssueKey(issue.key, newFile.path);
}
new Notice(`Issue ${issue.key} imported successfully`);
} catch (error) {
new Notice("Error creating issue note: " + (error.message || "Unknown error"));
} catch (error: unknown) {
new Notice('Error creating issue note: ' + ((error as Error).message || 'Unknown error'));
console.error(error);
}
}
async function createNewIssueFile(plugin: JiraPlugin, filePath: string): Promise<TFile> {
let initialContent = '';
async function createNewIssueFile(
plugin: JiraPlugin,
filePath: string
): Promise<TFile> {
let initialContent = "";
let templatePath = plugin.settings.global.templatePath
if (templatePath && !templatePath.endsWith(".md")) {
templatePath += ".md";
let templatePath = plugin.settings.global.templatePath;
if (templatePath && !templatePath.endsWith('.md')) {
templatePath += '.md';
}
if (templatePath && templatePath.trim() !== "") {
if (templatePath && templatePath.trim() !== '') {
const templateFile = plugin.app.vault.getFileByPath(templatePath);
if (templateFile) {
// Use the template as initial content
@ -119,7 +114,7 @@ async function createNewIssueFile(
new Notice(`Template file not found: ${templatePath}, using default template`);
}
}
if (initialContent === "") initialContent = defaultTemplate
if (initialContent === '') initialContent = defaultTemplate;
// Create the file with initial content
await plugin.app.vault.create(filePath, initialContent);
@ -127,9 +122,9 @@ async function createNewIssueFile(
// Get file reference and update frontmatter
const newFile = plugin.app.vault.getFileByPath(filePath);
if (!newFile) {
throw new Error("Could not create file");
throw new Error('Could not create file');
}
return newFile
return newFile;
}
async function findFileByIssueKey(plugin: JiraPlugin, issueKey: string): Promise<TFile | null> {

View file

@ -1,5 +1,5 @@
import { App } from "obsidian";
import JiraPlugin from "../main";
import { App } from 'obsidian';
import JiraPlugin from '../main';
/**
* Function type for field mappings
@ -46,4 +46,3 @@ export interface ValidationResult {
isValid: boolean;
errorMessage?: string;
}

View file

@ -53,9 +53,8 @@ async function compileLocale(locale) {
const data = yaml.parse(content);
const fileNameWithoutExt = path.basename(item, '.yaml');
const targetKeys = fileNameWithoutExt === 'index'
? parentKeys
: [...parentKeys, fileNameWithoutExt];
const targetKeys =
fileNameWithoutExt === 'index' ? parentKeys : [...parentKeys, fileNameWithoutExt];
let target = result;
for (const key of targetKeys.slice(0, -1)) {
@ -90,7 +89,6 @@ function deepMerge(target, source) {
return Object.assign(target || {}, source);
}
// Watch mode
if (process.argv.includes('--watch')) {
console.log('👀 Watching for changes...');
@ -105,10 +103,12 @@ if (process.argv.includes('--watch')) {
}, debounceDelay);
// Watch for changes in the compiled directory
chokidar.watch(SOURCE_DIR, {
chokidar
.watch(SOURCE_DIR, {
ignoreInitial: true,
ignored: /.*~$/, // Игнорировать скрытые файлы
}).on('all', (event, path) => {
})
.on('all', (event, path) => {
if (event === 'change' || event === 'add' || event === 'unlink') {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {

View file

@ -2,7 +2,7 @@ desc: Set the Jira issue summary
key:
name: Summary
placeholder: ""
placeholder: 'Enter issue summary...'
desc: Enter new task name (e.g., "My great issue")
submit: Submit

View file

@ -14,7 +14,7 @@ submit: Search Issues
preview:
total: Total {total} issues found
showing: " (showing first {limit} issues)"
error: "{error}"
showing: ' (showing first {limit} issues)'
error: '{error}'
loading: Loading...
empty_input: Enter a JQL query to see preview

View file

@ -2,20 +2,20 @@ name: Add work log
spent:
name: Time spent
desc: "Format: weeks days hours, minutes"
placeholder: "3w 4d 12h 30m"
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"
desc: 'Format: DD/MMM/YY HH:MM'
placeholder: '03/feb/25 15:22'
comment:
name: Work description
desc: Optional
placeholder: ""
placeholder: 'Enter work description...'
months: ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]
months: ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
submit: Submit

View file

@ -18,7 +18,7 @@ auth:
desc: Choose how to authenticate with Jira
options:
bearer: Bearer Token (PAT)
basic: Basic Auth (Username + PAT)
basic: Basic Auth (email + PAT)
session: Session Cookie (Username + Password)
pat:
@ -45,3 +45,12 @@ ping:
title: Ping Jira
desc: Check your connection to Jira
button: Ping Jira
name:
title: Connection name
desc: A friendly name for this connection
def: My Jira Connection
select_connection: Select connection
add_connection: Add connection
no_connection: No connection

View file

@ -3,9 +3,9 @@ title: Fetch issue settings
note:
desc0: Important!
desc1: " The following 'fields' and 'expand' fields will be used across most of the Jira Manager requests related with issues. Make sure you know what you're doing."
desc2: "For more info, go to official "
link_label: "docs"
desc3: ". While filling, list these params with ',' separator. Example: \"summary, description\""
desc2: 'For more info, go to official '
link_label: 'docs'
desc3: '. While filling, list these params with '','' separator. Example: "summary, description"'
key:
name: View raw issue data
@ -13,17 +13,16 @@ key:
fields:
name: Issue fields ('fields')
desc: "Main Jira fields"
def: "*all, -comment"
desc: 'Main Jira fields'
def: '*all, -comment'
expand:
name: Expand fields ('expand')
desc: "Extra Jira fields"
def: "renderedFields, changelog"
desc: 'Extra Jira fields'
def: 'renderedFields, changelog'
filenameTemplate:
name: Filename template
desc: |
Template for created issue filenames. Use {summary} for issue summary and {key} for issue key
placeholder: "{summary} ({key})"
placeholder: '{summary} ({key})'

View file

@ -4,7 +4,7 @@ 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: "
name: '⚠️ Note: '
desc: Field mapping uses JavaScript lambda functions.
example:
name: Example field mapping
@ -13,6 +13,7 @@ example:
from_jira: From Jira
to_jira: To Jira
field_name: Field name
field_name_placeholder: Enter field name
add_mapping: Add mapping
add_mapping_tooltip: Add new field mapping
@ -20,6 +21,7 @@ reset: Reset
reset_tooltip: Reset
reset_confirm_title: Reset field mapping
reset_confirm_text: Field mapping will be cleaned up. Are you sure?
remove_field: Remove field
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

@ -1,5 +1,5 @@
title: Timekeep work log statistics
description: Manage and send work logs to Jira
description: 'Manage and send work logs to Jira (supports Timekeep and Super Simple Time Tracker formats)'
settings:
send_comments:
@ -40,7 +40,7 @@ settings:
display:
select_period: Select Period
select_period_placeholder: Select a time period...
selected_period: "Selected: {period}"
selected_period: 'Selected: {period}'
all_periods: All Recent Periods
no_entries: No timekeep entries found.
period_of: Period of {date}
@ -60,8 +60,8 @@ actions:
messages:
select_period_first: Please select a time period first.
refresh_success: Data refreshed successfully
refresh_error: "Failed to refresh data: {error}"
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}"
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

@ -2,7 +2,7 @@ desc: Задать название задачи Jira
key:
name: Название задачи
placeholder: ""
placeholder: 'Введите название задачи...'
desc: Введите название задачи (например, "Моя замечательная задача")
submit: Подтвердить

View file

@ -13,8 +13,8 @@ maxResults:
submit: Найти задачи
preview:
total: "{total} задач найдено"
showing: " (показаны первые {limit} задач)"
error: "{error}"
total: '{total} задач найдено'
showing: ' (показаны первые {limit} задач)'
error: '{error}'
loading: Загрузка...
empty_input: Введите JQL запрос для просмотра превью задач

View file

@ -11,7 +11,7 @@ start:
comment:
name: Описание работы
desc: Необязательно
placeholder: ''
placeholder: 'Введите описание работы...'
months:
- янв

View file

@ -15,7 +15,7 @@ auth:
desc: Выберите способ подключения к Jira
options:
bearer: Bearer Token (PAT)
basic: Basic Auth (Имя пользователя + PAT)
basic: Basic Auth (email + PAT)
session: Session Cookie (Имя пользователя + Пароль)
pat:
title: Jira PAT
@ -37,3 +37,12 @@ ping:
title: Ping Jira
desc: Проверить подключение к Jira
button: Ping Jira
name:
title: Название подключения
desc: Понятное имя для этого подключения
def: Мое подключение к Jira
select_connection: Выбрать подключение
add_connection: Добавить подключение
no_connection: Нет подключения

View file

@ -2,22 +2,22 @@ title: Настройка получаемых полей
note:
desc0: Важно!
desc1: " Следующие поля 'fields' и 'expand' будут использоваться в большинстве запросах Jira Manager. Убедитесь, что вы знаете, что делаете."
desc2: "Для дополнительной информации можете посмотреть официальную "
link_label: "документацию"
desc3: ". Заполняя поля, перечислите параметры через разделитель ','. Пример: \"summary, description\""
desc2: 'Для дополнительной информации можете посмотреть официальную '
link_label: 'документацию'
desc3: '. Заполняя поля, перечислите параметры через разделитель '',''. Пример: "summary, description"'
key:
name: Просмотр необработанных данных задачи
desc: Введите номер задачи Jira, чтобы просмотреть необработанные данные, полученные из API
fields:
name: Поля задачи ('fields')
desc: "Главные поля Jira"
def: "*all, -comment"
desc: 'Главные поля Jira'
def: '*all, -comment'
expand:
name: Расширенные поля ('expand')
desc: "Дополнительные поля Jira"
def: "renderedFields, changelog"
desc: 'Дополнительные поля Jira'
def: 'renderedFields, changelog'
filenameTemplate:
name: Шаблон названия файла
desc: |
Шаблон для названий файлов созданных задач. Используйте {summary} для названия задачи и {key} для ключа задачи
placeholder: "{summary} ({key})"
placeholder: '{summary} ({key})'

View file

@ -6,7 +6,7 @@ fv:
desc: Проверять поля перед сохранением базируясь на стандартном ответе API
(при проверке не учитываются пользовательские поля)
secNote:
name: "⚠️ Примечание: "
name: '⚠️ Примечание: '
desc: Сопоставление полей использует JavaScript lambda функции.
example:
name: Примеры сопоставления полей
@ -14,11 +14,16 @@ example:
from_jira: Из Jira
to_jira: В Jira
field_name: Название поля
field_name_placeholder: Введите название поля
add_mapping: Добавить сопоставление
add_mapping_tooltip: Добавить новое сопоставление полей
reset: Убрать все
reset_tooltip: Убрать все
reset_confirm_title: Сбросить сопоставление полей
reset_confirm_text: Сопоставление полей будет очищено. Вы уверены?
remove_field: Удалить поле
reload_defaults: Загрузить по умолчанию
reload_defaults_tooltip: Обновить
reload_confirm_title: Обновить сопоставления полей по умолчанию
reload_confirm_text: Текущие сопоставления полей будут перезаписаны сопоставлениями по умолчанию. Вы уверены?

View file

@ -1,5 +1,5 @@
title: Статистика ведения журнала работы через Timekeep
description: Управление и отправка журнала работы в Jira
description: 'Управление и отправка журнала работы в Jira (поддерживает формат Timekeep и Super Simple Time Tracker)'
settings:
send_comments:
name: Отправлять комментарии
@ -38,6 +38,7 @@ display:
no_entries: Записи рабочего лога не найдены.
period_of: Период от {date}
table:
total: Всего
task: Задача
block_path: Подзадача
start_time: Время начала

View file

@ -7,5 +7,7 @@ invalid_exp: Некорректное выражение
from_jira: Из Jira
value: Значение
add_mapping: Добавить сопоставление
add_mapping_tooltip: Добавить тестовое сопоставление полей
reset: Сбросить
reset_tooltip: Сбросить

View file

@ -23,7 +23,7 @@ function replacePlaceholders(template: string, dict: Record<string, unknown>): s
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;
return Object.prototype.hasOwnProperty.call(dict, trimmedKey) ? String(dict[trimmedKey]) : match;
});
}
@ -63,6 +63,6 @@ export const useTranslations = (prefix?: string) => {
return {
t: translate,
locale: currentLocale,
setPrefix: (newPrefix: string) => useTranslations(newPrefix)
setPrefix: (newPrefix: string) => useTranslations(newPrefix),
};
};

View file

@ -3,65 +3,105 @@ const path = require('path');
const chokidar = require('chokidar');
const COMPILED_DIR = path.join(__dirname, 'compiled');
const SRC_DIR = path.join(__dirname, '../../src');
// 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;
if (typeof obj[key] === 'string') {
result.add(fullKey);
if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
} else 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;
}
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;
}
if (current[part] === undefined) return undefined;
current = current[part];
}
return current;
}
// Function to validate translations
function findTranslationsInTsFiles() {
const usedKeys = new Set();
const files = [];
function walkDir(dir) {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
walkDir(fullPath);
} else if (item.endsWith('.ts')) {
files.push(fullPath);
}
}
}
walkDir(SRC_DIR);
const useTranslationsRegex = /const\s+t\s*=\s*useTranslations\(["']([^"']+)["']\)\.t\s*;/g;
const tCallRegex = /(?<!\w)t\(["']([^"']+)["']\)/g;
for (const file of files) {
try {
const content = fs.readFileSync(file, 'utf8');
const relativePath = path.relative(SRC_DIR, file);
let match;
let prefix = null;
useTranslationsRegex.lastIndex = 0;
while ((match = useTranslationsRegex.exec(content)) !== null) {
prefix = match[1];
}
if (prefix) {
tCallRegex.lastIndex = 0;
while ((match = tCallRegex.exec(content)) !== null) {
const key = match[1];
const fullKey = `${prefix}.${key}`;
usedKeys.add(fullKey);
}
}
} catch (err) {
console.error(`Error reading ${file}:`, err.message);
}
}
return usedKeys;
}
function validateTranslations() {
console.log('🔍 Validating translation files...');
// Read all compiled JSON files
const files = fs.readdirSync(COMPILED_DIR).filter(file => file.endsWith('.json'));
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
console.log('\n📊 Scanning .ts files for used translation keys...');
const usedKeys = findTranslationsInTsFiles();
console.log(`📝 Found ${usedKeys.size} translation keys used in code`);
const translations = {};
for (const file of files) {
const locale = path.basename(file, '.json');
@ -69,7 +109,6 @@ function validateTranslations() {
translations[locale] = fs.readJsonSync(filePath);
}
// Get all locales
const locales = Object.keys(translations);
console.log(`📁 Found ${locales.length} locales: ${locales.join(', ')}`);
@ -78,116 +117,107 @@ function validateTranslations() {
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: {}
usedButNotInLocale: {},
inLocaleButNotUsed: {},
};
// Initialize stats for each locale
for (const locale of locales) {
if (locale !== referenceLocale) {
stats.missingKeys[locale] = [];
stats.extraKeys[locale] = [];
stats.typeErrors[locale] = [];
stats.usedButNotInLocale[locale] = [];
stats.inLocaleButNotUsed[locale] = [];
}
}
stats.inLocaleButNotUsed[referenceLocale] = [];
stats.usedButNotInLocale[referenceLocale] = [];
// 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);
for (const refKey of referenceKeys) {
if (!keyExists(translations[locale], refKey)) {
stats.missingKeys[locale].push(refKey);
}
}
if (typeof refValue !== typeof localeValue) {
stats.typeErrors[locale].push({
key,
refType: typeof refValue,
localeType: typeof localeValue
});
for (const locKey of localeKeys) {
if (!keyExists(translations[referenceLocale], locKey)) {
stats.extraKeys[locale].push(locKey);
}
}
for (const usedKey of usedKeys) {
if (!keyExists(translations[locale], usedKey)) {
stats.usedButNotInLocale[locale].push(usedKey);
}
}
for (const locKey of localeKeys) {
if (!usedKeys.has(locKey)) {
stats.inLocaleButNotUsed[locale].push(locKey);
}
}
}
// 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}`);
});
console.log(`\n${locale} is missing ${missing.length} keys from reference:`);
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}`);
});
console.log(`\n⚠️ ${locale} has ${extra.length} extra keys not in reference:`);
extra.forEach((key) => console.log(` - ${key}`));
}
}
// Print type errors
for (const locale in stats.typeErrors) {
const typeErrors = stats.typeErrors[locale];
if (typeErrors.length > 0) {
for (const locale in stats.usedButNotInLocale) {
const missing = stats.usedButNotInLocale[locale];
if (missing.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}`);
});
console.log(`\n🚨 ${locale}: ${missing.length} keys are used in code but NOT in localization:`);
missing.forEach((key) => console.log(` - ${key}`));
}
}
for (const locale in stats.inLocaleButNotUsed) {
const unused = stats.inLocaleButNotUsed[locale];
if (unused.length > 0) {
console.log(`\n💡 ${locale}: ${unused.length} keys are in localization but NOT used in code:`);
unused.forEach((key) => console.log(` - ${key}`));
}
}
// 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!');
console.log('\n✅ All locales have consistent structure and all used keys are defined!');
}
return hasIssues;
}
// Function to check for empty values
function checkEmptyValues(obj, locale, prefix = '') {
for (const key in obj) {
const fullKey = prefix ? `${prefix}.${key}` : key;
@ -203,42 +233,33 @@ function checkEmptyValues(obj, locale, prefix = '') {
}
}
// 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);
debounceTimer = setTimeout(() => main(), debounceDelay);
// Watch for changes in the compiled directory
chokidar.watch(COMPILED_DIR, {
chokidar
.watch([COMPILED_DIR, SRC_DIR], {
ignoreInitial: true,
ignored: /.*~$/, // Игнорировать скрытые файлы
}).on('all', (event, path) => {
ignored: /.*~$/,
})
.on('all', (event, path) => {
if (event === 'change' || event === 'add' || event === 'unlink') {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
console.log(`🔁 Detected changes in ${path} (${event}), revalidating...`);
console.log(`\n🔁 Detected changes in ${path} (${event}), revalidating...`);
main();
}, debounceDelay);
}
});
} else {
// Run once
main();
}

View file

@ -1,20 +1,24 @@
import {Plugin } from "obsidian";
import { JiraSettingTab } from "./settings/JiraSettingTab";
import {DEFAULT_SETTINGS, JiraSettingsInterface} from "./settings/default";
import { Plugin } from 'obsidian';
import { JiraSettingTab } from './settings/JiraSettingTab';
import { ConnectionSettingsInterface, DEFAULT_SETTINGS, JiraSettingsInterface } from './settings/default';
import {
registerUpdateIssueCommand, registerUpdateWorkLogManuallyCommand,
registerGetCurrentIssueCommand, registerUpdateWorkLogBatchCommand,
registerCreateIssueCommand, registerGetIssueCommandWithCustomKey, registerUpdateIssueStatusCommand,
registerBatchFetchIssuesCommand
} from "./commands";
import {transform_string_to_functions_mappings} from "./tools/convertFunctionString";
import {createJiraSyncExtension} from "./postprocessing/livePreview";
import {hideJiraPointersReading} from "./postprocessing/reading";
import { buildCacheFromFilesystem, validateCache } from "./tools/cacheUtils";
import {checkMigrateSettings} from "./tools/migrateSettings";
registerUpdateIssueCommand,
registerUpdateWorkLogManuallyCommand,
registerGetCurrentIssueCommand,
registerUpdateWorkLogBatchCommand,
registerCreateIssueCommand,
registerGetIssueCommandWithCustomKey,
registerUpdateIssueStatusCommand,
registerBatchFetchIssuesCommand,
} from './commands';
import { transform_string_to_functions_mappings } from './tools/convertFunctionString';
import { createJiraSyncExtension } from './postprocessing/livePreview';
import { hideJiraPointersReading } from './postprocessing/reading';
import { buildCacheFromFilesystem, validateCache } from './tools/cacheUtils';
import { checkMigrateSettings } from './tools/migrateSettings';
export default class JiraPlugin extends Plugin {
settings: JiraSettingsInterface;
settings!: JiraSettingsInterface;
// In-memory cache for instant access during synchronization
private issueKeyToFilePathCache: Map<string, string> = new Map();
@ -48,7 +52,6 @@ export default class JiraPlugin extends Plugin {
// Register vault event listeners for cache maintenance
this.registerVaultEventListeners();
}
async loadSettings() {
@ -57,7 +60,9 @@ export default class JiraPlugin extends Plugin {
const new_data = checkMigrateSettings(old_data, this.saveSettings);
this.settings = Object.assign({}, DEFAULT_SETTINGS, new_data);
this.settings.fieldMapping.fieldMappings = await transform_string_to_functions_mappings(this.settings.fieldMapping.fieldMappingsStrings);
this.settings.fieldMapping.fieldMappings = await transform_string_to_functions_mappings(
this.settings.fieldMapping.fieldMappingsStrings,
);
}
async saveSettings() {
@ -72,13 +77,20 @@ export default class JiraPlugin extends Plugin {
}
getAllIssueKeysMap(): Map<string, string> {
return this.issueKeyToFilePathCache
return this.issueKeyToFilePathCache;
}
getFilePathForIssueKey(issueKey: string): string | undefined {
return this.issueKeyToFilePathCache.get(issueKey);
}
getCurrentConnection(): ConnectionSettingsInterface | null {
if (!this.settings.connections || this.settings.connections.length === 0) {
return null;
}
return this.settings.connections[this.settings.currentConnectionIndex];
}
setFilePathForIssueKey(issueKey: string, filePath: string) {
this.issueKeyToFilePathCache.set(issueKey, filePath);
this.settings.issueKeyToFilePathCache[issueKey] = filePath;
@ -103,7 +115,6 @@ export default class JiraPlugin extends Plugin {
}
private registerVaultEventListeners() {
// Handle file renames
this.registerEvent(
this.app.vault.on('rename', (file, oldPath) => {
@ -113,7 +124,7 @@ export default class JiraPlugin extends Plugin {
break;
}
}
})
}),
);
// Handle file deletions
@ -125,7 +136,7 @@ export default class JiraPlugin extends Plugin {
break;
}
}
})
}),
);
}
}

View file

@ -1,13 +1,13 @@
import {App, Modal, Setting} from "obsidian";
import {useTranslations} from "../localization/translator";
import { App, Modal, Setting } from 'obsidian';
import { useTranslations } from '../localization/translator';
const t = useTranslations("modals.add_summary").t;
const t = useTranslations('modals.add_summary').t;
/**
* Modal for searching issues by key
*/
export class IssueAddSummaryModal extends Modal {
private onSubmit: (result: string) => void;
private summary: string = "";
private summary: string = '';
constructor(app: App, onSubmit: (result: string) => void) {
super(app);
@ -15,33 +15,30 @@ export class IssueAddSummaryModal extends Modal {
}
onOpen() {
this.contentEl.createEl("h2", {text: t("desc")});
this.contentEl.createEl('h2', { text: t('desc') });
new Setting(this.contentEl)
.setName(t("key.name"))
.setDesc(t("key.desc"))
.setName(t('key.name'))
.setDesc(t('key.desc'))
.addText((text) =>
text
.setPlaceholder(t("key.placeholder"))
.onChange((value) => {
text.setPlaceholder(t('key.placeholder')).onChange((value) => {
this.summary = value;
})
}),
);
new Setting(this.contentEl)
.addButton((btn) =>
new Setting(this.contentEl).addButton((btn) =>
btn
.setButtonText(t("submit"))
.setButtonText(t('submit'))
.setCta()
.onClick(() => {
this.close();
this.onSubmit(this.summary);
})
}),
);
// Handle Enter key
this.contentEl.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
this.contentEl.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.close();
this.onSubmit(this.summary);

View file

@ -1,16 +1,16 @@
import {App, Modal, Setting} from "obsidian";
import {useTranslations} from "../localization/translator";
import {JQLPreview} from "../settings/components/JQLPreview";
import JiraPlugin from "../main";
import { App, Modal, Setting } from 'obsidian';
import { useTranslations } from '../localization/translator';
import { JQLPreview } from '../settings/components/JQLPreview';
import JiraPlugin from '../main';
const t = useTranslations("modals.search").t;
const t = useTranslations('modals.search').t;
/**
* Modal for searching issues by key
*/
export class IssueSearchModal extends Modal {
private onSubmit: (result: string) => void;
private plugin: JiraPlugin;
private issueKey: string = "";
private issueKey: string = '';
private previewEl?: HTMLElement;
private preview?: JQLPreview;
private debounceTimer?: number;
@ -22,38 +22,35 @@ export class IssueSearchModal extends Modal {
}
onOpen() {
this.contentEl.createEl("h2", {text: t("desc")});
this.contentEl.createEl('h2', { text: t('desc') });
new Setting(this.contentEl)
.setName(t("key.name"))
.setDesc(t("key.desc"))
.setName(t('key.name'))
.setDesc(t('key.desc'))
.addText((text) =>
text
.setPlaceholder(t("key.placeholder"))
.onChange((value) => {
text.setPlaceholder(t('key.placeholder')).onChange((value) => {
this.issueKey = value;
this.schedulePreview();
})
}),
);
this.previewEl = this.contentEl.createDiv();
this.preview = new JQLPreview(this.plugin, this.previewEl, 1, false);
this.preview.loadPreview("")
this.preview.loadPreview('');
new Setting(this.contentEl)
.addButton((btn) =>
new Setting(this.contentEl).addButton((btn) =>
btn
.setButtonText(t("submit"))
.setButtonText(t('submit'))
.setCta()
.onClick(() => {
this.close();
this.onSubmit(this.issueKey);
})
}),
);
// Handle Enter key
this.contentEl.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
this.contentEl.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
this.close();
this.onSubmit(this.issueKey);
@ -73,7 +70,7 @@ export class IssueSearchModal extends Modal {
this.debounceTimer = window.setTimeout(() => {
if (this.preview) {
this.preview.loadPreview(this.issueKey? "key = "+this.issueKey : "").catch(() => {
this.preview.loadPreview(this.issueKey ? 'key = ' + this.issueKey : '').catch(() => {
// Error handling is done within the preview component
});
}

View file

@ -1,8 +1,8 @@
import {App, SuggestModal} from "obsidian";
import {JiraTransitionType} from "../interfaces";
import {useTranslations} from "../localization/translator";
import { App, SuggestModal } from 'obsidian';
import { JiraTransitionType } from '../interfaces';
import { useTranslations } from '../localization/translator';
const t = useTranslations("modals.status").t;
const t = useTranslations('modals.status').t;
/**
* Modal for selecting an issue status
@ -15,18 +15,19 @@ export class IssueStatusModal extends SuggestModal<JiraTransitionType> {
super(app);
this.onSubmit = onSubmit;
this.issueTransitions = issueTransitions;
this.setPlaceholder(t("placeholder"));
this.setPlaceholder(t('placeholder'));
}
getSuggestions(query: string): JiraTransitionType[] {
return this.issueTransitions.filter((transition) =>
transition.status && transition.status.toLowerCase().includes(query.toLowerCase()) ||
transition.action && transition.action.toLowerCase().includes(query.toLowerCase())
return this.issueTransitions.filter(
(transition) =>
(transition.status && transition.status.toLowerCase().includes(query.toLowerCase())) ||
(transition.action && transition.action.toLowerCase().includes(query.toLowerCase())),
);
}
renderSuggestion(type: JiraTransitionType, el: HTMLElement) {
let result = "";
let result = '';
if (type.action && type.status) {
if (type.action === type.status) {
result = type.action;
@ -38,7 +39,7 @@ export class IssueStatusModal extends SuggestModal<JiraTransitionType> {
} else if (type.status) {
result = type.status;
}
el.createEl("div", {text: result});
el.createEl('div', { text: result });
}
onChooseSuggestion(type: JiraTransitionType) {

View file

@ -1,8 +1,8 @@
import {App, SuggestModal} from "obsidian";
import {JiraIssueType} from "../interfaces";
import {useTranslations} from "../localization/translator";
import { App, SuggestModal } from 'obsidian';
import { JiraIssueType } from '../interfaces';
import { useTranslations } from '../localization/translator';
const t = useTranslations("modals.type").t;
const t = useTranslations('modals.type').t;
/**
* Modal for selecting an issue type
@ -15,17 +15,15 @@ export class IssueTypeModal extends SuggestModal<JiraIssueType> {
super(app);
this.onSubmit = onSubmit;
this.issueTypes = issueTypes;
this.setPlaceholder(t("placeholder"));
this.setPlaceholder(t('placeholder'));
}
getSuggestions(query: string): JiraIssueType[] {
return this.issueTypes.filter((type) =>
type.name.toLowerCase().includes(query.toLowerCase())
);
return this.issueTypes.filter((type) => type.name.toLowerCase().includes(query.toLowerCase()));
}
renderSuggestion(type: JiraIssueType, el: HTMLElement) {
el.createEl("div", {text: type.name});
el.createEl('div', { text: type.name });
}
onChooseSuggestion(type: JiraIssueType) {

View file

@ -1,19 +1,19 @@
import {App, Modal, Notice, Setting} from "obsidian";
import {useTranslations} from "../localization/translator";
import { App, Modal, Notice, Setting } from 'obsidian';
import { useTranslations } from '../localization/translator';
const t = useTranslations("modals.worklog").t;
const t = useTranslations('modals.worklog').t;
/**
* Modal for adding work log entries
*/
export class IssueWorkLogModal extends Modal {
private onSubmit: (timeSpent: string, startDate: string, workDescription: string) => void;
private timeSpent: string = "";
private startDate: string = "";
private workDescription: string = "";
private displayDate: string = "";
private timeSpentSetting: Setting;
private dateSetting: Setting;
private timeSpent: string = '';
private startDate: string = '';
private workDescription: string = '';
private displayDate: string = '';
private timeSpentSetting!: Setting;
private dateSetting!: Setting;
constructor(app: App, onSubmit: (timeSpent: string, startDate: string, workDescription: string) => void) {
super(app);
@ -26,7 +26,7 @@ export class IssueWorkLogModal extends Modal {
private initializeDates(date: Date) {
// Set the ISO format for API
this.startDate = date.toISOString().split('.')[0] + ".000+0000";
this.startDate = date.toISOString().split('.')[0] + '.000+0000';
// Set the display format (01/Jun/21 09:00 AM)
const months = t('months') as any;
@ -49,7 +49,7 @@ export class IssueWorkLogModal extends Modal {
const [hours, minutes] = timePart.split(':');
const months = Object.fromEntries(
(t('months') as any).map((month: string, index: number) => [month, index])
(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,
@ -62,11 +62,17 @@ export class IssueWorkLogModal extends Modal {
const fullYear = parseInt(year) + 2000;
// Create date object
const date = new Date(fullYear, months[month as keyof typeof months], parseInt(day), hour, parseInt(minutes));
if (isNaN(date.getTime())) throw new Error("Invalid date");
const date = new Date(
fullYear,
months[month as keyof typeof months],
parseInt(day),
hour,
parseInt(minutes),
);
if (isNaN(date.getTime())) throw new Error('Invalid date');
return date;
} catch (error) {
} catch {
return null;
}
}
@ -83,7 +89,7 @@ export class IssueWorkLogModal extends Modal {
.setValue(this.timeSpent)
.onChange((value) => {
this.timeSpent = value;
})
}),
);
this.dateSetting = new Setting(this.contentEl)
@ -95,7 +101,7 @@ export class IssueWorkLogModal extends Modal {
.setValue(this.displayDate)
.onChange((value) => {
this.displayDate = value;
})
}),
);
// Start Date field with date picker icon
@ -164,28 +170,25 @@ export class IssueWorkLogModal extends Modal {
// Work Description field
new Setting(this.contentEl)
.setName(t("comment.name"))
.setDesc(t("comment.name"))
.setName(t('comment.name'))
.setDesc(t('comment.name'))
.addTextArea((text) =>
text
.setPlaceholder(t("comment.placeholder"))
.onChange((value) => {
text.setPlaceholder(t('comment.placeholder')).onChange((value) => {
this.workDescription = value;
})
}),
)
.setClass("work-description-container");
.setClass('work-description-container');
// Make the textarea larger
const textareaComponent = this.contentEl.querySelector(".work-description-container textarea");
const textareaComponent = this.contentEl.querySelector('.work-description-container textarea');
if (textareaComponent) {
(textareaComponent as HTMLTextAreaElement).rows = 5;
}
// Submit button
new Setting(this.contentEl)
.addButton((btn) =>
new Setting(this.contentEl).addButton((btn) =>
btn
.setButtonText(t("submit"))
.setButtonText(t('submit'))
.setCta()
.onClick(() => {
if (!this.validateInputs()) {
@ -193,7 +196,7 @@ export class IssueWorkLogModal extends Modal {
}
this.close();
this.onSubmit(this.timeSpent, this.startDate, this.workDescription);
})
}),
);
}
@ -202,7 +205,7 @@ export class IssueWorkLogModal extends Modal {
*/
private validateInputs(): boolean {
if (!this.timeSpent.trim()) {
new Notice(t("warns.no_time"));
new Notice(t('warns.no_time'));
this.timeSpentSetting.setClass('invalid');
return false;
}
@ -210,20 +213,20 @@ export class IssueWorkLogModal extends Modal {
// Validate time spent format
const timeSpentPattern = /^(\d+[wdhm]\s*)+$/;
if (!timeSpentPattern.test(this.timeSpent)) {
new Notice(t("warns.invalid_time"));
new Notice(t('warns.invalid_time'));
this.timeSpentSetting.setClass('invalid');
return false;
}
if (!this.displayDate.trim()) {
new Notice(t("warns.no_start"));
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(t("warns.invalid_start"));
new Notice(t('warns.invalid_start'));
this.dateSetting.setClass('invalid');
return false;
}

View file

@ -1,9 +1,9 @@
import {App, Modal, Setting} from "obsidian";
import {useTranslations} from "../localization/translator";
import JiraPlugin from "../main";
import {JQLPreview} from "../settings/components/JQLPreview";
import { App, Modal, Setting } from 'obsidian';
import { useTranslations } from '../localization/translator';
import JiraPlugin from '../main';
import { JQLPreview } from '../settings/components/JQLPreview';
const t = useTranslations("modals.jql_search").t;
const t = useTranslations('modals.jql_search').t;
/**
* Modal for searching issues by JQL query
@ -11,7 +11,7 @@ const t = useTranslations("modals.jql_search").t;
export class JQLSearchModal extends Modal {
private onSubmit: (jql: string) => void;
private plugin: JiraPlugin;
private jql: string = "";
private jql: string = '';
private previewEl?: HTMLElement;
private preview?: JQLPreview;
private debounceTimer?: number;
@ -23,43 +23,41 @@ export class JQLSearchModal extends Modal {
}
onOpen() {
this.contentEl.createEl("h2", {text: t("desc")});
this.contentEl.createEl('h2', { text: t('desc') });
new Setting(this.contentEl)
.setName(t("jql.name"))
.setDesc(t("jql.desc"))
.setName(t('jql.name'))
.setDesc(t('jql.desc'))
.addTextArea((text) => {
text
.setPlaceholder(t("jql.placeholder"))
text.setPlaceholder(t('jql.placeholder'))
.setValue(this.jql)
.onChange((value) => {
this.jql = value;
this.schedulePreview();
});
text.inputEl.style.height = "100px";
text.inputEl.style.height = '100px';
});
// Create preview container and initialize preview component
this.previewEl = this.contentEl.createDiv();
this.preview = new JQLPreview(this.plugin, this.previewEl, 5);
this.preview.loadPreview("")
this.preview.loadPreview('');
new Setting(this.contentEl)
.addButton((btn) =>
new Setting(this.contentEl).addButton((btn) =>
btn
.setButtonText(t("submit"))
.setButtonText(t('submit'))
.setCta()
.onClick(() => {
if (this.jql.trim()) {
this.close();
this.onSubmit(this.jql.trim());
}
})
}),
);
// Handle Enter key (Ctrl+Enter for textarea)
this.contentEl.addEventListener("keydown", (event) => {
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
this.contentEl.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
event.preventDefault();
if (this.jql.trim()) {
this.close();

View file

@ -1,8 +1,8 @@
import {App, SuggestModal} from "obsidian";
import {JiraProject} from "../interfaces";
import {useTranslations} from "../localization/translator";
import { App, SuggestModal } from 'obsidian';
import { JiraProject } from '../interfaces';
import { useTranslations } from '../localization/translator';
const t = useTranslations("modals.project").t;
const t = useTranslations('modals.project').t;
/**
* Modal for selecting a project
@ -15,20 +15,20 @@ export class ProjectModal extends SuggestModal<JiraProject> {
super(app);
this.onSubmit = onSubmit;
this.projects = projects;
this.setPlaceholder(t("placeholder"));
this.setPlaceholder(t('placeholder'));
}
getSuggestions(query: string): JiraProject[] {
return this.projects.filter(
(project) =>
project.id.toLowerCase().includes(query.toLowerCase()) ||
project.name.toLowerCase().includes(query.toLowerCase())
project.name.toLowerCase().includes(query.toLowerCase()),
);
}
renderSuggestion(project: JiraProject, el: HTMLElement) {
el.createEl("div", {text: project.name});
el.createEl("small", {text: project.id});
el.createEl('div', { text: project.name });
el.createEl('small', { text: project.id });
}
onChooseSuggestion(project: JiraProject) {

View file

@ -1,6 +1,6 @@
export * from "./IssueSearchModal";
export * from "./IssueStatusModal";
export * from "./IssueTypeModal";
export * from "./IssueWorkLogModal";
export * from "./ProjectModal";
export * from "./JQLSearchModal";
export * from './IssueSearchModal';
export * from './IssueStatusModal';
export * from './IssueTypeModal';
export * from './IssueWorkLogModal';
export * from './ProjectModal';
export * from './JQLSearchModal';

View file

@ -1,11 +1,11 @@
import {Extension, RangeSetBuilder} from "@codemirror/state";
import {Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate} from "@codemirror/view";
import {MarkdownView} from "obsidian";
import JiraPlugin from "../main";
import { Extension, RangeSetBuilder } from '@codemirror/state';
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
import { MarkdownView } from 'obsidian';
import JiraPlugin from '../main';
export function createJiraSyncExtension(plugin: JiraPlugin): Extension {
return ViewPlugin.fromClass(class {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet = Decoration.none;
constructor(view: EditorView) {
@ -25,7 +25,7 @@ export function createJiraSyncExtension(plugin: JiraPlugin): Extension {
passDecoration() {
const mdView = plugin.app.workspace.getActiveViewOfType(MarkdownView);
if (!mdView) return false;
const current_state = mdView.getState()
const current_state = mdView.getState();
return current_state.mode === 'source' && current_state.source;
}
@ -47,17 +47,15 @@ export function createJiraSyncExtension(plugin: JiraPlugin): Extension {
continue;
}
let className = "jira-sync-hidden";
let className = 'jira-sync-hidden';
// Check if cursor is inside
const sel = view.state.selection;
if (sel.ranges.some(r => r.from <= contentEnd && r.to >= contentStart)) {
className += " jira-sync-active";
if (sel.ranges.some((r) => r.from <= contentEnd && r.to >= contentStart)) {
className += ' jira-sync-active';
}
builder.add(start, end,
Decoration.mark({ class: className })
);
builder.add(start, end, Decoration.mark({ class: className }));
}
}
@ -72,20 +70,19 @@ export function createJiraSyncExtension(plugin: JiraPlugin): Extension {
while ((match = regex.exec(text)) !== null) {
blocks.push({
from: offset + match.index,
to: offset + match.index + match[0].length
to: offset + match.index + match[0].length,
});
}
return blocks;
}
isInsideCodeBlock(start: number, end: number, codeBlocks: Array<{from: number, to: number}>) {
return codeBlocks.some(block =>
start >= block.from && end <= block.to
);
isInsideCodeBlock(start: number, end: number, codeBlocks: Array<{ from: number; to: number }>) {
return codeBlocks.some((block) => start >= block.from && end <= block.to);
}
}, {
decorations: v => v.decorations
});
},
{
decorations: (v) => v.decorations,
},
);
}

View file

@ -1,9 +1,9 @@
// For Reading mode - targets <code> elements in <p> tags
export function hideJiraPointersReading(element: HTMLElement, _: any) {
export function hideJiraPointersReading(element: HTMLElement) {
// In Reading mode, inline code becomes <code> elements
const codeElements = element.querySelectorAll(':not(pre) > code');
codeElements.forEach(codeEl => {
codeElements.forEach((codeEl) => {
const text = codeEl.textContent || '';
if (text.startsWith('jira-sync-')) {

View file

@ -1,8 +1,8 @@
import {setIcon} from "obsidian";
import JiraPlugin from "../main";
import { SettingsComponent } from "../interfaces/settingsTypes";
import { debugLog } from "../tools/debugLogging";
import {CollapsedSections} from "./default";
import { setIcon } from 'obsidian';
import JiraPlugin from '../main';
import { SettingsComponent } from '../interfaces/settingsTypes';
import { debugLog } from '../tools/debugLogging';
import { CollapsedSections } from './default';
export class CollapsibleSection {
private containerEl: HTMLDivElement;
@ -19,7 +19,7 @@ export class CollapsibleSection {
child: SettingsComponent,
headerText: string,
isOpenByDefault = false,
saveStateKey?: string
saveStateKey?: string,
) {
this.plugin = plugin;
this.child = child;
@ -29,37 +29,37 @@ export class CollapsibleSection {
// Create the container div for the entire collapsible section
this.containerEl = parentEl.createDiv({
cls: "jira-collapsible-section",
cls: 'jira-collapsible-section',
});
// Create the header element
const headerEl = this.containerEl.createDiv({
cls: "jira-collapsible-header",
cls: 'jira-collapsible-header',
text: headerText,
});
headerEl.style.cursor = "pointer";
headerEl.addEventListener("click", async () => {
headerEl.style.cursor = 'pointer';
headerEl.addEventListener('click', async () => {
await this.toggle();
});
const chevron = headerEl.createEl('span', {
cls: 'jira-collapse-icon',
});
setIcon(chevron, "chevron-down");
setIcon(chevron, 'chevron-down');
// Content wrapper
this.contentEl = this.containerEl.createDiv({
cls: "jira-collapsible-content",
cls: 'jira-collapsible-content',
});
this.child.render(this.contentEl);
if (!this.isOpen) {
this.contentEl.hide();
this.containerEl.addClass("collapsed");
this.containerEl.addClass('collapsed');
}
debugLog("Rendered", this.saveStateKey || this.headerText, "as", this.isOpen);
debugLog('Rendered', this.saveStateKey || this.headerText, 'as', this.isOpen);
}
public getContentContainer(): HTMLElement {
@ -68,17 +68,17 @@ export class CollapsibleSection {
private async toggle() {
this.isOpen = !this.isOpen;
debugLog("Toggled", this.saveStateKey || this.headerText, "to", this.isOpen);
debugLog('Toggled', this.saveStateKey || this.headerText, 'to', this.isOpen);
// Rebuild or update the toggle button if needed
// Or store a reference to the button and just update its properties
if (this.isOpen) {
this.contentEl.show();
this.containerEl.removeClass("collapsed");
this.containerEl.removeClass('collapsed');
} else {
this.contentEl.hide();
this.containerEl.addClass("collapsed");
this.containerEl.addClass('collapsed');
}
if (this.saveStateKey && this.plugin) {

View file

@ -1,17 +1,17 @@
import { App, PluginSettingTab } from "obsidian";
import JiraPlugin from "../main";
import { SettingsComponentProps } from "../interfaces/settingsTypes";
import { ConnectionSettingsComponent } from "./components/ConnectionSettingsComponent";
import { GeneralSettingsComponent } from "./components/GeneralSettingsComponent";
import { FieldMappingsComponent } from "./components/FieldMappingsComponent";
import { FetchIssueComponent } from "./components/FetchIssueComponent";
import { TestFieldMappingsComponent } from "./components/TestFieldMappingsComponent";
import { debugLog } from "../tools/debugLogging";
import { CollapsibleSection } from "./CollapsibleSection";
import {useTranslations} from "../localization/translator";
import {TimekeepSettingsComponent} from "./components/TimekeepSettingsComponent";
import { App, PluginSettingTab } from 'obsidian';
import JiraPlugin from '../main';
import { SettingsComponentProps } from '../interfaces/settingsTypes';
import { ConnectionSettingsComponent } from './components/ConnectionSettingsComponent';
import { GeneralSettingsComponent } from './components/GeneralSettingsComponent';
import { FieldMappingsComponent } from './components/FieldMappingsComponent';
import { FetchIssueComponent } from './components/FetchIssueComponent';
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;
const t = useTranslations('settings').t;
/**
* Settings tab for the Jira plugin
* Orchestrates all components and manages the overall settings UI
@ -33,7 +33,7 @@ export class JiraSettingTab extends PluginSettingTab {
const componentProps: SettingsComponentProps = {
app,
plugin,
onSettingsChange: this.handleSettingsChange.bind(this)
onSettingsChange: this.handleSettingsChange.bind(this),
};
// Initialize all settings components
@ -41,8 +41,12 @@ export class JiraSettingTab extends PluginSettingTab {
this.generalSettings = new GeneralSettingsComponent(componentProps);
this.fieldMappingsSettings = new FieldMappingsComponent(componentProps);
this.fetchIssue = new FetchIssueComponent(componentProps);
this.testFieldMappings = new TestFieldMappingsComponent({ ...componentProps, getCurrentIssue: () => this.fetchIssue.getCurrentIssue() });
this.fetchIssue.onIssueDataChange = () => this.testFieldMappings.setCurrentIssue(this.fetchIssue.getCurrentIssue());
this.testFieldMappings = new TestFieldMappingsComponent({
...componentProps,
getCurrentIssue: () => this.fetchIssue.getCurrentIssue(),
});
this.fetchIssue.onIssueDataChange = () =>
this.testFieldMappings.setCurrentIssue(this.fetchIssue.getCurrentIssue());
this.timekeepSettings = new TimekeepSettingsComponent(componentProps);
}
@ -51,7 +55,7 @@ export class JiraSettingTab extends PluginSettingTab {
* This can be used for cross-component updates or validation
*/
private async handleSettingsChange(): Promise<void> {
debugLog("Settings changed, handling updates");
debugLog('Settings changed, handling updates');
await this.plugin.saveSettings();
}
@ -65,23 +69,59 @@ export class JiraSettingTab extends PluginSettingTab {
// 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.connectionSettings,
t('connection.title'),
this.plugin.settings.collapsedSections.connection,
'connection',
).getContentContainer();
new CollapsibleSection(this.plugin, containerEl, this.generalSettings, t("general.title"),
this.plugin.settings.collapsedSections.general, "general").getContentContainer();
new CollapsibleSection(
this.plugin,
containerEl,
this.generalSettings,
t('general.title'),
this.plugin.settings.collapsedSections.general,
'general',
).getContentContainer();
new CollapsibleSection(this.plugin, containerEl, this.fieldMappingsSettings, t("fm.title"),
this.plugin.settings.collapsedSections.fieldMappings, "fieldMappings").getContentContainer();
new CollapsibleSection(
this.plugin,
containerEl,
this.fieldMappingsSettings,
t('fm.title'),
this.plugin.settings.collapsedSections.fieldMappings,
'fieldMappings',
).getContentContainer();
new CollapsibleSection(this.plugin, containerEl, this.fetchIssue, t("fetch_issue.title"),
this.plugin.settings.collapsedSections.fetchIssue, "fetchIssue").getContentContainer();
new CollapsibleSection(
this.plugin,
containerEl,
this.fetchIssue,
t('fetch_issue.title'),
this.plugin.settings.collapsedSections.fetchIssue,
'fetchIssue',
).getContentContainer();
new CollapsibleSection(this.plugin, containerEl, this.testFieldMappings, t("tfm.title"),
this.plugin.settings.collapsedSections.testFieldMappings, "testFieldMappings").getContentContainer();
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();
new CollapsibleSection(
this.plugin,
containerEl,
this.timekeepSettings,
t('statistics.title'),
this.plugin.settings.collapsedSections.statistics,
'statistics',
).getContentContainer();
}
/**

View file

@ -1,187 +1,267 @@
import {Notice, Setting} from "obsidian";
import { useTranslations } from "src/localization/translator"
import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes";
import { fetchSelf } from "../../api";
import {debugLog} from "../../tools/debugLogging";
import { Notice, Setting } from 'obsidian';
import { useTranslations } from '../../localization/translator';
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
import { fetchSelf } from '../../api';
import { debugLog } from '../../tools/debugLogging';
const t = useTranslations("settings.connection").t
const t = useTranslations('settings.connection').t;
/**
* Component for Jira connection settings
*/
export class ConnectionSettingsComponent implements SettingsComponent {
private props: SettingsComponentProps;
private currentAuthMethod: "bearer" | "basic" | "session";
private currentAuthMethod: 'bearer' | 'basic' | 'session';
private settingElements: Map<string, HTMLElement> = new Map();
private connectionDropdown: any;
private fieldsContainer!: HTMLElement;
constructor(props: SettingsComponentProps) {
this.props = props;
this.currentAuthMethod = props.plugin.settings.connection.authMethod || "bearer";
const currentConn = this.getCurrentConnection();
this.currentAuthMethod = currentConn?.authMethod || 'bearer';
}
private getCurrentConnection() {
const { plugin } = this.props;
if (!plugin.settings.connections || plugin.settings.connections.length === 0) {
return null;
}
return plugin.settings.connections[plugin.settings.currentConnectionIndex];
}
private getNextConnectionName(): string {
const { plugin } = this.props;
const count = plugin.settings.connections?.length || 0;
return `Connection ${count + 1}`;
}
render(containerEl: HTMLElement): void {
const { plugin } = this.props;
// Always show: Jira URL
// Connection selector dropdown and add button
new Setting(containerEl)
.setName(t("url.title"))
.setDesc(t("url.desc"))
.setName(t('select_connection'))
.addDropdown((cb) => {
this.connectionDropdown = cb;
this.populateConnectionDropdown(cb);
cb.onChange(async (value) => {
const index = parseInt(value, 10);
plugin.settings.currentConnectionIndex = index;
await plugin.saveSettings();
this.refreshFields();
});
})
.addButton((button) => {
button
.setButtonText('+')
.setTooltip(t('add_connection'))
.onClick(async () => {
const newConnection = {
name: this.getNextConnectionName(),
authMethod: 'bearer' as const,
apiToken: '',
username: '',
email: '',
password: '',
jiraUrl: '',
apiVersion: '2' as const,
};
plugin.settings.connections.push(newConnection);
plugin.settings.currentConnectionIndex = plugin.settings.connections.length - 1;
await plugin.saveSettings();
this.populateConnectionDropdown(this.connectionDropdown);
this.refreshFields();
});
});
// Container for connection fields
this.fieldsContainer = containerEl.createDiv();
this.fieldsContainer.addClass('connection-fields-container');
this.renderConnectionFields(this.fieldsContainer);
}
private populateConnectionDropdown(cb: any): void {
const { plugin } = this.props;
cb.selectEl.innerHTML = '';
plugin.settings.connections.forEach((conn, index) => {
cb.addOption(index.toString(), conn.name || `Connection ${index + 1}`);
});
cb.setValue(plugin.settings.currentConnectionIndex.toString());
}
private renderConnectionFields(containerEl: HTMLElement): void {
const { plugin } = this.props;
containerEl.empty();
this.settingElements.clear();
const currentConn = this.getCurrentConnection();
if (!currentConn) {
containerEl.createDiv({ text: t('no_connection') });
return;
}
new Setting(containerEl)
.setName(t('name.title'))
.setDesc(t('name.desc'))
.addText((text) =>
text
.setPlaceholder(t("url.def"))
.setValue(plugin.settings.connection.jiraUrl)
.setPlaceholder(t('name.def'))
.setValue(currentConn.name)
.onChange(async (value) => {
plugin.settings.connection.jiraUrl = value;
currentConn.name = value;
await plugin.saveSettings();
})
this.populateConnectionDropdown(this.connectionDropdown);
}),
);
// Always show: Jira URL
new Setting(containerEl)
.setName(t('url.title'))
.setDesc(t('url.desc'))
.addText((text) =>
text
.setPlaceholder(t('url.def'))
.setValue(currentConn.jiraUrl)
.onChange(async (value) => {
currentConn.jiraUrl = value;
await plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName(t("api_version.title"))
.setDesc(t("api_version.desc"))
.setName(t('api_version.title'))
.setDesc(t('api_version.desc'))
.addDropdown((cb) =>
cb
.addOption("2", t("api_version.options.2"))
.addOption("3", t("api_version.options.3"))
.setValue(plugin.settings.connection.apiVersion)
.onChange(async (value: "2" | "3") => {
plugin.settings.connection.apiVersion = value;
.addOption('2', t('api_version.options.2'))
.addOption('3', t('api_version.options.3'))
.setValue(currentConn.apiVersion)
.onChange(async (value: string) => {
currentConn.apiVersion = value as '2' | '3';
await plugin.saveSettings();
})
)
}),
);
// Auth Method Select
new Setting(containerEl)
.setName(t("auth.title"))
.setDesc(t("auth.desc"))
.setName(t('auth.title'))
.setDesc(t('auth.desc'))
.addDropdown((cb) =>
cb
.addOption("bearer", t("auth.options.bearer"))
.addOption("basic", t("auth.options.basic"))
.addOption("session", t("auth.options.session"))
.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.connection.authMethod = value;
.onChange(async (value: string) => {
currentConn.authMethod = value as 'bearer' | 'basic' | 'session';
await plugin.saveSettings();
this.currentAuthMethod = value;
this.currentAuthMethod = value as 'bearer' | 'basic' | 'session';
this.updateVisibility();
})
}),
);
// // Store reference for dynamic visibility control
// this.settingElements.set("authMethod", authMethodSetting.settingEl);
// Jira PAT
const patSetting = new Setting(containerEl)
.setName(t("pat.title"))
.setDesc(t("pat.desc"))
.setName(t('pat.title'))
.setDesc(t('pat.desc'))
.addText((text) => {
text.inputEl.type = "password";
text
.setPlaceholder(t("pat.def"))
.setValue(plugin.settings.connection.apiToken)
text.inputEl.type = 'password';
text.setPlaceholder(t('pat.def'))
.setValue(currentConn.apiToken)
.onChange(async (value) => {
plugin.settings.connection.apiToken = value;
currentConn.apiToken = value;
await plugin.saveSettings();
});
});
this.settingElements.set("pat", patSetting.settingEl);
this.settingElements.set('pat', patSetting.settingEl);
// Jira username
const usernameSetting = new Setting(containerEl)
.setName(t("username.title"))
.setDesc(t("username.desc"))
.setName(t('username.title'))
.setDesc(t('username.desc'))
.addText((text) =>
text
.setPlaceholder(t("username.def"))
.setValue(plugin.settings.connection.username)
.setPlaceholder(t('username.def'))
.setValue(currentConn.username)
.onChange(async (value) => {
plugin.settings.connection.username = value;
currentConn.username = value;
await plugin.saveSettings();
})
}),
);
this.settingElements.set("username", usernameSetting.settingEl);
this.settingElements.set('username', usernameSetting.settingEl);
// Jira username
// Jira email
const emailSetting = new Setting(containerEl)
.setName(t("username.title"))
.setDesc(t("username.desc"))
.setName(t('email.title'))
.setDesc(t('email.desc'))
.addText((text) =>
text
.setPlaceholder(t("username.def"))
.setValue(plugin.settings.connection.email)
.setPlaceholder(t('email.def'))
.setValue(currentConn.email)
.onChange(async (value) => {
plugin.settings.connection.email = value;
currentConn.email = value;
await plugin.saveSettings();
})
}),
);
this.settingElements.set("email", emailSetting.settingEl);
this.settingElements.set('email', emailSetting.settingEl);
// Jira password
const passwordSetting = new Setting(containerEl)
.setName(t("password.title"))
.setDesc(t("password.desc"))
.setName(t('password.title'))
.setDesc(t('password.desc'))
.addText((text) => {
text.inputEl.type = "password";
text
.setPlaceholder(t("password.def"))
.setValue(plugin.settings.connection.password)
text.inputEl.type = 'password';
text.setPlaceholder(t('password.def'))
.setValue(currentConn.password)
.onChange(async (value) => {
plugin.settings.connection.password = value;
currentConn.password = value;
await plugin.saveSettings();
});
});
this.settingElements.set("password", passwordSetting.settingEl);
// // Session cookie name
// const sessionCookieSetting = new Setting(containerEl)
// .setName("Session cookie name")
// .setDesc("Only needed for username+password authentication")
// .addText((text) =>
// text
// .setPlaceholder("JSESSIONID")
// .setValue(plugin.settings.sessionCookieName)
// .onChange(async (value) => {
// plugin.settings.sessionCookieName = value;
// await plugin.saveSettings();
// })
// );
// this.settingElements.set("sessionCookie", sessionCookieSetting.settingEl);
this.settingElements.set('password', passwordSetting.settingEl);
// Ping button to test connection
new Setting(containerEl)
.setName(t("ping.title"))
.setDesc(t("ping.desc"))
.setName(t('ping.title'))
.setDesc(t('ping.desc'))
.addButton((button) =>
button
.setButtonText(t("ping.button"))
.setButtonText(t('ping.button'))
.setCta()
.onClick(async () => {
await this.testConnection();
})
}),
);
// Initial visibility
this.updateVisibility();
}
private refreshFields(): void {
const currentConn = this.getCurrentConnection();
if (currentConn) {
this.currentAuthMethod = currentConn.authMethod;
}
this.renderConnectionFields(this.fieldsContainer);
}
private async testConnection(): Promise<void> {
const { plugin } = this.props;
try {
const notice = new Notice("Testing connection to Jira...", 2);
const notice = new Notice('Testing connection to Jira...', 2);
debugLog("Testing Jira connection...");
debugLog('Testing Jira connection...');
const result = await fetchSelf(plugin);
notice.hide();
const successMessage = "Successfully connected to Jira";
const successMessage = 'Successfully connected to Jira';
new Notice(successMessage);
debugLog(successMessage, result);
} catch (error) {
const errorMessage = `Failed to connect to Jira: ${error.message}`;
} catch (error: unknown) {
const errorMessage = `Failed to connect to Jira: ${(error as Error).message}`;
new Notice(errorMessage);
console.error(errorMessage, error);
}
@ -190,52 +270,38 @@ export class ConnectionSettingsComponent implements SettingsComponent {
private updateVisibility(): void {
const method = this.currentAuthMethod;
// // Show always
// this.showElement("authMethod");
// Show PAT if bearer or basic
if (method === "bearer" || method === "basic") {
this.showElement("pat");
if (method === 'bearer' || method === 'basic') {
this.showElement('pat');
} else {
this.hideElement("pat");
this.hideElement('pat');
}
// Show username if basic or session
if (method === "session") {
this.showElement("username");
if (method === 'session') {
this.showElement('username');
} else {
this.hideElement("username");
this.hideElement('username');
}
// Show email if basic or session
if (method === "basic") {
this.showElement("email");
if (method === 'basic') {
this.showElement('email');
} else {
this.hideElement("email");
this.hideElement('email');
}
// Show password only if session
if (method === "session") {
this.showElement("password");
if (method === 'session') {
this.showElement('password');
} else {
this.hideElement("password");
this.hideElement('password');
}
// // Show session cookie only if session
// if (method === "session") {
// this.showElement("sessionCookie");
// } else {
// this.hideElement("sessionCookie");
// }
}
private showElement(key: string): void {
const el = this.settingElements.get(key);
if (el) el.style.display = "";
if (el) el.style.display = '';
}
private hideElement(key: string): void {
const el = this.settingElements.get(key);
if (el) el.style.display = "none";
if (el) el.style.display = 'none';
}
}

View file

@ -1,19 +1,18 @@
import {Notice, Setting} from "obsidian";
import {SettingsComponent, SettingsComponentProps} from "../../interfaces/settingsTypes";
import {fetchIssue} from "../../api";
import debounce from "lodash/debounce";
import hljs from "highlight.js";
import {useTranslations} from "../../localization/translator";
import {setupArrayTextSetting} from "../tools/setupArrayTextString";
import { Notice, Setting } from 'obsidian';
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
import { fetchIssue } from '../../api';
import debounce from 'lodash/debounce';
import hljs from 'highlight.js';
import { useTranslations } from '../../localization/translator';
import { setupArrayTextSetting } from '../tools/setupArrayTextString';
const t = useTranslations("settings.fetch_issue").t;
const t = useTranslations('settings.fetch_issue').t;
export class FetchIssueComponent implements SettingsComponent {
private props: SettingsComponentProps;
private issueDataContainer: HTMLElement | null = null;
private debouncedFetch: ReturnType<typeof debounce>;
private currentIssueData: any = null;
private currentIssueKey: string = "";
private currentIssueKey: string = '';
public onIssueDataChange?: () => void;
constructor(props: SettingsComponentProps) {
@ -21,97 +20,87 @@ export class FetchIssueComponent implements SettingsComponent {
this.debouncedFetch = debounce(this.fetchIssueData.bind(this), 500);
}
render(containerEl: HTMLElement): void {
const { plugin } = this.props;
// Filename template setting
const filenameTemplateSetting = new Setting(containerEl)
.setName(t("filenameTemplate.name"))
.setDesc(t("filenameTemplate.desc"));
.setName(t('filenameTemplate.name'))
.setDesc(t('filenameTemplate.desc'));
filenameTemplateSetting.addText((text) => {
text
.setPlaceholder(t("filenameTemplate.placeholder"))
.setValue(plugin.settings.fetchIssue.filenameTemplate || "")
text.setPlaceholder(t('filenameTemplate.placeholder'))
.setValue(plugin.settings.fetchIssue.filenameTemplate || '')
.onChange(async (value) => {
plugin.settings.fetchIssue.filenameTemplate = value;
await plugin.saveSettings();
});
});
const link =
plugin.getCurrentConnection()?.apiVersion === '3'
? 'https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-post'
: 'https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-jql-post';
const link = plugin.settings.connection.apiVersion === "3" ? "https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-search-jql-post" : "https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-search/#api-rest-api-2-search-jql-post"
const securityNote = containerEl.createEl("p");
securityNote.createEl("strong", {
text: t("note.desc0")
const securityNote = containerEl.createEl('p');
securityNote.createEl('strong', {
text: t('note.desc0'),
});
securityNote.createSpan({
text: t("note.desc1")
text: t('note.desc1'),
});
securityNote.createEl("br");
securityNote.createEl("br");
securityNote.createEl('br');
securityNote.createEl('br');
securityNote.createSpan({
text: t("note.desc2")
})
securityNote.createEl("a", {
text: t("note.link_label"),
text: t('note.desc2'),
});
securityNote.createEl('a', {
text: t('note.link_label'),
href: link,
cls: "external-link"
})
cls: 'external-link',
});
securityNote.createSpan({
text: t("note.desc3")
text: t('note.desc3'),
});
new Setting(containerEl)
.setName(t("fields.name"))
.setDesc(t("fields.desc"))
.setName(t('fields.name'))
.setDesc(t('fields.desc'))
.addText((text) => {
text.setPlaceholder(t("fields.def"))
setupArrayTextSetting(
text,
plugin.settings.fetchIssue.fields,
async (array) => {
text.setPlaceholder(t('fields.def'));
setupArrayTextSetting(text, plugin.settings.fetchIssue.fields, async (array) => {
plugin.settings.fetchIssue.fields = array;
this.rerenderIssueData();
await plugin.saveSettings();
}
);
});
});
new Setting(containerEl)
.setName(t("expand.name"))
.setDesc(t("expand.desc"))
.setName(t('expand.name'))
.setDesc(t('expand.desc'))
.addText((text) => {
text.setPlaceholder(t("expand.def"))
setupArrayTextSetting(
text,
plugin.settings.fetchIssue.expand,
async (array) => {
text.setPlaceholder(t('expand.def'));
setupArrayTextSetting(text, plugin.settings.fetchIssue.expand, async (array) => {
plugin.settings.fetchIssue.expand = array;
this.rerenderIssueData();
await plugin.saveSettings();
}
);
});
});
// Add input field for issue key
new Setting(containerEl)
.setName(t("key.name"))
.setDesc(t("key.desc"))
.setName(t('key.name'))
.setDesc(t('key.desc'))
.addText((text) =>
text
.setPlaceholder("PROJ-123")
.onChange((value) => {
text.setPlaceholder('PROJ-123').onChange((value) => {
this.currentIssueKey = value;
this.rerenderIssueData();
})
}),
);
// Create container for issue data
this.issueDataContainer = containerEl.createDiv({
cls: "jira-raw-issue-container"
cls: 'jira-raw-issue-container',
});
}
@ -132,8 +121,8 @@ export class FetchIssueComponent implements SettingsComponent {
const issueData = await fetchIssue(this.props.plugin, value);
this.currentIssueData = issueData;
this.displayIssueData(issueData);
} catch (error) {
new Notice(`Failed to fetch issue: ${error.message}`);
} catch (error: unknown) {
new Notice(`Failed to fetch issue: ${(error as Error).message}`);
this.clearIssueData();
}
if (this.onIssueDataChange) {
@ -146,12 +135,12 @@ export class FetchIssueComponent implements SettingsComponent {
this.issueDataContainer.empty();
const pre = this.issueDataContainer.createEl("pre", {
cls: "jira-copyable",
const pre = this.issueDataContainer.createEl('pre', {
cls: 'jira-copyable',
});
const code = pre.createEl("code", {
cls: "language-json"
const code = pre.createEl('code', {
cls: 'language-json',
});
const jsonString = JSON.stringify(issueData, null, 2);

View file

@ -1,7 +1,7 @@
import { validateField } from "../tools/fieldValidation";
import {useTranslations} from "../../localization/translator";
import { validateField } from '../tools/fieldValidation';
import { useTranslations } from '../../localization/translator';
const t = useTranslations("settings.fm").t;
const t = useTranslations('settings.fm').t;
/**
* Interface for field mapping item props
*/
@ -19,10 +19,10 @@ export interface FieldMappingItemProps {
*/
export class FieldMappingItem {
private props: FieldMappingItemProps;
private fieldContainer: HTMLDivElement;
private fieldNameInput: HTMLInputElement;
private toJiraInput: HTMLTextAreaElement;
private fromJiraInput: HTMLTextAreaElement;
private fieldContainer!: HTMLDivElement;
private fieldNameInput!: HTMLInputElement;
private toJiraInput!: HTMLTextAreaElement;
private fromJiraInput!: HTMLTextAreaElement;
constructor(props: FieldMappingItemProps) {
this.props = props;
@ -36,7 +36,7 @@ export class FieldMappingItem {
return {
fieldName: this.fieldNameInput.value.trim(),
toJira: this.toJiraInput.value.trim(),
fromJira: this.fromJiraInput.value.trim()
fromJira: this.fromJiraInput.value.trim(),
};
}
@ -44,51 +44,71 @@ export class FieldMappingItem {
* Render the field mapping item
*/
private render() {
const { container, fieldName = "", toJira = "", fromJira = "", enableValidation } = this.props;
const { container, fieldName = '', toJira = '', fromJira = '', enableValidation } = this.props;
// Create the field container
this.fieldContainer = container.createDiv({ cls: "field-mapping-item" });
this.fieldContainer = container.createDiv({
cls: 'field-mapping-item',
});
// Create field name input
const fieldNameContainer = this.fieldContainer.createDiv({ cls: "field-name-container" });
fieldNameContainer.createEl("span", { text: t("field_name"), cls: "field-mapping-label" });
this.fieldNameInput = fieldNameContainer.createEl("input", {
type: "text",
const fieldNameContainer = this.fieldContainer.createDiv({
cls: 'field-name-container',
});
fieldNameContainer.createEl('span', {
text: t('field_name'),
cls: 'field-mapping-label',
});
this.fieldNameInput = fieldNameContainer.createEl('input', {
type: 'text',
value: fieldName,
cls: "field-name-input",
placeholder: t("field_name_placeholder") || "e.g., summary, assignee, priority"
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" });
const functionsContainer = this.fieldContainer.createDiv({
cls: 'functions-container',
});
// Create toJira function input
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"
const toJiraContainer = functionsContainer.createDiv({
cls: 'to-jira-container',
});
this.toJiraInput.placeholder = "(value) => {\n // Transform Obsidian value to Jira\n return value;\n}";
toJiraContainer.createEl('span', {
text: t('to_jira'),
cls: 'field-mapping-label',
});
this.toJiraInput = toJiraContainer.createEl('textarea', {
cls: 'to-jira-input',
});
this.toJiraInput.placeholder = '(value) => {\n // Transform Obsidian value to Jira\n return value;\n}';
this.toJiraInput.value = toJira;
// Create fromJira function input
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"
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) => {\n // Extract value from Jira issue\n return issue.fields.fieldName;\n}";
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",
attr: { 'aria-label': t("remove_field") || 'Remove field mapping' }
const removeBtn = this.fieldContainer.createEl('button', {
text: '✕',
cls: 'remove-field-btn',
attr: { 'aria-label': t('remove_field') || 'Remove field mapping' },
});
// Handle field removal
removeBtn.addEventListener("click", () => {
removeBtn.addEventListener('click', () => {
this.fieldContainer.remove();
this.props.onRemove();
});

View file

@ -1,20 +1,20 @@
import { Setting, setIcon } from "obsidian";
import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes";
import { FieldMappingItem } from "./FieldMappingItem";
import { collectFieldMappingsFromUI, processMappings } from "../tools/mappingTransformers";
import {jiraFunctionToString, transform_string_to_functions_mappings} from "../../tools/convertFunctionString";
import { obsidianJiraFieldMappings } from "../../default/obsidianJiraFieldsMapping";
import { debugLog } from "../../tools/debugLogging";
import {useTranslations} from "../../localization/translator";
import { Setting, setIcon } from 'obsidian';
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
import { FieldMappingItem } from './FieldMappingItem';
import { collectFieldMappingsFromUI, processMappings } from '../tools/mappingTransformers';
import { jiraFunctionToString, transform_string_to_functions_mappings } from '../../tools/convertFunctionString';
import { obsidianJiraFieldMappings } from '../../default/obsidianJiraFieldsMapping';
import { debugLog } from '../../tools/debugLogging';
import { useTranslations } from '../../localization/translator';
const t = useTranslations("settings.fm").t;
const t = useTranslations('settings.fm').t;
/**
* Component for field mappings section
*/
export class FieldMappingsComponent implements SettingsComponent {
private props: SettingsComponentProps;
private fieldsList: HTMLDivElement;
private fieldsList!: HTMLDivElement;
private mappingItems: FieldMappingItem[] = [];
constructor(props: SettingsComponentProps) {
@ -28,60 +28,69 @@ export class FieldMappingsComponent implements SettingsComponent {
const { plugin } = this.props;
// Create the mapping section
const mappingSection = containerEl.createDiv({ cls: "jira-field-mappings" });
const mappingSection = containerEl.createDiv({
cls: 'jira-field-mappings',
});
// Add explanation
mappingSection.createEl("p", {
text: t("desc")
mappingSection.createEl('p', {
text: t('desc'),
});
// Add validation toggle
new Setting(mappingSection)
.setName(t("fv.name"))
.setDesc(t("fv.desc"))
.addToggle(toggle => toggle
.setValue(plugin.settings.fieldMapping.enableFieldValidation)
.onChange(async (value) => {
.setName(t('fv.name'))
.setDesc(t('fv.desc'))
.addToggle((toggle) =>
toggle.setValue(plugin.settings.fieldMapping.enableFieldValidation).onChange(async (value) => {
plugin.settings.fieldMapping.enableFieldValidation = value;
await plugin.saveSettings();
// Update validation for all mapping items
this.mappingItems.forEach(item => {
this.mappingItems.forEach((item) => {
item.updateValidation(value);
});
}));
}),
);
// Create list container for field mappings
this.fieldsList = mappingSection.createDiv({ cls: "field-mappings-list" });
this.fieldsList = mappingSection.createDiv({
cls: 'field-mappings-list',
});
// Create button container
const buttonView = mappingSection.createDiv({ cls: "button-view" });
const buttonView = mappingSection.createDiv({ cls: 'button-view' });
// Add button to add new field mapping
const addFieldBtn = 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" }
const addFieldBtn = 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(addFieldBtn, "circle-plus");
setIcon(addFieldBtn, 'circle-plus');
addFieldBtn.addEventListener("click", () => {
addFieldBtn.addEventListener('click', () => {
this.addFieldMapping();
});
// Reset button
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" }
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");
setIcon(resetBtn, 'refresh-cw');
resetBtn.addEventListener("click", async () => {
resetBtn.addEventListener('click', async () => {
// 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."
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) {
@ -93,17 +102,19 @@ export class FieldMappingsComponent implements SettingsComponent {
});
// Reload default mappings button
const reloadBtn = buttonView.createEl("button", {
text: t("reload_defaults") || "Load Defaults",
cls: "reload-field-mappings-btn",
attr: { 'data-tooltip': t("reload_defaults_tooltip") || "Restore default field mappings" }
const reloadBtn = buttonView.createEl('button', {
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");
setIcon(reloadBtn, 'list-restart');
reloadBtn.addEventListener("click", async () => {
reloadBtn.addEventListener('click', async () => {
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?"
t('reload_confirm_title') || 'Load Default Mappings',
t('reload_confirm_text') || 'This will replace all current mappings with defaults. Continue?',
);
if (confirmed) {
@ -114,7 +125,7 @@ export class FieldMappingsComponent implements SettingsComponent {
for (const [fieldName, mapping] of Object.entries(obsidianJiraFieldMappings)) {
defaultMappingsStrings[fieldName] = {
toJira: jiraFunctionToString(mapping.toJira, false),
fromJira: jiraFunctionToString(mapping.fromJira, true)
fromJira: jiraFunctionToString(mapping.fromJira, true),
};
}
plugin.settings.fieldMapping.fieldMappingsStrings = defaultMappingsStrings;
@ -131,10 +142,12 @@ export class FieldMappingsComponent implements SettingsComponent {
this.renderExamples(mappingSection);
// Add security notice
const securityNote = containerEl.createEl("div", { cls: "setting-item-description" });
securityNote.createEl("strong", { text: t("secNote.name") });
const securityNote = containerEl.createEl('div', {
cls: 'setting-item-description',
});
securityNote.createEl('strong', { text: t('secNote.name') });
securityNote.createSpan({
text: t("secNote.desc")
text: t('secNote.desc'),
});
}
@ -173,7 +186,7 @@ export class FieldMappingsComponent implements SettingsComponent {
/**
* Add a new field mapping item
*/
addFieldMapping(fieldName = "", toJira = "", fromJira = ""): FieldMappingItem {
addFieldMapping(fieldName = '', toJira = '', fromJira = ''): FieldMappingItem {
const item = new FieldMappingItem({
container: this.fieldsList,
fieldName,
@ -186,7 +199,7 @@ export class FieldMappingsComponent implements SettingsComponent {
if (index !== -1) {
this.mappingItems.splice(index, 1);
}
}
},
});
this.mappingItems.push(item);
@ -206,28 +219,28 @@ export class FieldMappingsComponent implements SettingsComponent {
debugLog(`Loading mapping settings`);
// If we have string representations stored, use those
if (plugin.settings.fieldMapping.fieldMappingsStrings &&
Object.keys(plugin.settings.fieldMapping.fieldMappingsStrings).length > 0) {
if (
plugin.settings.fieldMapping.fieldMappingsStrings &&
Object.keys(plugin.settings.fieldMapping.fieldMappingsStrings).length > 0
) {
debugLog('Current mapping (string) is: ', plugin.settings.fieldMapping.fieldMappingsStrings);
const savedMappings = plugin.settings.fieldMapping.fieldMappingsStrings;
for (const [fieldName, mapping] of Object.entries(savedMappings)) {
if (mapping && typeof mapping === 'object' && 'toJira' in mapping && 'fromJira' in mapping) {
this.addFieldMapping(
fieldName,
mapping.toJira,
mapping.fromJira
);
this.addFieldMapping(fieldName, mapping.toJira, mapping.fromJira);
}
}
plugin.settings.fieldMapping.fieldMappings = await transform_string_to_functions_mappings(
plugin.settings.fieldMapping.fieldMappingsStrings,
plugin.settings.fieldMapping.enableFieldValidation
plugin.settings.fieldMapping.enableFieldValidation,
);
}
// Otherwise, try to use the function mappings and convert them to strings
else if (plugin.settings.fieldMapping.fieldMappings &&
Object.keys(plugin.settings.fieldMapping.fieldMappings).length > 0) {
else if (
plugin.settings.fieldMapping.fieldMappings &&
Object.keys(plugin.settings.fieldMapping.fieldMappings).length > 0
) {
debugLog('Current mapping is: ', plugin.settings.fieldMapping.fieldMappings);
const existingMappings = plugin.settings.fieldMapping.fieldMappings;
@ -236,7 +249,7 @@ export class FieldMappingsComponent implements SettingsComponent {
this.addFieldMapping(
fieldName,
jiraFunctionToString(mapping.toJira, false),
jiraFunctionToString(mapping.fromJira, true)
jiraFunctionToString(mapping.fromJira, true),
);
}
}
@ -252,7 +265,9 @@ export class FieldMappingsComponent implements SettingsComponent {
async saveFieldMappings(): Promise<void> {
const { plugin } = this.props;
debugLog(`From strings ${JSON.stringify(plugin.settings.fieldMapping.fieldMappingsStrings)}\nand funcs ${JSON.stringify(plugin.settings.fieldMapping.fieldMappings)}`);
debugLog(
`From strings ${JSON.stringify(plugin.settings.fieldMapping.fieldMappingsStrings)}\nand funcs ${JSON.stringify(plugin.settings.fieldMapping.fieldMappings)}`,
);
// Collect mappings from UI
const stringMappings = await collectFieldMappingsFromUI(this.fieldsList);
@ -260,7 +275,7 @@ export class FieldMappingsComponent implements SettingsComponent {
// Process mappings
const { stringMappings: newStringMappings, functionMappings } = await processMappings(
stringMappings,
plugin.settings.fieldMapping.enableFieldValidation
plugin.settings.fieldMapping.enableFieldValidation,
);
// Update settings
@ -276,45 +291,47 @@ export class FieldMappingsComponent implements SettingsComponent {
* Render example mappings
*/
private renderExamples(containerEl: HTMLElement): void {
const examplesSection = containerEl.createDiv({ cls: "mapping-examples" });
examplesSection.createEl("h4", { text: t("example.name") });
const examplesSection = containerEl.createDiv({
cls: 'mapping-examples',
});
examplesSection.createEl('h4', { text: t('example.name') });
const examplesList = examplesSection.createEl("ul");
const examplesList = examplesSection.createEl('ul');
const examples = [
{
name: "summary",
toJira: "value",
fromJira: "issue.fields.summary"
name: 'summary',
toJira: 'value',
fromJira: 'issue.fields.summary',
},
{
name: "reporter",
toJira: "null",
fromJira: "issue.fields.reporter.name"
name: 'reporter',
toJira: 'null',
fromJira: 'issue.fields.reporter.name',
},
{
name: "project",
toJira: "({ key: value })",
fromJira: "issue.fields.project ? issue.fields.project.key : \"\""
}
name: 'project',
toJira: '({ key: value })',
fromJira: 'issue.fields.project ? issue.fields.project.key : ""',
},
];
examples.forEach(example => {
const item = examplesList.createEl("li");
item.createEl("strong", { text: example.name });
examples.forEach((example) => {
const item = examplesList.createEl('li');
item.createEl('strong', { text: example.name });
// Add button to use this example
const useBtn = item.createEl("button", {
text: t("example.use"),
cls: "use-example-btn"
const useBtn = item.createEl('button', {
text: t('example.use'),
cls: 'use-example-btn',
});
item.createEl("br");
item.createEl('br');
item.createSpan({ text: `toJira: ${example.toJira}` });
item.createEl("br");
item.createEl('br');
item.createSpan({ text: `fromJira: ${example.fromJira}` });
useBtn.addEventListener("click", () => {
useBtn.addEventListener('click', () => {
this.addFieldMapping(example.name, example.toJira, example.fromJira);
});
});

View file

@ -1,5 +1,5 @@
import { App, AbstractInputSuggest, TFolder } from "obsidian";
import {debugLog} from "../../tools/debugLogging";
import { App, AbstractInputSuggest, TFolder } from 'obsidian';
import { debugLog } from '../../tools/debugLogging';
interface FolderOption {
path: string;
@ -27,7 +27,7 @@ export class FolderSuggest extends AbstractInputSuggest<FolderOption> {
if (this.cacheValid) return;
this.allFolders = [];
this.scanFolderForFolders(this.app.vault.getRoot(), "");
this.scanFolderForFolders(this.app.vault.getRoot(), '');
// Sort by display name
this.allFolders.sort((a, b) => a.displayName.localeCompare(b.displayName));
@ -39,10 +39,10 @@ export class FolderSuggest extends AbstractInputSuggest<FolderOption> {
*/
private scanFolderForFolders(folder: TFolder, prefix: string): void {
// Add root folder
if (prefix === "") {
if (prefix === '') {
this.allFolders.push({
path: "",
displayName: "/ (root)"
path: '',
displayName: '/ (root)',
});
}
@ -51,7 +51,7 @@ export class FolderSuggest extends AbstractInputSuggest<FolderOption> {
const displayName = prefix ? `${prefix}/${child.name}` : child.name;
this.allFolders.push({
path: child.path,
displayName: displayName
displayName: displayName,
});
// Recursively scan subfolders
this.scanFolderForFolders(child, displayName);
@ -82,22 +82,22 @@ export class FolderSuggest extends AbstractInputSuggest<FolderOption> {
}
// Fast filtering - match both display name and path
return this.allFolders.filter(option =>
option.displayName.toLowerCase().includes(query) ||
option.path.toLowerCase().includes(query)
return this.allFolders.filter(
(option) => option.displayName.toLowerCase().includes(query) || option.path.toLowerCase().includes(query),
);
}
renderSuggestion(folder: FolderOption, el: HTMLElement): void {
el.setText(folder.displayName);
el.setAttr("title", folder.path || "Root folder");
el.setAttr('title', folder.path || 'Root folder');
}
selectSuggestion(folder: FolderOption): void {
debugLog("selected Issues Folder", folder);
debugLog('selected Issues Folder', folder);
this.setValue(folder.path);
this.onChange && this.onChange(folder.path);
if (this.onChange) {
this.onChange(folder.path);
}
this.close();
}
}

View file

@ -1,10 +1,10 @@
import { App, Setting, normalizePath } from "obsidian";
import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes";
import { TemplateSuggest } from "./TemplateSuggest";
import { FolderSuggest } from "./FolderSuggest";
import {useTranslations} from "../../localization/translator";
import { App, Setting, normalizePath } from 'obsidian';
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
import { TemplateSuggest } from './TemplateSuggest';
import { FolderSuggest } from './FolderSuggest';
import { useTranslations } from '../../localization/translator';
const t = useTranslations("settings.general").t;
const t = useTranslations('settings.general').t;
interface TemplatePluginInfo {
coreTemplatesEnabled: boolean;
@ -24,17 +24,15 @@ export class GeneralSettingsComponent implements SettingsComponent {
const { plugin } = this.props;
// Issues folder setting with native search
const folderSetting = new Setting(containerEl)
.setName(t("folder.name"))
.setDesc(t("folder.desc"));
const folderSetting = new Setting(containerEl).setName(t('folder.name')).setDesc(t('folder.desc'));
folderSetting.addSearch((search) => {
const onChange = async (value: string) => {
plugin.settings.global.issuesFolder = value;
await plugin.saveSettings();
}
};
search
.setPlaceholder(t("folder.placeholder"))
.setPlaceholder(t('folder.placeholder'))
.setValue(plugin.settings.global.issuesFolder)
.onChange(onChange);
new FolderSuggest(plugin.app, search.inputEl, onChange);
@ -43,16 +41,14 @@ export class GeneralSettingsComponent implements SettingsComponent {
// Template path setting with native search
const templateInfo = this.detectTemplatePlugins(plugin.app);
const setting = new Setting(containerEl)
.setName(t("template.name"))
.setDesc(t("template.desc"));
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", {
div.createEl('small', {
text: templateInfo.warningMessage,
cls: "mod-warning"
cls: 'mod-warning',
});
});
}
@ -62,16 +58,15 @@ export class GeneralSettingsComponent implements SettingsComponent {
const onChange = async (value: string) => {
plugin.settings.global.templatePath = value ? normalizePath(value) : '';
await plugin.saveSettings();
}
};
search
.setPlaceholder(t("template.selector.placeholder"))
.setValue(plugin.settings.global.templatePath || "")
.setPlaceholder(t('template.selector.placeholder'))
.setValue(plugin.settings.global.templatePath || '')
.onChange(onChange);
new TemplateSuggest(plugin.app, search.inputEl, templateInfo.templateDirectory, onChange);
});
}
private detectTemplatePlugins(app: App): TemplatePluginInfo {
const coreTemplates = (app as any).internalPlugins?.plugins?.templates;
const templaterPlugin = (app as any).plugins?.plugins?.['templater-obsidian'];
@ -80,7 +75,7 @@ export class GeneralSettingsComponent implements SettingsComponent {
const templaterEnabled = (app as any).plugins?.enabledPlugins?.has('templater-obsidian') || false;
let templateDirectory: string | null = null;
let warningMessage = "";
let warningMessage = '';
if (coreTemplatesEnabled && coreTemplates.instance?.options?.folder) {
templateDirectory = coreTemplates.instance.options.folder;
@ -89,14 +84,14 @@ export class GeneralSettingsComponent implements SettingsComponent {
}
if (!coreTemplatesEnabled && !templaterEnabled) {
warningMessage = t("template.warning");
warningMessage = t('template.warning');
}
return {
coreTemplatesEnabled,
templaterEnabled,
templateDirectory,
warningMessage
warningMessage,
};
}
}

View file

@ -1,8 +1,8 @@
import {fetchCountIssuesByJQL, fetchIssuesByJQLRaw} from "../../api";
import JiraPlugin from "../../main";
import {useTranslations} from "../../localization/translator";
import { fetchCountIssuesByJQL, fetchIssuesByJQLRaw } from '../../api';
import JiraPlugin from '../../main';
import { useTranslations } from '../../localization/translator';
const t = useTranslations("modals.jql_search").t;
const t = useTranslations('modals.jql_search').t;
/**
* Reusable JQL Preview component
@ -23,49 +23,55 @@ export class JQLPreview {
}
private setupContainer() {
this.containerEl.addClass("jql-preview-container");
this.containerEl.addClass('jql-preview-container');
this.renderSkeleton();
}
private renderSkeleton() {
this.containerEl.empty();
this.loadingEl = this.containerEl.createDiv({ cls: "jql-preview-loading" });
this.loadingEl.createDiv({
cls: "jql-preview-loading-icon",
text: "🔍"
this.loadingEl = this.containerEl.createDiv({
cls: 'jql-preview-loading',
});
this.loadingEl.createDiv({
cls: "jql-preview-loading-text",
text: t("preview.loading")
cls: 'jql-preview-loading-icon',
text: '🔍',
});
this.loadingEl.createDiv({
cls: 'jql-preview-loading-text',
text: t('preview.loading'),
});
}
private renderEmpty() {
this.containerEl.empty();
const emptyEl = this.containerEl.createDiv({ cls: "jql-preview-empty" });
emptyEl.createDiv({
cls: "jql-preview-empty-icon",
text: "📝"
const emptyEl = this.containerEl.createDiv({
cls: 'jql-preview-empty',
});
emptyEl.createDiv({
cls: "jql-preview-empty-text",
text: t("preview.empty_input")
cls: 'jql-preview-empty-icon',
text: '📝',
});
emptyEl.createDiv({
cls: 'jql-preview-empty-text',
text: t('preview.empty_input'),
});
}
private renderError(error: string) {
this.containerEl.empty();
const errorEl = this.containerEl.createDiv({ cls: "jql-preview-error" });
errorEl.createDiv({
cls: "jql-preview-error-icon",
text: "⚠️"
const errorEl = this.containerEl.createDiv({
cls: 'jql-preview-error',
});
errorEl.createDiv({
cls: "jql-preview-error-text",
text: t("preview.error", { error })
cls: 'jql-preview-error-icon',
text: '⚠️',
});
errorEl.createDiv({
cls: 'jql-preview-error-text',
text: t('preview.error', { error }),
});
}
@ -74,217 +80,257 @@ export class JQLPreview {
// Header with total count
if (this.show_header) {
const headerEl = this.containerEl.createDiv({ cls: "jql-preview-header" });
const totalEl = headerEl.createDiv({ cls: "jql-preview-total" });
totalEl.createSpan({ cls: "jql-preview-total-text", text: t("preview.total", { total }) });
const headerEl = this.containerEl.createDiv({
cls: 'jql-preview-header',
});
const totalEl = headerEl.createDiv({ cls: 'jql-preview-total' });
totalEl.createSpan({
cls: 'jql-preview-total-text',
text: t('preview.total', { total }),
});
if (total > this.limit) {
totalEl.createSpan({
cls: "jql-preview-showing",
text: t("preview.showing", { limit: this.limit })
cls: 'jql-preview-showing',
text: t('preview.showing', { limit: this.limit }),
});
}
}
// Issues table
if (issues.length > 0) {
const tableWrapperEl = this.containerEl.createDiv({ cls: "jql-preview-table-wrapper" });
const tableEl = tableWrapperEl.createEl("table", { cls: "jql-preview-table" });
const tableWrapperEl = this.containerEl.createDiv({
cls: 'jql-preview-table-wrapper',
});
const tableEl = tableWrapperEl.createEl('table', {
cls: 'jql-preview-table',
});
// Table header
const theadEl = tableEl.createEl("thead");
const headerRowEl = theadEl.createEl("tr");
const theadEl = tableEl.createEl('thead');
const headerRowEl = theadEl.createEl('tr');
// Header columns
headerRowEl.createEl("th", {
cls: "jql-preview-th-key",
text: "Key"
headerRowEl.createEl('th', {
cls: 'jql-preview-th-key',
text: 'Key',
});
headerRowEl.createEl("th", {
cls: "jql-preview-th-summary",
text: "Summary"
headerRowEl.createEl('th', {
cls: 'jql-preview-th-summary',
text: 'Summary',
});
headerRowEl.createEl("th", {
cls: "jql-preview-th-icon",
text: "T"
headerRowEl.createEl('th', {
cls: 'jql-preview-th-icon',
text: 'T',
});
headerRowEl.createEl("th", {
cls: "jql-preview-th-date",
text: "Created"
headerRowEl.createEl('th', {
cls: 'jql-preview-th-date',
text: 'Created',
});
headerRowEl.createEl("th", {
cls: "jql-preview-th-date",
text: "Updated"
headerRowEl.createEl('th', {
cls: 'jql-preview-th-date',
text: 'Updated',
});
headerRowEl.createEl("th", {
cls: "jql-preview-th-icon",
text: "R"
headerRowEl.createEl('th', {
cls: 'jql-preview-th-icon',
text: 'R',
});
headerRowEl.createEl("th", {
cls: "jql-preview-th-icon",
text: "A"
headerRowEl.createEl('th', {
cls: 'jql-preview-th-icon',
text: 'A',
});
headerRowEl.createEl("th", {
cls: "jql-preview-th-icon",
text: "P"
headerRowEl.createEl('th', {
cls: 'jql-preview-th-icon',
text: 'P',
});
headerRowEl.createEl("th", {
cls: "jql-preview-th-status",
text: "Status"
headerRowEl.createEl('th', {
cls: 'jql-preview-th-status',
text: 'Status',
});
// Table body
const tbodyEl = tableEl.createEl("tbody");
const tbodyEl = tableEl.createEl('tbody');
const conn = this.plugin.getCurrentConnection();
const jiraUrl = conn?.jiraUrl || '';
issues.forEach((issue, _) => {
const rowEl = tbodyEl.createEl("tr", { cls: "jql-preview-row" });
issues.forEach((issue) => {
const rowEl = tbodyEl.createEl('tr', {
cls: 'jql-preview-row',
});
// Key column
const keyCellEl = rowEl.createEl("td", { cls: "jql-preview-cell-key" });
const keyLinkEl = keyCellEl.createEl("a", {
cls: "jql-preview-key-link",
text: issue.key || "N/A",
const keyCellEl = rowEl.createEl('td', {
cls: 'jql-preview-cell-key',
});
keyCellEl.createEl('a', {
cls: 'jql-preview-key-link',
text: issue.key || 'N/A',
attr: {
href: `${this.plugin.settings.connection.jiraUrl}/browse/${issue.key}`,
target: "_blank",
rel: "noopener noreferrer"
}
href: `${jiraUrl}/browse/${issue.key}`,
target: '_blank',
rel: 'noopener noreferrer',
},
});
// Summary column (S)
const summaryCellEl = rowEl.createEl("td", { cls: "jql-preview-cell-summary" });
const summary = issue.fields?.summary || "No summary available";
summaryCellEl.setAttr("title", summary); // Show full text on hover
summaryCellEl.setText(summary.length > 80 ? summary.substring(0, 80) + "..." : summary);
const summaryCellEl = rowEl.createEl('td', {
cls: 'jql-preview-cell-summary',
});
const summary = issue.fields?.summary || 'No summary available';
summaryCellEl.setAttr('title', summary); // Show full text on hover
summaryCellEl.setText(summary.length > 80 ? summary.substring(0, 80) + '...' : summary);
// Type column (T)
const typeCellEl = rowEl.createEl("td", { cls: "jql-preview-cell-icon" });
const typeCellEl = rowEl.createEl('td', {
cls: 'jql-preview-cell-icon',
});
const issueType = issue.fields?.issuetype;
if (issueType && issueType.iconUrl) {
const typeIconEl = typeCellEl.createEl("img", {
typeCellEl.createEl('img', {
attr: { src: issueType.iconUrl, alt: issueType.name },
cls: "jql-preview-type-icon"
cls: 'jql-preview-type-icon',
});
} else {
// Fallback icon for unknown type
const fallbackIconEl = typeCellEl.createEl("div", {
cls: "jql-preview-status-indicator status-undefined"
typeCellEl.createEl('div', {
cls: 'jql-preview-status-indicator status-undefined',
});
}
// Created date column
const createdCellEl = rowEl.createEl("td", { cls: "jql-preview-cell-date" });
const createdCellEl = rowEl.createEl('td', {
cls: 'jql-preview-cell-date',
});
const createdDate = issue.fields?.created;
if (createdDate) {
const date = new Date(createdDate);
createdCellEl.setText(date.toLocaleDateString("en-US", {
month: "numeric",
day: "numeric",
year: "numeric"
}));
createdCellEl.setText(
date.toLocaleDateString('en-US', {
month: 'numeric',
day: 'numeric',
year: 'numeric',
}),
);
} else {
createdCellEl.setText("N/A");
createdCellEl.setText('N/A');
}
// Updated date column
const updatedCellEl = rowEl.createEl("td", { cls: "jql-preview-cell-date" });
const updatedCellEl = rowEl.createEl('td', {
cls: 'jql-preview-cell-date',
});
const updatedDate = issue.fields?.updated;
if (updatedDate) {
const date = new Date(updatedDate);
updatedCellEl.setText(date.toLocaleDateString("en-US", {
month: "numeric",
day: "numeric",
year: "numeric"
}));
updatedCellEl.setText(
date.toLocaleDateString('en-US', {
month: 'numeric',
day: 'numeric',
year: 'numeric',
}),
);
} else {
updatedCellEl.setText("N/A");
updatedCellEl.setText('N/A');
}
// Reporter column (R)
const reporterCellEl = rowEl.createEl("td", { cls: "jql-preview-cell-icon" });
const reporterCellEl = rowEl.createEl('td', {
cls: 'jql-preview-cell-icon',
});
const reporter = issue.fields?.reporter;
if (reporter) {
const reporterLinkEl = reporterCellEl.createEl("a", {
const reporterLinkEl = reporterCellEl.createEl('a', {
attr: {
href: `${this.plugin.settings.connection.jiraUrl}/secure/ViewProfile.jspa?name=${reporter.name}`,
target: "_blank",
rel: "noopener noreferrer",
title: reporter.displayName || reporter.name
}
href: `${jiraUrl}/secure/ViewProfile.jspa?name=${reporter.name}`,
target: '_blank',
rel: 'noopener noreferrer',
title: reporter.displayName || reporter.name,
},
});
if (reporter.avatarUrls && reporter.avatarUrls["16x16"]) {
const avatarEl = reporterLinkEl.createEl("img", {
attr: { src: reporter.avatarUrls["16x16"], alt: reporter.displayName },
cls: "jql-preview-user-avatar"
if (reporter.avatarUrls && reporter.avatarUrls['16x16']) {
reporterLinkEl.createEl('img', {
attr: {
src: reporter.avatarUrls['16x16'],
alt: reporter.displayName,
},
cls: 'jql-preview-user-avatar',
});
} else {
const initialEl = reporterLinkEl.createEl("div", {
cls: "jql-preview-user-initial",
text: reporter.displayName ? reporter.displayName.charAt(0).toUpperCase() : "?"
reporterLinkEl.createEl('div', {
cls: 'jql-preview-user-initial',
text: reporter.displayName ? reporter.displayName.charAt(0).toUpperCase() : '?',
});
}
} else {
const unknownEl = reporterCellEl.createEl("div", {
cls: "jql-preview-status-indicator status-undefined"
reporterCellEl.createEl('div', {
cls: 'jql-preview-status-indicator status-undefined',
});
}
// Assignee column (A)
const assigneeCellEl = rowEl.createEl("td", { cls: "jql-preview-cell-icon" });
const assigneeCellEl = rowEl.createEl('td', {
cls: 'jql-preview-cell-icon',
});
const assignee = issue.fields?.assignee;
if (assignee) {
const assigneeLinkEl = assigneeCellEl.createEl("a", {
const assigneeLinkEl = assigneeCellEl.createEl('a', {
attr: {
href: `${this.plugin.settings.connection.jiraUrl}/secure/ViewProfile.jspa?name=${assignee.name}`,
target: "_blank",
rel: "noopener noreferrer",
title: assignee.displayName || assignee.name
}
href: `${jiraUrl}/secure/ViewProfile.jspa?name=${assignee.name}`,
target: '_blank',
rel: 'noopener noreferrer',
title: assignee.displayName || assignee.name,
},
});
if (assignee.avatarUrls && assignee.avatarUrls["16x16"]) {
const avatarEl = assigneeLinkEl.createEl("img", {
attr: { src: assignee.avatarUrls["16x16"], alt: assignee.displayName },
cls: "jql-preview-user-avatar"
if (assignee.avatarUrls && assignee.avatarUrls['16x16']) {
assigneeLinkEl.createEl('img', {
attr: {
src: assignee.avatarUrls['16x16'],
alt: assignee.displayName,
},
cls: 'jql-preview-user-avatar',
});
} else {
const initialEl = assigneeLinkEl.createEl("div", {
cls: "jql-preview-user-initial",
text: assignee.displayName ? assignee.displayName.charAt(0).toUpperCase() : "?"
assigneeLinkEl.createEl('div', {
cls: 'jql-preview-user-initial',
text: assignee.displayName ? assignee.displayName.charAt(0).toUpperCase() : '?',
});
}
} else {
const unknownEl = assigneeCellEl.createEl("div", {
cls: "jql-preview-status-indicator status-undefined"
assigneeCellEl.createEl('div', {
cls: 'jql-preview-status-indicator status-undefined',
});
}
// Priority column (P)
const priorityCellEl = rowEl.createEl("td", { cls: "jql-preview-cell-icon" });
const priorityCellEl = rowEl.createEl('td', {
cls: 'jql-preview-cell-icon',
});
const priority = issue.fields?.priority;
if (priority && priority.iconUrl) {
const priorityIconEl = priorityCellEl.createEl("img", {
priorityCellEl.createEl('img', {
attr: { src: priority.iconUrl, alt: priority.name },
cls: "jql-preview-priority-icon"
cls: 'jql-preview-priority-icon',
});
} else {
const unknownEl = priorityCellEl.createEl("div", {
cls: "jql-preview-status-indicator status-undefined"
priorityCellEl.createEl('div', {
cls: 'jql-preview-status-indicator status-undefined',
});
}
// Status column
const statusCellEl = rowEl.createEl("td", { cls: "jql-preview-cell-status" });
const statusCellEl = rowEl.createEl('td', {
cls: 'jql-preview-cell-status',
});
const status = issue.fields?.status;
if (status) {
const statusBadgeEl = statusCellEl.createEl("span", {
statusCellEl.createEl('span', {
cls: `jql-preview-status-badge status-${status.statusCategory?.key || 'default'}`,
text: status.name
text: status.name,
});
} else {
const statusBadgeEl = statusCellEl.createEl("span", {
cls: "jql-preview-status-badge status-default",
text: "Unknown"
statusCellEl.createEl('span', {
cls: 'jql-preview-status-badge status-default',
text: 'Unknown',
});
}
});
@ -306,15 +352,25 @@ export class JQLPreview {
try {
// Enhanced fields for better preview (matching Jira table columns)
const fields = ["summary", "issuetype", "key", "priority", "status", "created", "updated", "reporter", "assignee"];
const fields = [
'summary',
'issuetype',
'key',
'priority',
'status',
'created',
'updated',
'reporter',
'assignee',
];
const res = await fetchIssuesByJQLRaw(this.plugin, trimmedJql, this.limit, fields);
let total = 0;
switch (this.plugin.settings.connection.apiVersion) {
case "2":
switch (this.plugin.getCurrentConnection()?.apiVersion) {
case '2':
total = res.total;
break;
case "3":
case '3':
total = await fetchCountIssuesByJQL(this.plugin, trimmedJql);
break;
}

View file

@ -1,5 +1,5 @@
import { App, AbstractInputSuggest, TFile, TFolder } from "obsidian";
import {debugLog} from "../../tools/debugLogging";
import { App, AbstractInputSuggest, TFile, TFolder } from 'obsidian';
import { debugLog } from '../../tools/debugLogging';
interface TemplateOption {
path: string;
@ -15,7 +15,12 @@ export class TemplateSuggest extends AbstractInputSuggest<TemplateOption> {
private cacheValid: boolean = false;
private onChange?: (value: string) => void;
constructor(app: App, inputEl: HTMLInputElement, templateDirectory: string | null = null, onChange?: (value: string) => void) {
constructor(
app: App,
inputEl: HTMLInputElement,
templateDirectory: string | null = null,
onChange?: (value: string) => void,
) {
super(app, inputEl);
this.templateDirectory = templateDirectory;
this.onChange = onChange;
@ -34,9 +39,9 @@ export class TemplateSuggest extends AbstractInputSuggest<TemplateOption> {
: this.app.vault.getRoot();
if (searchFolder instanceof TFolder) {
this.scanFolderForTemplates(searchFolder, "");
this.scanFolderForTemplates(searchFolder, '');
} else {
this.scanFolderForTemplates(this.app.vault.getRoot(), "");
this.scanFolderForTemplates(this.app.vault.getRoot(), '');
}
this.allTemplates.sort((a, b) => a.displayName.localeCompare(b.displayName));
@ -52,7 +57,7 @@ export class TemplateSuggest extends AbstractInputSuggest<TemplateOption> {
const displayName = prefix ? `${prefix}/${child.basename}` : child.basename;
this.allTemplates.push({
path: child.path,
displayName: displayName
displayName: displayName,
});
} else if (child instanceof TFolder) {
const newPrefix = prefix ? `${prefix}/${child.name}` : child.name;
@ -85,22 +90,22 @@ export class TemplateSuggest extends AbstractInputSuggest<TemplateOption> {
}
// Fast filtering - only filter if we have a query
return this.allTemplates.filter(option =>
option.displayName.toLowerCase().includes(query) ||
option.path.toLowerCase().includes(query)
return this.allTemplates.filter(
(option) => option.displayName.toLowerCase().includes(query) || option.path.toLowerCase().includes(query),
);
}
renderSuggestion(template: TemplateOption, el: HTMLElement): void {
el.setText(template.displayName);
el.setAttr("title", template.path);
el.setAttr('title', template.path);
}
selectSuggestion(template: TemplateOption): void {
debugLog("selected Templates Folder", template);
debugLog('selected Templates Folder', template);
this.setValue(template.path);
this.onChange && this.onChange(template.path);
if (this.onChange) {
this.onChange(template.path);
}
this.close();
}
}

View file

@ -1,10 +1,10 @@
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";
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;
const t = useTranslations('settings.tfm').t;
export class TestFieldMappingsComponent implements SettingsComponent {
private props: SettingsComponentProps;
@ -23,40 +23,48 @@ export class TestFieldMappingsComponent implements SettingsComponent {
render(containerEl: HTMLElement): void {
// Add section header
this.testMappingsContainer = containerEl.createDiv({
cls: "jira-test-mappings-container"
cls: 'jira-test-mappings-container',
});
this.testMappingsContainer.createEl("p", {
text: t("desc"),
cls: "test-mappings-desc"
this.testMappingsContainer.createEl('p', {
text: t('desc'),
cls: 'test-mappings-desc',
});
// Container for all test mapping items
this._testMappingItemsContainer = this.testMappingsContainer.createDiv({ cls: "test-mapping-items-list" });
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" });
const buttonView = this.testMappingsContainer.createDiv({
cls: 'button-view',
});
// Add new mapping button (only once, at the bottom)
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" }
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", () => {
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" }
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", () => {
setIcon(resetBtn, 'refresh-cw');
resetBtn.addEventListener('click', () => {
this._testMappingItemsContainer?.empty();
});
}
@ -67,34 +75,44 @@ export class TestFieldMappingsComponent implements SettingsComponent {
if (!itemsContainer) return;
const itemContainer = itemsContainer.createDiv({
cls: "test-mapping-item"
cls: 'test-mapping-item',
});
// From Jira expression input
const fromJiraContainer = itemContainer.createDiv({ cls: "from-jira-container" });
fromJiraContainer.createEl("span", { text: t("from_jira")+":", cls: "field-mapping-label" });
const fromJiraContainer = itemContainer.createDiv({
cls: 'from-jira-container',
});
fromJiraContainer.createEl('span', {
text: t('from_jira') + ':',
cls: 'field-mapping-label',
});
const fromJiraInput = fromJiraContainer.createEl("textarea", {
cls: "from-jira-input"
const fromJiraInput = fromJiraContainer.createEl('textarea', {
cls: 'from-jira-input',
});
fromJiraInput.rows = 1;
fromJiraInput.placeholder = "(issue) => null";
fromJiraInput.placeholder = '(issue) => null';
// Value display
const valueContainer = itemContainer.createDiv({ cls: "value-container" });
valueContainer.createEl("span", { text: t("value")+":", cls: "field-mapping-label" });
const valueDisplay = valueContainer.createEl("div", {
cls: "value-display",
const valueContainer = itemContainer.createDiv({
cls: 'value-container',
});
valueContainer.createEl('span', {
text: t('value') + ':',
cls: 'field-mapping-label',
});
const valueDisplay = valueContainer.createEl('div', {
cls: 'value-display',
attr: {
style: "user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;"
}
style: 'user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;',
},
});
// Debounced evaluation function
const debouncedEvaluate = debounce(async () => {
const issueData = this.getCurrentIssue ? this.getCurrentIssue() : this.currentIssueData;
if (!issueData) {
valueDisplay.setText(t("no_data"));
valueDisplay.setText(t('no_data'));
return;
}
@ -104,22 +122,22 @@ export class TestFieldMappingsComponent implements SettingsComponent {
const result = fromJiraFn(issueData, null);
valueDisplay.setText(JSON.stringify(result, null, 2));
} else {
valueDisplay.setText(t("invalid_exp"));
valueDisplay.setText(t('invalid_exp'));
}
} catch (error) {
valueDisplay.setText(`Error: ${error.message}`);
} catch (error: unknown) {
valueDisplay.setText(`Error: ${(error as Error).message}`);
}
}, 500);
// Add event listeners
fromJiraInput.addEventListener("input", debouncedEvaluate);
fromJiraInput.addEventListener('input', debouncedEvaluate);
// Add remove button
const removeBtn = itemContainer.createEl("button", {
text: "✕",
cls: "remove-field-btn"
const removeBtn = itemContainer.createEl('button', {
text: '✕',
cls: 'remove-field-btn',
});
removeBtn.addEventListener("click", () => {
removeBtn.addEventListener('click', () => {
itemContainer.remove();
});
}
@ -128,8 +146,8 @@ export class TestFieldMappingsComponent implements SettingsComponent {
setCurrentIssue(issue: any) {
this.currentIssueData = issue;
// Trigger re-evaluation of all test mappings
this.testMappingsContainer?.querySelectorAll(".from-jira-input").forEach((input: HTMLTextAreaElement) => {
input.dispatchEvent(new Event("input"));
this.testMappingsContainer?.querySelectorAll('.from-jira-input').forEach((input) => {
(input as HTMLTextAreaElement).dispatchEvent(new Event('input'));
});
}

View file

@ -1,8 +1,15 @@
import {Setting, Notice, setIcon, DropdownComponent, ButtonComponent} from 'obsidian';
import { Setting, Notice, setIcon, DropdownComponent, ButtonComponent } from 'obsidian';
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
import { processWorkLogBatch } from "../../commands";
import {useTranslations} from "../../localization/translator";
import {debugLog} from "../../tools/debugLogging";
import { processWorkLogBatch } from '../../commands';
import { useTranslations } from '../../localization/translator';
import { debugLog } from '../../tools/debugLogging';
import {
parseTimeBlocks,
processTimeEntries,
ProcessedEntry,
GroupedEntries,
TimeTrackerEntry,
} from '../../tools/timeTracker';
type TimeRangeType = 'days' | 'weeks' | 'months' | 'custom';
type SendComment = 'no' | 'last_block' | 'block_path';
@ -12,40 +19,7 @@ interface Option {
label: string;
}
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;
lastBlockName: 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;
const t = useTranslations('settings.statistics').t;
export class TimekeepSettingsComponent implements SettingsComponent {
private props: SettingsComponentProps;
@ -81,30 +55,30 @@ export class TimekeepSettingsComponent implements SettingsComponent {
const settingsContainer = containerEl.createDiv('timekeep-settings');
new Setting(settingsContainer)
.setName(t("settings.send_comments.name"))
.setDesc(t("settings.send_comments.description"))
.addDropdown(dropdown => {
dropdown.addOption('no', t("settings.send_comments.options.no"));
dropdown.addOption('last_block', t("settings.send_comments.options.last_block"));
dropdown.addOption('block_path', t("settings.send_comments.options.block_path"));
.setName(t('settings.send_comments.name'))
.setDesc(t('settings.send_comments.description'))
.addDropdown((dropdown) => {
dropdown.addOption('no', t('settings.send_comments.options.no'));
dropdown.addOption('last_block', t('settings.send_comments.options.last_block'));
dropdown.addOption('block_path', t('settings.send_comments.options.block_path'));
dropdown.setValue(this.props.plugin.settings.timekeep.sendComments);
dropdown.onChange(async (value: SendComment) => {
this.props.plugin.settings.timekeep.sendComments = value;
dropdown.onChange(async (value: string) => {
this.props.plugin.settings.timekeep.sendComments = value as SendComment;
});
});
// 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"));
.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.timekeep.statisticsTimeType);
dropdown.onChange(async (value: TimeRangeType) => {
this.props.plugin.settings.timekeep.statisticsTimeType = value;
dropdown.onChange(async (value: string) => {
this.props.plugin.settings.timekeep.statisticsTimeType = value as TimeRangeType;
this.toggleCustomDateInputs();
await this.refreshData();
});
@ -112,10 +86,11 @@ export class TimekeepSettingsComponent implements SettingsComponent {
// Max items slider (hidden for custom range)
this.maxItemsSetting = new Setting(settingsContainer)
.setName(t("settings.max_items.name"))
.setDesc(t("settings.max_items.description"))
.addSlider(slider => {
slider.setLimits(1, 20, 1)
.setName(t('settings.max_items.name'))
.setDesc(t('settings.max_items.description'))
.addSlider((slider) => {
slider
.setLimits(1, 20, 1)
.setValue(this.props.plugin.settings.timekeep.maxItemsToShow)
.setDynamicTooltip()
.onChange(async (value) => {
@ -128,56 +103,59 @@ export class TimekeepSettingsComponent implements SettingsComponent {
this.customDatesContainer = settingsContainer.createDiv('custom-dates-container');
new Setting(this.customDatesContainer)
.setName(t("settings.date_from.name"))
.setDesc(t("settings.date_from.description"))
.addText(text => {
text.setPlaceholder(t("settings.date_from.placeholder"));
.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.timekeep.customDateRange.start);
text.onChange(async (value) => {
this.props.plugin.settings.timekeep.customDateRange.start = value;
if (this.props.plugin.settings.timekeep.statisticsTimeType === 'custom' && this.props.plugin.settings.timekeep.customDateRange.end) {
if (
this.props.plugin.settings.timekeep.statisticsTimeType === 'custom' &&
this.props.plugin.settings.timekeep.customDateRange.end
) {
await this.refreshData();
}
});
});
new Setting(this.customDatesContainer)
.setName(t("settings.date_to.name"))
.setDesc(t("settings.date_to.description"))
.addText(text => {
text.setPlaceholder(t("settings.date_to.placeholder"));
.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.timekeep.customDateRange.end);
text.onChange(async (value) => {
this.props.plugin.settings.timekeep.customDateRange.end = value;
if (this.props.plugin.settings.timekeep.statisticsTimeType === 'custom' && this.props.plugin.settings.timekeep.customDateRange.start) {
if (
this.props.plugin.settings.timekeep.statisticsTimeType === 'custom' &&
this.props.plugin.settings.timekeep.customDateRange.start
) {
await this.refreshData();
}
});
});
new Setting(settingsContainer)
.setName(t("display.select_period"))
.setName(t('display.select_period'))
.setClass('period-selector')
.addDropdown(dropdown => {
.addDropdown((dropdown) => {
this.periodDropdown = dropdown;
this.populateTimeRangeOptions(dropdown, []);
dropdown
.setValue(this.props.plugin.settings.timekeep.sendComments)
.onChange(async (selectedPeriod) => {
dropdown.setValue(this.props.plugin.settings.timekeep.sendComments).onChange(async (selectedPeriod) => {
this.selectedPeriodData = this.groupedByPeriod[selectedPeriod] || [];
this.sendButton? this.sendButton.setDisabled(false) : null;
if (this.sendButton) {
this.sendButton.setDisabled(false);
}
});
})
.addButton(
button => {
.addButton((button) => {
this.sendButton = button;
button.setDisabled(true);
button.setButtonText(t("actions.send_to_jira"));
button.setButtonText(t('actions.send_to_jira'));
button.onClick(() => this.sendWorkLogToJira());
button.setClass('timekeep-btn');
}
);
});
}
private toggleCustomDateInputs(): void {
@ -186,7 +164,7 @@ export class TimekeepSettingsComponent implements SettingsComponent {
const isCustom = this.props.plugin.settings.timekeep.statisticsTimeType === 'custom';
if (isCustom) {
this.maxItemsSetting.settingEl.hide()
this.maxItemsSetting.settingEl.hide();
this.customDatesContainer.show();
} else {
this.maxItemsSetting.settingEl.show();
@ -203,10 +181,14 @@ export class TimekeepSettingsComponent implements SettingsComponent {
try {
await this.processFiles();
this.updateDisplay();
if (!disableNotification) new Notice(t("messages.refresh_success"));
} catch (error) {
if (!disableNotification) new Notice(t('messages.refresh_success'));
} catch (error: unknown) {
console.error('Error refreshing timekeep data:', error);
new Notice(t("messages.refresh_error", { error: error.message }));
new Notice(
t('messages.refresh_error', {
error: (error as Error).message,
}),
);
} finally {
// Убираем индикатор загрузки
this.containerEl.removeClass('timekeep-loading');
@ -222,8 +204,8 @@ export class TimekeepSettingsComponent implements SettingsComponent {
if (Object.keys(this.groupedByPeriod).length === 0) {
this.containerEl.createEl('div', {
text: t("display.no_entries"),
cls: 'timekeep-no-data'
text: t('display.no_entries'),
cls: 'timekeep-no-data',
});
return;
}
@ -234,8 +216,8 @@ export class TimekeepSettingsComponent implements SettingsComponent {
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'
text: t('display.all_periods'),
cls: 'timekeep-section-title',
});
const sortedPeriods = Object.keys(this.groupedByPeriod).sort().reverse();
@ -253,25 +235,25 @@ export class TimekeepSettingsComponent implements SettingsComponent {
const headerContainer = periodDiv.createDiv('timekeep-period-header');
headerContainer.createEl('h4', {
text: this.formatPeriodLabel(period),
cls: 'timekeep-period-title'
cls: 'timekeep-period-title',
});
headerContainer.createEl('span', {
text: this.parseMsToDuration(totalMs),
cls: 'timekeep-period-total'
cls: 'timekeep-period-total',
});
this.createEntriesTable(periodDiv, this.groupedByPeriod[period]);
});
}
private groupAndSummarizeEntries(entries: ProcessedEntry[]):
Record<string, Record<string, { entries: ProcessedEntry[], totalMs: number }>>
{
private groupAndSummarizeEntries(
entries: ProcessedEntry[],
): Record<string, Record<string, { entries: ProcessedEntry[]; totalMs: number }>> {
const grouped: ReturnType<typeof this.groupAndSummarizeEntries> = {};
entries.forEach(entry => {
entries.forEach((entry) => {
const file = entry.file;
const issueKey = entry.issueKey || "no-issue";
const issueKey = entry.issueKey || 'no-issue';
if (!grouped[file]) grouped[file] = {};
if (!grouped[file][issueKey]) {
@ -311,37 +293,43 @@ export class TimekeepSettingsComponent implements SettingsComponent {
});
// Group title
const groupTitle = groupHeader.createEl('div', { cls: 'timekeep-group-title' });
const groupTitle = groupHeader.createEl('div', {
cls: 'timekeep-group-title',
});
groupTitle.createEl('span', { text: file });
// Group meta (issue key and duration)
const groupMeta = groupHeader.createEl('div', { cls: 'timekeep-group-meta' });
const groupMeta = groupHeader.createEl('div', {
cls: 'timekeep-group-meta',
});
const conn = this.props.plugin.getCurrentConnection();
const jiraUrl = conn?.jiraUrl || '';
if (issueKey && issueKey !== 'no-issue') {
const issueKeySpan = groupMeta.createEl('a', {
text: issueKey,
cls: 'timekeep-group-issue-key',
attr: {
href: `${this.props.plugin.settings.connection.jiraUrl}/browse/${issueKey}`,
target: "_blank",
rel: "noopener noreferrer"
}
href: `${jiraUrl}/browse/${issueKey}`,
target: '_blank',
rel: 'noopener noreferrer',
},
});
issueKeySpan.addEventListener('click', (e) => {
e.stopPropagation();
});
debugLog(`${this.props.plugin.settings.connection.jiraUrl}/browse/${issueKey}`)
debugLog(`${jiraUrl}/browse/${issueKey}`);
}
groupMeta.createEl('span', {
text: this.parseMsToDuration(group.totalMs),
cls: 'timekeep-group-duration'
cls: 'timekeep-group-duration',
});
// Collapse icon
const chevron = groupMeta.createEl('span', {
cls: 'jira-collapse-icon',
});
setIcon(chevron, "chevron-down");
setIcon(chevron, 'chevron-down');
// Create entries container
const entriesContainer = groupCard.createDiv('timekeep-entries-container');
@ -350,16 +338,16 @@ export class TimekeepSettingsComponent implements SettingsComponent {
if (group.entries.length > 0) {
const labelsDiv = entriesContainer.createDiv('timekeep-entry-labels');
labelsDiv.createEl('div', {
text: t("display.table.task"),
cls: 'timekeep-entry-label-task'
text: t('display.table.task'),
cls: 'timekeep-entry-label-task',
});
labelsDiv.createEl('div', {
text: t("display.table.start_time"),
cls: 'timekeep-entry-label-start'
text: t('display.table.start_time'),
cls: 'timekeep-entry-label-start',
});
labelsDiv.createEl('div', {
text: t("display.table.duration"),
cls: 'timekeep-entry-label-duration'
text: t('display.table.duration'),
cls: 'timekeep-entry-label-duration',
});
}
@ -371,21 +359,21 @@ export class TimekeepSettingsComponent implements SettingsComponent {
entryDiv.createEl('div', {
text: entry.blockPath,
cls: 'timekeep-entry-block-path',
title: entry.blockPath
title: entry.blockPath,
});
// Start time
entryDiv.createEl('div', {
text: entry.startTime,
cls: 'timekeep-entry-start-time',
title: entry.startTime
title: entry.startTime,
});
// Duration
entryDiv.createEl('div', {
text: entry.duration,
cls: 'timekeep-entry-duration',
title: entry.duration
title: entry.duration,
});
});
});
@ -398,7 +386,7 @@ export class TimekeepSettingsComponent implements SettingsComponent {
private async sendWorkLogToJira(): Promise<void> {
if (this.selectedPeriodData.length === 0) {
new Notice(t("messages.select_period_first"));
new Notice(t('messages.select_period_first'));
return;
}
@ -408,13 +396,13 @@ export class TimekeepSettingsComponent implements SettingsComponent {
case 'last_block':
data_to_send = this.selectedPeriodData.map((entry) => ({
...entry,
comment: entry.lastBlockName
comment: entry.lastBlockName,
}));
break;
case 'block_path':
data_to_send = this.selectedPeriodData.map((entry) => ({
...entry,
comment: entry.blockPath
comment: entry.blockPath,
}));
break;
default:
@ -422,15 +410,17 @@ export class TimekeepSettingsComponent implements SettingsComponent {
break;
}
await processWorkLogBatch(this.props.plugin, data_to_send);
new Notice(t("messages.send_success"));
} catch (error) {
new Notice(t('messages.send_success'));
} catch (error: unknown) {
console.error('Error sending work log to Jira:', error);
new Notice(t("messages.send_error", { error: error.message }));
new Notice(t('messages.send_error', { error: (error as Error).message }));
}
}
private formatPeriodLabel(periodKey: string): string {
return t("display.period_of", { date: this.formatPeriodDate(periodKey) });
return t('display.period_of', {
date: this.formatPeriodDate(periodKey),
});
}
private formatPeriodDate(periodKey: string): string {
@ -453,7 +443,10 @@ export class TimekeepSettingsComponent implements SettingsComponent {
return `${date.toISOString().split('T')[0]} ${weekEnd.toISOString().split('T')[0]}`;
}
case 'months':
return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long' });
return date.toLocaleDateString(undefined, {
year: 'numeric',
month: 'long',
});
default:
return date.toISOString().split('T')[0];
}
@ -463,9 +456,10 @@ export class TimekeepSettingsComponent implements SettingsComponent {
switch (this.props.plugin.settings.timekeep.statisticsTimeType) {
case 'days':
return date.toISOString().split('T')[0];
case 'weeks':
case 'weeks': {
const weekStart = this.getWeekStartDate(date);
return `week-${weekStart.toISOString().split('T')[0]}`;
}
case 'months':
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
case 'custom':
@ -478,7 +472,11 @@ export class TimekeepSettingsComponent implements SettingsComponent {
private isDateInRange(date: Date): boolean {
if (this.props.plugin.settings.timekeep.statisticsTimeType !== 'custom') return true;
if (!this.props.plugin.settings.timekeep.customDateRange.start || !this.props.plugin.settings.timekeep.customDateRange.end) return false;
if (
!this.props.plugin.settings.timekeep.customDateRange.start ||
!this.props.plugin.settings.timekeep.customDateRange.end
)
return false;
const from = new Date(this.props.plugin.settings.timekeep.customDateRange.start);
const to = new Date(this.props.plugin.settings.timekeep.customDateRange.end);
@ -490,8 +488,11 @@ export class TimekeepSettingsComponent implements SettingsComponent {
private validateCustomRange(): boolean {
if (this.props.plugin.settings.timekeep.statisticsTimeType !== 'custom') return true;
if (!this.props.plugin.settings.timekeep.customDateRange.start || !this.props.plugin.settings.timekeep.customDateRange.end) {
new Notice(t("messages.custom_range_required"));
if (
!this.props.plugin.settings.timekeep.customDateRange.start ||
!this.props.plugin.settings.timekeep.customDateRange.end
) {
new Notice(t('messages.custom_range_required'));
return false;
}
@ -499,7 +500,7 @@ export class TimekeepSettingsComponent implements SettingsComponent {
const to = new Date(this.props.plugin.settings.timekeep.customDateRange.end);
if (from > to) {
new Notice(t("messages.invalid_date_range"));
new Notice(t('messages.invalid_date_range'));
return false;
}
@ -517,11 +518,11 @@ export class TimekeepSettingsComponent implements SettingsComponent {
private parseMsToDuration(ms: number): string {
const units: { label: string; ms: number }[] = [
{ 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 }
{ 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;
@ -535,18 +536,18 @@ export class TimekeepSettingsComponent implements SettingsComponent {
}
}
return result.length > 0 ? result.join(" ") : "0s";
return result.length > 0 ? result.join(' ') : '0s';
}
private parseDurationToMs(duration: string): number {
if (duration === 'ongoing') return 0;
const units: { label: string; ms: number }[] = [
{ 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 }
{ 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 ms = 0;
@ -576,108 +577,65 @@ export class TimekeepSettingsComponent implements SettingsComponent {
}
private processEntries(
entries: Entry[] | undefined,
entries: TimeTrackerEntry[] | undefined,
fileName: string,
issueKey: string | undefined,
parentPath: string,
groupedEntries: GroupedEntries
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.parseMsToDuration(endTime.getTime() - startTime.getTime());
} else {
endTime = new Date();
duration = "ongoing";
}
groupedEntries[periodKey].push({
file: fileName.replace(".md", ""),
issueKey: issueKey,
blockPath: currentPath,
lastBlockName: entry.name,
startTime: this.toLocalIso(startTime),
endTime: entry.endTime ? this.toLocalIso(endTime) : "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);
}
}
processTimeEntries(
entries,
fileName,
issueKey,
parentPath,
groupedEntries,
(date) => this.isDateInRange(date),
(date) => this.getPeriodKey(date),
);
}
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.global.issuesFolder}/`)
);
const allBlocks: ReturnType<typeof parseTimeBlocks> = [];
const files = this.props.app.vault
.getMarkdownFiles()
.filter((file) => file.path.startsWith(`${this.props.plugin.settings.global.issuesFolder}/`));
await Promise.all(files.map(async file => {
await Promise.all(
files.map(async (file) => {
const content = await this.props.app.vault.read(file);
if (!content.includes("```timekeep")) return;
if (!content.includes('```timekeep') && !content.includes('```simple-time-tracker')) 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);
}
});
}));
const blocks = parseTimeBlocks(content, file, issueKey);
allBlocks.push(...blocks);
}),
);
this.groupedByPeriod = {};
timekeepBlocks.forEach(block => {
this.processEntries(block.data.entries, block.file, block.issueKey, "", this.groupedByPeriod);
allBlocks.forEach((block) => {
this.processEntries(block.data.entries, block.file, block.issueKey, '', this.groupedByPeriod);
});
debugLog("grouped entries", 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 => ({
const options = sortedPeriods.map((period) => ({
value: period,
label: this.formatPeriodLabel(period)
label: this.formatPeriodLabel(period),
}));
this.populateTimeRangeOptions(this.periodDropdown, options);
this.periodDropdown?.setValue("");
this.sendButton ? this.sendButton.disabled = true : null;
this.periodDropdown?.setValue('');
if (this.sendButton) {
this.sendButton.disabled = true;
}
}
private populateTimeRangeOptions(dropdown: DropdownComponent | null, options: Option[]): void {
@ -687,16 +645,17 @@ export class TimekeepSettingsComponent implements SettingsComponent {
dropdown.selectEl.empty();
// Add new options based on current state
const record_options = options.reduce((acc, curr) => {
const record_options = options.reduce(
(acc, curr) => {
acc[curr.value] = curr.label;
return acc;
}, {} as Record<string, string>);
},
{} as Record<string, string>,
);
dropdown.addOptions(record_options);
}
private toLocalIso(date: Date): string {
return date.toISOString().slice(0, 19).replace("T", " ");
return date.toISOString().slice(0, 19).replace('T', ' ');
}
}

View file

@ -1,10 +1,10 @@
import {FieldMapping} from "../default/obsidianJiraFieldsMapping";
import { FieldMapping } from '../default/obsidianJiraFieldsMapping';
export interface ConnectionSettingsInterface {
name: string;
jiraUrl: string;
apiVersion: "2" | "3";
authMethod: "bearer" | "basic" | "session";
apiVersion: '2' | '3';
authMethod: 'bearer' | 'basic' | 'session';
apiToken: string;
username: string;
email: string;
@ -29,7 +29,7 @@ export interface FetchIssueInterface {
}
export interface TimekeepSettingsInterface {
sendComments: "no" | "last_block" | "block_path";
sendComments: 'no' | 'last_block' | 'block_path';
statisticsTimeType: string;
maxItemsToShow: number;
customDateRange: { start: string; end: string };
@ -47,7 +47,8 @@ export interface CollapsedSections {
export interface JiraSettingsInterface {
collapsedSections: CollapsedSections;
connection: ConnectionSettingsInterface;
connections: ConnectionSettingsInterface[];
currentConnectionIndex: number;
global: GlobalSettingsInterface;
fieldMapping: FieldMappingSettingsInterface;
fetchIssue: FetchIssueInterface;
@ -67,19 +68,23 @@ export const DEFAULT_SETTINGS: JiraSettingsInterface = {
statistics: false,
},
connection: {
authMethod: "bearer",
apiToken: "",
username: "",
email: "",
password: "",
jiraUrl: "",
apiVersion: "2",
connections: [
{
name: 'Connection 1',
authMethod: 'bearer',
apiToken: '',
username: '',
email: '',
password: '',
jiraUrl: '',
apiVersion: '2',
},
],
currentConnectionIndex: 0,
global: {
issuesFolder: "jira-issues",
templatePath: "",
issuesFolder: 'jira-issues',
templatePath: '',
},
fieldMapping: {
@ -89,19 +94,18 @@ export const DEFAULT_SETTINGS: JiraSettingsInterface = {
},
fetchIssue: {
filenameTemplate: "{summary} ({key})",
fields: ["*all"],
filenameTemplate: '{summary} ({key})',
fields: ['*all'],
expand: [],
},
timekeep: {
sendComments: "no",
statisticsTimeType: "weeks",
sendComments: 'no',
statisticsTimeType: 'weeks',
maxItemsToShow: 10,
customDateRange: { start: "", end: "" },
customDateRange: { start: '', end: '' },
},
sessionCookieName: "JSESSIONID",
issueKeyToFilePathCache: {}
sessionCookieName: 'JSESSIONID',
issueKeyToFilePathCache: {},
};

View file

@ -1,7 +1,7 @@
import { Notice } from "obsidian";
import { ValidationResult } from "../../interfaces/settingsTypes";
import { validateFunctionString } from "../../tools/convertFunctionString";
import { debugLog } from "../../tools/debugLogging";
import { Notice } from 'obsidian';
import { ValidationResult } from '../../interfaces/settingsTypes';
import { validateFunctionString } from '../../tools/convertFunctionString';
import { debugLog } from '../../tools/debugLogging';
/**
* Validate a field and update its visual state
@ -10,7 +10,7 @@ export async function validateField(
input: HTMLInputElement | HTMLTextAreaElement,
requireValidating: boolean = true,
type: string = 'string',
validateParams: Array<string> = []
validateParams: Array<string> = [],
): Promise<void> {
const validatorFunction = async (event?: Event) => {
const consoleOutput = !!event;
@ -22,10 +22,10 @@ export async function validateField(
setupValidatorProperty(input, validatorFunction);
if (requireValidating) {
input.addEventListener("change", (input as any)._validatorFunction);
input.addEventListener('change', (input as any)._validatorFunction);
await validatorFunction(); // Run initial validation
} else {
input.removeEventListener("change", (input as any)._validatorFunction);
input.removeEventListener('change', (input as any)._validatorFunction);
updateFieldAppearance({ isValid: true }, input, false); // Clear any validation errors
}
}
@ -36,12 +36,12 @@ export async function validateField(
async function getValidationResult(
value: string,
type: string = 'string',
validateParams: Array<string> = []
validateParams: Array<string> = [],
): Promise<ValidationResult> {
switch(type) {
switch (type) {
case 'string':
return value.length === 0
? { isValid: false, errorMessage: "Field name cannot be empty" }
? { isValid: false, errorMessage: 'Field name cannot be empty' }
: { isValid: true };
case 'function':
return await validateFunctionString(value, validateParams);
@ -56,16 +56,16 @@ async function getValidationResult(
export function updateFieldAppearance(
validation: ValidationResult,
element: HTMLInputElement | HTMLTextAreaElement,
consoleOutput: boolean
consoleOutput: boolean,
): void {
if (!validation.isValid) {
element.classList.add("invalid");
element.classList.add('invalid');
if (consoleOutput && validation.errorMessage) {
debugLog(validation.errorMessage);
new Notice(validation.errorMessage);
}
} else {
element.classList.remove("invalid");
element.classList.remove('invalid');
}
}
@ -74,16 +74,16 @@ export function updateFieldAppearance(
*/
function setupValidatorProperty(
input: HTMLInputElement | HTMLTextAreaElement,
validatorFunction: (event?: Event) => Promise<void>
validatorFunction: (event?: Event) => Promise<void>,
): void {
if (!input.hasOwnProperty('_validatorFunction')) {
if (!Object.prototype.hasOwnProperty.call(input, '_validatorFunction')) {
Object.defineProperty(input, '_validatorFunction', {
value: validatorFunction,
writable: true,
configurable: true
configurable: true,
});
} else {
input.removeEventListener("change", (input as any)._validatorFunction);
input.removeEventListener('change', (input as any)._validatorFunction);
(input as any)._validatorFunction = validatorFunction;
}
}

View file

@ -1,16 +1,16 @@
import { JiraFieldMapping, JiraFieldMappingString } from "../../interfaces/settingsTypes";
import { JiraFieldMapping, JiraFieldMappingString } from '../../interfaces/settingsTypes';
import {
jiraFunctionToString,
transform_string_to_functions_mappings,
// validateFunctionString
} from "../../tools/convertFunctionString";
import { debugLog } from "../../tools/debugLogging";
} from '../../tools/convertFunctionString';
import { debugLog } from '../../tools/debugLogging';
/**
* Convert function mappings to their string representation
*/
export function convertFunctionMappingsToStrings(
mappings: Record<string, JiraFieldMapping>
mappings: Record<string, JiraFieldMapping>,
): Record<string, JiraFieldMappingString> {
const result: Record<string, JiraFieldMappingString> = {};
@ -18,7 +18,7 @@ export function convertFunctionMappingsToStrings(
if (mapping && typeof mapping === 'object' && 'toJira' in mapping && 'fromJira' in mapping) {
result[fieldName] = {
toJira: jiraFunctionToString(mapping.toJira, false),
fromJira: jiraFunctionToString(mapping.fromJira, true)
fromJira: jiraFunctionToString(mapping.fromJira, true),
};
}
}
@ -34,12 +34,12 @@ export async function collectFieldMappingsFromUI(
// enableValidation: boolean
): Promise<Record<string, JiraFieldMappingString>> {
const mappings: Record<string, JiraFieldMappingString> = {};
const fieldItems = element.querySelectorAll(".field-mapping-item");
const fieldItems = element.querySelectorAll('.field-mapping-item');
fieldItems.forEach(item => {
const fieldNameInput = item.querySelector(".field-name-input");
const toJiraInput = item.querySelector(".to-jira-input");
const fromJiraInput = item.querySelector(".from-jira-input");
fieldItems.forEach((item) => {
const fieldNameInput = item.querySelector('.field-name-input');
const toJiraInput = item.querySelector('.to-jira-input');
const fromJiraInput = item.querySelector('.from-jira-input');
if (!fieldNameInput || !toJiraInput || !fromJiraInput) {
return;
@ -53,7 +53,7 @@ export async function collectFieldMappingsFromUI(
if (fieldName) {
mappings[fieldName] = {
toJira: toJira,
fromJira: fromJira
fromJira: fromJira,
};
}
});
@ -67,19 +67,16 @@ export async function collectFieldMappingsFromUI(
*/
export async function processMappings(
stringMappings: Record<string, JiraFieldMappingString>,
enableValidation: boolean
enableValidation: boolean,
): Promise<{
stringMappings: Record<string, JiraFieldMappingString>,
functionMappings: Record<string, JiraFieldMapping>
stringMappings: Record<string, JiraFieldMappingString>;
functionMappings: Record<string, JiraFieldMapping>;
}> {
// Convert string mappings to function mappings
const functionMappings = await transform_string_to_functions_mappings(
stringMappings,
enableValidation
);
const functionMappings = await transform_string_to_functions_mappings(stringMappings, enableValidation);
return {
stringMappings,
functionMappings
functionMappings,
};
}

View file

@ -1,28 +1,26 @@
import {TextComponent} from "obsidian";
import { TextComponent } from 'obsidian';
export function setupArrayTextSetting(
text: TextComponent,
initialArray: string[],
onChange: (array: string[]) => Promise<void>
onChange: (array: string[]) => Promise<void>,
) {
text
.setValue(initialArray.join(", "))
.onChange(async (value) => {
text.setValue(initialArray.join(', ')).onChange(async (value) => {
const array = value
.split(",")
.map(field => field.trim())
.filter(field => field.length > 0);
.split(',')
.map((field) => field.trim())
.filter((field) => field.length > 0);
await onChange(array);
});
text.inputEl.addEventListener("blur", () => {
text.inputEl.addEventListener('blur', () => {
const currentValue = text.inputEl.value;
const cleaned = currentValue
.split(",")
.map(field => field.trim())
.filter(field => field.length > 0)
.join(", ");
.split(',')
.map((field) => field.trim())
.filter((field) => field.length > 0)
.join(', ');
if (currentValue !== cleaned) {
text.setValue(cleaned);

View file

@ -1,4 +1,3 @@
export function createLimiter(concurrency: number) {
let activeCount = 0;
const queue: (() => void)[] = [];
@ -13,7 +12,7 @@ export function createLimiter(concurrency: number) {
return async function limit<T>(fn: () => Promise<T>): Promise<T> {
if (activeCount >= concurrency) {
await new Promise<void>(resolve => queue.push(resolve));
await new Promise<void>((resolve) => queue.push(resolve));
}
activeCount++;
try {
@ -25,7 +24,5 @@ export function createLimiter(concurrency: number) {
}
export function chunkArray<T>(arr: T[], size: number): T[][] {
return Array.from({length: Math.ceil(arr.length / size)}, (_, i) =>
arr.slice(i * size, i * size + size)
);
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size));
}

View file

@ -1,5 +1,5 @@
import JiraPlugin from "../main";
import { TFile, TFolder } from "obsidian";
import JiraPlugin from '../main';
import { TFile, TFolder } from 'obsidian';
/**
* Utility functions for managing the issue key to file path cache
@ -51,7 +51,7 @@ export async function validateCache(plugin: JiraPlugin): Promise<void> {
}
// Remove invalid entries
entriesToRemove.forEach(issueKey => {
entriesToRemove.forEach((issueKey) => {
plugin.removeIssueKeyFromCache(issueKey);
});
}

View file

@ -1,6 +1,6 @@
import JiraPlugin from "../main";
import {validateSettings} from "../api";
import {Notice} from "obsidian";
import JiraPlugin from '../main';
import { validateSettings } from '../api';
import { Notice } from 'obsidian';
// import {debugLog} from "./debugLogging";
export function checkCommandCallback(
@ -8,7 +8,7 @@ export function checkCommandCallback(
checking: boolean,
functionToExecute: (...args: any[]) => Promise<void>,
frontmatterChecks: string[],
functionArgs: string[] = []
functionArgs: string[] = [],
): boolean {
const settings_are_valid = validateSettings(plugin);
const file = plugin.app.workspace.getActiveFile();
@ -19,8 +19,10 @@ export function checkCommandCallback(
}
const frontmatter = plugin.app.metadataCache.getFileCache(file)?.frontmatter;
const hasChecks = frontmatterChecks.every(key => frontmatter?.[key] !== undefined && frontmatter?.[key] !== null && frontmatter?.[key] !== '');
const functionArgsValues = functionArgs.map(arg => frontmatter?.[arg]);
const hasChecks = frontmatterChecks.every(
(key) => frontmatter?.[key] !== undefined && frontmatter?.[key] !== null && frontmatter?.[key] !== '',
);
const functionArgsValues = functionArgs.map((arg) => frontmatter?.[arg]);
// debugLog('Function checked:', settings_are_valid && hasChecks,
// 'for function to execute:', functionToExecute.name,
@ -29,12 +31,10 @@ export function checkCommandCallback(
if (settings_are_valid && hasChecks) {
if (!checking) {
functionToExecute(plugin, file, ...functionArgsValues).catch(
(error) => {
new Notice(`Error when doing ${functionToExecute.name}: ` + (error.message || "Unknown error"));
functionToExecute(plugin, file, ...functionArgsValues).catch((error) => {
new Notice(`Error when doing ${functionToExecute.name}: ` + (error.message || 'Unknown error'));
console.error(error);
}
);
});
}
return true;
}

View file

@ -1,20 +1,20 @@
import { Notice } from "obsidian";
import { JiraIssue } from "../interfaces";
import { parse } from "acorn";
import {debugLog} from "./debugLogging";
import {FieldMapping} from "../default/obsidianJiraFieldsMapping";
import {defaultIssue} from "../default/defaultIssue";
import {jiraToMarkdown, markdownToJira} from "./markdownHtml";
import { Notice } from 'obsidian';
import { JiraIssue } from '../interfaces';
import { parse } from 'acorn';
import { debugLog } from './debugLogging';
import { FieldMapping } from '../default/obsidianJiraFieldsMapping';
import { defaultIssue } from '../default/defaultIssue';
import { jiraToMarkdown, markdownToJira } from './markdownHtml';
// Constants for validation and error messages
const FORBIDDEN_PATTERNS = ["document", "window", "eval", "Function", "fetch", "setTimeout", "globalThis"];
const SYNTAX_KEYWORDS = ["return", "if", "else", "for", "while", "switch", "try", "catch"];
const FORBIDDEN_PATTERNS = ['document', 'window', 'eval', 'Function', 'fetch', 'setTimeout', 'globalThis'];
const SYNTAX_KEYWORDS = ['return', 'if', 'else', 'for', 'while', 'switch', 'try', 'catch'];
const SAFE_GLOBALS = {
jiraToMarkdown,
markdownToJira,
JSON: {
parse: JSON.parse,
stringify: JSON.stringify
stringify: JSON.stringify,
},
Math: Math,
Date: Date,
@ -22,88 +22,111 @@ const SAFE_GLOBALS = {
Number: Number,
Boolean: Boolean,
Array: Array,
Object: Object
Object: Object,
};
export function validateFunctionStringBrowser(fnString: string, approved_vars: string[] = []): Promise<{ isValid: boolean; errorMessage?: string }> {
export function validateFunctionStringBrowser(
fnString: string,
approved_vars: string[] = [],
): Promise<{ isValid: boolean; errorMessage?: string }> {
return new Promise((resolve) => {
try {
parse(fnString, { ecmaVersion: "latest" });
parse(fnString, { ecmaVersion: 'latest' });
} catch (error) {
return resolve({ isValid: false, errorMessage: `Syntax error: ${(error as Error).message}` });
return resolve({
isValid: false,
errorMessage: `Syntax error: ${(error as Error).message}`,
});
}
const iframe = document.createElement("iframe");
iframe.style.display = "none";
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
document.body.appendChild(iframe);
const iframeWindow = iframe.contentWindow as Window;
try {
let testFnString = fnString;
if (isSimpleExpression(fnString) || (fnString.startsWith("{") && fnString.endsWith("}") && !fnString.includes("return"))) {
const varDeclarations = approved_vars.map(varName => {
if (
isSimpleExpression(fnString) ||
(fnString.startsWith('{') && fnString.endsWith('}') && !fnString.includes('return'))
) {
const varDeclarations = approved_vars
.map((varName) => {
if (varName === 'issue') return `${varName} = ${JSON.stringify(defaultIssue)}`;
if (varName === 'value') return `${varName} = "test value"`;
if (varName === 'data_source') return `${varName} = {}`;
return `${varName} = {}`;
}).join(', ');
})
.join(', ');
testFnString = `(${varDeclarations}) => ${fnString}`;
}
debugLog(`checking function: ${testFnString.substring(0, 100)}`);
const context: Record<string, any> = {};
Object.keys(SAFE_GLOBALS).forEach(key => {
Object.keys(SAFE_GLOBALS).forEach((key) => {
context[key] = (SAFE_GLOBALS as any)[key];
});
const safeBuiltIns = ['Infinity', 'NaN', 'undefined', 'isFinite', 'isNaN',
'parseFloat', 'parseInt', 'decodeURI', 'decodeURIComponent',
'encodeURI', 'encodeURIComponent'];
safeBuiltIns.forEach(key => {
const safeBuiltIns = [
'Infinity',
'NaN',
'undefined',
'isFinite',
'isNaN',
'parseFloat',
'parseInt',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent',
];
safeBuiltIns.forEach((key) => {
context[key] = (iframeWindow as any)[key];
});
const testArgs = approved_vars.map(varName => {
const testArgs = approved_vars.map((varName) => {
if (varName === 'issue') return defaultIssue;
if (varName === 'value') return "test value";
if (varName === 'value') return 'test value';
if (varName === 'data_source') return {};
return {};
});
const contextKeys = Object.keys(context);
const contextValues = contextKeys.map(key => context[key]);
const contextValues = contextKeys.map((key) => context[key]);
const func = new Function(
...approved_vars,
...contextKeys,
`return (${fnString});`
);
const func = new Function(...approved_vars, ...contextKeys, `return (${fnString});`);
func(...testArgs, ...contextValues);
resolve({ isValid: true });
} catch (error) {
debugLog('Not valid')
resolve({ isValid: false, errorMessage: `Runtime error: ${(error as Error).message}` });
debugLog('Not valid');
resolve({
isValid: false,
errorMessage: `Runtime error: ${(error as Error).message}`,
});
} finally {
document.body.removeChild(iframe);
}
});
}
export async function validateFunctionString(fnString: string, approved_vars: string[] = []): Promise<{ isValid: boolean; errorMessage?: string }> {
export async function validateFunctionString(
fnString: string,
approved_vars: string[] = [],
): Promise<{ isValid: boolean; errorMessage?: string }> {
// Check for null, undefined or empty string
if (!fnString || fnString.trim().length === 0) {
return { isValid: true };
}
// Check for forbidden patterns (security)
const foundForbidden = FORBIDDEN_PATTERNS.find(pattern => fnString.includes(pattern));
const foundForbidden = FORBIDDEN_PATTERNS.find((pattern) => fnString.includes(pattern));
if (foundForbidden) {
return {
isValid: false,
errorMessage: `Unsafe reference detected: '${foundForbidden}' is not allowed`
errorMessage: `Unsafe reference detected: '${foundForbidden}' is not allowed`,
};
}
@ -124,13 +147,13 @@ function isSimpleExpression(fnString: string): boolean {
}
// Check if it has typical function body syntax elements
const hasComplexSyntax = SYNTAX_KEYWORDS.some(keyword =>
const hasComplexSyntax = SYNTAX_KEYWORDS.some((keyword) =>
// Match full keywords, not substrings
new RegExp(`\\b${keyword}\\b`).test(trimmed)
new RegExp(`\\b${keyword}\\b`).test(trimmed),
);
// Has curly braces which might indicate a code block
const hasCodeBlock = trimmed.includes("{") && trimmed.includes("}") && trimmed.includes("return");
const hasCodeBlock = trimmed.includes('{') && trimmed.includes('}') && trimmed.includes('return');
return !hasComplexSyntax && !hasCodeBlock;
}
@ -145,14 +168,17 @@ function isSimpleExpression(fnString: string): boolean {
export async function safeStringToFunction(
exprString: string,
type: 'toJira' | 'fromJira' = 'toJira',
extraValidate: boolean = true
): Promise<Function | null> {
extraValidate: boolean = true,
): Promise<((...args: any[]) => any) | null> {
try {
if (exprString.trim().length === 0) {
return () => null;
}
if (extraValidate) {
const validation = await validateFunctionString(exprString, type === 'fromJira' ? ['issue', 'data_source'] : ['value']);
const validation = await validateFunctionString(
exprString,
type === 'fromJira' ? ['issue', 'data_source'] : ['value'],
);
if (!validation.isValid) {
console.warn(`Invalid function: ${validation.errorMessage}`);
// new Notice(`Invalid function: ${validation.errorMessage}`);
@ -160,7 +186,6 @@ export async function safeStringToFunction(
}
}
const arrowFnMatch = exprString.match(/^\s*\((.*?)\)\s*=>\s*(.*)$/s);
let body = arrowFnMatch ? arrowFnMatch[2].trim() : exprString.trim();
@ -168,7 +193,7 @@ export async function safeStringToFunction(
body = `return ${body};`;
}
if (body.startsWith("{") && body.endsWith("}") && !body.includes("return")) {
if (body.startsWith('{') && body.endsWith('}') && !body.includes('return')) {
body = `{ return ${body.slice(1, -1)} }`;
}
@ -182,13 +207,15 @@ export async function safeStringToFunction(
Number,
Boolean,
Array,
Object
Object,
};
if (type === 'toJira') {
return function(value: any) {
const fn = new Function('value', 'context', `
return function (value: any) {
const fn = new Function(
'value',
'context',
`
with (context) {
try {
${body}
@ -197,14 +224,19 @@ export async function safeStringToFunction(
return null;
}
}
`);
`,
);
return fn.call(context, value, context);
};
} else { // fromJira
return function(issue: JiraIssue, data_source: Record<string, any> | null) {
const fn = new Function('issue', 'data_source', 'context', `
} else {
// fromJira
return function (issue: JiraIssue, data_source: Record<string, any> | null) {
const fn = new Function(
'issue',
'data_source',
'context',
`
with (context) {
try {
${body}
@ -213,19 +245,20 @@ export async function safeStringToFunction(
return null;
}
}
`);
`,
);
return fn.call(context, issue, data_source, context);
};
}
} catch (error) {
console.error("Failed to parse function:", error);
console.error('Failed to parse function:', error);
new Notice(`Failed to create function: ${(error as Error).message}`);
return null;
}
}
export function jiraFunctionToString(fn: Function, isFromJira: boolean = false): string {
export function jiraFunctionToString(fn: (...args: any[]) => any, isFromJira: boolean = false): string {
const baseStr = functionToExpressionString(fn);
if (!baseStr) return baseStr;
@ -235,7 +268,7 @@ export function jiraFunctionToString(fn: Function, isFromJira: boolean = false):
const paramMatch = fnStr.match(/^\s*(?:\(?([^)]*)\)?\s*=>|\s*function\s*\(([^)]*)\))/);
if (paramMatch) {
const params = (paramMatch[1] || paramMatch[2] || '').split(',').map(p => p.trim());
const params = (paramMatch[1] || paramMatch[2] || '').split(',').map((p) => p.trim());
let resultStr = baseStr;
@ -267,7 +300,7 @@ export function jiraFunctionToString(fn: Function, isFromJira: boolean = false):
return baseStr;
}
export function functionToExpressionString(fn: Function): string {
export function functionToExpressionString(fn: (...args: any[]) => any): string {
try {
const fnStr = fn.toString().trim();
@ -290,17 +323,19 @@ export function functionToExpressionString(fn: Function): string {
}
// If we can't parse it properly, return empty string
console.error("Failed to extract expression from function:", fnStr);
new Notice("Failed to extract expression from function");
return "";
console.error('Failed to extract expression from function:', fnStr);
new Notice('Failed to extract expression from function');
return '';
} catch (error) {
console.error("Error converting function to expression string:", error);
return "";
console.error('Error converting function to expression string:', error);
return '';
}
}
export async function transform_string_to_functions_mappings (
mappings: Record<string, { toJira: string; fromJira: string }>, extraValidate: boolean = true) {
export async function transform_string_to_functions_mappings(
mappings: Record<string, { toJira: string; fromJira: string }>,
extraValidate: boolean = true,
) {
// Also convert to functions for runtime use
const transformedMappings: Record<string, FieldMapping> = {};
for (const [fieldName, { toJira, fromJira }] of Object.entries(mappings)) {
@ -309,12 +344,12 @@ export async function transform_string_to_functions_mappings (
if (toJiraFn && fromJiraFn) {
transformedMappings[fieldName] = {
toJira: await toJiraFn as (value: any) => any,
fromJira: await fromJiraFn as (issue: JiraIssue, data_source: Record<string, any>) => any,
toJira: (await toJiraFn) as (value: any) => any,
fromJira: (await fromJiraFn) as (issue: JiraIssue, data_source: Record<string, any> | null) => any,
};
} else {
console.warn(`Invalid function in field: ${fieldName}`);
}
}
return transformedMappings
return transformedMappings;
}

View file

@ -1,4 +1,3 @@
// Create a debug utility module (debug.ts)
export const DEBUG_MODE = process.env.NODE_ENV !== 'production';
export function debugLog(...args: any[]): void {

View file

@ -1,4 +1,4 @@
import JiraPlugin from "../main";
import JiraPlugin from '../main';
/**
* Ensure the issues folder exists

View file

@ -1,14 +1,14 @@
import {JiraIssue} from "../interfaces";
import {jiraToMarkdown} from "./markdownHtml";
import {Notice, TFile} from "obsidian";
import JiraPlugin from "../main";
import {extractAllJiraSyncValuesFromContent, updateJiraSyncContent} from "./sectionTools";
import {FieldMapping, obsidianJiraFieldMappings} from "../default/obsidianJiraFieldsMapping";
import {debugLog} from "./debugLogging";
import { JiraIssue } from '../interfaces';
import { jiraToMarkdown } from './markdownHtml';
import { Notice, TFile } from 'obsidian';
import JiraPlugin from '../main';
import { extractAllJiraSyncValuesFromContent, updateJiraSyncContent } from './sectionTools';
import { FieldMapping, obsidianJiraFieldMappings } from '../default/obsidianJiraFieldsMapping';
import { debugLog } from './debugLogging';
export function localToJiraFields(
data_source: Record<string, any>, customFieldMappings: Record<string, FieldMapping>
data_source: Record<string, any>,
customFieldMappings: Record<string, FieldMapping>,
): Record<string, any> {
const jiraFields: Record<string, any> = {};
@ -31,7 +31,6 @@ export function localToJiraFields(
} catch (e) {
console.error(`Error mapping for ${key}: ${e}`);
new Notice(`Error mapping for ${key}: ${e}`);
}
}
// Handle custom fields
@ -43,15 +42,14 @@ export function localToJiraFields(
return jiraFields;
}
export async function updateJiraToLocal(
plugin: JiraPlugin,
file: TFile,
issue: JiraIssue
): Promise<void> {
export async function updateJiraToLocal(plugin: JiraPlugin, file: TFile, issue: JiraIssue): Promise<void> {
// First, update the frontmatter using processFrontMatter
await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
// Update frontmatter with Jira data
applyJiraDataToLocal(frontmatter, issue, {...obsidianJiraFieldMappings, ...plugin.settings.fieldMapping.fieldMappings});
applyJiraDataToLocal(frontmatter, issue, {
...obsidianJiraFieldMappings,
...plugin.settings.fieldMapping.fieldMappings,
});
});
// Then, process the file content to update sync sections
@ -60,7 +58,10 @@ export async function updateJiraToLocal(
const syncSections = extractAllJiraSyncValuesFromContent(fileContent);
// Update sync sections with Jira data
applyJiraDataToLocal(syncSections, issue, {...obsidianJiraFieldMappings, ...plugin.settings.fieldMapping.fieldMappings});
applyJiraDataToLocal(syncSections, issue, {
...obsidianJiraFieldMappings,
...plugin.settings.fieldMapping.fieldMappings,
});
// Update content sections from Jira fields
let updatedContent = fileContent;
@ -79,7 +80,7 @@ export async function updateJiraToLocal(
export function applyJiraDataToLocal(
localData: Record<string, any>,
issue: JiraIssue,
fieldMappings: Record<string, FieldMapping>
fieldMappings: Record<string, FieldMapping>,
): void {
// Process existing fields in local data
for (const key of Object.keys(localData)) {
@ -99,7 +100,7 @@ function updateFieldFromJira(
key: string,
targetObject: Record<string, any>,
issue: JiraIssue,
fieldMappings: Record<string, FieldMapping>
fieldMappings: Record<string, FieldMapping>,
): void {
try {
let value = issue.fields[key];
@ -116,28 +117,24 @@ function updateFieldFromJira(
}
} catch (e) {
// Log available mappings for debugging
logMappingDebugInfo(key, e, fieldMappings);
logMappingDebugInfo(key, e as Error, fieldMappings);
}
}
/**
* Log debug information for mapping errors
*/
function logMappingDebugInfo(
key: string,
error: Error,
fieldMappings: Record<string, FieldMapping>
): void {
function logMappingDebugInfo(key: string, error: Error, fieldMappings: Record<string, FieldMapping>): void {
console.error(`Error mapping for ${key}: ${error}`);
new Notice(`Error mapping for ${key}: ${error}`);
// Create debug info about available mappings
const mappingInfo: Record<string, { hasToJira: string, hasFromJira: string }> = {};
const mappingInfo: Record<string, { hasToJira: string; hasFromJira: string }> = {};
for (const mappingKey of Object.keys(fieldMappings)) {
mappingInfo[mappingKey] = {
hasToJira: typeof fieldMappings[mappingKey].toJira,
hasFromJira: typeof fieldMappings[mappingKey].fromJira
hasFromJira: typeof fieldMappings[mappingKey].fromJira,
};
}

View file

@ -8,16 +8,16 @@ export function jiraToMarkdown(str: any): string {
if (str === null || str === undefined) return '';
// Initial normalization to string
let content: string = "";
if (typeof str === "string") content = str;
else if (typeof str === "number") content = str.toString();
else if (typeof str === "object") content = JSON.stringify(str);
let content: string = '';
if (typeof str === 'string') content = str;
else if (typeof str === 'number') content = str.toString();
else if (typeof str === 'object') content = JSON.stringify(str);
else content = String(str);
// URL Protection: Store original URLs
const urlMap: Map<string, string> = new Map();
// Regex to find URLs
const urlRegex = /(https?:\/\/[^\s\(\)\[\]\{\}]+)/g;
const urlRegex = /(https?:\/\/[^\s()[\]{}]+)/g;
let urlCount = 0;
content = content.replace(urlRegex, (match) => {
@ -59,7 +59,7 @@ export function jiraToMarkdown(str: any): string {
// Code Blocks
.replace(
/\{code(:([a-z]+))?([:|]?(title|borderStyle|borderColor|borderWidth|bgColor|titleBGColor)=.+?)*\}([^]*?)\n?\{code\}/gm,
'```$2$5\n```'
'```$2$5\n```',
)
.replace(/{noformat}/g, '```')
// Images
@ -85,11 +85,10 @@ export function jiraToMarkdown(str: any): string {
});
return content;
} catch (e) {
console.error("Error converting Jira markup to Markdown", e);
console.error('Error converting Jira markup to Markdown', e);
// Fallback to basic string conversion if everything explodes
return typeof str === "string" ? str : (str ? str.toString() : '');
return typeof str === 'string' ? str : str ? str.toString() : '';
}
}
@ -101,10 +100,10 @@ export function jiraToMarkdown(str: any): string {
export function markdownToJira(str: string): string {
if (!str) return '';
const map: Record<string, string> = {
del: "-",
ins: "+",
sup: "^",
sub: "~",
del: '-',
ins: '+',
sup: '^',
sub: '~',
};
return (
@ -112,31 +111,23 @@ export function markdownToJira(str: string): string {
// Tables
.replace(
/^(\|[^\n]+\|\r?\n)((?:\|\s*:?[-]+:?\s*)+\|)(\n(?:\|[^\n]+\|\r?\n?)*)?$/gm,
(
match: string,
headerLine: string,
separatorLine: string,
rowstr: string
) => {
(match: string, headerLine: string, separatorLine: string, rowstr: string) => {
const headers = headerLine.match(/[^|]+(?=\|)/g) || [];
const separators =
separatorLine.match(/[^|]+(?=\|)/g) || [];
const separators = separatorLine.match(/[^|]+(?=\|)/g) || [];
if (headers.length !== separators.length) return match;
const rows = rowstr.split("\n");
const rows = rowstr.split('\n');
if (rows.length === 2 && headers.length === 1)
// Panel
return `{panel:title=${headers[0].trim()}}\n${rowstr
.replace(/^\|(.*)[ \t]*\|/, "$1")
.replace(/^\|(.*)[ \t]*\|/, '$1')
.trim()}\n{panel}\n`;
return `||${headers.join("||")}||${rowstr}`;
}
return `||${headers.join('||')}||${rowstr}`;
},
)
// Bold, Italic, and Combined (bold+italic)
.replace(
/([*_]+)(\S.*?)\1/g,
(match: string, wrapper: string, content: string) => {
.replace(/([*_]+)(\S.*?)\1/g, (match: string, wrapper: string, content: string) => {
switch (wrapper.length) {
case 1:
return `_${content}_`;
@ -147,71 +138,55 @@ export function markdownToJira(str: string): string {
default:
return wrapper + content + wrapper;
}
}
)
})
// All Headers (# format)
.replace(
/^([#]+)(.*?)$/gm,
(match: string, level: string, content: string) => {
.replace(/^([#]+)(.*?)$/gm, (match: string, level: string, content: string) => {
return `h${level.length}.${content}`;
}
)
})
// Headers (H1 and H2 underlines)
.replace(
/^(.*?)\n([=-]+)$/gm,
(match: string, content: string, level: string) => {
return `h${level[0] === "=" ? 1 : 2}. ${content}`;
}
)
.replace(/^(.*?)\n([=-]+)$/gm, (match: string, content: string, level: string) => {
return `h${level[0] === '=' ? 1 : 2}. ${content}`;
})
// Ordered lists
.replace(
/^([ \t]*)\d+\.\s+/gm,
(match: string, spaces: string) => {
.replace(/^([ \t]*)\d+\.\s+/gm, (match: string, spaces: string) => {
return `${Array(Math.floor(spaces.length / 3) + 1)
.fill("#")
.join("")} `;
}
)
.fill('#')
.join('')} `;
})
// Un-Ordered Lists
.replace(
/^([ \t]*)\*\s+/gm,
(match: string, spaces: string) => {
.replace(/^([ \t]*)\*\s+/gm, (match: string, spaces: string) => {
return `${Array(Math.floor(spaces.length / 2 + 1))
.fill("*")
.join("")} `;
}
)
.fill('*')
.join('')} `;
})
// Headers (h1 or h2) (lines "underlined" by ---- or =====)
// Citations, Inserts, Subscripts, Superscripts, and Strikethroughs
.replace(
new RegExp(`<(${Object.keys(map).join("|")})>(.*?)</\\1>`, "g"),
new RegExp(`<(${Object.keys(map).join('|')})>(.*?)</\\1>`, 'g'),
(match: string, from: string, content: string) => {
const to = map[from];
return to + content + to;
}
},
)
// Other kind of strikethrough
.replace(/(\s+)~~(.*?)~~(\s+)/g, "$1-$2-$3")
.replace(/(\s+)~~(.*?)~~(\s+)/g, '$1-$2-$3')
// Named/Un-Named Code Block
.replace(
/```(.+\n)?((?:.|\n)*?)```/g,
(match: string, synt: string, content: string) => {
let code = "{code}";
.replace(/```(.+\n)?((?:.|\n)*?)```/g, (match: string, synt: string, content: string) => {
let code = '{code}';
if (synt) {
code = `{code:${synt.replace(/\n/g, "")}}\n`;
code = `{code:${synt.replace(/\n/g, '')}}\n`;
}
return `${code}${content}{code}`;
}
)
})
// Inline-Preformatted Text
.replace(/`([^`]+)`/g, "{{$1}}")
.replace(/`([^`]+)`/g, '{{$1}}')
// Images
.replace(/!\[[^\]]*\]\(([^)]+)\)/g, "!$1!")
.replace(/!\[[^\]]*\]\(([^)]+)\)/g, '!$1!')
// Named Link
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "[$1|$2]")
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[$1|$2]')
// Un-Named Link
.replace(/<([^>]+)>/g, "[$1]")
.replace(/<([^>]+)>/g, '[$1]')
// Single Paragraph Blockquote
.replace(/^>/gm, "bq.")
.replace(/^>/gm, 'bq.')
);
}

View file

@ -2,10 +2,10 @@ import {
ConnectionSettingsInterface,
FieldMappingSettingsInterface,
GlobalSettingsInterface,
TimekeepSettingsInterface
} from "../settings/default";
TimekeepSettingsInterface,
} from '../settings/default';
export function checkMigrateSettings(data: any, saveSettings: ()=>void): any {
export function checkMigrateSettings(data: any, saveSettings: () => void): any {
if (!data || typeof data !== 'object') return data;
const result = { ...data };
@ -13,7 +13,13 @@ export function checkMigrateSettings(data: any, saveSettings: ()=>void): any {
// Migrate root-level connection fields
const connectionFields: (keyof ConnectionSettingsInterface)[] = [
'jiraUrl', 'apiVersion', 'authMethod', 'apiToken', 'username', 'email', 'password'
'jiraUrl',
'apiVersion',
'authMethod',
'apiToken',
'username',
'email',
'password',
];
for (const field of connectionFields) {
if (field in result) {
@ -37,7 +43,9 @@ export function checkMigrateSettings(data: any, saveSettings: ()=>void): any {
// Migrate root-level field mapping fields
const fieldMappingFields: (keyof FieldMappingSettingsInterface)[] = [
'fieldMappings', 'fieldMappingsStrings', 'enableFieldValidation'
'fieldMappings',
'fieldMappingsStrings',
'enableFieldValidation',
];
for (const field of fieldMappingFields) {
if (field in result) {
@ -50,7 +58,10 @@ export function checkMigrateSettings(data: any, saveSettings: ()=>void): any {
// Migrate root-level timekeep fields
const timekeepFields: (keyof TimekeepSettingsInterface)[] = [
'sendComments', 'statisticsTimeType', 'maxItemsToShow', 'customDateRange'
'sendComments',
'statisticsTimeType',
'maxItemsToShow',
'customDateRange',
];
for (const field of timekeepFields) {
if (field in result) {
@ -61,6 +72,25 @@ export function checkMigrateSettings(data: any, saveSettings: ()=>void): any {
}
}
// Migrate old single connection to new connections array
if (result.connection && !result.connections) {
result.connections = [
{
name: result.connection.name || 'Connection 1',
jiraUrl: result.connection.jiraUrl || '',
apiVersion: result.connection.apiVersion || '2',
authMethod: result.connection.authMethod || 'bearer',
apiToken: result.connection.apiToken || '',
username: result.connection.username || '',
email: result.connection.email || '',
password: result.connection.password || '',
},
];
result.currentConnectionIndex = 0;
delete result.connection;
data_changed = true;
}
if (data_changed) {
saveSettings();
}

View file

@ -1,9 +1,8 @@
/**
* Sanitize a file name to ensure it's valid
* @param fileName The original file name
* @returns Sanitized file name
*/
export function sanitizeFileName(fileName: string): string {
return fileName.replace(/[\\/:*?"<>|]/g, "-");
return fileName.replace(/[\\/:*?"<>|]/g, '-');
}

View file

@ -1,9 +1,10 @@
import { debugLog } from "./debugLogging";
import { debugLog } from './debugLogging';
const UNIFIED_REGEX = /`jira-sync-(section|line|inline-start|block-start)-([\w-]+)`([\s\S]*?)(?:`jira-sync-?[^-]*-end`|(?=`jira-sync-|$))/g; // TODO: delete deprecated endings in a year
const UNIFIED_REGEX =
/`jira-sync-(section|line|inline-start|block-start)-([\w-]+)`([\s\S]*?)(?:`jira-sync-?[^-]*-end`|(?=`jira-sync-|$))/g; // TODO: delete deprecated endings in a year
interface ParsedBlock {
type: "section" | "line" | "inline" | "block";
type: 'section' | 'line' | 'inline' | 'block';
name: string;
content: string;
startIndex: number;
@ -20,37 +21,37 @@ export function parseFileContent(fileContent: string): ParsedBlock[] {
while ((match = UNIFIED_REGEX.exec(fileContent)) !== null) {
const [fullMatch, typeRaw, name, rawContent] = match;
let type: ParsedBlock["type"];
let type: ParsedBlock['type'];
let extractedContent: string;
let actualEndIndex: number;
// Determine actual type and extract content
if (typeRaw === "section") {
type = "section";
if (typeRaw === 'section') {
type = 'section';
// Cut at first heading or another jira-sync-*
const headingMatch = rawContent.match(/\n#+? /);
const contentBeforeHeading = headingMatch
? rawContent.substring(0, (headingMatch.index||0)+1)
? rawContent.substring(0, (headingMatch.index || 0) + 1)
: rawContent;
actualEndIndex = match.index + fullMatch.length - (rawContent.length - contentBeforeHeading.length);
extractedContent = contentBeforeHeading.trim();
} else if (typeRaw === "line") {
type = "line";
} else if (typeRaw === 'line') {
type = 'line';
// Content is on the same line only
const firstLine = rawContent.split("\n")[0];
const firstLine = rawContent.split('\n')[0];
actualEndIndex = match.index + fullMatch.length - (rawContent.length - firstLine.length);
extractedContent = firstLine.trim();
} else if (typeRaw === "inline-start") {
type = "inline";
} else if (typeRaw === 'inline-start') {
type = 'inline';
// Content between inline-start and inline-end (markers included in fullMatch)
actualEndIndex = match.index + fullMatch.length;
extractedContent = rawContent.trim();
} else if (typeRaw === "block-start") {
type = "block";
} else if (typeRaw === 'block-start') {
type = 'block';
// Content between block-start and block-end
const lines = rawContent.split("\n");
const contentLines = lines.slice(lines[0] === "" ? 1 : 0, -1);
const joinedContent = contentLines.join("\n");
const lines = rawContent.split('\n');
const contentLines = lines.slice(lines[0] === '' ? 1 : 0, -1);
const joinedContent = contentLines.join('\n');
actualEndIndex = match.index + fullMatch.length;
extractedContent = joinedContent.trim();
} else {
@ -64,16 +65,14 @@ export function parseFileContent(fileContent: string): ParsedBlock[] {
startIndex: match.index,
endIndex: actualEndIndex,
// indexesContent: fileContent.substring(match.index, actualEndIndex),
fullMatch
fullMatch,
});
}
return blocks;
}
export function extractAllJiraSyncValuesFromContent(
fileContent: string
): Record<string, string> {
export function extractAllJiraSyncValuesFromContent(fileContent: string): Record<string, string> {
const blocks = parseFileContent(fileContent);
const result: Record<string, string> = {};
@ -85,10 +84,7 @@ export function extractAllJiraSyncValuesFromContent(
return result;
}
export function updateJiraSyncContent(
fileContent: string,
updates: Record<string, string>
): string {
export function updateJiraSyncContent(fileContent: string, updates: Record<string, string>): string {
const blocks = parseFileContent(fileContent);
// Sort blocks in reverse order to replace from end to start
@ -106,16 +102,16 @@ export function updateJiraSyncContent(
let newBlock: string;
switch (block.type) {
case "section":
case 'section':
newBlock = `\`jira-sync-section-${block.name}\`\n${newContent}\n`;
break;
case "line":
newBlock = `\`jira-sync-line-${block.name}\`${newContent.split("\n")[0]}`;
case 'line':
newBlock = `\`jira-sync-line-${block.name}\`${newContent.split('\n')[0]}`;
break;
case "inline":
case 'inline':
newBlock = `\`jira-sync-inline-start-${block.name}\`${newContent}\`jira-sync-end\``;
break;
case "block":
case 'block':
newBlock = `\`jira-sync-block-start-${block.name}\`\n${newContent}\n\`jira-sync-end\``;
break;
}

173
src/tools/timeTracker.ts Normal file
View file

@ -0,0 +1,173 @@
export type TimeTrackerFormat = 'timekeep' | 'simple-time-tracker';
export interface TimeTrackerEntry {
name: string;
startTime?: string | null;
endTime?: string | null;
subEntries?: TimeTrackerEntry[];
collapsed?: boolean;
}
export interface TimeTrackerData {
entries: TimeTrackerEntry[];
}
export interface TimeTrackerBlock {
format: TimeTrackerFormat;
file: string;
path: string;
issueKey?: string;
data: TimeTrackerData;
}
export interface ProcessedEntry {
file: string;
issueKey?: string;
blockPath: string;
lastBlockName: string;
startTime: string;
endTime: string;
duration: string;
timestamp: number;
}
export interface GroupedEntries {
[periodKey: string]: ProcessedEntry[];
}
export function parseTimeBlocks(
content: string,
file: { name: string; path: string },
issueKey?: string,
): TimeTrackerBlock[] {
const blocks: TimeTrackerBlock[] = [];
const timekeepMatches = content.match(/```timekeep([\s\S]*?)```/g);
if (timekeepMatches) {
timekeepMatches.forEach((block) => {
try {
const jsonContent = block.slice(11, -3).trim();
const data = JSON.parse(jsonContent) as TimeTrackerData;
blocks.push({
format: 'timekeep',
file: file.name,
path: file.path,
issueKey,
data,
});
} catch (error) {
console.error(`Error parsing timekeep block in ${file.name}:`, error);
}
});
}
const simpleTrackerMatches = content.match(/```simple-time-tracker([\s\S]*?)```/g);
if (simpleTrackerMatches) {
simpleTrackerMatches.forEach((block) => {
try {
const jsonContent = block.slice(22, -3).trim();
const data = JSON.parse(jsonContent) as TimeTrackerData;
blocks.push({
format: 'simple-time-tracker',
file: file.name,
path: file.path,
issueKey,
data,
});
} catch (error) {
console.error(`Error parsing simple-time-tracker block in ${file.name}:`, error);
}
});
}
return blocks;
}
function parseMsToDuration(ms: number): string {
if (ms < 0) return '0s';
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = minutes / 60;
if (hours >= 1) {
const h = Math.floor(hours);
const m = Math.floor((hours - h) * 60);
return m > 0 ? `${h}h ${m}m` : `${h}h`;
}
if (minutes >= 1) {
const m = Math.floor(minutes);
const s = seconds % 60;
return s > 0 ? `${m}m ${s}s` : `${m}m`;
}
return `${seconds}s`;
}
function toLocalIso(date: Date): string {
const offset = date.getTimezoneOffset();
const localDate = new Date(date.getTime() - offset * 60 * 1000);
return localDate.toISOString().slice(0, 16).replace('T', ' ');
}
export function processTimeEntries(
entries: TimeTrackerEntry[] | undefined,
fileName: string,
issueKey: string | undefined,
parentPath: string,
groupedEntries: GroupedEntries,
isDateInRange: (date: Date) => boolean,
getPeriodKey: (date: Date) => string,
): 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);
if (!isDateInRange(startTime)) continue;
const periodKey = getPeriodKey(startTime);
if (!groupedEntries[periodKey]) groupedEntries[periodKey] = [];
let duration: string;
let endTime: Date;
if (entry.endTime) {
endTime = new Date(entry.endTime);
duration = parseMsToDuration(endTime.getTime() - startTime.getTime());
} else {
endTime = new Date();
duration = 'ongoing';
}
groupedEntries[periodKey].push({
file: fileName.replace('.md', ''),
issueKey: issueKey,
blockPath: currentPath,
lastBlockName: entry.name,
startTime: toLocalIso(startTime),
endTime: entry.endTime ? toLocalIso(endTime) : 'ongoing',
duration: duration,
timestamp: startTime.getTime(),
});
}
if (entry.subEntries && Array.isArray(entry.subEntries)) {
processTimeEntries(
entry.subEntries,
fileName,
issueKey,
currentPath,
groupedEntries,
isDateInRange,
getPeriodKey,
);
}
}
}

View file

@ -18,7 +18,6 @@
padding-left: 20px;
}
/* Template Selector Styles */
.suggest-select-container {
display: flex;
@ -53,14 +52,12 @@
align-self: flex-end;
}
.mod-warning {
color: var(--text-warning);
font-style: italic;
margin-top: 4px;
}
.test-mappings-header {
margin-bottom: 0.2em;
}
@ -81,7 +78,7 @@
border-radius: 8px;
padding: 1em 1em 1em 1.2em;
position: relative;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
}
.value-container {
@ -168,7 +165,6 @@
border-bottom: 2px solid var(--background-modifier-border);
}
.field-mapping-label {
font-weight: 600;
color: var(--text-normal);
@ -217,7 +213,6 @@
border: 0;
}
.to-jira-input,
.from-jira-input {
width: 100%;
@ -402,10 +397,10 @@
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em
padding: 1em;
}
code.hljs {
padding: 3px 5px
padding: 3px 5px;
}
/*!
Theme: GitHub
@ -419,7 +414,7 @@ code.hljs {
*/
.hljs {
color: #24292e;
background: #ffffff
background: #ffffff;
}
.hljs-doctag,
.hljs-keyword,
@ -429,88 +424,87 @@ code.hljs {
.hljs-type,
.hljs-variable.language_ {
/* prettylights-syntax-keyword */
color: #f6cc42
color: #f6cc42;
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
/* prettylights-syntax-entity */
color: #6f42c1
color: #6f42c1;
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
/* prettylights-syntax-constant */
color: #6ecbfa
color: #6ecbfa;
}
.hljs-number {
/* prettylights-syntax-numeric-literal */
color: #93cda7
color: #93cda7;
}
.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
/* prettylights-syntax-string */
color: #cd905c
color: #cd905c;
}
.hljs-built_in,
.hljs-symbol {
/* prettylights-syntax-variable */
color: #e36209
color: #e36209;
}
.hljs-comment,
.hljs-code,
.hljs-formula {
/* prettylights-syntax-comment */
color: #6a737d
color: #6a737d;
}
.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {
/* prettylights-syntax-entity-tag */
color: #22863a
color: #22863a;
}
.hljs-subst {
/* prettylights-syntax-storage-modifier-import */
color: #24292e
color: #24292e;
}
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #005cc5;
font-weight: bold
font-weight: bold;
}
.hljs-bullet {
/* prettylights-syntax-markup-list */
color: #735c0f
color: #735c0f;
}
.hljs-emphasis {
/* prettylights-syntax-markup-italic */
color: #24292e;
font-style: italic
font-style: italic;
}
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #24292e;
font-weight: bold
font-weight: bold;
}
.hljs-addition {
/* prettylights-syntax-markup-inserted */
color: #22863a;
background-color: #f0fff4
background-color: #f0fff4;
}
.hljs-deletion {
/* prettylights-syntax-markup-deleted */
color: #b31d28;
background-color: #ffeef0
background-color: #ffeef0;
}
/* These highlight.js classes are intentionally left unstyled */
@ -1343,7 +1337,8 @@ code.hljs {
/* Animations */
@keyframes pulse {
0%, 100% {
0%,
100% {
opacity: 0.7;
}
50% {

View file

@ -1,6 +1,5 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "node16",
@ -9,18 +8,11 @@
"resolveJsonModule": true,
"esModuleInterop": true,
"noImplicitAny": true,
"moduleResolution": "node16",
"importHelpers": true,
"isolatedModules": false,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
"lib": ["DOM", "ES2017", "ES2018", "ES2019", "ES2020"],
"types": ["node"]
},
"include": [
"**/*.ts"
]
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
}

View file

@ -1,14 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
import { readFileSync, writeFileSync } from 'fs';
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
let manifest = JSON.parse(readFileSync('manifest.json', 'utf8'));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
writeFileSync('manifest.json', JSON.stringify(manifest, null, '\t'));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
let versions = JSON.parse(readFileSync('versions.json', 'utf8'));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));

876
yarn.lock
View file

@ -1,876 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@codemirror/autocomplete@^6.0.0":
version "6.20.0"
resolved "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz"
integrity sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==
dependencies:
"@codemirror/language" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@codemirror/lang-javascript@^6.2.4":
version "6.2.4"
resolved "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz"
integrity sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==
dependencies:
"@codemirror/autocomplete" "^6.0.0"
"@codemirror/language" "^6.6.0"
"@codemirror/lint" "^6.0.0"
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.17.0"
"@lezer/common" "^1.0.0"
"@lezer/javascript" "^1.0.0"
"@codemirror/language@^6.0.0", "@codemirror/language@^6.11.3", "@codemirror/language@^6.6.0":
version "6.11.3"
resolved "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz"
integrity sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.23.0"
"@lezer/common" "^1.1.0"
"@lezer/highlight" "^1.0.0"
"@lezer/lr" "^1.0.0"
style-mod "^4.0.0"
"@codemirror/lint@^6.0.0":
version "6.9.2"
resolved "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.2.tgz"
integrity sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==
dependencies:
"@codemirror/state" "^6.0.0"
"@codemirror/view" "^6.35.0"
crelt "^1.0.5"
"@codemirror/state@^6.0.0", "@codemirror/state@^6.5.0", "@codemirror/state@^6.5.2":
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"
"@codemirror/view@^6.17.0", "@codemirror/view@^6.23.0", "@codemirror/view@^6.35.0", "@codemirror/view@^6.38.1":
version "6.39.4"
resolved "https://registry.npmjs.org/@codemirror/view/-/view-6.39.4.tgz"
integrity sha512-xMF6OfEAUVY5Waega4juo1QGACfNkNF+aJLqpd8oUJz96ms2zbfQ9Gh35/tI3y8akEV31FruKfj7hBnIU/nkqA==
dependencies:
"@codemirror/state" "^6.5.0"
crelt "^1.0.6"
style-mod "^4.1.0"
w3c-keyname "^2.2.4"
"@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==
"@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==
"@lezer/common@^1.0.0", "@lezer/common@^1.1.0", "@lezer/common@^1.2.0", "@lezer/common@^1.3.0":
version "1.4.0"
resolved "https://registry.npmjs.org/@lezer/common/-/common-1.4.0.tgz"
integrity sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==
"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
version "1.2.3"
resolved "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz"
integrity sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==
dependencies:
"@lezer/common" "^1.3.0"
"@lezer/javascript@^1.0.0":
version "1.5.4"
resolved "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz"
integrity sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==
dependencies:
"@lezer/common" "^1.2.0"
"@lezer/highlight" "^1.1.3"
"@lezer/lr" "^1.3.0"
"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0":
version "1.4.5"
resolved "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.5.tgz"
integrity sha512-/YTRKP5yPPSo1xImYQk7AZZMAgap0kegzqCSYHjAL9x1AZ0ZQW+IpcEzMKagCsbTsLnVeWkxYrCNeXG8xEPrjg==
dependencies:
"@lezer/common" "^1.0.0"
"@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"
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
dependencies:
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@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":
version "1.2.8"
resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
dependencies:
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@types/codemirror@5.60.8":
version "5.60.8"
resolved "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz"
integrity sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==
dependencies:
"@types/tern" "*"
"@types/estree@*":
version "1.0.6"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz"
integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==
"@types/json-schema@^7.0.9":
version "7.0.15"
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
"@types/lodash@^4.17.16":
version "4.17.16"
resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.16.tgz"
integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g==
"@types/node@^16.11.6":
version "16.18.121"
resolved "https://registry.npmjs.org/@types/node/-/node-16.18.121.tgz"
integrity sha512-Gk/pOy8H0cvX8qNrwzElYIECpcUn87w4EAEFXFvPJ8qsP9QR/YqukUORSy0zmyDyvdo149idPpy4W6iC5aSbQA==
"@types/tern@*":
version "0.23.9"
resolved "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz"
integrity sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==
dependencies:
"@types/estree" "*"
"@typescript-eslint/eslint-plugin@5.29.0":
version "5.29.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz"
integrity sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w==
dependencies:
"@typescript-eslint/scope-manager" "5.29.0"
"@typescript-eslint/type-utils" "5.29.0"
"@typescript-eslint/utils" "5.29.0"
debug "^4.3.4"
functional-red-black-tree "^1.0.1"
ignore "^5.2.0"
regexpp "^3.2.0"
semver "^7.3.7"
tsutils "^3.21.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==
dependencies:
"@typescript-eslint/scope-manager" "5.29.0"
"@typescript-eslint/types" "5.29.0"
"@typescript-eslint/typescript-estree" "5.29.0"
debug "^4.3.4"
"@typescript-eslint/scope-manager@5.29.0":
version "5.29.0"
resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.29.0.tgz"
integrity sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA==
dependencies:
"@typescript-eslint/types" "5.29.0"
"@typescript-eslint/visitor-keys" "5.29.0"
"@typescript-eslint/type-utils@5.29.0":
version "5.29.0"
resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.29.0.tgz"
integrity sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg==
dependencies:
"@typescript-eslint/utils" "5.29.0"
debug "^4.3.4"
tsutils "^3.21.0"
"@typescript-eslint/types@5.29.0":
version "5.29.0"
resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.29.0.tgz"
integrity sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg==
"@typescript-eslint/typescript-estree@5.29.0":
version "5.29.0"
resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.29.0.tgz"
integrity sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ==
dependencies:
"@typescript-eslint/types" "5.29.0"
"@typescript-eslint/visitor-keys" "5.29.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/utils@5.29.0":
version "5.29.0"
resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.29.0.tgz"
integrity sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A==
dependencies:
"@types/json-schema" "^7.0.9"
"@typescript-eslint/scope-manager" "5.29.0"
"@typescript-eslint/types" "5.29.0"
"@typescript-eslint/typescript-estree" "5.29.0"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
"@typescript-eslint/visitor-keys@5.29.0":
version "5.29.0"
resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.29.0.tgz"
integrity sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ==
dependencies:
"@typescript-eslint/types" "5.29.0"
eslint-visitor-keys "^3.3.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==
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.0.0, 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"
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==
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
builtin-modules@3.3.0:
version "3.3.0"
resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz"
integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==
chalk@4.1.2:
version "4.1.2"
resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chokidar@^4.0.3:
version "4.0.3"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
dependencies:
readdirp "^4.0.1"
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz"
integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.1"
wrap-ansi "^7.0.0"
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==
concurrently@^9.2.1:
version "9.2.1"
resolved "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz"
integrity sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==
dependencies:
chalk "4.1.2"
rxjs "7.8.2"
shell-quote "1.8.3"
supports-color "8.1.1"
tree-kill "1.2.2"
yargs "17.7.2"
crelt@^1.0.5, crelt@^1.0.6:
version "1.0.6"
resolved "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz"
integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==
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"
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz"
integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
dependencies:
path-type "^4.0.0"
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
esbuild@0.25.0:
version "0.25.0"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz"
integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==
optionalDependencies:
"@esbuild/aix-ppc64" "0.25.0"
"@esbuild/android-arm" "0.25.0"
"@esbuild/android-arm64" "0.25.0"
"@esbuild/android-x64" "0.25.0"
"@esbuild/darwin-arm64" "0.25.0"
"@esbuild/darwin-x64" "0.25.0"
"@esbuild/freebsd-arm64" "0.25.0"
"@esbuild/freebsd-x64" "0.25.0"
"@esbuild/linux-arm" "0.25.0"
"@esbuild/linux-arm64" "0.25.0"
"@esbuild/linux-ia32" "0.25.0"
"@esbuild/linux-loong64" "0.25.0"
"@esbuild/linux-mips64el" "0.25.0"
"@esbuild/linux-ppc64" "0.25.0"
"@esbuild/linux-riscv64" "0.25.0"
"@esbuild/linux-s390x" "0.25.0"
"@esbuild/linux-x64" "0.25.0"
"@esbuild/netbsd-arm64" "0.25.0"
"@esbuild/netbsd-x64" "0.25.0"
"@esbuild/openbsd-arm64" "0.25.0"
"@esbuild/openbsd-x64" "0.25.0"
"@esbuild/sunos-x64" "0.25.0"
"@esbuild/win32-arm64" "0.25.0"
"@esbuild/win32-ia32" "0.25.0"
"@esbuild/win32-x64" "0.25.0"
escalade@^3.1.1:
version "3.2.0"
resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz"
integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
dependencies:
esrecurse "^4.3.0"
estraverse "^4.1.1"
eslint-utils@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz"
integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
dependencies:
eslint-visitor-keys "^2.0.0"
eslint-visitor-keys@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
eslint-visitor-keys@^3.3.0:
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==
esrecurse@^4.3.0:
version "4.3.0"
resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz"
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
dependencies:
estraverse "^5.2.0"
estraverse@^4.1.1:
version "4.3.0"
resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
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==
fast-glob@^3.2.9:
version "3.3.2"
resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fastq@^1.6.0:
version "1.17.1"
resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz"
integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
dependencies:
reusify "^1.0.4"
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
fs-extra@^11.3.0:
version "11.3.2"
resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz"
integrity sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz"
integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==
get-caller-file@^2.0.5:
version "2.0.5"
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
glob-parent@^5.1.2:
version "5.1.2"
resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
globby@^11.1.0:
version "11.1.0"
resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz"
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
dependencies:
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.2.9"
ignore "^5.2.0"
merge2 "^1.4.1"
slash "^3.0.0"
graceful-fs@^4.1.6, graceful-fs@^4.2.0:
version "4.2.11"
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
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==
highlight.js@^11.11.1:
version "11.11.1"
resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz"
integrity sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==
husky@^9.1.7:
version "9.1.7"
resolved "https://registry.yarnpkg.com/husky/-/husky-9.1.7.tgz#d46a38035d101b46a70456a850ff4201344c0b2d"
integrity sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==
ignore@^5.2.0:
version "5.3.2"
resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz"
integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
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-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
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==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
jsonfile@^6.0.1:
version "6.2.0"
resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz"
integrity sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==
dependencies:
universalify "^2.0.0"
optionalDependencies:
graceful-fs "^4.1.6"
lodash@^4.17.23:
version "4.17.23"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a"
integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==
merge2@^1.3.0, merge2@^1.4.1:
version "1.4.1"
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4:
version "4.0.8"
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.3"
picomatch "^2.3.1"
moment@2.29.4:
version "2.29.4"
resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz"
integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==
ms@^2.1.3:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
obsidian@latest:
version "1.8.7"
resolved "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz"
integrity sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==
dependencies:
"@types/codemirror" "5.60.8"
moment "2.29.4"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
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.npmjs.org/readdirp/-/readdirp-4.1.2.tgz"
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==
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
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==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
dependencies:
queue-microtask "^1.2.2"
rxjs@7.8.2:
version "7.8.2"
resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz"
integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==
dependencies:
tslib "^2.1.0"
semver@^7.3.7:
version "7.6.3"
resolved "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz"
integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==
shell-quote@1.8.3:
version "1.8.3"
resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz"
integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
strip-ansi@^6.0.0, 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"
style-mod@^4.0.0, 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@8.1.1:
version "8.1.1"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz"
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
dependencies:
has-flag "^4.0.0"
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"
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"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
tree-kill@1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz"
integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==
tslib@2.4.0, tslib@^2.1.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"
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
dependencies:
tslib "^1.8.1"
typescript@4.7.4:
version "4.7.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
universalify@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
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==
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
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==
yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs@17.7.2:
version "17.7.2"
resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz"
integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==
dependencies:
cliui "^8.0.1"
escalade "^3.1.1"
get-caller-file "^2.0.5"
require-directory "^2.1.1"
string-width "^4.2.3"
y18n "^5.0.5"
yargs-parser "^21.1.1"