mirror of
https://github.com/alamion/obsidian-jira-sync.git
synced 2026-07-22 05:43:04 +00:00
Feats
- `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:
parent
e603b2545d
commit
0786db1535
100 changed files with 3479 additions and 6038 deletions
|
|
@ -1,3 +0,0 @@
|
||||||
node_modules/
|
|
||||||
|
|
||||||
main.js
|
|
||||||
23
.eslintrc
23
.eslintrc
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
48
.github/workflows/release.yml
vendored
48
.github/workflows/release.yml
vendored
|
|
@ -1,34 +1,34 @@
|
||||||
name: Release Obsidian plugin
|
name: Release Obsidian plugin
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- '*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: "18.x"
|
node-version: '18.x'
|
||||||
|
|
||||||
- name: Build plugin
|
- name: Build plugin
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
tag="${GITHUB_REF#refs/tags/}"
|
tag="${GITHUB_REF#refs/tags/}"
|
||||||
|
|
||||||
gh release create "$tag" \
|
gh release create "$tag" \
|
||||||
--title="$tag" \
|
--title="$tag" \
|
||||||
--draft \
|
--draft \
|
||||||
main.js manifest.json styles.css
|
main.js manifest.json styles.css
|
||||||
|
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -7,11 +7,15 @@
|
||||||
|
|
||||||
# npm
|
# npm
|
||||||
node_modules
|
node_modules
|
||||||
|
yarn.lock
|
||||||
|
|
||||||
# Don't include the compiled main.js file in the repo.
|
# Don't include the compiled main.js file in the repo.
|
||||||
# They should be uploaded to GitHub releases instead.
|
# They should be uploaded to GitHub releases instead.
|
||||||
*.js
|
*.js
|
||||||
|
*.cjs
|
||||||
|
!eslint.config.js
|
||||||
!/src/localization/**/*.js
|
!/src/localization/**/*.js
|
||||||
|
/src/localization/translator.js
|
||||||
!check-version.js
|
!check-version.js
|
||||||
|
|
||||||
# Exclude sourcemaps
|
# Exclude sourcemaps
|
||||||
|
|
|
||||||
|
|
@ -1 +1,5 @@
|
||||||
node check-version.js
|
node check-version.js
|
||||||
|
yarn run format
|
||||||
|
yarn run lint:fix
|
||||||
|
yarn run build
|
||||||
|
yarn run validate_locale_once
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"tabWidth": 4,
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 120,
|
||||||
|
"semi": true
|
||||||
|
}
|
||||||
1
.yarnrc.yml
Normal file
1
.yarnrc.yml
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
nodeLinker: node-modules
|
||||||
265
AGENTS.md
Normal file
265
AGENTS.md
Normal 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
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Alamion
|
Copyright (c) 2025-2026 Alamion
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
32
README.md
32
README.md
|
|
@ -7,6 +7,7 @@ Originally forked from [obsidian-to-jira](https://github.com/angelperezasenjo/ob
|
||||||
## Why this plugin exists
|
## Why this plugin exists
|
||||||
|
|
||||||
Other plugins treat Jira issues as read-only reference material. This one lets you:
|
Other plugins treat Jira issues as read-only reference material. This one lets you:
|
||||||
|
|
||||||
- **Build full-featured issue templates** that mirror your Jira workflow
|
- **Build full-featured issue templates** that mirror your Jira workflow
|
||||||
- **Sync ANY field**—even custom ones from plugins like ScriptRunner or Insight
|
- **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)
|
- **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
|
## Killer features
|
||||||
|
|
||||||
### 🧩 Your Jira, your template
|
### 🧩 Your Jira, your template
|
||||||
|
|
||||||
Create Obsidian notes that look exactly like your team's Jira workflow. Pull in:
|
Create Obsidian notes that look exactly like your team's Jira workflow. Pull in:
|
||||||
|
|
||||||
- Standard fields (status, assignee, priority)
|
- Standard fields (status, assignee, priority)
|
||||||
- Custom fields (progress bars, sprint IDs, epic links)
|
- Custom fields (progress bars, sprint IDs, epic links)
|
||||||
- Plugin fields (ScriptRunner outputs, Insight assets)
|
- 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:
|
### Brief example:
|
||||||
|
|
||||||
#### Here is how template can look like:
|
#### Here is how template can look like:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
---
|
---
|
||||||
key: ""
|
key: ""
|
||||||
|
|
@ -37,16 +41,17 @@ epic: "" <!-- Another Custom field -->
|
||||||
link: "" <!-- Built-in auto-generated link -->
|
link: "" <!-- Built-in auto-generated link -->
|
||||||
---
|
---
|
||||||
|
|
||||||
### Customer Impact `jira-sync-section-customfield_10842` <!-- From your CRM plugin -->
|
### Customer Impact `jira-sync-section-customfield_10842` <!-- From your CRM plugin -->
|
||||||
|
|
||||||
|
|
||||||
### Other
|
### 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 -->
|
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:
|
#### Here is how it will look after syncing:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
---
|
---
|
||||||
key: JIR-1234
|
key: JIR-1234
|
||||||
|
|
@ -58,15 +63,18 @@ link: http://jira.local:8000/browse/JIR-1234
|
||||||
---
|
---
|
||||||
|
|
||||||
### Customer Impact `jira-sync-section-customfield_10842`
|
### Customer Impact `jira-sync-section-customfield_10842`
|
||||||
|
|
||||||
The current API structure is too fragmented and requires standardization.
|
The current API structure is too fragmented and requires standardization.
|
||||||
|
|
||||||
### Other
|
### Other
|
||||||
|
|
||||||
User `jira-sync-inline-start-assignee`Jack A.M.`jira-sync-inline-end` should be working on this.
|
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
|
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):
|
#### Here is how the user will see it most cases (all indicators are hidden from view):
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
---
|
---
|
||||||
key: JIR-1234
|
key: JIR-1234
|
||||||
|
|
@ -78,16 +86,20 @@ link: http://jira.local:8000/browse/JIR-1234
|
||||||
---
|
---
|
||||||
|
|
||||||
### Customer Impact
|
### Customer Impact
|
||||||
|
|
||||||
The current API structure is too fragmented and requires standardization.
|
The current API structure is too fragmented and requires standardization.
|
||||||
|
|
||||||
### Other
|
### Other
|
||||||
|
|
||||||
User Jack A.M. should be working on this.
|
User Jack A.M. should be working on this.
|
||||||
|
|
||||||
Expected time spent on the task: 1w 3d
|
Expected time spent on the task: 1w 3d
|
||||||
```
|
```
|
||||||
|
|
||||||
### 🔐 Multiple authentication methods
|
### 🔐 Multiple authentication methods
|
||||||
|
|
||||||
Choose the authentication that fits your security requirements:
|
Choose the authentication that fits your security requirements:
|
||||||
|
|
||||||
- **Bearer Token (PAT)** - Personal Access Token for secure API access
|
- **Bearer Token (PAT)** - Personal Access Token for secure API access
|
||||||
- **Basic Auth (Username + PAT)** - Username with Personal Access Token
|
- **Basic Auth (Username + PAT)** - Username with Personal Access Token
|
||||||
- **Session Cookie (Username + Password)** - Traditional authentication
|
- **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.
|
> **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
|
### 🔄 Two-way sync that doesn't fight you
|
||||||
|
|
||||||
- **Smart conflict resolution** when notes change locally while syncing
|
- **Smart conflict resolution** when notes change locally while syncing
|
||||||
- **Partial updates**—edit just the fields you care about
|
- **Partial updates**—edit just the fields you care about
|
||||||
- **Worklog batching** push a week's worth of time entries at once
|
- **Worklog batching** push a week's worth of time entries at once
|
||||||
- **Status management** update issue status directly from Obsidian
|
- **Status management** update issue status directly from Obsidian
|
||||||
|
|
||||||
### ⚙️ Field mapping kitchen
|
### ⚙️ Field mapping kitchen
|
||||||
|
|
||||||
We include mappings for:
|
We include mappings for:
|
||||||
|
|
||||||
- Basic fields (like status, priority, assignee)
|
- Basic fields (like status, priority, assignee)
|
||||||
- Temporal fields (created, updated, due dates)
|
- Temporal fields (created, updated, due dates)
|
||||||
- Calculated fields (progress %, time estimates, custom formulas)
|
- Calculated fields (progress %, time estimates, custom formulas)
|
||||||
- **Bring your own** for custom integrations and complex transformations
|
- **Bring your own** for custom integrations and complex transformations
|
||||||
|
|
||||||
### 📊 Integrated work log statistics
|
### 📊 Integrated work log statistics
|
||||||
|
|
||||||
- **No external plugins required** - everything is built-in
|
- **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
|
- **Dynamic time period selection** - view stats for days, weeks, or months
|
||||||
- **Batch work log submission** - send multiple time entries at once
|
- **Batch work log submission** - send multiple time entries at once
|
||||||
- **Visual progress tracking** - see your work patterns at a glance
|
- **Visual progress tracking** - see your work patterns at a glance
|
||||||
|
|
@ -118,9 +135,9 @@ We include mappings for:
|
||||||
1. **Install**: Community plugins → Search "Jira Issue Manager"
|
1. **Install**: Community plugins → Search "Jira Issue Manager"
|
||||||
2. **Connect**: Settings → Choose authentication method + add your Jira URL + credentials
|
2. **Connect**: Settings → Choose authentication method + add your Jira URL + credentials
|
||||||
3. **Go**:
|
3. **Go**:
|
||||||
- `Get issue from Jira with custom key` from command palette
|
- `Get issue from Jira with custom key` from command palette
|
||||||
- Edit like any note using our flexible indicator system
|
- Edit like any note using our flexible indicator system
|
||||||
- When you are ready → `Update issue in Jira` - changes sync back
|
- When you are ready → `Update issue in Jira` - changes sync back
|
||||||
|
|
||||||
## What makes us different
|
## What makes us different
|
||||||
|
|
||||||
|
|
@ -133,6 +150,7 @@ We include mappings for:
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
For detailed setup, configuration, and advanced usage:
|
For detailed setup, configuration, and advanced usage:
|
||||||
|
|
||||||
- [English Guide](docs/how_to_en.md)
|
- [English Guide](docs/how_to_en.md)
|
||||||
- [Russian Guide](docs/how_to_ru.md)
|
- [Russian Guide](docs/how_to_ru.md)
|
||||||
- [Template Examples](docs/template_example.md)
|
- [Template Examples](docs/template_example.md)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
|
|
@ -2,15 +2,14 @@
|
||||||
|
|
||||||
const { execSync } = require('child_process');
|
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()
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
const hasCodeChanges = stagedFiles.some(file =>
|
const hasCodeChanges = stagedFiles.some((file) => file.endsWith('.ts') || file.endsWith('.tsx'));
|
||||||
file.endsWith('.ts') ||
|
|
||||||
file.endsWith('.tsx')
|
|
||||||
);
|
|
||||||
|
|
||||||
const manifestChanged = stagedFiles.includes('manifest.json');
|
const manifestChanged = stagedFiles.includes('manifest.json');
|
||||||
|
|
||||||
|
|
@ -26,7 +25,9 @@ if (hasCodeChanges && !manifestChanged) {
|
||||||
|
|
||||||
if (manifestChanged && hasCodeChanges) {
|
if (manifestChanged && hasCodeChanges) {
|
||||||
try {
|
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"')) {
|
if (!manifestDiff.includes('"version"')) {
|
||||||
console.error('\x1b[33m⚠️ manifest.json has been modified, but the version has not been updated!\x1b[0m');
|
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('');
|
console.error('');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('\x1b[32m✓ Version check passed\x1b[0m');
|
console.log('\x1b[32m✓ Version check passed\x1b[0m');
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
The functionality of the plugin can be divided into several parts.
|
The functionality of the plugin can be divided into several parts.
|
||||||
|
|
||||||
### Basic Functionality
|
### Basic Functionality
|
||||||
|
|
||||||
For basic functionality, it is enough to specify Jira credentials and the folder for created tasks.
|
For basic functionality, it is enough to specify Jira credentials and the folder for created tasks.
|
||||||
|
|
||||||
**Authentication Methods**
|
**Authentication Methods**
|
||||||
The plugin supports three authentication methods:
|
The plugin supports three authentication methods:
|
||||||
|
|
||||||
- **Bearer Token (PAT)** - Personal Access Token authentication
|
- **Bearer Token (PAT)** - Personal Access Token authentication
|
||||||
- **Basic Auth (Username + PAT)** - Username with Personal Access Token
|
- **Basic Auth (Username + PAT)** - Username with Personal Access Token
|
||||||
- **Session Cookie (Username + Password)** - Traditional username and password authentication
|
- **Session Cookie (Username + Password)** - Traditional username and password authentication
|
||||||
|
|
@ -16,43 +18,56 @@ 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.
|
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
|
#### Template
|
||||||
|
|
||||||
The template can consist of several different parts:
|
The template can consist of several different parts:
|
||||||
|
|
||||||
##### Frontmatter
|
##### 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.
|
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
|
##### 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:
|
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.
|
||||||
|
|
||||||
- `line` reads and writes values from the current line.
|
The difference between these options is as follows:
|
||||||
|
|
||||||
|
- `line` reads and writes values from the current line.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```md
|
```md
|
||||||
The responsible person for this task is `jira-sync-line-assignee` Bob
|
The responsible person for this task is `jira-sync-line-assignee` Bob
|
||||||
The description is `jira-sync-line-description` Some description
|
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.
|
- `section` reads values from multiple lines after the indicator, stopping only at any other indicator or a heading.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```md
|
```md
|
||||||
### Responsible `jira-sync-section-assignee`
|
### Responsible `jira-sync-section-assignee`
|
||||||
|
|
||||||
Bob
|
Bob
|
||||||
|
|
||||||
### Description `jira-sync-section-description`
|
### Description `jira-sync-section-description`
|
||||||
|
|
||||||
Some description
|
Some description
|
||||||
`jira-sync-section-customfield_10842`
|
`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`).
|
- `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:
|
Example:
|
||||||
|
|
||||||
```md
|
```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`.
|
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.
|
- `block` is basically the same as `inline`, but with line break before and after indicators.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```md
|
```md
|
||||||
The responsible person for this task is `jira-sync-block-assignee`
|
The responsible person for this task is `jira-sync-block-assignee`
|
||||||
Bob
|
Bob
|
||||||
|
|
@ -72,6 +87,7 @@ Not all fields are predefined, and some may need adjustments. To add any custom
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
Currently, the plugin provides the following commands:
|
Currently, the plugin provides the following commands:
|
||||||
|
|
||||||
- `Get issue from Jira with custom key` - allows creating a file in the configured folder that imports information from Jira using a manually specified ID.
|
- `Get issue from Jira 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.
|
- `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).
|
- `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
|
### Advanced Usage
|
||||||
|
|
||||||
#### Field Mapping
|
#### Field Mapping
|
||||||
|
|
||||||
In the settings, you can configure custom mapping for any additional fields received from Jira. To do this:
|
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).
|
- 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).
|
- 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.
|
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:
|
Additionally, you can use some of the built-in functions:
|
||||||
|
|
||||||
- `jiraToMarkdown` - converts Jira markup to Markdown.
|
- `jiraToMarkdown` - converts Jira markup to Markdown.
|
||||||
- `markdownToJira` - converts Markdown to Jira markup.
|
- `markdownToJira` - converts Markdown to Jira markup.
|
||||||
- `JSON.parse` - converts a JSON string to an object.
|
- `JSON.parse` - converts a JSON string to an object.
|
||||||
- `JSON.stringify` - converts an object to a JSON string.
|
- `JSON.stringify` - converts an object to a JSON string.
|
||||||
|
|
||||||
and modules: (original Javascript syntax)
|
and modules: (original Javascript syntax)
|
||||||
|
|
||||||
- `Math` - provides access to mathematical functions, such as `Math.round()`.
|
- `Math` - provides access to mathematical functions, such as `Math.round()`.
|
||||||
- `Date` - provides access to date functions, such as `Date.now()`.
|
- `Date` - provides access to date functions, such as `Date.now()`.
|
||||||
- `String` - provides access to string functions, such as `String.fromCharCode()`.
|
- `String` - provides access to string functions, such as `String.fromCharCode()`.
|
||||||
|
|
@ -109,8 +129,10 @@ Field mapping can like this:
|
||||||
|
|
||||||

|

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

|

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

|
Поддерживаются форматы временных записей: timekeep и simple-time-tracker.
|
||||||
|
|
||||||
|
Выглядит табличка так:
|
||||||
|
|
||||||
|

|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ link: http://jira.local:8000/browse/JIR-1234
|
||||||
This is a comprehensive test file for the jira-sync parser.
|
This is a comprehensive test file for the jira-sync parser.
|
||||||
|
|
||||||
## Summary `jira-sync-section-summary`
|
## Summary `jira-sync-section-summary`
|
||||||
|
|
||||||
Complete API standardization and consolidation across all microservices to improve developer experience and reduce integration complexity.
|
Complete API standardization and consolidation across all microservices to improve developer experience and reduce integration complexity.
|
||||||
|
|
||||||
This section continues here with more details.
|
This section continues here with more details.
|
||||||
|
|
@ -20,7 +21,9 @@ Multiple paragraphs are supported.
|
||||||
## Details Section
|
## Details Section
|
||||||
|
|
||||||
### Customer Impact `jira-sync-section-customfield_10842`
|
### 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:
|
The current API structure is too fragmented and requires standardization. Customers are experiencing difficulties integrating with our services due to:
|
||||||
|
|
||||||
- Inconsistent authentication methods
|
- Inconsistent authentication methods
|
||||||
- Different response formats
|
- Different response formats
|
||||||
- Lack of versioning strategy
|
- 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.
|
This impacts approximately 2,300 enterprise customers.
|
||||||
|
|
||||||
### Next Heading Should Stop Section
|
### Next Heading Should Stop Section
|
||||||
|
|
||||||
This content should NOT be part of customfield_10842.
|
This content should NOT be part of customfield_10842.
|
||||||
|
|
||||||
## Assignment Info
|
## Assignment Info
|
||||||
|
|
@ -45,6 +49,7 @@ Actual time logged so far: `jira-sync-line-timeSpent`2d 4h
|
||||||
Remaining estimate: `jira-sync-line-remainingEstimate`5d
|
Remaining estimate: `jira-sync-line-remainingEstimate`5d
|
||||||
|
|
||||||
## Acceptance Criteria `jira-sync-section-customfield_10100`
|
## Acceptance Criteria `jira-sync-section-customfield_10100`
|
||||||
|
|
||||||
1. All endpoints follow RESTful conventions
|
1. All endpoints follow RESTful conventions
|
||||||
2. API versioning implemented via headers
|
2. API versioning implemented via headers
|
||||||
3. Consistent error handling across services
|
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.
|
All criteria must be met before moving to Done.
|
||||||
|
|
||||||
### Technical Requirements
|
### Technical Requirements
|
||||||
|
|
||||||
This is a separate section that shouldn't be included above.
|
This is a separate section that shouldn't be included above.
|
||||||
|
|
||||||
## Code Block Examples
|
## Code Block Examples
|
||||||
|
|
@ -80,20 +86,21 @@ ssl: true
|
||||||
## Edge Cases Testing
|
## Edge Cases Testing
|
||||||
|
|
||||||
### Empty Inline
|
### Empty Inline
|
||||||
|
|
||||||
Status indicator: `jira-sync-inline-start-status_indicator``jira-sync-end`
|
Status indicator: `jira-sync-inline-start-status_indicator``jira-sync-end`
|
||||||
|
|
||||||
### Whitespace Heavy Section `jira-sync-section-whitespace_test`
|
### Whitespace Heavy Section `jira-sync-section-whitespace_test`
|
||||||
|
|
||||||
|
|
||||||
This section has leading and trailing whitespace
|
This section has leading and trailing whitespace
|
||||||
|
|
||||||
|
|
||||||
Should be trimmed properly before sending to Jira.
|
Should be trimmed properly before sending to Jira.
|
||||||
|
|
||||||
### Single Line Section `jira-sync-section-oneliner`
|
### Single Line Section `jira-sync-section-oneliner`
|
||||||
|
|
||||||
Just one line here.
|
Just one line here.
|
||||||
|
|
||||||
## Multi-line Inline Test
|
## Multi-line Inline Test
|
||||||
Complex description: `jira-sync-inline-start-description` This is a description
|
|
||||||
|
Complex description: `jira-sync-inline-start-description` This is a description
|
||||||
that spans multiple lines
|
that spans multiple lines
|
||||||
with various formatting `jira-sync-inline-end` and continues here.
|
with various formatting `jira-sync-inline-end` and continues here.
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,45 @@
|
||||||
import esbuild from "esbuild";
|
import esbuild from 'esbuild';
|
||||||
import process from "process";
|
import process from 'process';
|
||||||
import builtins from "builtin-modules";
|
import builtins from 'builtin-modules';
|
||||||
|
|
||||||
const banner =
|
const banner = `/*
|
||||||
`/*
|
|
||||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
if you want to view the source, please visit the github repository of this plugin
|
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({
|
const context = await esbuild.context({
|
||||||
banner: {
|
banner: {
|
||||||
js: banner,
|
js: banner,
|
||||||
},
|
},
|
||||||
entryPoints: ["src/main.ts"],
|
entryPoints: ['src/main.ts'],
|
||||||
bundle: true,
|
bundle: true,
|
||||||
external: [
|
external: [
|
||||||
"obsidian",
|
'obsidian',
|
||||||
"electron",
|
'electron',
|
||||||
"@codemirror/autocomplete",
|
'@codemirror/autocomplete',
|
||||||
"@codemirror/collab",
|
'@codemirror/collab',
|
||||||
"@codemirror/commands",
|
'@codemirror/commands',
|
||||||
"@codemirror/language",
|
'@codemirror/language',
|
||||||
"@codemirror/lint",
|
'@codemirror/lint',
|
||||||
"@codemirror/search",
|
'@codemirror/search',
|
||||||
"@codemirror/state",
|
'@codemirror/state',
|
||||||
"@codemirror/view",
|
'@codemirror/view',
|
||||||
"@lezer/common",
|
'@lezer/common',
|
||||||
"@lezer/highlight",
|
'@lezer/highlight',
|
||||||
"@lezer/lr",
|
'@lezer/lr',
|
||||||
...builtins],
|
...builtins,
|
||||||
format: "cjs",
|
],
|
||||||
target: "es2018",
|
format: 'cjs',
|
||||||
logLevel: prod ? "info" : "debug",
|
target: 'es2018',
|
||||||
sourcemap: prod ? false : "inline",
|
logLevel: prod ? 'info' : 'debug',
|
||||||
|
sourcemap: prod ? false : 'inline',
|
||||||
treeShaking: true,
|
treeShaking: true,
|
||||||
outfile: "main.js",
|
outfile: 'main.js',
|
||||||
minify: prod,
|
minify: prod,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
34
eslint.config.js
Normal file
34
eslint.config.js
Normal 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',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
{
|
{
|
||||||
"id": "jira-sync",
|
"id": "jira-sync",
|
||||||
"name": "Jira Issue Manager",
|
"name": "Jira Issue Manager",
|
||||||
"version": "1.4.4",
|
"version": "1.5.0",
|
||||||
"minAppVersion": "1.10.1",
|
"minAppVersion": "1.10.1",
|
||||||
"description": "Get Jira issues, create and update them. Issue status and worklog management.",
|
"description": "Get Jira issues, create and update them. Issue status and worklog management.",
|
||||||
"author": "Alamion",
|
"author": "Alamion",
|
||||||
|
"authorUrl": "https://github.com/Alamion",
|
||||||
"isDesktopOnly": false
|
"isDesktopOnly": false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2418
package-lock.json
generated
2418
package-lock.json
generated
File diff suppressed because it is too large
Load diff
18
package.json
18
package.json
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "obsidian-sample-plugin",
|
"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)",
|
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -11,7 +12,10 @@
|
||||||
"dev": "concurrently --kill-others-on-fail \"node esbuild.config.mjs\" \"yarn run compile_locale\"",
|
"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",
|
"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",
|
"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": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|
@ -21,18 +25,22 @@
|
||||||
"@codemirror/language": "^6.11.3",
|
"@codemirror/language": "^6.11.3",
|
||||||
"@codemirror/state": "^6.5.2",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.38.1",
|
"@codemirror/view": "^6.38.1",
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/lodash": "^4.17.16",
|
"@types/lodash": "^4.17.16",
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^16.11.6",
|
||||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
"@typescript-eslint/eslint-plugin": "^8.59.1",
|
||||||
"@typescript-eslint/parser": "5.29.0",
|
"@typescript-eslint/parser": "^8.59.1",
|
||||||
"builtin-modules": "3.3.0",
|
"builtin-modules": "3.3.0",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"esbuild": "0.25.0",
|
"esbuild": "0.25.0",
|
||||||
|
"eslint": "^10.2.1",
|
||||||
|
"globals": "^17.5.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lodash": "^4.17.23",
|
"lodash": "^4.17.23",
|
||||||
"obsidian": "latest",
|
"obsidian": "latest",
|
||||||
|
"prettier": "^3.8.3",
|
||||||
"tslib": "2.4.0",
|
"tslib": "2.4.0",
|
||||||
"typescript": "4.7.4"
|
"typescript": "^6.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"acorn": "^8.14.1",
|
"acorn": "^8.14.1",
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { Notice, requestUrl } from "obsidian";
|
import { Notice, requestUrl } from 'obsidian';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
|
|
||||||
export function getSessionCookieKey(plugin: JiraPlugin): string {
|
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
|
// Session cookie only
|
||||||
|
|
@ -12,38 +12,39 @@ export async function authenticate(plugin: JiraPlugin): Promise<boolean> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const conn = plugin.getCurrentConnection();
|
||||||
|
if (!conn) return false;
|
||||||
|
|
||||||
const response = await requestUrl({
|
const response = await requestUrl({
|
||||||
url: `${plugin.settings.connection.jiraUrl}/rest/auth/1/session`,
|
url: `${conn.jiraUrl}/rest/auth/1/session`,
|
||||||
method: "post",
|
method: 'post',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username: plugin.settings.connection.username,
|
username: conn.username,
|
||||||
password: plugin.settings.connection.password,
|
password: conn.password,
|
||||||
}),
|
}),
|
||||||
contentType: "application/json",
|
contentType: 'application/json',
|
||||||
headers: {
|
headers: {
|
||||||
"Content-type": "application/json",
|
'Content-type': 'application/json',
|
||||||
Origin: plugin.settings.connection.jiraUrl,
|
Origin: conn.jiraUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(getSessionCookieKey(plugin), response.json.session.value);
|
||||||
getSessionCookieKey(plugin),
|
|
||||||
response.json.session.value
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
new Notice("Authentication failed: " + (error.message || "Unknown error"));
|
new Notice('Authentication failed: ' + ((error as Error).message || 'Unknown error'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateSettings(plugin: JiraPlugin): boolean {
|
export function validateSettings(plugin: JiraPlugin): boolean {
|
||||||
if (!plugin.settings.connection.jiraUrl) {
|
const conn = plugin.getCurrentConnection();
|
||||||
new Notice("Please configure Jira URL in plugin settings");
|
if (!conn || !conn.jiraUrl) {
|
||||||
|
new Notice('Please configure Jira URL in plugin settings');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ((!plugin.settings.connection.username || !plugin.settings.connection.password) && !plugin.settings.connection.apiToken) {
|
if ((!conn.username || !conn.password) && !conn.apiToken) {
|
||||||
new Notice("Please configure Jira username and password or PAT API token in plugin settings");
|
new Notice('Please configure Jira username and password or PAT API token in plugin settings');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -58,17 +59,20 @@ function createBasicAuthHeader(email: string, token: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAuthHeaders(plugin: JiraPlugin): Promise<Record<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
|
// Common headers
|
||||||
const commonHeaders = {
|
const commonHeaders = {
|
||||||
"Content-type": "application/json",
|
'Content-type': 'application/json',
|
||||||
Origin: plugin.settings.connection.jiraUrl,
|
Origin: conn.jiraUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Personal Access Token
|
// Personal Access Token
|
||||||
const pat = plugin.settings.connection.apiToken?.trim() || "";
|
const pat = conn.apiToken?.trim() || '';
|
||||||
|
|
||||||
// Option 1: Bearer token (PAT)
|
// Option 1: Bearer token (PAT)
|
||||||
if (plugin.settings.connection.authMethod === "bearer" || !plugin.settings.connection.authMethod) {
|
if (conn.authMethod === 'bearer' || !conn.authMethod) {
|
||||||
return {
|
return {
|
||||||
...commonHeaders,
|
...commonHeaders,
|
||||||
Authorization: `Bearer ${pat}`,
|
Authorization: `Bearer ${pat}`,
|
||||||
|
|
@ -76,14 +80,12 @@ export async function getAuthHeaders(plugin: JiraPlugin): Promise<Record<string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Option 2: Basic Auth with API token
|
// Option 2: Basic Auth with API token
|
||||||
else if (plugin.settings.connection.authMethod === "basic") {
|
else if (conn.authMethod === 'basic') {
|
||||||
return {
|
return {
|
||||||
...commonHeaders,
|
...commonHeaders,
|
||||||
Authorization: createBasicAuthHeader(plugin.settings.connection.email || "", pat),
|
Authorization: createBasicAuthHeader(conn.email || '', pat),
|
||||||
};
|
};
|
||||||
}
|
} else if (conn.authMethod === 'session') {
|
||||||
|
|
||||||
else if (plugin.settings.connection.authMethod === "session") {
|
|
||||||
// Option 3: Session cookie
|
// Option 3: Session cookie
|
||||||
let cookie = localStorage.getItem(getSessionCookieKey(plugin));
|
let cookie = localStorage.getItem(getSessionCookieKey(plugin));
|
||||||
if (!cookie) {
|
if (!cookie) {
|
||||||
|
|
@ -100,5 +102,5 @@ export async function getAuthHeaders(plugin: JiraPlugin): Promise<Record<string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// No valid auth method found
|
// No valid auth method found
|
||||||
throw new Error("No valid authentication method available");
|
throw new Error('No valid authentication method available');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {requestUrl} from "obsidian";
|
import { requestUrl } from 'obsidian';
|
||||||
import {authenticate, getAuthHeaders} from "./auth";
|
import { authenticate, getAuthHeaders } from './auth';
|
||||||
import {debugLog} from "../tools/debugLogging";
|
import { debugLog } from '../tools/debugLogging';
|
||||||
|
|
||||||
export async function baseRequest(
|
export async function baseRequest(
|
||||||
plugin: JiraPlugin,
|
plugin: JiraPlugin,
|
||||||
|
|
@ -9,53 +9,59 @@ export async function baseRequest(
|
||||||
additional_url_path: string,
|
additional_url_path: string,
|
||||||
body?: string,
|
body?: string,
|
||||||
params?: Record<string, any>,
|
params?: Record<string, any>,
|
||||||
retries: number = 1
|
retries: number = 1,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
const queryString = params && Object.keys(params).length > 0 ? '?' + new URLSearchParams(params).toString() : '';
|
||||||
|
|
||||||
const queryString = params && Object.keys(params).length > 0
|
const conn = plugin.getCurrentConnection();
|
||||||
? "?" + new URLSearchParams(params).toString()
|
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 = {
|
const requestParams = {
|
||||||
url,
|
url,
|
||||||
method: method,
|
method: method,
|
||||||
headers: await getAuthHeaders(plugin),
|
headers: await getAuthHeaders(plugin),
|
||||||
contentType: "application/json",
|
contentType: 'application/json',
|
||||||
accept: "application/json",
|
accept: 'application/json',
|
||||||
throw: false,
|
throw: false,
|
||||||
body
|
body,
|
||||||
}
|
};
|
||||||
debugLog("Request:\n", requestParams)
|
debugLog('Request:\n', requestParams);
|
||||||
const response = await requestUrl(requestParams);
|
const response = await requestUrl(requestParams);
|
||||||
debugLog("Response:\n", response);
|
debugLog('Response:\n', response);
|
||||||
if (response.status < 200 || response.status >= 300) {
|
if (response.status < 200 || response.status >= 300) {
|
||||||
if (response.status === 401 && retries > 0) {
|
if (response.status === 401 && retries > 0) {
|
||||||
await authenticate(plugin);
|
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);
|
// console.error(error);
|
||||||
throw new Error(`
|
throw new Error(`
|
||||||
${response.text || "Unknown error"}
|
${response.text || 'Unknown error'}
|
||||||
${body && '\nbody:' + body || ""}`);
|
${(body && '\nbody:' + body) || ''}`);
|
||||||
}
|
}
|
||||||
debugLog("Additional request info:\n", {"url": url,
|
debugLog('Additional request info:\n', {
|
||||||
"method": method, "body": body, params: params});
|
url: url,
|
||||||
|
method: method,
|
||||||
|
body: body,
|
||||||
|
params: params,
|
||||||
|
});
|
||||||
return response.status === 204 ? null : response.json;
|
return response.status === 204 ? null : response.json;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeObject(obj: any): any {
|
export function sanitizeObject(obj: any): any {
|
||||||
if (Array.isArray(obj)) {
|
if (Array.isArray(obj)) {
|
||||||
obj = obj.map((item: any) => sanitizeObject(item));
|
obj = obj.map((item: any) => sanitizeObject(item));
|
||||||
}
|
} else if (typeof obj === 'object') {
|
||||||
else if (typeof obj === "object") {
|
obj = Object.entries(obj)
|
||||||
obj = Object.entries(obj).filter(([, v]) => v !== null && v !== undefined).reduce((acc, [k, v]) => {
|
.filter(([, v]) => v !== null && v !== undefined)
|
||||||
acc[k] = sanitizeObject(v)
|
.reduce(
|
||||||
return acc
|
(acc, [k, v]) => {
|
||||||
}, {} as Record<string, any>);
|
acc[k] = sanitizeObject(v);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, any>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {JiraIssue, JiraTransitionType} from "../interfaces";
|
import { JiraIssue, JiraTransitionType } from '../interfaces';
|
||||||
import {baseRequest, sanitizeObject} from "./base";
|
import { baseRequest, sanitizeObject } from './base';
|
||||||
import {Notice} from "obsidian";
|
import { Notice } from 'obsidian';
|
||||||
import {chunkArray, createLimiter} from "../tools/asyncLimiter";
|
import { chunkArray, createLimiter } from '../tools/asyncLimiter';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch an issue from Jira by its key
|
* Fetch an issue from Jira by its key
|
||||||
|
|
@ -12,10 +11,10 @@ import {chunkArray, createLimiter} from "../tools/asyncLimiter";
|
||||||
* @returns The issue data
|
* @returns The issue data
|
||||||
*/
|
*/
|
||||||
export async function fetchIssue(plugin: JiraPlugin, issueKey: string): Promise<JiraIssue> {
|
export async function fetchIssue(plugin: JiraPlugin, issueKey: string): Promise<JiraIssue> {
|
||||||
return await baseRequest(plugin, 'get', `/issue/${issueKey}`, undefined, {
|
return (await baseRequest(plugin, 'get', `/issue/${issueKey}`, undefined, {
|
||||||
fields: plugin.settings.fetchIssue.fields || ["*all"],
|
fields: plugin.settings.fetchIssue.fields || ['*all'],
|
||||||
expand: plugin.settings.fetchIssue.expand.join(",") || "",
|
expand: plugin.settings.fetchIssue.expand.join(',') || '',
|
||||||
}) as Promise<JiraIssue>;
|
})) as Promise<JiraIssue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -26,21 +25,22 @@ export async function fetchIssuesByJQL(
|
||||||
plugin: JiraPlugin,
|
plugin: JiraPlugin,
|
||||||
jql: string,
|
jql: string,
|
||||||
limit?: number,
|
limit?: number,
|
||||||
fields?: string[]
|
fields?: string[],
|
||||||
): Promise<JiraIssue[]> {
|
): Promise<JiraIssue[]> {
|
||||||
let totalAvailable = 0;
|
let totalAvailable = 0;
|
||||||
let useNextPageToken = false;
|
let useNextPageToken = false;
|
||||||
|
|
||||||
switch (plugin.settings.connection.apiVersion) {
|
switch (plugin.getCurrentConnection()?.apiVersion) {
|
||||||
case "2":
|
case '2': {
|
||||||
const result = await fetchIssuesByJQLRaw(plugin, jql, 1, fields);
|
const result = await fetchIssuesByJQLRaw(plugin, jql, 1, fields);
|
||||||
totalAvailable = result.total;
|
totalAvailable = result.total;
|
||||||
break;
|
break;
|
||||||
case "3":
|
}
|
||||||
|
case '3':
|
||||||
totalAvailable = await fetchCountIssuesByJQL(plugin, jql);
|
totalAvailable = await fetchCountIssuesByJQL(plugin, jql);
|
||||||
useNextPageToken = true;
|
useNextPageToken = true;
|
||||||
if (!fields) {
|
if (!fields) {
|
||||||
fields = ["*all"];
|
fields = ['*all'];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +101,6 @@ export async function fetchIssuesByJQL(
|
||||||
// return results.flat();
|
// return results.flat();
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch issues by JQL and return the raw search response (includes total, startAt, etc.).
|
* Fetch issues by JQL and return the raw search response (includes total, startAt, etc.).
|
||||||
* Useful for previewing results and counts.
|
* Useful for previewing results and counts.
|
||||||
|
|
@ -121,23 +120,27 @@ export async function fetchIssuesByJQLRaw(
|
||||||
nextPageToken,
|
nextPageToken,
|
||||||
fields: fields && fields.length > 0 ? fields : plugin.settings.fetchIssue.fields || [],
|
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.nextPageToken = nextPageToken;
|
||||||
requestBody.expand = plugin.settings.fetchIssue.expand.join(",") || "";
|
requestBody.expand = plugin.settings.fetchIssue.expand.join(',') || '';
|
||||||
} else {
|
} else {
|
||||||
requestBody.startAt = startAt;
|
requestBody.startAt = startAt;
|
||||||
requestBody.expand = plugin.settings.fetchIssue.expand || [];
|
requestBody.expand = plugin.settings.fetchIssue.expand || [];
|
||||||
}
|
}
|
||||||
const body = JSON.stringify(sanitizeObject(requestBody));
|
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> {
|
export async function fetchCountIssuesByJQL(plugin: JiraPlugin, jql: string): Promise<number> {
|
||||||
const body = JSON.stringify(sanitizeObject({ jql }));
|
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
|
* Fetch an issue from Jira by its key
|
||||||
* @param plugin The plugin instance
|
* @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[]> {
|
export async function fetchIssueTransitions(plugin: JiraPlugin, issueKey: string): Promise<JiraTransitionType[]> {
|
||||||
const result = await baseRequest(plugin, 'get', `/issue/${issueKey}/transitions`);
|
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,
|
id,
|
||||||
action: name,
|
action: name,
|
||||||
status: to.name
|
status: to.name,
|
||||||
})) as JiraTransitionType[];
|
})) as JiraTransitionType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,48 +165,37 @@ export async function fetchIssueTransitions(plugin: JiraPlugin, issueKey: string
|
||||||
export async function updateJiraIssue(
|
export async function updateJiraIssue(
|
||||||
plugin: JiraPlugin,
|
plugin: JiraPlugin,
|
||||||
issueKey: string,
|
issueKey: string,
|
||||||
fields: Record<string, any>
|
fields: Record<string, any>,
|
||||||
): Promise<JiraIssue> {
|
): Promise<JiraIssue> {
|
||||||
const body = JSON.stringify({ fields })
|
const body = JSON.stringify({ fields });
|
||||||
return await baseRequest(plugin, 'put', `/issue/${issueKey}`, body) as Promise<JiraIssue>;
|
return (await baseRequest(plugin, 'put', `/issue/${issueKey}`, body)) as Promise<JiraIssue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function bulkUpdateJiraIssues(
|
export async function bulkUpdateJiraIssues(
|
||||||
plugin: JiraPlugin,
|
plugin: JiraPlugin,
|
||||||
updates: { issueKey: string, fields: Record<string, any> }[]
|
updates: { issueKey: string; fields: Record<string, any> }[],
|
||||||
): Promise<any[]> {
|
): Promise<any[]> {
|
||||||
const limit = createLimiter(5);
|
const limit = createLimiter(5);
|
||||||
const promises = updates.map(update =>
|
const promises = updates.map((update) => limit(() => updateJiraIssue(plugin, update.issueKey, update.fields)));
|
||||||
limit(() => updateJiraIssue(plugin, update.issueKey, update.fields))
|
|
||||||
);
|
|
||||||
return await Promise.allSettled(promises);
|
return await Promise.allSettled(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new issue in Jira
|
* Create a new issue in Jira
|
||||||
* @param plugin The plugin instance
|
* @param plugin The plugin instance
|
||||||
* @param fields The fields for the new issue
|
* @param fields The fields for the new issue
|
||||||
* @returns The created issue key
|
* @returns The created issue key
|
||||||
*/
|
*/
|
||||||
export async function createJiraIssue(
|
export async function createJiraIssue(plugin: JiraPlugin, fields: Record<string, any>): Promise<JiraIssue> {
|
||||||
plugin: JiraPlugin,
|
|
||||||
fields: Record<string, any>
|
|
||||||
): Promise<JiraIssue> {
|
|
||||||
// Ensure required fields are present
|
// Ensure required fields are present
|
||||||
if (!fields.project || !fields.issuetype || !fields.summary) {
|
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 })
|
const body = JSON.stringify({ fields });
|
||||||
return await baseRequest(plugin, 'post', `/issue/`, body) as Promise<JiraIssue>;
|
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);
|
const chunks = chunkArray(issues, 50);
|
||||||
let results: any[] = [];
|
let results: any[] = [];
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
|
|
@ -214,23 +206,17 @@ export async function bulkCreateJiraIssues(
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an issue status in Jira
|
* Update an issue status in Jira
|
||||||
* @param plugin The plugin instance
|
* @param plugin The plugin instance
|
||||||
* @param issueKey The issue key to update
|
* @param issueKey The issue key to update
|
||||||
* @param status The status to update the issue to
|
* @param status The status to update the issue to
|
||||||
*/
|
*/
|
||||||
export async function updateJiraStatus(
|
export async function updateJiraStatus(plugin: JiraPlugin, issueKey: string, status: string): Promise<JiraIssue> {
|
||||||
plugin: JiraPlugin,
|
const body = JSON.stringify({ transition: { id: status } });
|
||||||
issueKey: string,
|
return (await baseRequest(plugin, 'post', `/issue/${issueKey}/transitions`, body)) as Promise<JiraIssue>;
|
||||||
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
|
* Add a work log entry to a Jira issue
|
||||||
*/
|
*/
|
||||||
|
|
@ -239,13 +225,13 @@ export async function addWorkLog(
|
||||||
issueKey: string,
|
issueKey: string,
|
||||||
timeSpent: string,
|
timeSpent: string,
|
||||||
startedAt: string,
|
startedAt: string,
|
||||||
comment: string = "",
|
comment: string = '',
|
||||||
showNotice: boolean = true
|
showNotice: boolean = true,
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
const payload = {
|
const payload = {
|
||||||
timeSpent,
|
timeSpent,
|
||||||
started: startedAt,
|
started: startedAt,
|
||||||
comment
|
comment,
|
||||||
};
|
};
|
||||||
const response = await baseRequest(plugin, 'post', `/issue/${issueKey}/worklog`, JSON.stringify(payload));
|
const response = await baseRequest(plugin, 'post', `/issue/${issueKey}/worklog`, JSON.stringify(payload));
|
||||||
|
|
||||||
|
|
@ -256,15 +242,21 @@ export async function addWorkLog(
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function bulkAddWorkLog(
|
export async function bulkAddWorkLog(
|
||||||
plugin: JiraPlugin,
|
plugin: JiraPlugin,
|
||||||
worklogs: { issueKey: string, timeSpent: string, startedAt: string, comment: string }[],
|
worklogs: {
|
||||||
showNotice: boolean = true
|
issueKey: string;
|
||||||
|
timeSpent: string;
|
||||||
|
startedAt: string;
|
||||||
|
comment: string;
|
||||||
|
}[],
|
||||||
|
showNotice: boolean = true,
|
||||||
): Promise<any[]> {
|
): Promise<any[]> {
|
||||||
const limit = createLimiter(5);
|
const limit = createLimiter(5);
|
||||||
const promises = worklogs.map(worklog =>
|
const promises = worklogs.map((worklog) =>
|
||||||
limit(() => addWorkLog(plugin, worklog.issueKey, worklog.timeSpent, worklog.startedAt, worklog.comment, showNotice))
|
limit(() =>
|
||||||
|
addWorkLog(plugin, worklog.issueKey, worklog.timeSpent, worklog.startedAt, worklog.comment, showNotice),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return await Promise.allSettled(promises);
|
return await Promise.allSettled(promises);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {baseRequest} from "./base";
|
import { baseRequest } from './base';
|
||||||
|
|
||||||
export async function fetchProjects(plugin: JiraPlugin): Promise<Record<string, any>[]> {
|
export async function fetchProjects(plugin: JiraPlugin): Promise<Record<string, any>[]> {
|
||||||
return await baseRequest(plugin, 'get', '/project');
|
return await baseRequest(plugin, 'get', '/project');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchIssueTypes(plugin: JiraPlugin, projectKey: string): Promise<Record<string, any>> {
|
export async function fetchIssueTypes(plugin: JiraPlugin, projectKey: string): Promise<Record<string, any>> {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {baseRequest} from "./base";
|
import { baseRequest } from './base';
|
||||||
|
|
||||||
export async function fetchSelf(plugin: JiraPlugin): Promise<Record<string, any>[]> {
|
export async function fetchSelf(plugin: JiraPlugin): Promise<Record<string, any>[]> {
|
||||||
return await baseRequest(plugin, 'get', '/myself');
|
return await baseRequest(plugin, 'get', '/myself');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,24 @@
|
||||||
import {Notice, TFile} from "obsidian";
|
import { Notice, TFile } from 'obsidian';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {addWorkLog} from "../api";
|
import { addWorkLog } from '../api';
|
||||||
import {debugLog} from "../tools/debugLogging";
|
import { debugLog } from '../tools/debugLogging';
|
||||||
import {checkCommandCallback} from "../tools/checkCommandCallback";
|
import { checkCommandCallback } from '../tools/checkCommandCallback';
|
||||||
import {useTranslations} from "../localization/translator";
|
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 {
|
export function registerUpdateWorkLogBatchCommand(plugin: JiraPlugin): void {
|
||||||
plugin.addCommand({
|
plugin.addCommand({
|
||||||
id: "update-work-log-jira-batch",
|
id: 'update-work-log-jira-batch',
|
||||||
name: t("name"),
|
name: t('name'),
|
||||||
checkCallback: (checking: boolean) => {
|
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 {
|
} else {
|
||||||
throw new TypeError(`jira_worklog_batch has an invalid type: ${typeof jira_worklog_batch}`);
|
throw new TypeError(`jira_worklog_batch has an invalid type: ${typeof jira_worklog_batch}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
new Notice("Failed to parse jira_worklog_batch");
|
new Notice('Failed to parse jira_worklog_batch');
|
||||||
console.error("Failed to parse jira_worklog_batch:", error);
|
console.error('Failed to parse jira_worklog_batch:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundData || workLogData.length === 0) {
|
if (!foundData || workLogData.length === 0) {
|
||||||
new Notice("No work log data to process");
|
new Notice('No work log data to process');
|
||||||
console.warn("No work log data to process");
|
console.warn('No work log data to process');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await processWorkLogBatch(plugin, workLogData);
|
await processWorkLogBatch(plugin, workLogData);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error("Error processing frontmatter work logs:", error);
|
console.error('Error processing frontmatter work logs:', error);
|
||||||
new Notice("Failed to process work log data from frontmatter");
|
new Notice('Failed to process work log data from frontmatter');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,7 +59,7 @@ export async function processWorkLogBatch(plugin: JiraPlugin, workLogs: any[]):
|
||||||
const results = {
|
const results = {
|
||||||
success: 0,
|
success: 0,
|
||||||
total: workLogs.length,
|
total: workLogs.length,
|
||||||
failures: [] as { reason: string; issueKeys: string[] }[]
|
failures: [] as { reason: string; issueKeys: string[] }[],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const workLog of workLogs) {
|
for (const workLog of workLogs) {
|
||||||
|
|
@ -61,48 +67,56 @@ export async function processWorkLogBatch(plugin: JiraPlugin, workLogs: any[]):
|
||||||
const { issueKey, startTime, duration, comment } = workLog;
|
const { issueKey, startTime, duration, comment } = workLog;
|
||||||
|
|
||||||
if (!issueKey || !startTime || !duration) {
|
if (!issueKey || !startTime || !duration) {
|
||||||
addFailure(results, "Missing required fields", issueKey || "unknown");
|
addFailure(results, 'Missing required fields', issueKey || 'unknown');
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const startDate = convertToStandardDate(startTime);
|
const startDate = convertToStandardDate(startTime);
|
||||||
if (!startDate) {
|
if (!startDate) {
|
||||||
addFailure(results, "Invalid start time format", issueKey);
|
addFailure(results, 'Invalid start time format', issueKey);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const parsed_duration = parseDuration(duration);
|
const parsed_duration = parseDuration(duration);
|
||||||
debugLog(`Duration: ${duration}, Parsed duration: ${parsed_duration}`);
|
debugLog(`Duration: ${duration}, Parsed duration: ${parsed_duration}`);
|
||||||
if (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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await addWorkLog(plugin, issueKey, parsed_duration, startDate, comment || "", false);
|
await addWorkLog(plugin, issueKey, parsed_duration, startDate, comment || '', false);
|
||||||
results.success++;
|
results.success++;
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
addFailure(results, error.message || "Unknown error", workLog.issueKey || "unknown");
|
addFailure(results, (error as Error).message || 'Unknown error', workLog.issueKey || 'unknown');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
showResults(results);
|
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`);
|
new Notice(`Work logs updated: ${results.success}/${results.total} succeeded`);
|
||||||
|
|
||||||
if (results.failures.length > 0) {
|
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) {
|
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 {
|
function addFailure(
|
||||||
const existingFailure = results.failures.find(f => f.reason === reason);
|
results: { failures: { reason: string; issueKeys: string[] }[] },
|
||||||
|
reason: string,
|
||||||
|
issueKey: string,
|
||||||
|
): void {
|
||||||
|
const existingFailure = results.failures.find((f) => f.reason === reason);
|
||||||
if (existingFailure) {
|
if (existingFailure) {
|
||||||
existingFailure.issueKeys.push(issueKey);
|
existingFailure.issueKeys.push(issueKey);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -116,20 +130,18 @@ function convertToStandardDate(dateString: string): string | null {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
|
|
||||||
if (isNaN(date.getTime())) {
|
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
|
// Format to ISO string and adjust to desired format
|
||||||
const isoString = date.toISOString();
|
const isoString = date.toISOString();
|
||||||
return isoString.replace('Z', '+0000');
|
return isoString.replace('Z', '+0000');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error converting date:", error);
|
console.error('Error converting date:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function parseDuration(input: string): string {
|
function parseDuration(input: string): string {
|
||||||
const regex = /(\d+)([wdhms])/g;
|
const regex = /(\d+)([wdhms])/g;
|
||||||
let match;
|
let match;
|
||||||
|
|
|
||||||
|
|
@ -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";
|
const t = useTranslations('commands.add_worklog.manual').t;
|
||||||
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;
|
|
||||||
|
|
||||||
export function registerUpdateWorkLogManuallyCommand(plugin: JiraPlugin): void {
|
export function registerUpdateWorkLogManuallyCommand(plugin: JiraPlugin): void {
|
||||||
plugin.addCommand({
|
plugin.addCommand({
|
||||||
id: "update-work-log-jira-manually",
|
id: 'update-work-log-jira-manually',
|
||||||
name: t("name"),
|
name: t('name'),
|
||||||
checkCallback: (checking: boolean) => {
|
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> {
|
async function processManualWorkLog(plugin: JiraPlugin, _: TFile, issueKey: string): Promise<void> {
|
||||||
|
|
||||||
new IssueWorkLogModal(plugin.app, async (timeSpent: string, startDate: string, comment: string) => {
|
new IssueWorkLogModal(plugin.app, async (timeSpent: string, startDate: string, comment: string) => {
|
||||||
await addWorkLog(plugin, issueKey, timeSpent, startDate, comment);
|
await addWorkLog(plugin, issueKey, timeSpent, startDate, comment);
|
||||||
}).open();
|
}).open();
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {JQLSearchModal} from "../modals";
|
import { JQLSearchModal } from '../modals';
|
||||||
import {fetchIssuesByJQL, validateSettings} from "../api";
|
import { fetchIssuesByJQL, validateSettings } from '../api';
|
||||||
import {createOrUpdateIssueNote} from "../file_operations/getIssue";
|
import { createOrUpdateIssueNote } from '../file_operations/getIssue';
|
||||||
import {useTranslations} from "../localization/translator";
|
import { useTranslations } from '../localization/translator';
|
||||||
import {Notice} from "obsidian";
|
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
|
* Register the batch fetch issues command
|
||||||
*/
|
*/
|
||||||
export function registerBatchFetchIssuesCommand(plugin: JiraPlugin): void {
|
export function registerBatchFetchIssuesCommand(plugin: JiraPlugin): void {
|
||||||
plugin.addCommand({
|
plugin.addCommand({
|
||||||
id: "batch-fetch-issues-jira",
|
id: 'batch-fetch-issues-jira',
|
||||||
name: t("name"),
|
name: t('name'),
|
||||||
checkCallback: (checking: boolean) => {
|
checkCallback: (checking: boolean) => {
|
||||||
const settings_are_valid = validateSettings(plugin);
|
const settings_are_valid = validateSettings(plugin);
|
||||||
if (settings_are_valid) {
|
if (settings_are_valid) {
|
||||||
|
|
@ -33,16 +33,16 @@ function openJQLModal(plugin: JiraPlugin): void {
|
||||||
|
|
||||||
export async function batchFetchAndCreateIssues(plugin: JiraPlugin, jql: string): Promise<void> {
|
export async function batchFetchAndCreateIssues(plugin: JiraPlugin, jql: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
new Notice(t("fetching_started"));
|
new Notice(t('fetching_started'));
|
||||||
|
|
||||||
const issues = await fetchIssuesByJQL(plugin, jql);
|
const issues = await fetchIssuesByJQL(plugin, jql);
|
||||||
|
|
||||||
if (issues.length === 0) {
|
if (issues.length === 0) {
|
||||||
new Notice(t("no_issues_found"));
|
new Notice(t('no_issues_found'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
new Notice(t("fetching_completed", { count: issues.length.toString() }));
|
new Notice(t('fetching_completed', { count: issues.length.toString() }));
|
||||||
|
|
||||||
// Process each issue
|
// Process each issue
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
|
|
@ -53,26 +53,27 @@ export async function batchFetchAndCreateIssues(plugin: JiraPlugin, jql: string)
|
||||||
try {
|
try {
|
||||||
await createOrUpdateIssueNote(plugin, issue);
|
await createOrUpdateIssueNote(plugin, issue);
|
||||||
successCount++;
|
successCount++;
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
errorCount++;
|
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);
|
console.error(`Error processing issue ${issue.key}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show results
|
// Show results
|
||||||
if (errorCount === 0) {
|
if (errorCount === 0) {
|
||||||
new Notice(t("all_success", { count: successCount.toString() }));
|
new Notice(t('all_success', { count: successCount.toString() }));
|
||||||
} else {
|
} else {
|
||||||
new Notice(t("partial_success", {
|
new Notice(
|
||||||
success: successCount.toString(),
|
t('partial_success', {
|
||||||
errors: errorCount.toString()
|
success: successCount.toString(),
|
||||||
}));
|
errors: errorCount.toString(),
|
||||||
console.error("Batch fetch errors:", errors);
|
}),
|
||||||
|
);
|
||||||
|
console.error('Batch fetch errors:', errors);
|
||||||
}
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
} catch (error) {
|
new Notice(t('fetch_error') + ': ' + ((error as Error).message || 'Unknown error'));
|
||||||
new Notice(t("fetch_error") + ": " + (error.message || "Unknown error"));
|
console.error('Batch fetch error:', error);
|
||||||
console.error("Batch fetch error:", error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,20 @@
|
||||||
import { Notice, TFile } from "obsidian";
|
import { Notice, TFile } from 'obsidian';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import { IssueTypeModal, ProjectModal } from "../modals";
|
import { IssueTypeModal, ProjectModal } from '../modals';
|
||||||
import {fetchIssueTypes, fetchProjects} from "../api";
|
import { fetchIssueTypes, fetchProjects } from '../api';
|
||||||
import {createIssueFromFile} from "../file_operations/createUpdateIssue";
|
import { createIssueFromFile } from '../file_operations/createUpdateIssue';
|
||||||
import {checkCommandCallback} from "../tools/checkCommandCallback";
|
import { checkCommandCallback } from '../tools/checkCommandCallback';
|
||||||
import {useTranslations} from "../localization/translator";
|
import { useTranslations } from '../localization/translator';
|
||||||
import {readJiraFieldsFromFile} from "../file_operations/commonPrepareData";
|
import { readJiraFieldsFromFile } from '../file_operations/commonPrepareData';
|
||||||
import {JiraIssueType, JiraProject} from "../interfaces";
|
import { JiraIssueType, JiraProject } from '../interfaces';
|
||||||
import {IssueAddSummaryModal} from "../modals/IssueAddSummaryModal";
|
import { IssueAddSummaryModal } from '../modals/IssueAddSummaryModal';
|
||||||
|
|
||||||
const t = useTranslations("commands.create_issue").t;
|
|
||||||
|
|
||||||
|
const t = useTranslations('commands.create_issue').t;
|
||||||
|
|
||||||
export function registerCreateIssueCommand(plugin: JiraPlugin): void {
|
export function registerCreateIssueCommand(plugin: JiraPlugin): void {
|
||||||
plugin.addCommand({
|
plugin.addCommand({
|
||||||
id: "create-issue-jira",
|
id: 'create-issue-jira',
|
||||||
name: t("name"),
|
name: t('name'),
|
||||||
checkCallback: (checking: boolean) => {
|
checkCallback: (checking: boolean) => {
|
||||||
return checkCommandCallback(plugin, checking, createIssue, []);
|
return checkCommandCallback(plugin, checking, createIssue, []);
|
||||||
},
|
},
|
||||||
|
|
@ -29,9 +28,9 @@ export async function createIssue(plugin: JiraPlugin, file: TFile): Promise<void
|
||||||
await checkIssueTypes(plugin, fields);
|
await checkIssueTypes(plugin, fields);
|
||||||
await checkSummary(plugin, fields);
|
await checkSummary(plugin, fields);
|
||||||
const issueKey = await createIssueFromFile(plugin, file, fields);
|
const issueKey = await createIssueFromFile(plugin, file, fields);
|
||||||
new Notice(t('success', {issueKey}));
|
new Notice(t('success', { issueKey }));
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
new Notice(t('error') + ": " + (error.message || "Unknown error"), 3000);
|
new Notice(t('error') + ': ' + ((error as Error).message || 'Unknown error'), 3000);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +39,8 @@ export async function checkProjects(plugin: JiraPlugin, fields: any): Promise<vo
|
||||||
const projects = await fetchProjects(plugin);
|
const projects = await fetchProjects(plugin);
|
||||||
if (!fields.project || !projects.map((project: any) => project.key).includes(fields.project)) {
|
if (!fields.project || !projects.map((project: any) => project.key).includes(fields.project)) {
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
new ProjectModal(plugin.app,
|
new ProjectModal(
|
||||||
|
plugin.app,
|
||||||
projects.map((project: any) => ({
|
projects.map((project: any) => ({
|
||||||
id: project.key,
|
id: project.key,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
|
|
@ -48,37 +48,39 @@ export async function checkProjects(plugin: JiraPlugin, fields: any): Promise<vo
|
||||||
async (projectKey: string) => {
|
async (projectKey: string) => {
|
||||||
fields.project = projectKey;
|
fields.project = projectKey;
|
||||||
resolve();
|
resolve();
|
||||||
}).open();
|
},
|
||||||
|
).open();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkIssueTypes(plugin: JiraPlugin, fields: any): Promise<void> {
|
async function checkIssueTypes(plugin: JiraPlugin, fields: any): Promise<void> {
|
||||||
const issueTypesResponse = await fetchIssueTypes(plugin, fields.project);
|
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)) {
|
if (!fields.issuetype || !issueTypes.map((issueType: any) => issueType.name).includes(fields.issuetype)) {
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
new IssueTypeModal(plugin.app,
|
new IssueTypeModal(
|
||||||
|
plugin.app,
|
||||||
issueTypes.map((type: any) => ({
|
issueTypes.map((type: any) => ({
|
||||||
name: type.name,
|
name: type.name,
|
||||||
})) as JiraIssueType[],
|
})) as JiraIssueType[],
|
||||||
async (issueType: string) => {
|
async (issueType: string) => {
|
||||||
fields.issuetype = issueType;
|
fields.issuetype = issueType;
|
||||||
resolve();
|
resolve();
|
||||||
}).open();
|
},
|
||||||
|
).open();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkSummary(plugin: JiraPlugin, fields: any): Promise<void> {
|
async function checkSummary(plugin: JiraPlugin, fields: any): Promise<void> {
|
||||||
if (!fields.summary) {
|
if (!fields.summary) {
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
new IssueAddSummaryModal(plugin.app,
|
new IssueAddSummaryModal(plugin.app, async (summary: string) => {
|
||||||
async (summary: string) => {
|
fields.summary = summary;
|
||||||
fields.summary = summary;
|
resolve();
|
||||||
resolve();
|
}).open();
|
||||||
}).open();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import {TFile} from "obsidian";
|
import { TFile } from 'obsidian';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {IssueSearchModal} from "../modals";
|
import { IssueSearchModal } from '../modals';
|
||||||
import {fetchIssue, validateSettings} from "../api";
|
import { fetchIssue, validateSettings } from '../api';
|
||||||
import {createOrUpdateIssueNote} from "../file_operations/getIssue";
|
import { createOrUpdateIssueNote } from '../file_operations/getIssue';
|
||||||
import {checkCommandCallback} from "../tools/checkCommandCallback";
|
import { checkCommandCallback } from '../tools/checkCommandCallback';
|
||||||
import {useTranslations} from "../localization/translator";
|
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 {
|
export function registerGetIssueCommandWithCustomKey(plugin: JiraPlugin): void {
|
||||||
plugin.addCommand({
|
plugin.addCommand({
|
||||||
id: "get-issue-jira-key",
|
id: 'get-issue-jira-key',
|
||||||
name: t("with_key"),
|
name: t('with_key'),
|
||||||
checkCallback: (checking: boolean) => {
|
checkCallback: (checking: boolean) => {
|
||||||
const settings_are_valid = validateSettings(plugin);
|
const settings_are_valid = validateSettings(plugin);
|
||||||
if (settings_are_valid) {
|
if (settings_are_valid) {
|
||||||
|
|
@ -25,10 +25,10 @@ export function registerGetIssueCommandWithCustomKey(plugin: JiraPlugin): void {
|
||||||
|
|
||||||
export function registerGetCurrentIssueCommand(plugin: JiraPlugin): void {
|
export function registerGetCurrentIssueCommand(plugin: JiraPlugin): void {
|
||||||
plugin.addCommand({
|
plugin.addCommand({
|
||||||
id: "get-issue-jira",
|
id: 'get-issue-jira',
|
||||||
name: t("without_key"),
|
name: t('without_key'),
|
||||||
checkCallback: (checking: boolean) => {
|
checkCallback: (checking: boolean) => {
|
||||||
return checkCommandCallback(plugin, checking, fetchAndOpenIssue, ["key"], ["key"]);
|
return checkCommandCallback(plugin, checking, fetchAndOpenIssue, ['key'], ['key']);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import {Notice, TFile} from "obsidian";
|
import { Notice, TFile } from 'obsidian';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {updateIssueFromFile} from "../file_operations/createUpdateIssue";
|
import { updateIssueFromFile } from '../file_operations/createUpdateIssue';
|
||||||
import {checkCommandCallback} from "../tools/checkCommandCallback";
|
import { checkCommandCallback } from '../tools/checkCommandCallback';
|
||||||
import {useTranslations} from "../localization/translator";
|
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 {
|
export function registerUpdateIssueCommand(plugin: JiraPlugin): void {
|
||||||
plugin.addCommand({
|
plugin.addCommand({
|
||||||
id: "update-issue-jira",
|
id: 'update-issue-jira',
|
||||||
name: t("name"),
|
name: t('name'),
|
||||||
checkCallback: (checking: boolean) => {
|
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 {
|
try {
|
||||||
// Update the issue with all data from the file
|
// Update the issue with all data from the file
|
||||||
const issueKey = await updateIssueFromFile(plugin, file);
|
const issueKey = await updateIssueFromFile(plugin, file);
|
||||||
new Notice(t('success', {issueKey}));
|
new Notice(t('success', { issueKey }));
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
new Notice(t('error') + ": " + (error.message || "Unknown error"), 3000);
|
new Notice(t('error') + ': ' + ((error as Error).message || 'Unknown error'), 3000);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
new Notice(t('error') + ": " + (error.message || "Unknown error"));
|
new Notice(t('error') + ': ' + ((error as Error).message || 'Unknown error'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import {Notice, TFile} from "obsidian";
|
import { Notice, TFile } from 'obsidian';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {fetchIssueTransitions} from "../api";
|
import { fetchIssueTransitions } from '../api';
|
||||||
import {updateStatusFromFile} from "../file_operations/createUpdateIssue";
|
import { updateStatusFromFile } from '../file_operations/createUpdateIssue';
|
||||||
import {IssueStatusModal} from "../modals";
|
import { IssueStatusModal } from '../modals';
|
||||||
import {JiraTransitionType} from "../interfaces";
|
import { JiraTransitionType } from '../interfaces';
|
||||||
import {checkCommandCallback} from "../tools/checkCommandCallback";
|
import { checkCommandCallback } from '../tools/checkCommandCallback';
|
||||||
import {useTranslations} from "../localization/translator";
|
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 {
|
export function registerUpdateIssueStatusCommand(plugin: JiraPlugin): void {
|
||||||
plugin.addCommand({
|
plugin.addCommand({
|
||||||
id: "update-issue-status-jira",
|
id: 'update-issue-status-jira',
|
||||||
name: t('name'),
|
name: t('name'),
|
||||||
checkCallback: (checking: boolean) => {
|
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) => {
|
new IssueStatusModal(plugin.app, issueTransitions, async (transition: JiraTransitionType) => {
|
||||||
try {
|
try {
|
||||||
await updateStatusFromFile(plugin, file, transition);
|
await updateStatusFromFile(plugin, file, transition);
|
||||||
new Notice(t('success', {issueKey}));
|
new Notice(t('success', { issueKey }));
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
new Notice(t('error') + ": " + (error.message || "Unknown error"), 3000);
|
new Notice(t('error') + ': ' + ((error as Error).message || 'Unknown error'), 3000);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}).open();
|
}).open();
|
||||||
|
} catch (error: unknown) {
|
||||||
} catch (error) {
|
new Notice(t('error') + ': ' + ((error as Error).message || 'Unknown error'));
|
||||||
new Notice(t('error') + ": " + (error.message || "Unknown error"));
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,405 +1,405 @@
|
||||||
export const defaultIssue = {
|
export const defaultIssue = {
|
||||||
"expand": "",
|
expand: '',
|
||||||
"id": "",
|
id: '',
|
||||||
"self": "",
|
self: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"fields": {
|
fields: {
|
||||||
"issuetype": {
|
issuetype: {
|
||||||
"self": "",
|
self: '',
|
||||||
"id": "",
|
id: '',
|
||||||
"description": "",
|
description: '',
|
||||||
"iconUrl": "",
|
iconUrl: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"subtask": 0,
|
subtask: 0,
|
||||||
"avatarId": 0
|
avatarId: 0,
|
||||||
},
|
},
|
||||||
"timespent": 0,
|
timespent: 0,
|
||||||
"project": {
|
project: {
|
||||||
"self": "",
|
self: '',
|
||||||
"id": "",
|
id: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"projectTypeKey": "",
|
projectTypeKey: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
}
|
|
||||||
},
|
|
||||||
"fixVersions": [],
|
|
||||||
"aggregatetimespent": 0,
|
|
||||||
"resolution": null,
|
|
||||||
"resolutiondate": null,
|
|
||||||
"workratio": 0,
|
|
||||||
"lastViewed": "",
|
|
||||||
"watches": {
|
|
||||||
"self": "",
|
|
||||||
"watchCount": 0,
|
|
||||||
"isWatching": 0
|
|
||||||
},
|
|
||||||
"created": "",
|
|
||||||
"priority": {
|
|
||||||
"self": "",
|
|
||||||
"iconUrl": "",
|
|
||||||
"name": "",
|
|
||||||
"id": ""
|
|
||||||
},
|
|
||||||
"labels": [],
|
|
||||||
"timeestimate": 0,
|
|
||||||
"aggregatetimeoriginalestimate": null,
|
|
||||||
"versions": [],
|
|
||||||
"issuelinks": [],
|
|
||||||
"assignee": {
|
|
||||||
"self": "",
|
|
||||||
"name": "",
|
|
||||||
"key": "",
|
|
||||||
"emailAddress": "",
|
|
||||||
"avatarUrls": {
|
|
||||||
"48x48": "",
|
|
||||||
"24x24": "",
|
|
||||||
"16x16": "",
|
|
||||||
"32x32": ""
|
|
||||||
},
|
},
|
||||||
"displayName": "",
|
|
||||||
"active": 0,
|
|
||||||
"timeZone": ""
|
|
||||||
},
|
},
|
||||||
"updated": "",
|
fixVersions: [],
|
||||||
"status": {
|
aggregatetimespent: 0,
|
||||||
"self": "",
|
resolution: null,
|
||||||
"description": "",
|
resolutiondate: null,
|
||||||
"iconUrl": "",
|
workratio: 0,
|
||||||
"name": "",
|
lastViewed: '',
|
||||||
"id": "",
|
watches: {
|
||||||
"statusCategory": {
|
self: '',
|
||||||
"self": "",
|
watchCount: 0,
|
||||||
"id": 0,
|
isWatching: 0,
|
||||||
"key": "",
|
|
||||||
"colorName": "",
|
|
||||||
"name": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"components": [],
|
created: '',
|
||||||
"timeoriginalestimate": null,
|
priority: {
|
||||||
"description": "",
|
self: '',
|
||||||
"archiveddate": null,
|
iconUrl: '',
|
||||||
"timetracking": {
|
name: '',
|
||||||
"remainingEstimate": "",
|
id: '',
|
||||||
"timeSpent": "",
|
|
||||||
"remainingEstimateSeconds": 0,
|
|
||||||
"timeSpentSeconds": 0
|
|
||||||
},
|
},
|
||||||
"attachment": [],
|
labels: [],
|
||||||
"aggregatetimeestimate": 0,
|
timeestimate: 0,
|
||||||
"summary": "",
|
aggregatetimeoriginalestimate: null,
|
||||||
"creator": {
|
versions: [],
|
||||||
"self": "",
|
issuelinks: [],
|
||||||
"name": "",
|
assignee: {
|
||||||
"key": "",
|
self: '',
|
||||||
"emailAddress": "",
|
name: '',
|
||||||
"avatarUrls": {
|
key: '',
|
||||||
"48x48": "",
|
emailAddress: '',
|
||||||
"24x24": "",
|
avatarUrls: {
|
||||||
"16x16": "",
|
'48x48': '',
|
||||||
"32x32": ""
|
'24x24': '',
|
||||||
|
'16x16': '',
|
||||||
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"subtasks": [],
|
updated: '',
|
||||||
"reporter": {
|
status: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
description: '',
|
||||||
"key": "",
|
iconUrl: '',
|
||||||
"emailAddress": "",
|
name: '',
|
||||||
"avatarUrls": {
|
id: '',
|
||||||
"48x48": "",
|
statusCategory: {
|
||||||
"24x24": "",
|
self: '',
|
||||||
"16x16": "",
|
id: 0,
|
||||||
"32x32": ""
|
key: '',
|
||||||
|
colorName: '',
|
||||||
|
name: '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
|
||||||
"active": 0,
|
|
||||||
"timeZone": ""
|
|
||||||
},
|
},
|
||||||
"aggregateprogress": {
|
components: [],
|
||||||
"progress": 0,
|
timeoriginalestimate: null,
|
||||||
"total": 0,
|
description: '',
|
||||||
"percent": 0
|
archiveddate: null,
|
||||||
|
timetracking: {
|
||||||
|
remainingEstimate: '',
|
||||||
|
timeSpent: '',
|
||||||
|
remainingEstimateSeconds: 0,
|
||||||
|
timeSpentSeconds: 0,
|
||||||
},
|
},
|
||||||
"environment": null,
|
attachment: [],
|
||||||
"duedate": null,
|
aggregatetimeestimate: 0,
|
||||||
"progress": {
|
summary: '',
|
||||||
"progress": 0,
|
creator: {
|
||||||
"total": 0,
|
self: '',
|
||||||
"percent": 0
|
name: '',
|
||||||
|
key: '',
|
||||||
|
emailAddress: '',
|
||||||
|
avatarUrls: {
|
||||||
|
'48x48': '',
|
||||||
|
'24x24': '',
|
||||||
|
'16x16': '',
|
||||||
|
'32x32': '',
|
||||||
|
},
|
||||||
|
displayName: '',
|
||||||
|
active: 0,
|
||||||
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"comment": {
|
subtasks: [],
|
||||||
"comments": [],
|
reporter: {
|
||||||
"maxResults": 0,
|
self: '',
|
||||||
"total": 0,
|
name: '',
|
||||||
"startAt": 0
|
key: '',
|
||||||
|
emailAddress: '',
|
||||||
|
avatarUrls: {
|
||||||
|
'48x48': '',
|
||||||
|
'24x24': '',
|
||||||
|
'16x16': '',
|
||||||
|
'32x32': '',
|
||||||
|
},
|
||||||
|
displayName: '',
|
||||||
|
active: 0,
|
||||||
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"votes": {
|
aggregateprogress: {
|
||||||
"self": "",
|
progress: 0,
|
||||||
"votes": 0,
|
total: 0,
|
||||||
"hasVoted": 0
|
percent: 0,
|
||||||
},
|
},
|
||||||
"worklog": {
|
environment: null,
|
||||||
"startAt": 0,
|
duedate: null,
|
||||||
"maxResults": 0,
|
progress: {
|
||||||
"total": 0,
|
progress: 0,
|
||||||
"worklogs": [
|
total: 0,
|
||||||
|
percent: 0,
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
comments: [],
|
||||||
|
maxResults: 0,
|
||||||
|
total: 0,
|
||||||
|
startAt: 0,
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
self: '',
|
||||||
|
votes: 0,
|
||||||
|
hasVoted: 0,
|
||||||
|
},
|
||||||
|
worklog: {
|
||||||
|
startAt: 0,
|
||||||
|
maxResults: 0,
|
||||||
|
total: 0,
|
||||||
|
worklogs: [
|
||||||
{
|
{
|
||||||
"self": "",
|
self: '',
|
||||||
"author": {
|
author: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"updateAuthor": {
|
updateAuthor: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"comment": "",
|
comment: '',
|
||||||
"created": "",
|
created: '',
|
||||||
"updated": "",
|
updated: '',
|
||||||
"started": "",
|
started: '',
|
||||||
"timeSpent": "",
|
timeSpent: '',
|
||||||
"timeSpentSeconds": 0,
|
timeSpentSeconds: 0,
|
||||||
"id": "",
|
id: '',
|
||||||
"issueId": ""
|
issueId: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"self": "",
|
self: '',
|
||||||
"author": {
|
author: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"updateAuthor": {
|
updateAuthor: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"comment": "",
|
comment: '',
|
||||||
"created": "",
|
created: '',
|
||||||
"updated": "",
|
updated: '',
|
||||||
"started": "",
|
started: '',
|
||||||
"timeSpent": "",
|
timeSpent: '',
|
||||||
"timeSpentSeconds": 0,
|
timeSpentSeconds: 0,
|
||||||
"id": "",
|
id: '',
|
||||||
"issueId": ""
|
issueId: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"self": "",
|
self: '',
|
||||||
"author": {
|
author: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"updateAuthor": {
|
updateAuthor: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"comment": "",
|
comment: '',
|
||||||
"created": "",
|
created: '',
|
||||||
"updated": "",
|
updated: '',
|
||||||
"started": "",
|
started: '',
|
||||||
"timeSpent": "",
|
timeSpent: '',
|
||||||
"timeSpentSeconds": 0,
|
timeSpentSeconds: 0,
|
||||||
"id": "",
|
id: '',
|
||||||
"issueId": ""
|
issueId: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"self": "",
|
self: '',
|
||||||
"author": {
|
author: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"updateAuthor": {
|
updateAuthor: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"comment": "",
|
comment: '',
|
||||||
"created": "",
|
created: '',
|
||||||
"updated": "",
|
updated: '',
|
||||||
"started": "",
|
started: '',
|
||||||
"timeSpent": "",
|
timeSpent: '',
|
||||||
"timeSpentSeconds": 0,
|
timeSpentSeconds: 0,
|
||||||
"id": "",
|
id: '',
|
||||||
"issueId": ""
|
issueId: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"self": "",
|
self: '',
|
||||||
"author": {
|
author: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"updateAuthor": {
|
updateAuthor: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"comment": "",
|
comment: '',
|
||||||
"created": "",
|
created: '',
|
||||||
"updated": "",
|
updated: '',
|
||||||
"started": "",
|
started: '',
|
||||||
"timeSpent": "",
|
timeSpent: '',
|
||||||
"timeSpentSeconds": 0,
|
timeSpentSeconds: 0,
|
||||||
"id": "",
|
id: '',
|
||||||
"issueId": ""
|
issueId: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"self": "",
|
self: '',
|
||||||
"author": {
|
author: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"updateAuthor": {
|
updateAuthor: {
|
||||||
"self": "",
|
self: '',
|
||||||
"name": "",
|
name: '',
|
||||||
"key": "",
|
key: '',
|
||||||
"emailAddress": "",
|
emailAddress: '',
|
||||||
"avatarUrls": {
|
avatarUrls: {
|
||||||
"48x48": "",
|
'48x48': '',
|
||||||
"24x24": "",
|
'24x24': '',
|
||||||
"16x16": "",
|
'16x16': '',
|
||||||
"32x32": ""
|
'32x32': '',
|
||||||
},
|
},
|
||||||
"displayName": "",
|
displayName: '',
|
||||||
"active": 0,
|
active: 0,
|
||||||
"timeZone": ""
|
timeZone: '',
|
||||||
},
|
},
|
||||||
"comment": "",
|
comment: '',
|
||||||
"created": "",
|
created: '',
|
||||||
"updated": "",
|
updated: '',
|
||||||
"started": "",
|
started: '',
|
||||||
"timeSpent": "",
|
timeSpent: '',
|
||||||
"timeSpentSeconds": 0,
|
timeSpentSeconds: 0,
|
||||||
"id": "",
|
id: '',
|
||||||
"issueId": ""
|
issueId: '',
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
"archivedby": null
|
archivedby: null,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
export const defaultTemplate =
|
export const defaultTemplate = `---
|
||||||
`---
|
|
||||||
key:
|
key:
|
||||||
summary:
|
summary:
|
||||||
status:
|
status:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {JiraIssue} from "../interfaces";
|
import { JiraIssue } from '../interfaces';
|
||||||
|
|
||||||
export interface FieldMapping {
|
export interface FieldMapping {
|
||||||
toJira: (value: any) => any;
|
toJira: (value: any) => any;
|
||||||
|
|
@ -6,74 +6,72 @@ export interface FieldMapping {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const obsidianJiraFieldMappings: Record<string, FieldMapping> = {
|
export const obsidianJiraFieldMappings: Record<string, FieldMapping> = {
|
||||||
"summary": {
|
summary: {
|
||||||
toJira: (value) => value,
|
toJira: (value) => value,
|
||||||
fromJira: (issue) => issue.fields.summary,
|
fromJira: (issue) => issue.fields.summary,
|
||||||
},
|
},
|
||||||
"description": {
|
description: {
|
||||||
toJira: () => null,
|
toJira: () => null,
|
||||||
fromJira: (issue) => issue.fields.description,
|
fromJira: (issue) => issue.fields.description,
|
||||||
},
|
},
|
||||||
"key": {
|
key: {
|
||||||
toJira: () => null,
|
toJira: () => null,
|
||||||
fromJira: (issue) => issue.key,
|
fromJira: (issue) => issue.key,
|
||||||
},
|
},
|
||||||
"self": {
|
self: {
|
||||||
toJira: () => null,
|
toJira: () => null,
|
||||||
fromJira: (issue) => issue.self,
|
fromJira: (issue) => issue.self,
|
||||||
},
|
},
|
||||||
"project": {
|
project: {
|
||||||
toJira: (value) => ({ key: value }),
|
toJira: (value) => ({ key: value }),
|
||||||
fromJira: (issue) =>
|
fromJira: (issue) => (issue.fields.project ? issue.fields.project.key : ''),
|
||||||
issue.fields.project ?issue.fields.project.key :"",
|
|
||||||
},
|
},
|
||||||
"issuetype": {
|
issuetype: {
|
||||||
toJira: (value) => ({ name: value }),
|
toJira: (value) => ({ name: value }),
|
||||||
fromJira: (issue) =>
|
fromJira: (issue) => (issue.fields.issuetype ? issue.fields.issuetype.name : ''),
|
||||||
issue.fields.issuetype ?issue.fields.issuetype.name :"",
|
|
||||||
},
|
},
|
||||||
"priority": {
|
priority: {
|
||||||
toJira: (value) => ({ name: value }),
|
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,
|
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 }),
|
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 }),
|
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,
|
toJira: () => null,
|
||||||
fromJira: (issue) =>issue.fields.creator ?issue.fields.creator.name :"",
|
fromJira: (issue) => (issue.fields.creator ? issue.fields.creator.name : ''),
|
||||||
},
|
},
|
||||||
"lastViewed": {
|
lastViewed: {
|
||||||
toJira: () => null,
|
toJira: () => null,
|
||||||
fromJira: (issue) => issue.fields.lastViewed,
|
fromJira: (issue) => issue.fields.lastViewed,
|
||||||
},
|
},
|
||||||
"updated": {
|
updated: {
|
||||||
toJira: () => null,
|
toJira: () => null,
|
||||||
fromJira: (issue) => issue.fields.updated,
|
fromJira: (issue) => issue.fields.updated,
|
||||||
},
|
},
|
||||||
"created": {
|
created: {
|
||||||
toJira: () => null,
|
toJira: () => null,
|
||||||
fromJira: (issue) => issue.fields.created,
|
fromJira: (issue) => issue.fields.created,
|
||||||
},
|
},
|
||||||
"link": {
|
link: {
|
||||||
toJira: () => null,
|
toJira: () => null,
|
||||||
fromJira: (issue) => issue.self.replace(/(\w+:\/\/\S+?)\/.*/, `$1/browse/${issue.key}`),
|
fromJira: (issue) => issue.self.replace(/(\w+:\/\/\S+?)\/.*/, `$1/browse/${issue.key}`),
|
||||||
},
|
},
|
||||||
"openLink": {
|
openLink: {
|
||||||
toJira: () => null,
|
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,
|
toJira: () => null,
|
||||||
fromJira: (issue) => issue.fields.aggregateprogress.percent+'%',
|
fromJira: (issue) => issue.fields.aggregateprogress.percent + '%',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {TFile} from "obsidian";
|
import { TFile } from 'obsidian';
|
||||||
import {extractAllJiraSyncValuesFromContent} from "../tools/sectionTools";
|
import { extractAllJiraSyncValuesFromContent } from '../tools/sectionTools';
|
||||||
|
|
||||||
export async function prepareJiraFieldsFromFile(
|
export async function prepareJiraFieldsFromFile(plugin: JiraPlugin, file: TFile): Promise<Record<string, any>> {
|
||||||
plugin: JiraPlugin,
|
|
||||||
file: TFile
|
|
||||||
): Promise<Record<string, any>> {
|
|
||||||
const fileContent = await plugin.app.vault.read(file);
|
const fileContent = await plugin.app.vault.read(file);
|
||||||
|
|
||||||
const syncSections = extractAllJiraSyncValuesFromContent(fileContent);
|
const syncSections = extractAllJiraSyncValuesFromContent(fileContent);
|
||||||
|
|
@ -13,16 +10,13 @@ export async function prepareJiraFieldsFromFile(
|
||||||
let fields: Record<string, any> = {};
|
let fields: Record<string, any> = {};
|
||||||
|
|
||||||
await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||||
fields = {...syncSections, ...frontmatter};
|
fields = { ...syncSections, ...frontmatter };
|
||||||
});
|
});
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readJiraFieldsFromFile(
|
export async function readJiraFieldsFromFile(plugin: JiraPlugin, file: TFile): Promise<Record<string, any>> {
|
||||||
plugin: JiraPlugin,
|
|
||||||
file: TFile
|
|
||||||
): Promise<Record<string, any>> {
|
|
||||||
const fileContent = await plugin.app.vault.cachedRead(file);
|
const fileContent = await plugin.app.vault.cachedRead(file);
|
||||||
|
|
||||||
const syncSections = extractAllJiraSyncValuesFromContent(fileContent);
|
const syncSections = extractAllJiraSyncValuesFromContent(fileContent);
|
||||||
|
|
@ -30,5 +24,5 @@ export async function readJiraFieldsFromFile(
|
||||||
const cachedMetadata = plugin.app.metadataCache.getFileCache(file);
|
const cachedMetadata = plugin.app.metadataCache.getFileCache(file);
|
||||||
const frontmatter = cachedMetadata?.frontmatter || {};
|
const frontmatter = cachedMetadata?.frontmatter || {};
|
||||||
|
|
||||||
return {...syncSections, ...frontmatter};
|
return { ...syncSections, ...frontmatter };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,23 @@
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {TFile} from "obsidian";
|
import { TFile } from 'obsidian';
|
||||||
import {createJiraIssue, updateJiraIssue, updateJiraStatus} from "../api";
|
import { createJiraIssue, updateJiraIssue, updateJiraStatus } from '../api';
|
||||||
import {prepareJiraFieldsFromFile} from "./commonPrepareData";
|
import { prepareJiraFieldsFromFile } from './commonPrepareData';
|
||||||
import {localToJiraFields, updateJiraToLocal} from "../tools/mapObsidianJiraFields";
|
import { localToJiraFields, updateJiraToLocal } from '../tools/mapObsidianJiraFields';
|
||||||
import {JiraIssue, JiraTransitionType} from "../interfaces";
|
import { JiraIssue, JiraTransitionType } from '../interfaces';
|
||||||
import {obsidianJiraFieldMappings} from "../default/obsidianJiraFieldsMapping";
|
import { obsidianJiraFieldMappings } from '../default/obsidianJiraFieldsMapping';
|
||||||
|
|
||||||
export async function updateIssueFromFile(plugin: JiraPlugin, file: TFile): Promise<string> {
|
export async function updateIssueFromFile(plugin: JiraPlugin, file: TFile): Promise<string> {
|
||||||
let fields = await prepareJiraFieldsFromFile(plugin, file);
|
let fields = await prepareJiraFieldsFromFile(plugin, file);
|
||||||
const issueKey = fields.key;
|
const issueKey = fields.key;
|
||||||
|
|
||||||
if (!issueKey) {
|
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);
|
await updateJiraIssue(plugin, issueKey, fields);
|
||||||
return issueKey;
|
return issueKey;
|
||||||
}
|
}
|
||||||
|
|
@ -27,27 +30,36 @@ export async function createIssueFromFile(
|
||||||
if (!fields) {
|
if (!fields) {
|
||||||
fields = await prepareJiraFieldsFromFile(plugin, file);
|
fields = await prepareJiraFieldsFromFile(plugin, file);
|
||||||
}
|
}
|
||||||
fields = localToJiraFields(fields, {...obsidianJiraFieldMappings, ...plugin.settings.fieldMapping.fieldMappings});
|
fields = localToJiraFields(fields, {
|
||||||
|
...obsidianJiraFieldMappings,
|
||||||
|
...plugin.settings.fieldMapping.fieldMappings,
|
||||||
|
});
|
||||||
// Create the issue
|
// Create the issue
|
||||||
const issueData = await createJiraIssue(plugin, fields);
|
const issueData = await createJiraIssue(plugin, fields);
|
||||||
const issueKey = issueData.key;
|
const issueKey = issueData.key;
|
||||||
|
|
||||||
// Update frontmatter with the new issue key
|
// Update frontmatter with the new issue key
|
||||||
await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||||
frontmatter["key"] = issueKey;
|
frontmatter['key'] = issueKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
return 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);
|
const fields = await prepareJiraFieldsFromFile(plugin, file);
|
||||||
|
|
||||||
if (!fields.key) {
|
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 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;
|
return fields.key;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,36 @@
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {JiraIssue} from "../interfaces";
|
import { JiraIssue } from '../interfaces';
|
||||||
import {ensureIssuesFolder} from "../tools/filesUtils";
|
import { ensureIssuesFolder } from '../tools/filesUtils';
|
||||||
import {sanitizeFileName} from "../tools/sanitizers";
|
import { sanitizeFileName } from '../tools/sanitizers';
|
||||||
import {updateJiraToLocal} from "../tools/mapObsidianJiraFields";
|
import { updateJiraToLocal } from '../tools/mapObsidianJiraFields';
|
||||||
import {Notice, TFile, TFolder} from "obsidian";
|
import { Notice, TFile, TFolder } from 'obsidian';
|
||||||
import {defaultTemplate} from "../default/defaultTemplate";
|
import { defaultTemplate } from '../default/defaultTemplate';
|
||||||
import { debugLog } from "src/tools/debugLogging";
|
import { debugLog } from '../tools/debugLogging';
|
||||||
|
|
||||||
function generateFilenameFromTemplate(template: string, issue: JiraIssue): string {
|
function generateFilenameFromTemplate(template: string, issue: JiraIssue): string {
|
||||||
let filename = template;
|
let filename = template;
|
||||||
|
|
||||||
// Replace {summary} with sanitized summary
|
// Replace {summary} with sanitized summary
|
||||||
const summary = issue.fields?.summary || "";
|
const summary = issue.fields?.summary || '';
|
||||||
const sanitizedSummary = sanitizeFileName(summary);
|
const sanitizedSummary = sanitizeFileName(summary);
|
||||||
filename = filename.replace(/\{summary\}/g, sanitizedSummary);
|
filename = filename.replace(/\{summary\}/g, sanitizedSummary);
|
||||||
|
|
||||||
// Replace {key} with issue key
|
// Replace {key} with issue key
|
||||||
const key = issue.key || "";
|
const key = issue.key || '';
|
||||||
filename = filename.replace(/\{key\}/g, key);
|
filename = filename.replace(/\{key\}/g, key);
|
||||||
|
|
||||||
// Sanitize the entire filename to remove any prohibited characters
|
// Sanitize the entire filename to remove any prohibited characters
|
||||||
filename = sanitizeFileName(filename);
|
filename = sanitizeFileName(filename);
|
||||||
|
|
||||||
// Fallback if the result is empty or only whitespace
|
// 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
|
// Use key if available, otherwise use a default
|
||||||
if (key) {
|
if (key) {
|
||||||
filename = key;
|
filename = key;
|
||||||
} else if (sanitizedSummary) {
|
} else if (sanitizedSummary) {
|
||||||
filename = sanitizedSummary;
|
filename = sanitizedSummary;
|
||||||
} else {
|
} else {
|
||||||
filename = "jira-issue";
|
filename = 'jira-issue';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ export async function createOrUpdateIssueNote(plugin: JiraPlugin, issue: JiraIss
|
||||||
await ensureIssuesFolder(plugin);
|
await ensureIssuesFolder(plugin);
|
||||||
|
|
||||||
let targetFile: TFile | null = null;
|
let targetFile: TFile | null = null;
|
||||||
let targetPath: string = "";
|
let targetPath: string = '';
|
||||||
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
targetPath = filePath;
|
targetPath = filePath;
|
||||||
|
|
@ -74,7 +74,7 @@ export async function createOrUpdateIssueNote(plugin: JiraPlugin, issue: JiraIss
|
||||||
|
|
||||||
// If still not found, create new file
|
// If still not found, create new file
|
||||||
if (!targetFile) {
|
if (!targetFile) {
|
||||||
const template = plugin.settings.fetchIssue.filenameTemplate || "{summary} ({key})";
|
const template = plugin.settings.fetchIssue.filenameTemplate || '{summary} ({key})';
|
||||||
const filename = generateFilenameFromTemplate(template, issue);
|
const filename = generateFilenameFromTemplate(template, issue);
|
||||||
targetPath = `${plugin.settings.global.issuesFolder}/${filename}.md`;
|
targetPath = `${plugin.settings.global.issuesFolder}/${filename}.md`;
|
||||||
debugLog(`Issue ${issue.key} not found in cache or filesystem, creating new file: ${targetPath}`);
|
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) {
|
if (targetFile) {
|
||||||
await updateJiraToLocal(plugin, targetFile, issue)
|
await updateJiraToLocal(plugin, targetFile, issue);
|
||||||
await plugin.app.workspace.openLinkText(targetFile.path, "");
|
await plugin.app.workspace.openLinkText(targetFile.path, '');
|
||||||
} else {
|
} else {
|
||||||
const newFile = await createNewIssueFile(plugin, targetPath);
|
const newFile = await createNewIssueFile(plugin, targetPath);
|
||||||
await updateJiraToLocal(plugin, newFile, issue)
|
await updateJiraToLocal(plugin, newFile, issue);
|
||||||
await plugin.app.workspace.openLinkText(newFile.path, "");
|
await plugin.app.workspace.openLinkText(newFile.path, '');
|
||||||
// Add new file to cache
|
// Add new file to cache
|
||||||
plugin.setFilePathForIssueKey(issue.key, newFile.path);
|
plugin.setFilePathForIssueKey(issue.key, newFile.path);
|
||||||
}
|
}
|
||||||
new Notice(`Issue ${issue.key} imported successfully`);
|
new Notice(`Issue ${issue.key} imported successfully`);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
new Notice("Error creating issue note: " + (error.message || "Unknown error"));
|
new Notice('Error creating issue note: ' + ((error as Error).message || 'Unknown error'));
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createNewIssueFile(plugin: JiraPlugin, filePath: string): Promise<TFile> {
|
||||||
|
let initialContent = '';
|
||||||
|
|
||||||
async function createNewIssueFile(
|
let templatePath = plugin.settings.global.templatePath;
|
||||||
plugin: JiraPlugin,
|
if (templatePath && !templatePath.endsWith('.md')) {
|
||||||
filePath: string
|
templatePath += '.md';
|
||||||
): Promise<TFile> {
|
|
||||||
let initialContent = "";
|
|
||||||
|
|
||||||
|
|
||||||
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);
|
const templateFile = plugin.app.vault.getFileByPath(templatePath);
|
||||||
if (templateFile) {
|
if (templateFile) {
|
||||||
// Use the template as initial content
|
// Use the template as initial content
|
||||||
|
|
@ -119,7 +114,7 @@ async function createNewIssueFile(
|
||||||
new Notice(`Template file not found: ${templatePath}, using default template`);
|
new Notice(`Template file not found: ${templatePath}, using default template`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (initialContent === "") initialContent = defaultTemplate
|
if (initialContent === '') initialContent = defaultTemplate;
|
||||||
|
|
||||||
// Create the file with initial content
|
// Create the file with initial content
|
||||||
await plugin.app.vault.create(filePath, initialContent);
|
await plugin.app.vault.create(filePath, initialContent);
|
||||||
|
|
@ -127,9 +122,9 @@ async function createNewIssueFile(
|
||||||
// Get file reference and update frontmatter
|
// Get file reference and update frontmatter
|
||||||
const newFile = plugin.app.vault.getFileByPath(filePath);
|
const newFile = plugin.app.vault.getFileByPath(filePath);
|
||||||
if (!newFile) {
|
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> {
|
async function findFileByIssueKey(plugin: JiraPlugin, issueKey: string): Promise<TFile | null> {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { App } from "obsidian";
|
import { App } from 'obsidian';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function type for field mappings
|
* Function type for field mappings
|
||||||
|
|
@ -46,4 +46,3 @@ export interface ValidationResult {
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,9 +53,8 @@ async function compileLocale(locale) {
|
||||||
const data = yaml.parse(content);
|
const data = yaml.parse(content);
|
||||||
const fileNameWithoutExt = path.basename(item, '.yaml');
|
const fileNameWithoutExt = path.basename(item, '.yaml');
|
||||||
|
|
||||||
const targetKeys = fileNameWithoutExt === 'index'
|
const targetKeys =
|
||||||
? parentKeys
|
fileNameWithoutExt === 'index' ? parentKeys : [...parentKeys, fileNameWithoutExt];
|
||||||
: [...parentKeys, fileNameWithoutExt];
|
|
||||||
|
|
||||||
let target = result;
|
let target = result;
|
||||||
for (const key of targetKeys.slice(0, -1)) {
|
for (const key of targetKeys.slice(0, -1)) {
|
||||||
|
|
@ -90,7 +89,6 @@ function deepMerge(target, source) {
|
||||||
return Object.assign(target || {}, source);
|
return Object.assign(target || {}, source);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Watch mode
|
// Watch mode
|
||||||
if (process.argv.includes('--watch')) {
|
if (process.argv.includes('--watch')) {
|
||||||
console.log('👀 Watching for changes...');
|
console.log('👀 Watching for changes...');
|
||||||
|
|
@ -105,18 +103,20 @@ if (process.argv.includes('--watch')) {
|
||||||
}, debounceDelay);
|
}, debounceDelay);
|
||||||
|
|
||||||
// Watch for changes in the compiled directory
|
// Watch for changes in the compiled directory
|
||||||
chokidar.watch(SOURCE_DIR, {
|
chokidar
|
||||||
ignoreInitial: true,
|
.watch(SOURCE_DIR, {
|
||||||
ignored: /.*~$/, // Игнорировать скрытые файлы
|
ignoreInitial: true,
|
||||||
}).on('all', (event, path) => {
|
ignored: /.*~$/, // Игнорировать скрытые файлы
|
||||||
if (event === 'change' || event === 'add' || event === 'unlink') {
|
})
|
||||||
clearTimeout(debounceTimer);
|
.on('all', (event, path) => {
|
||||||
debounceTimer = setTimeout(() => {
|
if (event === 'change' || event === 'add' || event === 'unlink') {
|
||||||
console.log(`🔁 Detected changes in ${path} (${event}), recompiling...`);
|
clearTimeout(debounceTimer);
|
||||||
main();
|
debounceTimer = setTimeout(() => {
|
||||||
}, debounceDelay);
|
console.log(`🔁 Detected changes in ${path} (${event}), recompiling...`);
|
||||||
}
|
main();
|
||||||
});
|
}, debounceDelay);
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Run once
|
// Run once
|
||||||
main();
|
main();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ desc: Set the Jira issue summary
|
||||||
|
|
||||||
key:
|
key:
|
||||||
name: Summary
|
name: Summary
|
||||||
placeholder: ""
|
placeholder: 'Enter issue summary...'
|
||||||
desc: Enter new task name (e.g., "My great issue")
|
desc: Enter new task name (e.g., "My great issue")
|
||||||
|
|
||||||
submit: Submit
|
submit: Submit
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ submit: Search Issues
|
||||||
|
|
||||||
preview:
|
preview:
|
||||||
total: Total {total} issues found
|
total: Total {total} issues found
|
||||||
showing: " (showing first {limit} issues)"
|
showing: ' (showing first {limit} issues)'
|
||||||
error: "{error}"
|
error: '{error}'
|
||||||
loading: Loading...
|
loading: Loading...
|
||||||
empty_input: Enter a JQL query to see preview
|
empty_input: Enter a JQL query to see preview
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,20 @@ name: Add work log
|
||||||
|
|
||||||
spent:
|
spent:
|
||||||
name: Time spent
|
name: Time spent
|
||||||
desc: "Format: weeks days hours, minutes"
|
desc: 'Format: weeks days hours, minutes'
|
||||||
placeholder: "3w 4d 12h 30m"
|
placeholder: '3w 4d 12h 30m'
|
||||||
|
|
||||||
start:
|
start:
|
||||||
name: Start datetime
|
name: Start datetime
|
||||||
desc: "Format: DD/MMM/YY HH:MM"
|
desc: 'Format: DD/MMM/YY HH:MM'
|
||||||
placeholder: "03/feb/25 15:22"
|
placeholder: '03/feb/25 15:22'
|
||||||
|
|
||||||
comment:
|
comment:
|
||||||
name: Work description
|
name: Work description
|
||||||
desc: Optional
|
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
|
submit: Submit
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ auth:
|
||||||
desc: Choose how to authenticate with Jira
|
desc: Choose how to authenticate with Jira
|
||||||
options:
|
options:
|
||||||
bearer: Bearer Token (PAT)
|
bearer: Bearer Token (PAT)
|
||||||
basic: Basic Auth (Username + PAT)
|
basic: Basic Auth (email + PAT)
|
||||||
session: Session Cookie (Username + Password)
|
session: Session Cookie (Username + Password)
|
||||||
|
|
||||||
pat:
|
pat:
|
||||||
|
|
@ -45,3 +45,12 @@ ping:
|
||||||
title: Ping Jira
|
title: Ping Jira
|
||||||
desc: Check your connection to Jira
|
desc: Check your connection to Jira
|
||||||
button: Ping 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
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ title: Fetch issue settings
|
||||||
note:
|
note:
|
||||||
desc0: Important!
|
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."
|
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 "
|
desc2: 'For more info, go to official '
|
||||||
link_label: "docs"
|
link_label: 'docs'
|
||||||
desc3: ". While filling, list these params with ',' separator. Example: \"summary, description\""
|
desc3: '. While filling, list these params with '','' separator. Example: "summary, description"'
|
||||||
|
|
||||||
key:
|
key:
|
||||||
name: View raw issue data
|
name: View raw issue data
|
||||||
|
|
@ -13,17 +13,16 @@ key:
|
||||||
|
|
||||||
fields:
|
fields:
|
||||||
name: Issue fields ('fields')
|
name: Issue fields ('fields')
|
||||||
desc: "Main Jira fields"
|
desc: 'Main Jira fields'
|
||||||
def: "*all, -comment"
|
def: '*all, -comment'
|
||||||
|
|
||||||
expand:
|
expand:
|
||||||
name: Expand fields ('expand')
|
name: Expand fields ('expand')
|
||||||
desc: "Extra Jira fields"
|
desc: 'Extra Jira fields'
|
||||||
def: "renderedFields, changelog"
|
def: 'renderedFields, changelog'
|
||||||
|
|
||||||
|
|
||||||
filenameTemplate:
|
filenameTemplate:
|
||||||
name: Filename template
|
name: Filename template
|
||||||
desc: |
|
desc: |
|
||||||
Template for created issue filenames. Use {summary} for issue summary and {key} for issue key
|
Template for created issue filenames. Use {summary} for issue summary and {key} for issue key
|
||||||
placeholder: "{summary} ({key})"
|
placeholder: '{summary} ({key})'
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ fv:
|
||||||
name: Enable field validation
|
name: Enable field validation
|
||||||
desc: Check fields before saving based on the standard API response (validation does not account for custom fields)
|
desc: Check fields before saving based on the standard API response (validation does not account for custom fields)
|
||||||
secNote:
|
secNote:
|
||||||
name: "⚠️ Note: "
|
name: '⚠️ Note: '
|
||||||
desc: Field mapping uses JavaScript lambda functions.
|
desc: Field mapping uses JavaScript lambda functions.
|
||||||
example:
|
example:
|
||||||
name: Example field mapping
|
name: Example field mapping
|
||||||
|
|
@ -13,6 +13,7 @@ example:
|
||||||
from_jira: From Jira
|
from_jira: From Jira
|
||||||
to_jira: To Jira
|
to_jira: To Jira
|
||||||
field_name: Field name
|
field_name: Field name
|
||||||
|
field_name_placeholder: Enter field name
|
||||||
|
|
||||||
add_mapping: Add mapping
|
add_mapping: Add mapping
|
||||||
add_mapping_tooltip: Add new field mapping
|
add_mapping_tooltip: Add new field mapping
|
||||||
|
|
@ -20,6 +21,7 @@ reset: Reset
|
||||||
reset_tooltip: Reset
|
reset_tooltip: Reset
|
||||||
reset_confirm_title: Reset field mapping
|
reset_confirm_title: Reset field mapping
|
||||||
reset_confirm_text: Field mapping will be cleaned up. Are you sure?
|
reset_confirm_text: Field mapping will be cleaned up. Are you sure?
|
||||||
|
remove_field: Remove field
|
||||||
reload_defaults_tooltip: Reload
|
reload_defaults_tooltip: Reload
|
||||||
reload_confirm_title: Reload default field mapping
|
reload_confirm_title: Reload default field mapping
|
||||||
reload_confirm_text: Current field mappings will be overwritten with the default mappings. Are you sure?
|
reload_confirm_text: Current field mappings will be overwritten with the default mappings. Are you sure?
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
title: Timekeep work log statistics
|
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:
|
settings:
|
||||||
send_comments:
|
send_comments:
|
||||||
|
|
@ -13,7 +13,7 @@ settings:
|
||||||
max_weeks:
|
max_weeks:
|
||||||
name: Maximum Weeks to Show
|
name: Maximum Weeks to Show
|
||||||
description: Number of recent weeks to display
|
description: Number of recent weeks to display
|
||||||
|
|
||||||
time_range:
|
time_range:
|
||||||
name: Time Range
|
name: Time Range
|
||||||
description: Select the time range for grouping entries
|
description: Select the time range for grouping entries
|
||||||
|
|
@ -22,16 +22,16 @@ settings:
|
||||||
weeks: Weeks
|
weeks: Weeks
|
||||||
months: Months
|
months: Months
|
||||||
custom: Custom Range
|
custom: Custom Range
|
||||||
|
|
||||||
max_items:
|
max_items:
|
||||||
name: Maximum Items to Show
|
name: Maximum Items to Show
|
||||||
description: Number of recent time periods to display
|
description: Number of recent time periods to display
|
||||||
|
|
||||||
date_from:
|
date_from:
|
||||||
name: From Date
|
name: From Date
|
||||||
description: Start date for custom range
|
description: Start date for custom range
|
||||||
placeholder: YYYY-MM-DD
|
placeholder: YYYY-MM-DD
|
||||||
|
|
||||||
date_to:
|
date_to:
|
||||||
name: To Date
|
name: To Date
|
||||||
description: End date for custom range
|
description: End date for custom range
|
||||||
|
|
@ -40,11 +40,11 @@ settings:
|
||||||
display:
|
display:
|
||||||
select_period: Select Period
|
select_period: Select Period
|
||||||
select_period_placeholder: Select a time period...
|
select_period_placeholder: Select a time period...
|
||||||
selected_period: "Selected: {period}"
|
selected_period: 'Selected: {period}'
|
||||||
all_periods: All Recent Periods
|
all_periods: All Recent Periods
|
||||||
no_entries: No timekeep entries found.
|
no_entries: No timekeep entries found.
|
||||||
period_of: Period of {date}
|
period_of: Period of {date}
|
||||||
|
|
||||||
table:
|
table:
|
||||||
total: Total
|
total: Total
|
||||||
task: Task
|
task: Task
|
||||||
|
|
@ -60,8 +60,8 @@ actions:
|
||||||
messages:
|
messages:
|
||||||
select_period_first: Please select a time period first.
|
select_period_first: Please select a time period first.
|
||||||
refresh_success: Data refreshed successfully
|
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_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.
|
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.
|
custom_range_required: Please specify both start and end dates for custom range.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ desc: Задать название задачи Jira
|
||||||
|
|
||||||
key:
|
key:
|
||||||
name: Название задачи
|
name: Название задачи
|
||||||
placeholder: ""
|
placeholder: 'Введите название задачи...'
|
||||||
desc: Введите название задачи (например, "Моя замечательная задача")
|
desc: Введите название задачи (например, "Моя замечательная задача")
|
||||||
|
|
||||||
submit: Подтвердить
|
submit: Подтвердить
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@ maxResults:
|
||||||
submit: Найти задачи
|
submit: Найти задачи
|
||||||
|
|
||||||
preview:
|
preview:
|
||||||
total: "{total} задач найдено"
|
total: '{total} задач найдено'
|
||||||
showing: " (показаны первые {limit} задач)"
|
showing: ' (показаны первые {limit} задач)'
|
||||||
error: "{error}"
|
error: '{error}'
|
||||||
loading: Загрузка...
|
loading: Загрузка...
|
||||||
empty_input: Введите JQL запрос для просмотра превью задач
|
empty_input: Введите JQL запрос для просмотра превью задач
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ start:
|
||||||
comment:
|
comment:
|
||||||
name: Описание работы
|
name: Описание работы
|
||||||
desc: Необязательно
|
desc: Необязательно
|
||||||
placeholder: ''
|
placeholder: 'Введите описание работы...'
|
||||||
|
|
||||||
months:
|
months:
|
||||||
- янв
|
- янв
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ auth:
|
||||||
desc: Выберите способ подключения к Jira
|
desc: Выберите способ подключения к Jira
|
||||||
options:
|
options:
|
||||||
bearer: Bearer Token (PAT)
|
bearer: Bearer Token (PAT)
|
||||||
basic: Basic Auth (Имя пользователя + PAT)
|
basic: Basic Auth (email + PAT)
|
||||||
session: Session Cookie (Имя пользователя + Пароль)
|
session: Session Cookie (Имя пользователя + Пароль)
|
||||||
pat:
|
pat:
|
||||||
title: Jira PAT
|
title: Jira PAT
|
||||||
|
|
@ -37,3 +37,12 @@ ping:
|
||||||
title: Ping Jira
|
title: Ping Jira
|
||||||
desc: Проверить подключение к Jira
|
desc: Проверить подключение к Jira
|
||||||
button: Ping Jira
|
button: Ping Jira
|
||||||
|
|
||||||
|
name:
|
||||||
|
title: Название подключения
|
||||||
|
desc: Понятное имя для этого подключения
|
||||||
|
def: Мое подключение к Jira
|
||||||
|
|
||||||
|
select_connection: Выбрать подключение
|
||||||
|
add_connection: Добавить подключение
|
||||||
|
no_connection: Нет подключения
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,22 @@ title: Настройка получаемых полей
|
||||||
note:
|
note:
|
||||||
desc0: Важно!
|
desc0: Важно!
|
||||||
desc1: " Следующие поля 'fields' и 'expand' будут использоваться в большинстве запросах Jira Manager. Убедитесь, что вы знаете, что делаете."
|
desc1: " Следующие поля 'fields' и 'expand' будут использоваться в большинстве запросах Jira Manager. Убедитесь, что вы знаете, что делаете."
|
||||||
desc2: "Для дополнительной информации можете посмотреть официальную "
|
desc2: 'Для дополнительной информации можете посмотреть официальную '
|
||||||
link_label: "документацию"
|
link_label: 'документацию'
|
||||||
desc3: ". Заполняя поля, перечислите параметры через разделитель ','. Пример: \"summary, description\""
|
desc3: '. Заполняя поля, перечислите параметры через разделитель '',''. Пример: "summary, description"'
|
||||||
key:
|
key:
|
||||||
name: Просмотр необработанных данных задачи
|
name: Просмотр необработанных данных задачи
|
||||||
desc: Введите номер задачи Jira, чтобы просмотреть необработанные данные, полученные из API
|
desc: Введите номер задачи Jira, чтобы просмотреть необработанные данные, полученные из API
|
||||||
fields:
|
fields:
|
||||||
name: Поля задачи ('fields')
|
name: Поля задачи ('fields')
|
||||||
desc: "Главные поля Jira"
|
desc: 'Главные поля Jira'
|
||||||
def: "*all, -comment"
|
def: '*all, -comment'
|
||||||
expand:
|
expand:
|
||||||
name: Расширенные поля ('expand')
|
name: Расширенные поля ('expand')
|
||||||
desc: "Дополнительные поля Jira"
|
desc: 'Дополнительные поля Jira'
|
||||||
def: "renderedFields, changelog"
|
def: 'renderedFields, changelog'
|
||||||
filenameTemplate:
|
filenameTemplate:
|
||||||
name: Шаблон названия файла
|
name: Шаблон названия файла
|
||||||
desc: |
|
desc: |
|
||||||
Шаблон для названий файлов созданных задач. Используйте {summary} для названия задачи и {key} для ключа задачи
|
Шаблон для названий файлов созданных задач. Используйте {summary} для названия задачи и {key} для ключа задачи
|
||||||
placeholder: "{summary} ({key})"
|
placeholder: '{summary} ({key})'
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ desc: Настройте, как поля будут сопоставлятьс
|
||||||
поля требуется функция преобразования 'В Jira' и 'Из Jira'.
|
поля требуется функция преобразования 'В Jira' и 'Из Jira'.
|
||||||
fv:
|
fv:
|
||||||
name: Включить проверку полей
|
name: Включить проверку полей
|
||||||
desc: Проверять поля перед сохранением базируясь на стандартном ответе API
|
desc: Проверять поля перед сохранением базируясь на стандартном ответе API
|
||||||
(при проверке не учитываются пользовательские поля)
|
(при проверке не учитываются пользовательские поля)
|
||||||
secNote:
|
secNote:
|
||||||
name: "⚠️ Примечание: "
|
name: '⚠️ Примечание: '
|
||||||
desc: Сопоставление полей использует JavaScript lambda функции.
|
desc: Сопоставление полей использует JavaScript lambda функции.
|
||||||
example:
|
example:
|
||||||
name: Примеры сопоставления полей
|
name: Примеры сопоставления полей
|
||||||
|
|
@ -14,11 +14,16 @@ example:
|
||||||
from_jira: Из Jira
|
from_jira: Из Jira
|
||||||
to_jira: В Jira
|
to_jira: В Jira
|
||||||
field_name: Название поля
|
field_name: Название поля
|
||||||
|
field_name_placeholder: Введите название поля
|
||||||
|
|
||||||
|
add_mapping: Добавить сопоставление
|
||||||
add_mapping_tooltip: Добавить новое сопоставление полей
|
add_mapping_tooltip: Добавить новое сопоставление полей
|
||||||
|
reset: Убрать все
|
||||||
reset_tooltip: Убрать все
|
reset_tooltip: Убрать все
|
||||||
reset_confirm_title: Сбросить сопоставление полей
|
reset_confirm_title: Сбросить сопоставление полей
|
||||||
reset_confirm_text: Сопоставление полей будет очищено. Вы уверены?
|
reset_confirm_text: Сопоставление полей будет очищено. Вы уверены?
|
||||||
|
remove_field: Удалить поле
|
||||||
|
reload_defaults: Загрузить по умолчанию
|
||||||
reload_defaults_tooltip: Обновить
|
reload_defaults_tooltip: Обновить
|
||||||
reload_confirm_title: Обновить сопоставления полей по умолчанию
|
reload_confirm_title: Обновить сопоставления полей по умолчанию
|
||||||
reload_confirm_text: Текущие сопоставления полей будут перезаписаны сопоставлениями по умолчанию. Вы уверены?
|
reload_confirm_text: Текущие сопоставления полей будут перезаписаны сопоставлениями по умолчанию. Вы уверены?
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
title: Статистика ведения журнала работы через Timekeep
|
title: Статистика ведения журнала работы через Timekeep
|
||||||
description: Управление и отправка журнала работы в Jira
|
description: 'Управление и отправка журнала работы в Jira (поддерживает формат Timekeep и Super Simple Time Tracker)'
|
||||||
settings:
|
settings:
|
||||||
send_comments:
|
send_comments:
|
||||||
name: Отправлять комментарии
|
name: Отправлять комментарии
|
||||||
|
|
@ -38,6 +38,7 @@ display:
|
||||||
no_entries: Записи рабочего лога не найдены.
|
no_entries: Записи рабочего лога не найдены.
|
||||||
period_of: Период от {date}
|
period_of: Период от {date}
|
||||||
table:
|
table:
|
||||||
|
total: Всего
|
||||||
task: Задача
|
task: Задача
|
||||||
block_path: Подзадача
|
block_path: Подзадача
|
||||||
start_time: Время начала
|
start_time: Время начала
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,7 @@ invalid_exp: Некорректное выражение
|
||||||
from_jira: Из Jira
|
from_jira: Из Jira
|
||||||
value: Значение
|
value: Значение
|
||||||
|
|
||||||
|
add_mapping: Добавить сопоставление
|
||||||
add_mapping_tooltip: Добавить тестовое сопоставление полей
|
add_mapping_tooltip: Добавить тестовое сопоставление полей
|
||||||
|
reset: Сбросить
|
||||||
reset_tooltip: Сбросить
|
reset_tooltip: Сбросить
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ function replacePlaceholders(template: string, dict: Record<string, unknown>): s
|
||||||
return template.replace(/{([^{}]+)}/g, (match, key) => {
|
return template.replace(/{([^{}]+)}/g, (match, key) => {
|
||||||
// Trim the key in case there's whitespace inside the braces
|
// Trim the key in case there's whitespace inside the braces
|
||||||
const trimmedKey = key.trim();
|
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 {
|
return {
|
||||||
t: translate,
|
t: translate,
|
||||||
locale: currentLocale,
|
locale: currentLocale,
|
||||||
setPrefix: (newPrefix: string) => useTranslations(newPrefix)
|
setPrefix: (newPrefix: string) => useTranslations(newPrefix),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,242 +3,263 @@ const path = require('path');
|
||||||
const chokidar = require('chokidar');
|
const chokidar = require('chokidar');
|
||||||
|
|
||||||
const COMPILED_DIR = path.join(__dirname, 'compiled');
|
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()) {
|
function getAllKeys(obj, prefix = '', result = new Set()) {
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||||
|
if (typeof obj[key] === 'string') {
|
||||||
result.add(fullKey);
|
result.add(fullKey);
|
||||||
|
} else if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
||||||
if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
getAllKeys(obj[key], fullKey, result);
|
||||||
getAllKeys(obj[key], fullKey, result);
|
}
|
||||||
}
|
}
|
||||||
}
|
return result;
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if a key exists in an object
|
|
||||||
function keyExists(obj, keyPath) {
|
function keyExists(obj, keyPath) {
|
||||||
const parts = keyPath.split('.');
|
const parts = keyPath.split('.');
|
||||||
let current = obj;
|
let current = obj;
|
||||||
|
for (const part of parts) {
|
||||||
for (const part of parts) {
|
if (current[part] === undefined) return false;
|
||||||
if (current[part] === undefined) {
|
current = current[part];
|
||||||
return false;
|
}
|
||||||
}
|
return true;
|
||||||
current = current[part];
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get value from nested object using dot notation
|
|
||||||
function getValue(obj, keyPath) {
|
function getValue(obj, keyPath) {
|
||||||
const parts = keyPath.split('.');
|
const parts = keyPath.split('.');
|
||||||
let current = obj;
|
let current = obj;
|
||||||
|
for (const part of parts) {
|
||||||
for (const part of parts) {
|
if (current[part] === undefined) return undefined;
|
||||||
if (current[part] === undefined) {
|
current = current[part];
|
||||||
return undefined;
|
}
|
||||||
}
|
return current;
|
||||||
current = current[part];
|
}
|
||||||
}
|
|
||||||
|
function findTranslationsInTsFiles() {
|
||||||
return current;
|
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 to validate translations
|
|
||||||
function validateTranslations() {
|
function validateTranslations() {
|
||||||
console.log('🔍 Validating translation files...');
|
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) {
|
if (files.length === 0) {
|
||||||
console.log('⚠️ No compiled translation files found');
|
console.log('⚠️ No compiled translation files found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load all translation data
|
console.log('\n📊 Scanning .ts files for used translation keys...');
|
||||||
const translations = {};
|
const usedKeys = findTranslationsInTsFiles();
|
||||||
for (const file of files) {
|
console.log(`📝 Found ${usedKeys.size} translation keys used in code`);
|
||||||
const locale = path.basename(file, '.json');
|
|
||||||
const filePath = path.join(COMPILED_DIR, file);
|
|
||||||
translations[locale] = fs.readJsonSync(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all locales
|
const translations = {};
|
||||||
const locales = Object.keys(translations);
|
for (const file of files) {
|
||||||
console.log(`📁 Found ${locales.length} locales: ${locales.join(', ')}`);
|
const locale = path.basename(file, '.json');
|
||||||
|
const filePath = path.join(COMPILED_DIR, file);
|
||||||
|
translations[locale] = fs.readJsonSync(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
if (locales.length < 2) {
|
const locales = Object.keys(translations);
|
||||||
console.log('⚠️ Need at least 2 locales to compare');
|
console.log(`📁 Found ${locales.length} locales: ${locales.join(', ')}`);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Choose the first locale as reference
|
if (locales.length < 2) {
|
||||||
const referenceLocale = locales[0];
|
console.log('⚠️ Need at least 2 locales to compare');
|
||||||
console.log(`🔑 Using ${referenceLocale} as reference locale`);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get all keys from reference locale
|
const referenceLocale = locales[0];
|
||||||
const referenceKeys = getAllKeys(translations[referenceLocale]);
|
console.log(`🔑 Using ${referenceLocale} as reference locale`);
|
||||||
console.log(`🔢 Reference locale has ${referenceKeys.size} unique keys`);
|
|
||||||
|
|
||||||
// Track statistics
|
const referenceKeys = getAllKeys(translations[referenceLocale]);
|
||||||
const stats = {
|
console.log(`🔢 Reference locale has ${referenceKeys.size} unique keys`);
|
||||||
missingKeys: {},
|
|
||||||
extraKeys: {},
|
|
||||||
typeErrors: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize stats for each locale
|
const stats = {
|
||||||
for (const locale of locales) {
|
missingKeys: {},
|
||||||
if (locale !== referenceLocale) {
|
extraKeys: {},
|
||||||
stats.missingKeys[locale] = [];
|
usedButNotInLocale: {},
|
||||||
stats.extraKeys[locale] = [];
|
inLocaleButNotUsed: {},
|
||||||
stats.typeErrors[locale] = [];
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check each locale against the reference
|
for (const locale of locales) {
|
||||||
for (const locale of locales) {
|
if (locale !== referenceLocale) {
|
||||||
if (locale === referenceLocale) continue;
|
stats.missingKeys[locale] = [];
|
||||||
|
stats.extraKeys[locale] = [];
|
||||||
|
stats.usedButNotInLocale[locale] = [];
|
||||||
|
stats.inLocaleButNotUsed[locale] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stats.inLocaleButNotUsed[referenceLocale] = [];
|
||||||
|
stats.usedButNotInLocale[referenceLocale] = [];
|
||||||
|
|
||||||
const localeKeys = getAllKeys(translations[locale]);
|
for (const locale of locales) {
|
||||||
|
const localeKeys = getAllKeys(translations[locale]);
|
||||||
|
|
||||||
// Check for missing keys
|
for (const refKey of referenceKeys) {
|
||||||
for (const key of referenceKeys) {
|
if (!keyExists(translations[locale], refKey)) {
|
||||||
if (!keyExists(translations[locale], key)) {
|
stats.missingKeys[locale].push(refKey);
|
||||||
stats.missingKeys[locale].push(key);
|
}
|
||||||
} else {
|
}
|
||||||
// Check for type mismatches
|
|
||||||
const refValue = getValue(translations[referenceLocale], key);
|
|
||||||
const localeValue = getValue(translations[locale], key);
|
|
||||||
|
|
||||||
if (typeof refValue !== typeof localeValue) {
|
for (const locKey of localeKeys) {
|
||||||
stats.typeErrors[locale].push({
|
if (!keyExists(translations[referenceLocale], locKey)) {
|
||||||
key,
|
stats.extraKeys[locale].push(locKey);
|
||||||
refType: typeof refValue,
|
}
|
||||||
localeType: typeof localeValue
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for extra keys
|
for (const usedKey of usedKeys) {
|
||||||
for (const key of localeKeys) {
|
if (!keyExists(translations[locale], usedKey)) {
|
||||||
if (!keyExists(translations[referenceLocale], key)) {
|
stats.usedButNotInLocale[locale].push(usedKey);
|
||||||
stats.extraKeys[locale].push(key);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print results
|
for (const locKey of localeKeys) {
|
||||||
let hasIssues = false;
|
if (!usedKeys.has(locKey)) {
|
||||||
|
stats.inLocaleButNotUsed[locale].push(locKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Print missing keys
|
let hasIssues = false;
|
||||||
for (const locale in stats.missingKeys) {
|
|
||||||
const missing = stats.missingKeys[locale];
|
|
||||||
if (missing.length > 0) {
|
|
||||||
hasIssues = true;
|
|
||||||
console.log(`❌ ${locale} is missing ${missing.length} keys:`);
|
|
||||||
missing.forEach(key => {
|
|
||||||
console.log(` - ${key}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print extra keys
|
for (const locale in stats.missingKeys) {
|
||||||
for (const locale in stats.extraKeys) {
|
const missing = stats.missingKeys[locale];
|
||||||
const extra = stats.extraKeys[locale];
|
if (missing.length > 0) {
|
||||||
if (extra.length > 0) {
|
hasIssues = true;
|
||||||
hasIssues = true;
|
console.log(`\n❌ ${locale} is missing ${missing.length} keys from reference:`);
|
||||||
console.log(`⚠️ ${locale} has ${extra.length} extra keys:`);
|
missing.forEach((key) => console.log(` - ${key}`));
|
||||||
extra.forEach(key => {
|
}
|
||||||
console.log(` - ${key}`);
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print type errors
|
for (const locale in stats.extraKeys) {
|
||||||
for (const locale in stats.typeErrors) {
|
const extra = stats.extraKeys[locale];
|
||||||
const typeErrors = stats.typeErrors[locale];
|
if (extra.length > 0) {
|
||||||
if (typeErrors.length > 0) {
|
hasIssues = true;
|
||||||
hasIssues = true;
|
console.log(`\n⚠️ ${locale} has ${extra.length} extra keys not in reference:`);
|
||||||
console.log(`⚠️ ${locale} has ${typeErrors.length} type mismatches:`);
|
extra.forEach((key) => console.log(` - ${key}`));
|
||||||
typeErrors.forEach(err => {
|
}
|
||||||
console.log(` - ${err.key}: expected ${err.refType}, got ${err.localeType}`);
|
}
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print empty values check if needed
|
for (const locale in stats.usedButNotInLocale) {
|
||||||
console.log('\n📊 Checking for empty values...');
|
const missing = stats.usedButNotInLocale[locale];
|
||||||
for (const locale of locales) {
|
if (missing.length > 0) {
|
||||||
checkEmptyValues(translations[locale], locale);
|
hasIssues = true;
|
||||||
}
|
console.log(`\n🚨 ${locale}: ${missing.length} keys are used in code but NOT in localization:`);
|
||||||
|
missing.forEach((key) => console.log(` - ${key}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasIssues) {
|
for (const locale in stats.inLocaleButNotUsed) {
|
||||||
console.log('✅ All locales have consistent structure!');
|
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}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return hasIssues;
|
console.log('\n📊 Checking for empty values...');
|
||||||
|
for (const locale of locales) {
|
||||||
|
checkEmptyValues(translations[locale], locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasIssues) {
|
||||||
|
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 = '') {
|
function checkEmptyValues(obj, locale, prefix = '') {
|
||||||
for (const key in obj) {
|
for (const key in obj) {
|
||||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||||
const value = obj[key];
|
const value = obj[key];
|
||||||
|
|
||||||
if (value === '') {
|
if (value === '') {
|
||||||
console.log(`⚠️ ${locale} has empty string at ${fullKey}`);
|
console.log(`⚠️ ${locale} has empty string at ${fullKey}`);
|
||||||
} else if (value === null) {
|
} else if (value === null) {
|
||||||
console.log(`⚠️ ${locale} has null value at ${fullKey}`);
|
console.log(`⚠️ ${locale} has null value at ${fullKey}`);
|
||||||
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
||||||
checkEmptyValues(value, locale, fullKey);
|
checkEmptyValues(value, locale, fullKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main function
|
|
||||||
function main() {
|
function main() {
|
||||||
// Create compiled directory if it doesn't exist
|
fs.ensureDirSync(COMPILED_DIR);
|
||||||
fs.ensureDirSync(COMPILED_DIR);
|
validateTranslations();
|
||||||
|
|
||||||
// Run validation
|
|
||||||
validateTranslations();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch mode
|
|
||||||
if (process.argv.includes('--watch')) {
|
if (process.argv.includes('--watch')) {
|
||||||
console.log('👀 Watching for changes...');
|
console.log('👀 Watching for changes...');
|
||||||
|
let debounceTimer;
|
||||||
|
const debounceDelay = 100;
|
||||||
|
|
||||||
let debounceTimer;
|
clearTimeout(debounceTimer);
|
||||||
const debounceDelay = 100;
|
debounceTimer = setTimeout(() => main(), debounceDelay);
|
||||||
|
|
||||||
// Initial validation
|
chokidar
|
||||||
clearTimeout(debounceTimer);
|
.watch([COMPILED_DIR, SRC_DIR], {
|
||||||
debounceTimer = setTimeout(() => {
|
ignoreInitial: true,
|
||||||
main();
|
ignored: /.*~$/,
|
||||||
}, debounceDelay);
|
})
|
||||||
|
.on('all', (event, path) => {
|
||||||
// Watch for changes in the compiled directory
|
if (event === 'change' || event === 'add' || event === 'unlink') {
|
||||||
chokidar.watch(COMPILED_DIR, {
|
clearTimeout(debounceTimer);
|
||||||
ignoreInitial: true,
|
debounceTimer = setTimeout(() => {
|
||||||
ignored: /.*~$/, // Игнорировать скрытые файлы
|
console.log(`\n🔁 Detected changes in ${path} (${event}), revalidating...`);
|
||||||
}).on('all', (event, path) => {
|
main();
|
||||||
if (event === 'change' || event === 'add' || event === 'unlink') {
|
}, debounceDelay);
|
||||||
clearTimeout(debounceTimer);
|
}
|
||||||
debounceTimer = setTimeout(() => {
|
});
|
||||||
console.log(`🔁 Detected changes in ${path} (${event}), revalidating...`);
|
|
||||||
main();
|
|
||||||
}, debounceDelay);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Run once
|
main();
|
||||||
main();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
src/main.ts
51
src/main.ts
|
|
@ -1,20 +1,24 @@
|
||||||
import {Plugin } from "obsidian";
|
import { Plugin } from 'obsidian';
|
||||||
import { JiraSettingTab } from "./settings/JiraSettingTab";
|
import { JiraSettingTab } from './settings/JiraSettingTab';
|
||||||
import {DEFAULT_SETTINGS, JiraSettingsInterface} from "./settings/default";
|
import { ConnectionSettingsInterface, DEFAULT_SETTINGS, JiraSettingsInterface } from './settings/default';
|
||||||
import {
|
import {
|
||||||
registerUpdateIssueCommand, registerUpdateWorkLogManuallyCommand,
|
registerUpdateIssueCommand,
|
||||||
registerGetCurrentIssueCommand, registerUpdateWorkLogBatchCommand,
|
registerUpdateWorkLogManuallyCommand,
|
||||||
registerCreateIssueCommand, registerGetIssueCommandWithCustomKey, registerUpdateIssueStatusCommand,
|
registerGetCurrentIssueCommand,
|
||||||
registerBatchFetchIssuesCommand
|
registerUpdateWorkLogBatchCommand,
|
||||||
} from "./commands";
|
registerCreateIssueCommand,
|
||||||
import {transform_string_to_functions_mappings} from "./tools/convertFunctionString";
|
registerGetIssueCommandWithCustomKey,
|
||||||
import {createJiraSyncExtension} from "./postprocessing/livePreview";
|
registerUpdateIssueStatusCommand,
|
||||||
import {hideJiraPointersReading} from "./postprocessing/reading";
|
registerBatchFetchIssuesCommand,
|
||||||
import { buildCacheFromFilesystem, validateCache } from "./tools/cacheUtils";
|
} from './commands';
|
||||||
import {checkMigrateSettings} from "./tools/migrateSettings";
|
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 {
|
export default class JiraPlugin extends Plugin {
|
||||||
settings: JiraSettingsInterface;
|
settings!: JiraSettingsInterface;
|
||||||
|
|
||||||
// In-memory cache for instant access during synchronization
|
// In-memory cache for instant access during synchronization
|
||||||
private issueKeyToFilePathCache: Map<string, string> = new Map();
|
private issueKeyToFilePathCache: Map<string, string> = new Map();
|
||||||
|
|
@ -48,7 +52,6 @@ export default class JiraPlugin extends Plugin {
|
||||||
|
|
||||||
// Register vault event listeners for cache maintenance
|
// Register vault event listeners for cache maintenance
|
||||||
this.registerVaultEventListeners();
|
this.registerVaultEventListeners();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings() {
|
async loadSettings() {
|
||||||
|
|
@ -57,7 +60,9 @@ export default class JiraPlugin extends Plugin {
|
||||||
const new_data = checkMigrateSettings(old_data, this.saveSettings);
|
const new_data = checkMigrateSettings(old_data, this.saveSettings);
|
||||||
|
|
||||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, new_data);
|
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() {
|
async saveSettings() {
|
||||||
|
|
@ -72,13 +77,20 @@ export default class JiraPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllIssueKeysMap(): Map<string, string> {
|
getAllIssueKeysMap(): Map<string, string> {
|
||||||
return this.issueKeyToFilePathCache
|
return this.issueKeyToFilePathCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
getFilePathForIssueKey(issueKey: string): string | undefined {
|
getFilePathForIssueKey(issueKey: string): string | undefined {
|
||||||
return this.issueKeyToFilePathCache.get(issueKey);
|
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) {
|
setFilePathForIssueKey(issueKey: string, filePath: string) {
|
||||||
this.issueKeyToFilePathCache.set(issueKey, filePath);
|
this.issueKeyToFilePathCache.set(issueKey, filePath);
|
||||||
this.settings.issueKeyToFilePathCache[issueKey] = filePath;
|
this.settings.issueKeyToFilePathCache[issueKey] = filePath;
|
||||||
|
|
@ -103,7 +115,6 @@ export default class JiraPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerVaultEventListeners() {
|
private registerVaultEventListeners() {
|
||||||
|
|
||||||
// Handle file renames
|
// Handle file renames
|
||||||
this.registerEvent(
|
this.registerEvent(
|
||||||
this.app.vault.on('rename', (file, oldPath) => {
|
this.app.vault.on('rename', (file, oldPath) => {
|
||||||
|
|
@ -113,7 +124,7 @@ export default class JiraPlugin extends Plugin {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle file deletions
|
// Handle file deletions
|
||||||
|
|
@ -125,7 +136,7 @@ export default class JiraPlugin extends Plugin {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import {App, Modal, Setting} from "obsidian";
|
import { App, Modal, Setting } from 'obsidian';
|
||||||
import {useTranslations} from "../localization/translator";
|
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
|
* Modal for searching issues by key
|
||||||
*/
|
*/
|
||||||
export class IssueAddSummaryModal extends Modal {
|
export class IssueAddSummaryModal extends Modal {
|
||||||
private onSubmit: (result: string) => void;
|
private onSubmit: (result: string) => void;
|
||||||
private summary: string = "";
|
private summary: string = '';
|
||||||
|
|
||||||
constructor(app: App, onSubmit: (result: string) => void) {
|
constructor(app: App, onSubmit: (result: string) => void) {
|
||||||
super(app);
|
super(app);
|
||||||
|
|
@ -15,33 +15,30 @@ export class IssueAddSummaryModal extends Modal {
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.contentEl.createEl("h2", {text: t("desc")});
|
this.contentEl.createEl('h2', { text: t('desc') });
|
||||||
|
|
||||||
new Setting(this.contentEl)
|
new Setting(this.contentEl)
|
||||||
.setName(t("key.name"))
|
.setName(t('key.name'))
|
||||||
.setDesc(t("key.desc"))
|
.setDesc(t('key.desc'))
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
text
|
text.setPlaceholder(t('key.placeholder')).onChange((value) => {
|
||||||
.setPlaceholder(t("key.placeholder"))
|
this.summary = value;
|
||||||
.onChange((value) => {
|
}),
|
||||||
this.summary = value;
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
new Setting(this.contentEl)
|
new Setting(this.contentEl).addButton((btn) =>
|
||||||
.addButton((btn) =>
|
btn
|
||||||
btn
|
.setButtonText(t('submit'))
|
||||||
.setButtonText(t("submit"))
|
.setCta()
|
||||||
.setCta()
|
.onClick(() => {
|
||||||
.onClick(() => {
|
this.close();
|
||||||
this.close();
|
this.onSubmit(this.summary);
|
||||||
this.onSubmit(this.summary);
|
}),
|
||||||
})
|
);
|
||||||
);
|
|
||||||
|
|
||||||
// Handle Enter key
|
// Handle Enter key
|
||||||
this.contentEl.addEventListener("keydown", (event) => {
|
this.contentEl.addEventListener('keydown', (event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.close();
|
this.close();
|
||||||
this.onSubmit(this.summary);
|
this.onSubmit(this.summary);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import {App, Modal, Setting} from "obsidian";
|
import { App, Modal, Setting } from 'obsidian';
|
||||||
import {useTranslations} from "../localization/translator";
|
import { useTranslations } from '../localization/translator';
|
||||||
import {JQLPreview} from "../settings/components/JQLPreview";
|
import { JQLPreview } from '../settings/components/JQLPreview';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
|
|
||||||
const t = useTranslations("modals.search").t;
|
const t = useTranslations('modals.search').t;
|
||||||
/**
|
/**
|
||||||
* Modal for searching issues by key
|
* Modal for searching issues by key
|
||||||
*/
|
*/
|
||||||
export class IssueSearchModal extends Modal {
|
export class IssueSearchModal extends Modal {
|
||||||
private onSubmit: (result: string) => void;
|
private onSubmit: (result: string) => void;
|
||||||
private plugin: JiraPlugin;
|
private plugin: JiraPlugin;
|
||||||
private issueKey: string = "";
|
private issueKey: string = '';
|
||||||
private previewEl?: HTMLElement;
|
private previewEl?: HTMLElement;
|
||||||
private preview?: JQLPreview;
|
private preview?: JQLPreview;
|
||||||
private debounceTimer?: number;
|
private debounceTimer?: number;
|
||||||
|
|
@ -22,38 +22,35 @@ export class IssueSearchModal extends Modal {
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.contentEl.createEl("h2", {text: t("desc")});
|
this.contentEl.createEl('h2', { text: t('desc') });
|
||||||
|
|
||||||
new Setting(this.contentEl)
|
new Setting(this.contentEl)
|
||||||
.setName(t("key.name"))
|
.setName(t('key.name'))
|
||||||
.setDesc(t("key.desc"))
|
.setDesc(t('key.desc'))
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
text
|
text.setPlaceholder(t('key.placeholder')).onChange((value) => {
|
||||||
.setPlaceholder(t("key.placeholder"))
|
this.issueKey = value;
|
||||||
.onChange((value) => {
|
this.schedulePreview();
|
||||||
this.issueKey = value;
|
}),
|
||||||
this.schedulePreview();
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.previewEl = this.contentEl.createDiv();
|
this.previewEl = this.contentEl.createDiv();
|
||||||
this.preview = new JQLPreview(this.plugin, this.previewEl, 1, false);
|
this.preview = new JQLPreview(this.plugin, this.previewEl, 1, false);
|
||||||
this.preview.loadPreview("")
|
this.preview.loadPreview('');
|
||||||
|
|
||||||
new Setting(this.contentEl)
|
new Setting(this.contentEl).addButton((btn) =>
|
||||||
.addButton((btn) =>
|
btn
|
||||||
btn
|
.setButtonText(t('submit'))
|
||||||
.setButtonText(t("submit"))
|
.setCta()
|
||||||
.setCta()
|
.onClick(() => {
|
||||||
.onClick(() => {
|
this.close();
|
||||||
this.close();
|
this.onSubmit(this.issueKey);
|
||||||
this.onSubmit(this.issueKey);
|
}),
|
||||||
})
|
);
|
||||||
);
|
|
||||||
|
|
||||||
// Handle Enter key
|
// Handle Enter key
|
||||||
this.contentEl.addEventListener("keydown", (event) => {
|
this.contentEl.addEventListener('keydown', (event) => {
|
||||||
if (event.key === "Enter") {
|
if (event.key === 'Enter') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.close();
|
this.close();
|
||||||
this.onSubmit(this.issueKey);
|
this.onSubmit(this.issueKey);
|
||||||
|
|
@ -73,7 +70,7 @@ export class IssueSearchModal extends Modal {
|
||||||
|
|
||||||
this.debounceTimer = window.setTimeout(() => {
|
this.debounceTimer = window.setTimeout(() => {
|
||||||
if (this.preview) {
|
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
|
// Error handling is done within the preview component
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import {App, SuggestModal} from "obsidian";
|
import { App, SuggestModal } from 'obsidian';
|
||||||
import {JiraTransitionType} from "../interfaces";
|
import { JiraTransitionType } from '../interfaces';
|
||||||
import {useTranslations} from "../localization/translator";
|
import { useTranslations } from '../localization/translator';
|
||||||
|
|
||||||
const t = useTranslations("modals.status").t;
|
const t = useTranslations('modals.status').t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal for selecting an issue status
|
* Modal for selecting an issue status
|
||||||
|
|
@ -15,18 +15,19 @@ export class IssueStatusModal extends SuggestModal<JiraTransitionType> {
|
||||||
super(app);
|
super(app);
|
||||||
this.onSubmit = onSubmit;
|
this.onSubmit = onSubmit;
|
||||||
this.issueTransitions = issueTransitions;
|
this.issueTransitions = issueTransitions;
|
||||||
this.setPlaceholder(t("placeholder"));
|
this.setPlaceholder(t('placeholder'));
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuggestions(query: string): JiraTransitionType[] {
|
getSuggestions(query: string): JiraTransitionType[] {
|
||||||
return this.issueTransitions.filter((transition) =>
|
return this.issueTransitions.filter(
|
||||||
transition.status && transition.status.toLowerCase().includes(query.toLowerCase()) ||
|
(transition) =>
|
||||||
transition.action && transition.action.toLowerCase().includes(query.toLowerCase())
|
(transition.status && transition.status.toLowerCase().includes(query.toLowerCase())) ||
|
||||||
|
(transition.action && transition.action.toLowerCase().includes(query.toLowerCase())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSuggestion(type: JiraTransitionType, el: HTMLElement) {
|
renderSuggestion(type: JiraTransitionType, el: HTMLElement) {
|
||||||
let result = "";
|
let result = '';
|
||||||
if (type.action && type.status) {
|
if (type.action && type.status) {
|
||||||
if (type.action === type.status) {
|
if (type.action === type.status) {
|
||||||
result = type.action;
|
result = type.action;
|
||||||
|
|
@ -38,7 +39,7 @@ export class IssueStatusModal extends SuggestModal<JiraTransitionType> {
|
||||||
} else if (type.status) {
|
} else if (type.status) {
|
||||||
result = type.status;
|
result = type.status;
|
||||||
}
|
}
|
||||||
el.createEl("div", {text: result});
|
el.createEl('div', { text: result });
|
||||||
}
|
}
|
||||||
|
|
||||||
onChooseSuggestion(type: JiraTransitionType) {
|
onChooseSuggestion(type: JiraTransitionType) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import {App, SuggestModal} from "obsidian";
|
import { App, SuggestModal } from 'obsidian';
|
||||||
import {JiraIssueType} from "../interfaces";
|
import { JiraIssueType } from '../interfaces';
|
||||||
import {useTranslations} from "../localization/translator";
|
import { useTranslations } from '../localization/translator';
|
||||||
|
|
||||||
const t = useTranslations("modals.type").t;
|
const t = useTranslations('modals.type').t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal for selecting an issue type
|
* Modal for selecting an issue type
|
||||||
|
|
@ -15,17 +15,15 @@ export class IssueTypeModal extends SuggestModal<JiraIssueType> {
|
||||||
super(app);
|
super(app);
|
||||||
this.onSubmit = onSubmit;
|
this.onSubmit = onSubmit;
|
||||||
this.issueTypes = issueTypes;
|
this.issueTypes = issueTypes;
|
||||||
this.setPlaceholder(t("placeholder"));
|
this.setPlaceholder(t('placeholder'));
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuggestions(query: string): JiraIssueType[] {
|
getSuggestions(query: string): JiraIssueType[] {
|
||||||
return this.issueTypes.filter((type) =>
|
return this.issueTypes.filter((type) => type.name.toLowerCase().includes(query.toLowerCase()));
|
||||||
type.name.toLowerCase().includes(query.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSuggestion(type: JiraIssueType, el: HTMLElement) {
|
renderSuggestion(type: JiraIssueType, el: HTMLElement) {
|
||||||
el.createEl("div", {text: type.name});
|
el.createEl('div', { text: type.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
onChooseSuggestion(type: JiraIssueType) {
|
onChooseSuggestion(type: JiraIssueType) {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import {App, Modal, Notice, Setting} from "obsidian";
|
import { App, Modal, Notice, Setting } from 'obsidian';
|
||||||
import {useTranslations} from "../localization/translator";
|
import { useTranslations } from '../localization/translator';
|
||||||
|
|
||||||
const t = useTranslations("modals.worklog").t;
|
const t = useTranslations('modals.worklog').t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal for adding work log entries
|
* Modal for adding work log entries
|
||||||
*/
|
*/
|
||||||
export class IssueWorkLogModal extends Modal {
|
export class IssueWorkLogModal extends Modal {
|
||||||
private onSubmit: (timeSpent: string, startDate: string, workDescription: string) => void;
|
private onSubmit: (timeSpent: string, startDate: string, workDescription: string) => void;
|
||||||
private timeSpent: string = "";
|
private timeSpent: string = '';
|
||||||
private startDate: string = "";
|
private startDate: string = '';
|
||||||
private workDescription: string = "";
|
private workDescription: string = '';
|
||||||
private displayDate: string = "";
|
private displayDate: string = '';
|
||||||
private timeSpentSetting: Setting;
|
private timeSpentSetting!: Setting;
|
||||||
private dateSetting: Setting;
|
private dateSetting!: Setting;
|
||||||
|
|
||||||
constructor(app: App, onSubmit: (timeSpent: string, startDate: string, workDescription: string) => void) {
|
constructor(app: App, onSubmit: (timeSpent: string, startDate: string, workDescription: string) => void) {
|
||||||
super(app);
|
super(app);
|
||||||
|
|
@ -26,7 +26,7 @@ export class IssueWorkLogModal extends Modal {
|
||||||
|
|
||||||
private initializeDates(date: Date) {
|
private initializeDates(date: Date) {
|
||||||
// Set the ISO format for API
|
// 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)
|
// Set the display format (01/Jun/21 09:00 AM)
|
||||||
const months = t('months') as any;
|
const months = t('months') as any;
|
||||||
|
|
@ -49,7 +49,7 @@ export class IssueWorkLogModal extends Modal {
|
||||||
const [hours, minutes] = timePart.split(':');
|
const [hours, minutes] = timePart.split(':');
|
||||||
|
|
||||||
const months = Object.fromEntries(
|
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>;
|
) as Record<string, number>;
|
||||||
|
|
||||||
// {'Jan': 0, 'Feb': 1, 'Mar': 2, 'Apr': 3, 'May': 4, 'Jun': 5,
|
// {'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;
|
const fullYear = parseInt(year) + 2000;
|
||||||
|
|
||||||
// Create date object
|
// Create date object
|
||||||
const date = new Date(fullYear, months[month as keyof typeof months], parseInt(day), hour, parseInt(minutes));
|
const date = new Date(
|
||||||
if (isNaN(date.getTime())) throw new Error("Invalid date");
|
fullYear,
|
||||||
|
months[month as keyof typeof months],
|
||||||
|
parseInt(day),
|
||||||
|
hour,
|
||||||
|
parseInt(minutes),
|
||||||
|
);
|
||||||
|
if (isNaN(date.getTime())) throw new Error('Invalid date');
|
||||||
|
|
||||||
return date;
|
return date;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +89,7 @@ export class IssueWorkLogModal extends Modal {
|
||||||
.setValue(this.timeSpent)
|
.setValue(this.timeSpent)
|
||||||
.onChange((value) => {
|
.onChange((value) => {
|
||||||
this.timeSpent = value;
|
this.timeSpent = value;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.dateSetting = new Setting(this.contentEl)
|
this.dateSetting = new Setting(this.contentEl)
|
||||||
|
|
@ -95,7 +101,7 @@ export class IssueWorkLogModal extends Modal {
|
||||||
.setValue(this.displayDate)
|
.setValue(this.displayDate)
|
||||||
.onChange((value) => {
|
.onChange((value) => {
|
||||||
this.displayDate = value;
|
this.displayDate = value;
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start Date field with date picker icon
|
// Start Date field with date picker icon
|
||||||
|
|
@ -164,37 +170,34 @@ export class IssueWorkLogModal extends Modal {
|
||||||
|
|
||||||
// Work Description field
|
// Work Description field
|
||||||
new Setting(this.contentEl)
|
new Setting(this.contentEl)
|
||||||
.setName(t("comment.name"))
|
.setName(t('comment.name'))
|
||||||
.setDesc(t("comment.name"))
|
.setDesc(t('comment.name'))
|
||||||
.addTextArea((text) =>
|
.addTextArea((text) =>
|
||||||
text
|
text.setPlaceholder(t('comment.placeholder')).onChange((value) => {
|
||||||
.setPlaceholder(t("comment.placeholder"))
|
this.workDescription = value;
|
||||||
.onChange((value) => {
|
}),
|
||||||
this.workDescription = value;
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
.setClass("work-description-container");
|
.setClass('work-description-container');
|
||||||
|
|
||||||
// Make the textarea larger
|
// Make the textarea larger
|
||||||
const textareaComponent = this.contentEl.querySelector(".work-description-container textarea");
|
const textareaComponent = this.contentEl.querySelector('.work-description-container textarea');
|
||||||
if (textareaComponent) {
|
if (textareaComponent) {
|
||||||
(textareaComponent as HTMLTextAreaElement).rows = 5;
|
(textareaComponent as HTMLTextAreaElement).rows = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit button
|
// Submit button
|
||||||
new Setting(this.contentEl)
|
new Setting(this.contentEl).addButton((btn) =>
|
||||||
.addButton((btn) =>
|
btn
|
||||||
btn
|
.setButtonText(t('submit'))
|
||||||
.setButtonText(t("submit"))
|
.setCta()
|
||||||
.setCta()
|
.onClick(() => {
|
||||||
.onClick(() => {
|
if (!this.validateInputs()) {
|
||||||
if (!this.validateInputs()) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
this.close();
|
||||||
this.close();
|
this.onSubmit(this.timeSpent, this.startDate, this.workDescription);
|
||||||
this.onSubmit(this.timeSpent, this.startDate, this.workDescription);
|
}),
|
||||||
})
|
);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -202,7 +205,7 @@ export class IssueWorkLogModal extends Modal {
|
||||||
*/
|
*/
|
||||||
private validateInputs(): boolean {
|
private validateInputs(): boolean {
|
||||||
if (!this.timeSpent.trim()) {
|
if (!this.timeSpent.trim()) {
|
||||||
new Notice(t("warns.no_time"));
|
new Notice(t('warns.no_time'));
|
||||||
this.timeSpentSetting.setClass('invalid');
|
this.timeSpentSetting.setClass('invalid');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -210,20 +213,20 @@ export class IssueWorkLogModal extends Modal {
|
||||||
// Validate time spent format
|
// Validate time spent format
|
||||||
const timeSpentPattern = /^(\d+[wdhm]\s*)+$/;
|
const timeSpentPattern = /^(\d+[wdhm]\s*)+$/;
|
||||||
if (!timeSpentPattern.test(this.timeSpent)) {
|
if (!timeSpentPattern.test(this.timeSpent)) {
|
||||||
new Notice(t("warns.invalid_time"));
|
new Notice(t('warns.invalid_time'));
|
||||||
this.timeSpentSetting.setClass('invalid');
|
this.timeSpentSetting.setClass('invalid');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.displayDate.trim()) {
|
if (!this.displayDate.trim()) {
|
||||||
new Notice(t("warns.no_start"));
|
new Notice(t('warns.no_start'));
|
||||||
this.dateSetting.setClass('invalid');
|
this.dateSetting.setClass('invalid');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate date format by trying to parse it
|
// Validate date format by trying to parse it
|
||||||
if (!this.parseDisplayDate(this.displayDate)) {
|
if (!this.parseDisplayDate(this.displayDate)) {
|
||||||
new Notice(t("warns.invalid_start"));
|
new Notice(t('warns.invalid_start'));
|
||||||
this.dateSetting.setClass('invalid');
|
this.dateSetting.setClass('invalid');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import {App, Modal, Setting} from "obsidian";
|
import { App, Modal, Setting } from 'obsidian';
|
||||||
import {useTranslations} from "../localization/translator";
|
import { useTranslations } from '../localization/translator';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {JQLPreview} from "../settings/components/JQLPreview";
|
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
|
* Modal for searching issues by JQL query
|
||||||
|
|
@ -11,7 +11,7 @@ const t = useTranslations("modals.jql_search").t;
|
||||||
export class JQLSearchModal extends Modal {
|
export class JQLSearchModal extends Modal {
|
||||||
private onSubmit: (jql: string) => void;
|
private onSubmit: (jql: string) => void;
|
||||||
private plugin: JiraPlugin;
|
private plugin: JiraPlugin;
|
||||||
private jql: string = "";
|
private jql: string = '';
|
||||||
private previewEl?: HTMLElement;
|
private previewEl?: HTMLElement;
|
||||||
private preview?: JQLPreview;
|
private preview?: JQLPreview;
|
||||||
private debounceTimer?: number;
|
private debounceTimer?: number;
|
||||||
|
|
@ -23,43 +23,41 @@ export class JQLSearchModal extends Modal {
|
||||||
}
|
}
|
||||||
|
|
||||||
onOpen() {
|
onOpen() {
|
||||||
this.contentEl.createEl("h2", {text: t("desc")});
|
this.contentEl.createEl('h2', { text: t('desc') });
|
||||||
|
|
||||||
new Setting(this.contentEl)
|
new Setting(this.contentEl)
|
||||||
.setName(t("jql.name"))
|
.setName(t('jql.name'))
|
||||||
.setDesc(t("jql.desc"))
|
.setDesc(t('jql.desc'))
|
||||||
.addTextArea((text) => {
|
.addTextArea((text) => {
|
||||||
text
|
text.setPlaceholder(t('jql.placeholder'))
|
||||||
.setPlaceholder(t("jql.placeholder"))
|
|
||||||
.setValue(this.jql)
|
.setValue(this.jql)
|
||||||
.onChange((value) => {
|
.onChange((value) => {
|
||||||
this.jql = value;
|
this.jql = value;
|
||||||
this.schedulePreview();
|
this.schedulePreview();
|
||||||
});
|
});
|
||||||
text.inputEl.style.height = "100px";
|
text.inputEl.style.height = '100px';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create preview container and initialize preview component
|
// Create preview container and initialize preview component
|
||||||
this.previewEl = this.contentEl.createDiv();
|
this.previewEl = this.contentEl.createDiv();
|
||||||
this.preview = new JQLPreview(this.plugin, this.previewEl, 5);
|
this.preview = new JQLPreview(this.plugin, this.previewEl, 5);
|
||||||
this.preview.loadPreview("")
|
this.preview.loadPreview('');
|
||||||
|
|
||||||
new Setting(this.contentEl)
|
new Setting(this.contentEl).addButton((btn) =>
|
||||||
.addButton((btn) =>
|
btn
|
||||||
btn
|
.setButtonText(t('submit'))
|
||||||
.setButtonText(t("submit"))
|
.setCta()
|
||||||
.setCta()
|
.onClick(() => {
|
||||||
.onClick(() => {
|
if (this.jql.trim()) {
|
||||||
if (this.jql.trim()) {
|
this.close();
|
||||||
this.close();
|
this.onSubmit(this.jql.trim());
|
||||||
this.onSubmit(this.jql.trim());
|
}
|
||||||
}
|
}),
|
||||||
})
|
);
|
||||||
);
|
|
||||||
|
|
||||||
// Handle Enter key (Ctrl+Enter for textarea)
|
// Handle Enter key (Ctrl+Enter for textarea)
|
||||||
this.contentEl.addEventListener("keydown", (event) => {
|
this.contentEl.addEventListener('keydown', (event) => {
|
||||||
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (this.jql.trim()) {
|
if (this.jql.trim()) {
|
||||||
this.close();
|
this.close();
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import {App, SuggestModal} from "obsidian";
|
import { App, SuggestModal } from 'obsidian';
|
||||||
import {JiraProject} from "../interfaces";
|
import { JiraProject } from '../interfaces';
|
||||||
import {useTranslations} from "../localization/translator";
|
import { useTranslations } from '../localization/translator';
|
||||||
|
|
||||||
const t = useTranslations("modals.project").t;
|
const t = useTranslations('modals.project').t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal for selecting a project
|
* Modal for selecting a project
|
||||||
|
|
@ -15,20 +15,20 @@ export class ProjectModal extends SuggestModal<JiraProject> {
|
||||||
super(app);
|
super(app);
|
||||||
this.onSubmit = onSubmit;
|
this.onSubmit = onSubmit;
|
||||||
this.projects = projects;
|
this.projects = projects;
|
||||||
this.setPlaceholder(t("placeholder"));
|
this.setPlaceholder(t('placeholder'));
|
||||||
}
|
}
|
||||||
|
|
||||||
getSuggestions(query: string): JiraProject[] {
|
getSuggestions(query: string): JiraProject[] {
|
||||||
return this.projects.filter(
|
return this.projects.filter(
|
||||||
(project) =>
|
(project) =>
|
||||||
project.id.toLowerCase().includes(query.toLowerCase()) ||
|
project.id.toLowerCase().includes(query.toLowerCase()) ||
|
||||||
project.name.toLowerCase().includes(query.toLowerCase())
|
project.name.toLowerCase().includes(query.toLowerCase()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSuggestion(project: JiraProject, el: HTMLElement) {
|
renderSuggestion(project: JiraProject, el: HTMLElement) {
|
||||||
el.createEl("div", {text: project.name});
|
el.createEl('div', { text: project.name });
|
||||||
el.createEl("small", {text: project.id});
|
el.createEl('small', { text: project.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
onChooseSuggestion(project: JiraProject) {
|
onChooseSuggestion(project: JiraProject) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
export * from "./IssueSearchModal";
|
export * from './IssueSearchModal';
|
||||||
export * from "./IssueStatusModal";
|
export * from './IssueStatusModal';
|
||||||
export * from "./IssueTypeModal";
|
export * from './IssueTypeModal';
|
||||||
export * from "./IssueWorkLogModal";
|
export * from './IssueWorkLogModal';
|
||||||
export * from "./ProjectModal";
|
export * from './ProjectModal';
|
||||||
export * from "./JQLSearchModal";
|
export * from './JQLSearchModal';
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,88 @@
|
||||||
import {Extension, RangeSetBuilder} from "@codemirror/state";
|
import { Extension, RangeSetBuilder } from '@codemirror/state';
|
||||||
import {Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate} from "@codemirror/view";
|
import { Decoration, DecorationSet, EditorView, ViewPlugin, ViewUpdate } from '@codemirror/view';
|
||||||
import {MarkdownView} from "obsidian";
|
import { MarkdownView } from 'obsidian';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
|
|
||||||
|
|
||||||
export function createJiraSyncExtension(plugin: JiraPlugin): Extension {
|
export function createJiraSyncExtension(plugin: JiraPlugin): Extension {
|
||||||
return ViewPlugin.fromClass(class {
|
return ViewPlugin.fromClass(
|
||||||
decorations: DecorationSet = Decoration.none;
|
class {
|
||||||
|
decorations: DecorationSet = Decoration.none;
|
||||||
|
|
||||||
constructor(view: EditorView) {
|
constructor(view: EditorView) {
|
||||||
this.decorations = this.buildDecorations(view);
|
this.decorations = this.buildDecorations(view);
|
||||||
}
|
|
||||||
|
|
||||||
update(update: ViewUpdate) {
|
|
||||||
if (this.passDecoration()) {
|
|
||||||
this.decorations = Decoration.none;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
|
||||||
this.decorations = this.buildDecorations(update.view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
passDecoration() {
|
update(update: ViewUpdate) {
|
||||||
const mdView = plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
if (this.passDecoration()) {
|
||||||
if (!mdView) return false;
|
this.decorations = Decoration.none;
|
||||||
const current_state = mdView.getState()
|
return;
|
||||||
return current_state.mode === 'source' && current_state.source;
|
}
|
||||||
}
|
if (update.docChanged || update.viewportChanged || update.selectionSet) {
|
||||||
|
this.decorations = this.buildDecorations(update.view);
|
||||||
buildDecorations(view: EditorView) {
|
|
||||||
const builder = new RangeSetBuilder<Decoration>();
|
|
||||||
|
|
||||||
for (let { from, to } of view.visibleRanges) {
|
|
||||||
const text = view.state.doc.sliceString(from, to);
|
|
||||||
const codeBlocks = this.findCodeBlocks(text, from);
|
|
||||||
|
|
||||||
for (const match of text.matchAll(/`(jira-sync-[^`]+)`/g)) {
|
|
||||||
const start = from + match.index!;
|
|
||||||
const end = start + match[0].length;
|
|
||||||
const contentStart = start; // без первой `
|
|
||||||
const contentEnd = end; // без последней `
|
|
||||||
|
|
||||||
// Find code blocks to ignore
|
|
||||||
if (this.isInsideCodeBlock(contentStart, contentEnd, codeBlocks)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.add(start, end,
|
|
||||||
Decoration.mark({ class: className })
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.finish();
|
passDecoration() {
|
||||||
}
|
const mdView = plugin.app.workspace.getActiveViewOfType(MarkdownView);
|
||||||
|
if (!mdView) return false;
|
||||||
findCodeBlocks(text: string, offset: number) {
|
const current_state = mdView.getState();
|
||||||
const blocks = [];
|
return current_state.mode === 'source' && current_state.source;
|
||||||
const regex = /```[\s\S]*?```/g;
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = regex.exec(text)) !== null) {
|
|
||||||
blocks.push({
|
|
||||||
from: offset + match.index,
|
|
||||||
to: offset + match.index + match[0].length
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocks;
|
buildDecorations(view: EditorView) {
|
||||||
}
|
const builder = new RangeSetBuilder<Decoration>();
|
||||||
|
|
||||||
isInsideCodeBlock(start: number, end: number, codeBlocks: Array<{from: number, to: number}>) {
|
for (let { from, to } of view.visibleRanges) {
|
||||||
return codeBlocks.some(block =>
|
const text = view.state.doc.sliceString(from, to);
|
||||||
start >= block.from && end <= block.to
|
const codeBlocks = this.findCodeBlocks(text, from);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}, {
|
for (const match of text.matchAll(/`(jira-sync-[^`]+)`/g)) {
|
||||||
decorations: v => v.decorations
|
const start = from + match.index!;
|
||||||
});
|
const end = start + match[0].length;
|
||||||
|
const contentStart = start; // без первой `
|
||||||
|
const contentEnd = end; // без последней `
|
||||||
|
|
||||||
|
// Find code blocks to ignore
|
||||||
|
if (this.isInsideCodeBlock(contentStart, contentEnd, codeBlocks)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.add(start, end, Decoration.mark({ class: className }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
findCodeBlocks(text: string, offset: number) {
|
||||||
|
const blocks = [];
|
||||||
|
const regex = /```[\s\S]*?```/g;
|
||||||
|
let match;
|
||||||
|
|
||||||
|
while ((match = regex.exec(text)) !== null) {
|
||||||
|
blocks.push({
|
||||||
|
from: offset + match.index,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (v) => v.decorations,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// For Reading mode - targets <code> elements in <p> tags
|
// 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
|
// In Reading mode, inline code becomes <code> elements
|
||||||
const codeElements = element.querySelectorAll(':not(pre) > code');
|
const codeElements = element.querySelectorAll(':not(pre) > code');
|
||||||
|
|
||||||
codeElements.forEach(codeEl => {
|
codeElements.forEach((codeEl) => {
|
||||||
const text = codeEl.textContent || '';
|
const text = codeEl.textContent || '';
|
||||||
|
|
||||||
if (text.startsWith('jira-sync-')) {
|
if (text.startsWith('jira-sync-')) {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import {setIcon} from "obsidian";
|
import { setIcon } from 'obsidian';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import { SettingsComponent } from "../interfaces/settingsTypes";
|
import { SettingsComponent } from '../interfaces/settingsTypes';
|
||||||
import { debugLog } from "../tools/debugLogging";
|
import { debugLog } from '../tools/debugLogging';
|
||||||
import {CollapsedSections} from "./default";
|
import { CollapsedSections } from './default';
|
||||||
|
|
||||||
export class CollapsibleSection {
|
export class CollapsibleSection {
|
||||||
private containerEl: HTMLDivElement;
|
private containerEl: HTMLDivElement;
|
||||||
|
|
@ -19,7 +19,7 @@ export class CollapsibleSection {
|
||||||
child: SettingsComponent,
|
child: SettingsComponent,
|
||||||
headerText: string,
|
headerText: string,
|
||||||
isOpenByDefault = false,
|
isOpenByDefault = false,
|
||||||
saveStateKey?: string
|
saveStateKey?: string,
|
||||||
) {
|
) {
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
this.child = child;
|
this.child = child;
|
||||||
|
|
@ -29,37 +29,37 @@ export class CollapsibleSection {
|
||||||
|
|
||||||
// Create the container div for the entire collapsible section
|
// Create the container div for the entire collapsible section
|
||||||
this.containerEl = parentEl.createDiv({
|
this.containerEl = parentEl.createDiv({
|
||||||
cls: "jira-collapsible-section",
|
cls: 'jira-collapsible-section',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create the header element
|
// Create the header element
|
||||||
const headerEl = this.containerEl.createDiv({
|
const headerEl = this.containerEl.createDiv({
|
||||||
cls: "jira-collapsible-header",
|
cls: 'jira-collapsible-header',
|
||||||
text: headerText,
|
text: headerText,
|
||||||
});
|
});
|
||||||
headerEl.style.cursor = "pointer";
|
headerEl.style.cursor = 'pointer';
|
||||||
headerEl.addEventListener("click", async () => {
|
headerEl.addEventListener('click', async () => {
|
||||||
await this.toggle();
|
await this.toggle();
|
||||||
});
|
});
|
||||||
|
|
||||||
const chevron = headerEl.createEl('span', {
|
const chevron = headerEl.createEl('span', {
|
||||||
cls: 'jira-collapse-icon',
|
cls: 'jira-collapse-icon',
|
||||||
});
|
});
|
||||||
setIcon(chevron, "chevron-down");
|
setIcon(chevron, 'chevron-down');
|
||||||
|
|
||||||
// Content wrapper
|
// Content wrapper
|
||||||
this.contentEl = this.containerEl.createDiv({
|
this.contentEl = this.containerEl.createDiv({
|
||||||
cls: "jira-collapsible-content",
|
cls: 'jira-collapsible-content',
|
||||||
});
|
});
|
||||||
|
|
||||||
this.child.render(this.contentEl);
|
this.child.render(this.contentEl);
|
||||||
|
|
||||||
if (!this.isOpen) {
|
if (!this.isOpen) {
|
||||||
this.contentEl.hide();
|
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 {
|
public getContentContainer(): HTMLElement {
|
||||||
|
|
@ -68,17 +68,17 @@ export class CollapsibleSection {
|
||||||
|
|
||||||
private async toggle() {
|
private async toggle() {
|
||||||
this.isOpen = !this.isOpen;
|
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
|
// Rebuild or update the toggle button if needed
|
||||||
// Or store a reference to the button and just update its properties
|
// Or store a reference to the button and just update its properties
|
||||||
|
|
||||||
if (this.isOpen) {
|
if (this.isOpen) {
|
||||||
this.contentEl.show();
|
this.contentEl.show();
|
||||||
this.containerEl.removeClass("collapsed");
|
this.containerEl.removeClass('collapsed');
|
||||||
} else {
|
} else {
|
||||||
this.contentEl.hide();
|
this.contentEl.hide();
|
||||||
this.containerEl.addClass("collapsed");
|
this.containerEl.addClass('collapsed');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.saveStateKey && this.plugin) {
|
if (this.saveStateKey && this.plugin) {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import { App, PluginSettingTab } from "obsidian";
|
import { App, PluginSettingTab } from 'obsidian';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import { SettingsComponentProps } from "../interfaces/settingsTypes";
|
import { SettingsComponentProps } from '../interfaces/settingsTypes';
|
||||||
import { ConnectionSettingsComponent } from "./components/ConnectionSettingsComponent";
|
import { ConnectionSettingsComponent } from './components/ConnectionSettingsComponent';
|
||||||
import { GeneralSettingsComponent } from "./components/GeneralSettingsComponent";
|
import { GeneralSettingsComponent } from './components/GeneralSettingsComponent';
|
||||||
import { FieldMappingsComponent } from "./components/FieldMappingsComponent";
|
import { FieldMappingsComponent } from './components/FieldMappingsComponent';
|
||||||
import { FetchIssueComponent } from "./components/FetchIssueComponent";
|
import { FetchIssueComponent } from './components/FetchIssueComponent';
|
||||||
import { TestFieldMappingsComponent } from "./components/TestFieldMappingsComponent";
|
import { TestFieldMappingsComponent } from './components/TestFieldMappingsComponent';
|
||||||
import { debugLog } from "../tools/debugLogging";
|
import { debugLog } from '../tools/debugLogging';
|
||||||
import { CollapsibleSection } from "./CollapsibleSection";
|
import { CollapsibleSection } from './CollapsibleSection';
|
||||||
import {useTranslations} from "../localization/translator";
|
import { useTranslations } from '../localization/translator';
|
||||||
import {TimekeepSettingsComponent} from "./components/TimekeepSettingsComponent";
|
import { TimekeepSettingsComponent } from './components/TimekeepSettingsComponent';
|
||||||
|
|
||||||
const t = useTranslations("settings").t;
|
const t = useTranslations('settings').t;
|
||||||
/**
|
/**
|
||||||
* Settings tab for the Jira plugin
|
* Settings tab for the Jira plugin
|
||||||
* Orchestrates all components and manages the overall settings UI
|
* Orchestrates all components and manages the overall settings UI
|
||||||
|
|
@ -33,7 +33,7 @@ export class JiraSettingTab extends PluginSettingTab {
|
||||||
const componentProps: SettingsComponentProps = {
|
const componentProps: SettingsComponentProps = {
|
||||||
app,
|
app,
|
||||||
plugin,
|
plugin,
|
||||||
onSettingsChange: this.handleSettingsChange.bind(this)
|
onSettingsChange: this.handleSettingsChange.bind(this),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize all settings components
|
// Initialize all settings components
|
||||||
|
|
@ -41,8 +41,12 @@ export class JiraSettingTab extends PluginSettingTab {
|
||||||
this.generalSettings = new GeneralSettingsComponent(componentProps);
|
this.generalSettings = new GeneralSettingsComponent(componentProps);
|
||||||
this.fieldMappingsSettings = new FieldMappingsComponent(componentProps);
|
this.fieldMappingsSettings = new FieldMappingsComponent(componentProps);
|
||||||
this.fetchIssue = new FetchIssueComponent(componentProps);
|
this.fetchIssue = new FetchIssueComponent(componentProps);
|
||||||
this.testFieldMappings = new TestFieldMappingsComponent({ ...componentProps, getCurrentIssue: () => this.fetchIssue.getCurrentIssue() });
|
this.testFieldMappings = new TestFieldMappingsComponent({
|
||||||
this.fetchIssue.onIssueDataChange = () => this.testFieldMappings.setCurrentIssue(this.fetchIssue.getCurrentIssue());
|
...componentProps,
|
||||||
|
getCurrentIssue: () => this.fetchIssue.getCurrentIssue(),
|
||||||
|
});
|
||||||
|
this.fetchIssue.onIssueDataChange = () =>
|
||||||
|
this.testFieldMappings.setCurrentIssue(this.fetchIssue.getCurrentIssue());
|
||||||
this.timekeepSettings = new TimekeepSettingsComponent(componentProps);
|
this.timekeepSettings = new TimekeepSettingsComponent(componentProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +55,7 @@ export class JiraSettingTab extends PluginSettingTab {
|
||||||
* This can be used for cross-component updates or validation
|
* This can be used for cross-component updates or validation
|
||||||
*/
|
*/
|
||||||
private async handleSettingsChange(): Promise<void> {
|
private async handleSettingsChange(): Promise<void> {
|
||||||
debugLog("Settings changed, handling updates");
|
debugLog('Settings changed, handling updates');
|
||||||
await this.plugin.saveSettings();
|
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
|
// 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"),
|
new CollapsibleSection(
|
||||||
this.plugin.settings.collapsedSections.connection, "connection").getContentContainer();
|
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"),
|
new CollapsibleSection(
|
||||||
this.plugin.settings.collapsedSections.general, "general").getContentContainer();
|
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"),
|
new CollapsibleSection(
|
||||||
this.plugin.settings.collapsedSections.fieldMappings, "fieldMappings").getContentContainer();
|
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"),
|
new CollapsibleSection(
|
||||||
this.plugin.settings.collapsedSections.fetchIssue, "fetchIssue").getContentContainer();
|
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"),
|
new CollapsibleSection(
|
||||||
this.plugin.settings.collapsedSections.testFieldMappings, "testFieldMappings").getContentContainer();
|
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"),
|
new CollapsibleSection(
|
||||||
this.plugin.settings.collapsedSections.statistics, "statistics").getContentContainer();
|
this.plugin,
|
||||||
|
containerEl,
|
||||||
|
this.timekeepSettings,
|
||||||
|
t('statistics.title'),
|
||||||
|
this.plugin.settings.collapsedSections.statistics,
|
||||||
|
'statistics',
|
||||||
|
).getContentContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,187 +1,267 @@
|
||||||
import {Notice, Setting} from "obsidian";
|
import { Notice, Setting } from 'obsidian';
|
||||||
import { useTranslations } from "src/localization/translator"
|
import { useTranslations } from '../../localization/translator';
|
||||||
import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes";
|
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
|
||||||
import { fetchSelf } from "../../api";
|
import { fetchSelf } from '../../api';
|
||||||
import {debugLog} from "../../tools/debugLogging";
|
import { debugLog } from '../../tools/debugLogging';
|
||||||
|
|
||||||
const t = useTranslations("settings.connection").t
|
const t = useTranslations('settings.connection').t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for Jira connection settings
|
* Component for Jira connection settings
|
||||||
*/
|
*/
|
||||||
export class ConnectionSettingsComponent implements SettingsComponent {
|
export class ConnectionSettingsComponent implements SettingsComponent {
|
||||||
private props: SettingsComponentProps;
|
private props: SettingsComponentProps;
|
||||||
private currentAuthMethod: "bearer" | "basic" | "session";
|
private currentAuthMethod: 'bearer' | 'basic' | 'session';
|
||||||
private settingElements: Map<string, HTMLElement> = new Map();
|
private settingElements: Map<string, HTMLElement> = new Map();
|
||||||
|
private connectionDropdown: any;
|
||||||
|
private fieldsContainer!: HTMLElement;
|
||||||
|
|
||||||
constructor(props: SettingsComponentProps) {
|
constructor(props: SettingsComponentProps) {
|
||||||
this.props = props;
|
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 {
|
render(containerEl: HTMLElement): void {
|
||||||
const { plugin } = this.props;
|
const { plugin } = this.props;
|
||||||
|
|
||||||
// Always show: Jira URL
|
// Connection selector dropdown and add button
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("url.title"))
|
.setName(t('select_connection'))
|
||||||
.setDesc(t("url.desc"))
|
.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) =>
|
.addText((text) =>
|
||||||
text
|
text
|
||||||
.setPlaceholder(t("url.def"))
|
.setPlaceholder(t('name.def'))
|
||||||
.setValue(plugin.settings.connection.jiraUrl)
|
.setValue(currentConn.name)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
plugin.settings.connection.jiraUrl = value;
|
currentConn.name = value;
|
||||||
await plugin.saveSettings();
|
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)
|
new Setting(containerEl)
|
||||||
.setName(t("api_version.title"))
|
.setName(t('api_version.title'))
|
||||||
.setDesc(t("api_version.desc"))
|
.setDesc(t('api_version.desc'))
|
||||||
.addDropdown((cb) =>
|
.addDropdown((cb) =>
|
||||||
cb
|
cb
|
||||||
.addOption("2", t("api_version.options.2"))
|
.addOption('2', t('api_version.options.2'))
|
||||||
.addOption("3", t("api_version.options.3"))
|
.addOption('3', t('api_version.options.3'))
|
||||||
.setValue(plugin.settings.connection.apiVersion)
|
.setValue(currentConn.apiVersion)
|
||||||
.onChange(async (value: "2" | "3") => {
|
.onChange(async (value: string) => {
|
||||||
plugin.settings.connection.apiVersion = value;
|
currentConn.apiVersion = value as '2' | '3';
|
||||||
await plugin.saveSettings();
|
await plugin.saveSettings();
|
||||||
})
|
}),
|
||||||
)
|
);
|
||||||
|
|
||||||
// Auth Method Select
|
// Auth Method Select
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("auth.title"))
|
.setName(t('auth.title'))
|
||||||
.setDesc(t("auth.desc"))
|
.setDesc(t('auth.desc'))
|
||||||
.addDropdown((cb) =>
|
.addDropdown((cb) =>
|
||||||
cb
|
cb
|
||||||
.addOption("bearer", t("auth.options.bearer"))
|
.addOption('bearer', t('auth.options.bearer'))
|
||||||
.addOption("basic", t("auth.options.basic"))
|
.addOption('basic', t('auth.options.basic'))
|
||||||
.addOption("session", t("auth.options.session"))
|
.addOption('session', t('auth.options.session'))
|
||||||
.setValue(this.currentAuthMethod)
|
.setValue(this.currentAuthMethod)
|
||||||
.onChange(async (value: "bearer" | "basic" | "session") => {
|
.onChange(async (value: string) => {
|
||||||
plugin.settings.connection.authMethod = value;
|
currentConn.authMethod = value as 'bearer' | 'basic' | 'session';
|
||||||
await plugin.saveSettings();
|
await plugin.saveSettings();
|
||||||
this.currentAuthMethod = value;
|
this.currentAuthMethod = value as 'bearer' | 'basic' | 'session';
|
||||||
this.updateVisibility();
|
this.updateVisibility();
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// // Store reference for dynamic visibility control
|
|
||||||
// this.settingElements.set("authMethod", authMethodSetting.settingEl);
|
|
||||||
|
|
||||||
// Jira PAT
|
// Jira PAT
|
||||||
const patSetting = new Setting(containerEl)
|
const patSetting = new Setting(containerEl)
|
||||||
.setName(t("pat.title"))
|
.setName(t('pat.title'))
|
||||||
.setDesc(t("pat.desc"))
|
.setDesc(t('pat.desc'))
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.inputEl.type = "password";
|
text.inputEl.type = 'password';
|
||||||
text
|
text.setPlaceholder(t('pat.def'))
|
||||||
.setPlaceholder(t("pat.def"))
|
.setValue(currentConn.apiToken)
|
||||||
.setValue(plugin.settings.connection.apiToken)
|
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
plugin.settings.connection.apiToken = value;
|
currentConn.apiToken = value;
|
||||||
await plugin.saveSettings();
|
await plugin.saveSettings();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.settingElements.set("pat", patSetting.settingEl);
|
this.settingElements.set('pat', patSetting.settingEl);
|
||||||
|
|
||||||
// Jira username
|
// Jira username
|
||||||
const usernameSetting = new Setting(containerEl)
|
const usernameSetting = new Setting(containerEl)
|
||||||
.setName(t("username.title"))
|
.setName(t('username.title'))
|
||||||
.setDesc(t("username.desc"))
|
.setDesc(t('username.desc'))
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
text
|
text
|
||||||
.setPlaceholder(t("username.def"))
|
.setPlaceholder(t('username.def'))
|
||||||
.setValue(plugin.settings.connection.username)
|
.setValue(currentConn.username)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
plugin.settings.connection.username = value;
|
currentConn.username = value;
|
||||||
await plugin.saveSettings();
|
await plugin.saveSettings();
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
this.settingElements.set("username", usernameSetting.settingEl);
|
this.settingElements.set('username', usernameSetting.settingEl);
|
||||||
|
|
||||||
// Jira username
|
// Jira email
|
||||||
const emailSetting = new Setting(containerEl)
|
const emailSetting = new Setting(containerEl)
|
||||||
.setName(t("username.title"))
|
.setName(t('email.title'))
|
||||||
.setDesc(t("username.desc"))
|
.setDesc(t('email.desc'))
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
text
|
text
|
||||||
.setPlaceholder(t("username.def"))
|
.setPlaceholder(t('email.def'))
|
||||||
.setValue(plugin.settings.connection.email)
|
.setValue(currentConn.email)
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
plugin.settings.connection.email = value;
|
currentConn.email = value;
|
||||||
await plugin.saveSettings();
|
await plugin.saveSettings();
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
this.settingElements.set("email", emailSetting.settingEl);
|
this.settingElements.set('email', emailSetting.settingEl);
|
||||||
|
|
||||||
// Jira password
|
// Jira password
|
||||||
const passwordSetting = new Setting(containerEl)
|
const passwordSetting = new Setting(containerEl)
|
||||||
.setName(t("password.title"))
|
.setName(t('password.title'))
|
||||||
.setDesc(t("password.desc"))
|
.setDesc(t('password.desc'))
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.inputEl.type = "password";
|
text.inputEl.type = 'password';
|
||||||
text
|
text.setPlaceholder(t('password.def'))
|
||||||
.setPlaceholder(t("password.def"))
|
.setValue(currentConn.password)
|
||||||
.setValue(plugin.settings.connection.password)
|
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
plugin.settings.connection.password = value;
|
currentConn.password = value;
|
||||||
await plugin.saveSettings();
|
await plugin.saveSettings();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.settingElements.set("password", passwordSetting.settingEl);
|
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);
|
|
||||||
|
|
||||||
// Ping button to test connection
|
// Ping button to test connection
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("ping.title"))
|
.setName(t('ping.title'))
|
||||||
.setDesc(t("ping.desc"))
|
.setDesc(t('ping.desc'))
|
||||||
.addButton((button) =>
|
.addButton((button) =>
|
||||||
button
|
button
|
||||||
.setButtonText(t("ping.button"))
|
.setButtonText(t('ping.button'))
|
||||||
.setCta()
|
.setCta()
|
||||||
.onClick(async () => {
|
.onClick(async () => {
|
||||||
await this.testConnection();
|
await this.testConnection();
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initial visibility
|
// Initial visibility
|
||||||
this.updateVisibility();
|
this.updateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private refreshFields(): void {
|
||||||
|
const currentConn = this.getCurrentConnection();
|
||||||
|
if (currentConn) {
|
||||||
|
this.currentAuthMethod = currentConn.authMethod;
|
||||||
|
}
|
||||||
|
this.renderConnectionFields(this.fieldsContainer);
|
||||||
|
}
|
||||||
|
|
||||||
private async testConnection(): Promise<void> {
|
private async testConnection(): Promise<void> {
|
||||||
const { plugin } = this.props;
|
const { plugin } = this.props;
|
||||||
|
|
||||||
try {
|
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);
|
const result = await fetchSelf(plugin);
|
||||||
notice.hide();
|
notice.hide();
|
||||||
|
|
||||||
const successMessage = "Successfully connected to Jira";
|
const successMessage = 'Successfully connected to Jira';
|
||||||
new Notice(successMessage);
|
new Notice(successMessage);
|
||||||
debugLog(successMessage, result);
|
debugLog(successMessage, result);
|
||||||
|
} catch (error: unknown) {
|
||||||
} catch (error) {
|
const errorMessage = `Failed to connect to Jira: ${(error as Error).message}`;
|
||||||
const errorMessage = `Failed to connect to Jira: ${error.message}`;
|
|
||||||
new Notice(errorMessage);
|
new Notice(errorMessage);
|
||||||
console.error(errorMessage, error);
|
console.error(errorMessage, error);
|
||||||
}
|
}
|
||||||
|
|
@ -190,52 +270,38 @@ export class ConnectionSettingsComponent implements SettingsComponent {
|
||||||
private updateVisibility(): void {
|
private updateVisibility(): void {
|
||||||
const method = this.currentAuthMethod;
|
const method = this.currentAuthMethod;
|
||||||
|
|
||||||
// // Show always
|
if (method === 'bearer' || method === 'basic') {
|
||||||
// this.showElement("authMethod");
|
this.showElement('pat');
|
||||||
|
|
||||||
// Show PAT if bearer or basic
|
|
||||||
if (method === "bearer" || method === "basic") {
|
|
||||||
this.showElement("pat");
|
|
||||||
} else {
|
} else {
|
||||||
this.hideElement("pat");
|
this.hideElement('pat');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show username if basic or session
|
if (method === 'session') {
|
||||||
if (method === "session") {
|
this.showElement('username');
|
||||||
this.showElement("username");
|
|
||||||
} else {
|
} else {
|
||||||
this.hideElement("username");
|
this.hideElement('username');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show email if basic or session
|
if (method === 'basic') {
|
||||||
if (method === "basic") {
|
this.showElement('email');
|
||||||
this.showElement("email");
|
|
||||||
} else {
|
} else {
|
||||||
this.hideElement("email");
|
this.hideElement('email');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show password only if session
|
if (method === 'session') {
|
||||||
if (method === "session") {
|
this.showElement('password');
|
||||||
this.showElement("password");
|
|
||||||
} else {
|
} 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 {
|
private showElement(key: string): void {
|
||||||
const el = this.settingElements.get(key);
|
const el = this.settingElements.get(key);
|
||||||
if (el) el.style.display = "";
|
if (el) el.style.display = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
private hideElement(key: string): void {
|
private hideElement(key: string): void {
|
||||||
const el = this.settingElements.get(key);
|
const el = this.settingElements.get(key);
|
||||||
if (el) el.style.display = "none";
|
if (el) el.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,119 +1,108 @@
|
||||||
import {Notice, Setting} from "obsidian";
|
import { Notice, Setting } from 'obsidian';
|
||||||
import {SettingsComponent, SettingsComponentProps} from "../../interfaces/settingsTypes";
|
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
|
||||||
import {fetchIssue} from "../../api";
|
import { fetchIssue } from '../../api';
|
||||||
import debounce from "lodash/debounce";
|
import debounce from 'lodash/debounce';
|
||||||
import hljs from "highlight.js";
|
import hljs from 'highlight.js';
|
||||||
import {useTranslations} from "../../localization/translator";
|
import { useTranslations } from '../../localization/translator';
|
||||||
import {setupArrayTextSetting} from "../tools/setupArrayTextString";
|
import { setupArrayTextSetting } from '../tools/setupArrayTextString';
|
||||||
|
|
||||||
|
const t = useTranslations('settings.fetch_issue').t;
|
||||||
const t = useTranslations("settings.fetch_issue").t;
|
|
||||||
export class FetchIssueComponent implements SettingsComponent {
|
export class FetchIssueComponent implements SettingsComponent {
|
||||||
private props: SettingsComponentProps;
|
private props: SettingsComponentProps;
|
||||||
private issueDataContainer: HTMLElement | null = null;
|
private issueDataContainer: HTMLElement | null = null;
|
||||||
private debouncedFetch: ReturnType<typeof debounce>;
|
private debouncedFetch: ReturnType<typeof debounce>;
|
||||||
private currentIssueData: any = null;
|
private currentIssueData: any = null;
|
||||||
private currentIssueKey: string = "";
|
private currentIssueKey: string = '';
|
||||||
public onIssueDataChange?: () => void;
|
public onIssueDataChange?: () => void;
|
||||||
|
|
||||||
constructor(props: SettingsComponentProps) {
|
constructor(props: SettingsComponentProps) {
|
||||||
this.props = props;
|
this.props = props;
|
||||||
this.debouncedFetch = debounce(this.fetchIssueData.bind(this), 500);
|
this.debouncedFetch = debounce(this.fetchIssueData.bind(this), 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render(containerEl: HTMLElement): void {
|
||||||
render(containerEl: HTMLElement): void {
|
|
||||||
const { plugin } = this.props;
|
const { plugin } = this.props;
|
||||||
|
|
||||||
// Filename template setting
|
// Filename template setting
|
||||||
const filenameTemplateSetting = new Setting(containerEl)
|
const filenameTemplateSetting = new Setting(containerEl)
|
||||||
.setName(t("filenameTemplate.name"))
|
.setName(t('filenameTemplate.name'))
|
||||||
.setDesc(t("filenameTemplate.desc"));
|
.setDesc(t('filenameTemplate.desc'));
|
||||||
|
|
||||||
filenameTemplateSetting.addText((text) => {
|
filenameTemplateSetting.addText((text) => {
|
||||||
text
|
text.setPlaceholder(t('filenameTemplate.placeholder'))
|
||||||
.setPlaceholder(t("filenameTemplate.placeholder"))
|
.setValue(plugin.settings.fetchIssue.filenameTemplate || '')
|
||||||
.setValue(plugin.settings.fetchIssue.filenameTemplate || "")
|
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
plugin.settings.fetchIssue.filenameTemplate = value;
|
plugin.settings.fetchIssue.filenameTemplate = value;
|
||||||
await plugin.saveSettings();
|
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', {
|
||||||
const securityNote = containerEl.createEl("p");
|
text: t('note.desc0'),
|
||||||
securityNote.createEl("strong", {
|
|
||||||
text: t("note.desc0")
|
|
||||||
});
|
});
|
||||||
securityNote.createSpan({
|
securityNote.createSpan({
|
||||||
text: t("note.desc1")
|
text: t('note.desc1'),
|
||||||
});
|
});
|
||||||
securityNote.createEl("br");
|
securityNote.createEl('br');
|
||||||
securityNote.createEl("br");
|
securityNote.createEl('br');
|
||||||
securityNote.createSpan({
|
securityNote.createSpan({
|
||||||
text: t("note.desc2")
|
text: t('note.desc2'),
|
||||||
})
|
});
|
||||||
securityNote.createEl("a", {
|
securityNote.createEl('a', {
|
||||||
text: t("note.link_label"),
|
text: t('note.link_label'),
|
||||||
href: link,
|
href: link,
|
||||||
cls: "external-link"
|
cls: 'external-link',
|
||||||
})
|
});
|
||||||
securityNote.createSpan({
|
securityNote.createSpan({
|
||||||
text: t("note.desc3")
|
text: t('note.desc3'),
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("fields.name"))
|
.setName(t('fields.name'))
|
||||||
.setDesc(t("fields.desc"))
|
.setDesc(t('fields.desc'))
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.setPlaceholder(t("fields.def"))
|
text.setPlaceholder(t('fields.def'));
|
||||||
setupArrayTextSetting(
|
setupArrayTextSetting(text, plugin.settings.fetchIssue.fields, async (array) => {
|
||||||
text,
|
plugin.settings.fetchIssue.fields = array;
|
||||||
plugin.settings.fetchIssue.fields,
|
this.rerenderIssueData();
|
||||||
async (array) => {
|
await plugin.saveSettings();
|
||||||
plugin.settings.fetchIssue.fields = array;
|
});
|
||||||
this.rerenderIssueData();
|
|
||||||
await plugin.saveSettings();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("expand.name"))
|
.setName(t('expand.name'))
|
||||||
.setDesc(t("expand.desc"))
|
.setDesc(t('expand.desc'))
|
||||||
.addText((text) => {
|
.addText((text) => {
|
||||||
text.setPlaceholder(t("expand.def"))
|
text.setPlaceholder(t('expand.def'));
|
||||||
setupArrayTextSetting(
|
setupArrayTextSetting(text, plugin.settings.fetchIssue.expand, async (array) => {
|
||||||
text,
|
plugin.settings.fetchIssue.expand = array;
|
||||||
plugin.settings.fetchIssue.expand,
|
this.rerenderIssueData();
|
||||||
async (array) => {
|
await plugin.saveSettings();
|
||||||
plugin.settings.fetchIssue.expand = array;
|
});
|
||||||
this.rerenderIssueData();
|
|
||||||
await plugin.saveSettings();
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add input field for issue key
|
// Add input field for issue key
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName(t("key.name"))
|
.setName(t('key.name'))
|
||||||
.setDesc(t("key.desc"))
|
.setDesc(t('key.desc'))
|
||||||
.addText((text) =>
|
.addText((text) =>
|
||||||
text
|
text.setPlaceholder('PROJ-123').onChange((value) => {
|
||||||
.setPlaceholder("PROJ-123")
|
this.currentIssueKey = value;
|
||||||
.onChange((value) => {
|
this.rerenderIssueData();
|
||||||
this.currentIssueKey = value;
|
}),
|
||||||
this.rerenderIssueData();
|
);
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create container for issue data
|
// Create container for issue data
|
||||||
this.issueDataContainer = containerEl.createDiv({
|
this.issueDataContainer = containerEl.createDiv({
|
||||||
cls: "jira-raw-issue-container"
|
cls: 'jira-raw-issue-container',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private rerenderIssueData() {
|
private rerenderIssueData() {
|
||||||
if (this.currentIssueKey) {
|
if (this.currentIssueKey) {
|
||||||
|
|
@ -123,51 +112,51 @@ export class FetchIssueComponent implements SettingsComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCurrentIssue(): any {
|
public getCurrentIssue(): any {
|
||||||
return this.currentIssueData;
|
return this.currentIssueData;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchIssueData(value: string): Promise<void> {
|
private async fetchIssueData(value: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const issueData = await fetchIssue(this.props.plugin, value);
|
const issueData = await fetchIssue(this.props.plugin, value);
|
||||||
this.currentIssueData = issueData;
|
this.currentIssueData = issueData;
|
||||||
this.displayIssueData(issueData);
|
this.displayIssueData(issueData);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
new Notice(`Failed to fetch issue: ${error.message}`);
|
new Notice(`Failed to fetch issue: ${(error as Error).message}`);
|
||||||
this.clearIssueData();
|
this.clearIssueData();
|
||||||
}
|
}
|
||||||
if (this.onIssueDataChange) {
|
if (this.onIssueDataChange) {
|
||||||
this.onIssueDataChange();
|
this.onIssueDataChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private displayIssueData(issueData: any): void {
|
private displayIssueData(issueData: any): void {
|
||||||
if (!this.issueDataContainer) return;
|
if (!this.issueDataContainer) return;
|
||||||
|
|
||||||
this.issueDataContainer.empty();
|
this.issueDataContainer.empty();
|
||||||
|
|
||||||
const pre = this.issueDataContainer.createEl("pre", {
|
const pre = this.issueDataContainer.createEl('pre', {
|
||||||
cls: "jira-copyable",
|
cls: 'jira-copyable',
|
||||||
});
|
});
|
||||||
|
|
||||||
const code = pre.createEl("code", {
|
const code = pre.createEl('code', {
|
||||||
cls: "language-json"
|
cls: 'language-json',
|
||||||
});
|
});
|
||||||
|
|
||||||
const jsonString = JSON.stringify(issueData, null, 2);
|
const jsonString = JSON.stringify(issueData, null, 2);
|
||||||
code.setText(jsonString);
|
code.setText(jsonString);
|
||||||
hljs.highlightElement(code);
|
hljs.highlightElement(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
private clearIssueData(): void {
|
private clearIssueData(): void {
|
||||||
if (this.issueDataContainer) {
|
if (this.issueDataContainer) {
|
||||||
this.issueDataContainer.empty();
|
this.issueDataContainer.empty();
|
||||||
}
|
}
|
||||||
this.currentIssueData = null;
|
this.currentIssueData = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
hide(): void {
|
hide(): void {
|
||||||
this.clearIssueData();
|
this.clearIssueData();
|
||||||
this.debouncedFetch.cancel();
|
this.debouncedFetch.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { validateField } from "../tools/fieldValidation";
|
import { validateField } from '../tools/fieldValidation';
|
||||||
import {useTranslations} from "../../localization/translator";
|
import { useTranslations } from '../../localization/translator';
|
||||||
|
|
||||||
const t = useTranslations("settings.fm").t;
|
const t = useTranslations('settings.fm').t;
|
||||||
/**
|
/**
|
||||||
* Interface for field mapping item props
|
* Interface for field mapping item props
|
||||||
*/
|
*/
|
||||||
|
|
@ -19,10 +19,10 @@ export interface FieldMappingItemProps {
|
||||||
*/
|
*/
|
||||||
export class FieldMappingItem {
|
export class FieldMappingItem {
|
||||||
private props: FieldMappingItemProps;
|
private props: FieldMappingItemProps;
|
||||||
private fieldContainer: HTMLDivElement;
|
private fieldContainer!: HTMLDivElement;
|
||||||
private fieldNameInput: HTMLInputElement;
|
private fieldNameInput!: HTMLInputElement;
|
||||||
private toJiraInput: HTMLTextAreaElement;
|
private toJiraInput!: HTMLTextAreaElement;
|
||||||
private fromJiraInput: HTMLTextAreaElement;
|
private fromJiraInput!: HTMLTextAreaElement;
|
||||||
|
|
||||||
constructor(props: FieldMappingItemProps) {
|
constructor(props: FieldMappingItemProps) {
|
||||||
this.props = props;
|
this.props = props;
|
||||||
|
|
@ -36,7 +36,7 @@ export class FieldMappingItem {
|
||||||
return {
|
return {
|
||||||
fieldName: this.fieldNameInput.value.trim(),
|
fieldName: this.fieldNameInput.value.trim(),
|
||||||
toJira: this.toJiraInput.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
|
* Render the field mapping item
|
||||||
*/
|
*/
|
||||||
private render() {
|
private render() {
|
||||||
const { container, fieldName = "", toJira = "", fromJira = "", enableValidation } = this.props;
|
const { container, fieldName = '', toJira = '', fromJira = '', enableValidation } = this.props;
|
||||||
|
|
||||||
// Create the field container
|
// Create the field container
|
||||||
this.fieldContainer = container.createDiv({ cls: "field-mapping-item" });
|
this.fieldContainer = container.createDiv({
|
||||||
|
cls: 'field-mapping-item',
|
||||||
|
});
|
||||||
|
|
||||||
// Create field name input
|
// Create field name input
|
||||||
const fieldNameContainer = this.fieldContainer.createDiv({ cls: "field-name-container" });
|
const fieldNameContainer = this.fieldContainer.createDiv({
|
||||||
fieldNameContainer.createEl("span", { text: t("field_name"), cls: "field-mapping-label" });
|
cls: 'field-name-container',
|
||||||
this.fieldNameInput = fieldNameContainer.createEl("input", {
|
});
|
||||||
type: "text",
|
fieldNameContainer.createEl('span', {
|
||||||
|
text: t('field_name'),
|
||||||
|
cls: 'field-mapping-label',
|
||||||
|
});
|
||||||
|
this.fieldNameInput = fieldNameContainer.createEl('input', {
|
||||||
|
type: 'text',
|
||||||
value: fieldName,
|
value: fieldName,
|
||||||
cls: "field-name-input",
|
cls: 'field-name-input',
|
||||||
placeholder: t("field_name_placeholder") || "e.g., summary, assignee, priority"
|
placeholder: t('field_name_placeholder') || 'e.g., summary, assignee, priority',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create functions container (grid layout)
|
// 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
|
// Create toJira function input
|
||||||
const toJiraContainer = functionsContainer.createDiv({ cls: "to-jira-container" });
|
const toJiraContainer = functionsContainer.createDiv({
|
||||||
toJiraContainer.createEl("span", { text: t("to_jira"), cls: "field-mapping-label" });
|
cls: 'to-jira-container',
|
||||||
this.toJiraInput = toJiraContainer.createEl("textarea", {
|
|
||||||
cls: "to-jira-input"
|
|
||||||
});
|
});
|
||||||
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;
|
this.toJiraInput.value = toJira;
|
||||||
|
|
||||||
// Create fromJira function input
|
// Create fromJira function input
|
||||||
const fromJiraContainer = functionsContainer.createDiv({ cls: "from-jira-container" });
|
const fromJiraContainer = functionsContainer.createDiv({
|
||||||
fromJiraContainer.createEl("span", { text: t("from_jira"), cls: "field-mapping-label" });
|
cls: 'from-jira-container',
|
||||||
this.fromJiraInput = fromJiraContainer.createEl("textarea", {
|
});
|
||||||
cls: "from-jira-input"
|
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.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
|
// Add remove button
|
||||||
const removeBtn = this.fieldContainer.createEl("button", {
|
const removeBtn = this.fieldContainer.createEl('button', {
|
||||||
text: "✕",
|
text: '✕',
|
||||||
cls: "remove-field-btn",
|
cls: 'remove-field-btn',
|
||||||
attr: { 'aria-label': t("remove_field") || 'Remove field mapping' }
|
attr: { 'aria-label': t('remove_field') || 'Remove field mapping' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle field removal
|
// Handle field removal
|
||||||
removeBtn.addEventListener("click", () => {
|
removeBtn.addEventListener('click', () => {
|
||||||
this.fieldContainer.remove();
|
this.fieldContainer.remove();
|
||||||
this.props.onRemove();
|
this.props.onRemove();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import { Setting, setIcon } from "obsidian";
|
import { Setting, setIcon } from 'obsidian';
|
||||||
import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes";
|
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
|
||||||
import { FieldMappingItem } from "./FieldMappingItem";
|
import { FieldMappingItem } from './FieldMappingItem';
|
||||||
import { collectFieldMappingsFromUI, processMappings } from "../tools/mappingTransformers";
|
import { collectFieldMappingsFromUI, processMappings } from '../tools/mappingTransformers';
|
||||||
import {jiraFunctionToString, transform_string_to_functions_mappings} from "../../tools/convertFunctionString";
|
import { jiraFunctionToString, transform_string_to_functions_mappings } from '../../tools/convertFunctionString';
|
||||||
import { obsidianJiraFieldMappings } from "../../default/obsidianJiraFieldsMapping";
|
import { obsidianJiraFieldMappings } from '../../default/obsidianJiraFieldsMapping';
|
||||||
import { debugLog } from "../../tools/debugLogging";
|
import { debugLog } from '../../tools/debugLogging';
|
||||||
import {useTranslations} from "../../localization/translator";
|
import { useTranslations } from '../../localization/translator';
|
||||||
|
|
||||||
const t = useTranslations("settings.fm").t;
|
const t = useTranslations('settings.fm').t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component for field mappings section
|
* Component for field mappings section
|
||||||
*/
|
*/
|
||||||
export class FieldMappingsComponent implements SettingsComponent {
|
export class FieldMappingsComponent implements SettingsComponent {
|
||||||
private props: SettingsComponentProps;
|
private props: SettingsComponentProps;
|
||||||
private fieldsList: HTMLDivElement;
|
private fieldsList!: HTMLDivElement;
|
||||||
private mappingItems: FieldMappingItem[] = [];
|
private mappingItems: FieldMappingItem[] = [];
|
||||||
|
|
||||||
constructor(props: SettingsComponentProps) {
|
constructor(props: SettingsComponentProps) {
|
||||||
|
|
@ -28,60 +28,69 @@ export class FieldMappingsComponent implements SettingsComponent {
|
||||||
const { plugin } = this.props;
|
const { plugin } = this.props;
|
||||||
|
|
||||||
// Create the mapping section
|
// Create the mapping section
|
||||||
const mappingSection = containerEl.createDiv({ cls: "jira-field-mappings" });
|
const mappingSection = containerEl.createDiv({
|
||||||
|
cls: 'jira-field-mappings',
|
||||||
|
});
|
||||||
|
|
||||||
// Add explanation
|
// Add explanation
|
||||||
mappingSection.createEl("p", {
|
mappingSection.createEl('p', {
|
||||||
text: t("desc")
|
text: t('desc'),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add validation toggle
|
// Add validation toggle
|
||||||
new Setting(mappingSection)
|
new Setting(mappingSection)
|
||||||
.setName(t("fv.name"))
|
.setName(t('fv.name'))
|
||||||
.setDesc(t("fv.desc"))
|
.setDesc(t('fv.desc'))
|
||||||
.addToggle(toggle => toggle
|
.addToggle((toggle) =>
|
||||||
.setValue(plugin.settings.fieldMapping.enableFieldValidation)
|
toggle.setValue(plugin.settings.fieldMapping.enableFieldValidation).onChange(async (value) => {
|
||||||
.onChange(async (value) => {
|
|
||||||
plugin.settings.fieldMapping.enableFieldValidation = value;
|
plugin.settings.fieldMapping.enableFieldValidation = value;
|
||||||
await plugin.saveSettings();
|
await plugin.saveSettings();
|
||||||
|
|
||||||
// Update validation for all mapping items
|
// Update validation for all mapping items
|
||||||
this.mappingItems.forEach(item => {
|
this.mappingItems.forEach((item) => {
|
||||||
item.updateValidation(value);
|
item.updateValidation(value);
|
||||||
});
|
});
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Create list container for field mappings
|
// 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
|
// Create button container
|
||||||
const buttonView = mappingSection.createDiv({ cls: "button-view" });
|
const buttonView = mappingSection.createDiv({ cls: 'button-view' });
|
||||||
|
|
||||||
// Add button to add new field mapping
|
// Add button to add new field mapping
|
||||||
const addFieldBtn = buttonView.createEl("button", {
|
const addFieldBtn = buttonView.createEl('button', {
|
||||||
text: t("add_mapping") || "Add Mapping",
|
text: t('add_mapping') || 'Add Mapping',
|
||||||
cls: "add-field-mapping-btn",
|
cls: 'add-field-mapping-btn',
|
||||||
attr: { 'data-tooltip': t("add_mapping_tooltip") || "Add new field mapping" }
|
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();
|
this.addFieldMapping();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset button
|
// Reset button
|
||||||
const resetBtn = buttonView.createEl("button", {
|
const resetBtn = buttonView.createEl('button', {
|
||||||
text: t("reset") || "Reset All",
|
text: t('reset') || 'Reset All',
|
||||||
cls: "reset-field-mappings-btn",
|
cls: 'reset-field-mappings-btn',
|
||||||
attr: { 'data-tooltip': t("reset_tooltip") || "Clear all field mappings" }
|
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
|
// Add confirmation dialog
|
||||||
const confirmed = await this.showConfirmDialog(
|
const confirmed = await this.showConfirmDialog(
|
||||||
t("reset_confirm_title") || "Reset Field Mappings",
|
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_text') ||
|
||||||
|
'Are you sure you want to clear all field mappings? This action cannot be undone.',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
|
|
@ -93,17 +102,19 @@ export class FieldMappingsComponent implements SettingsComponent {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reload default mappings button
|
// Reload default mappings button
|
||||||
const reloadBtn = buttonView.createEl("button", {
|
const reloadBtn = buttonView.createEl('button', {
|
||||||
text: t("reload_defaults") || "Load Defaults",
|
text: t('reload_defaults') || 'Load Defaults',
|
||||||
cls: "reload-field-mappings-btn",
|
cls: 'reload-field-mappings-btn',
|
||||||
attr: { 'data-tooltip': t("reload_defaults_tooltip") || "Restore default field mappings" }
|
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(
|
const confirmed = await this.showConfirmDialog(
|
||||||
t("reload_confirm_title") || "Load Default Mappings",
|
t('reload_confirm_title') || 'Load Default Mappings',
|
||||||
t("reload_confirm_text") || "This will replace all current mappings with defaults. Continue?"
|
t('reload_confirm_text') || 'This will replace all current mappings with defaults. Continue?',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
|
|
@ -114,7 +125,7 @@ export class FieldMappingsComponent implements SettingsComponent {
|
||||||
for (const [fieldName, mapping] of Object.entries(obsidianJiraFieldMappings)) {
|
for (const [fieldName, mapping] of Object.entries(obsidianJiraFieldMappings)) {
|
||||||
defaultMappingsStrings[fieldName] = {
|
defaultMappingsStrings[fieldName] = {
|
||||||
toJira: jiraFunctionToString(mapping.toJira, false),
|
toJira: jiraFunctionToString(mapping.toJira, false),
|
||||||
fromJira: jiraFunctionToString(mapping.fromJira, true)
|
fromJira: jiraFunctionToString(mapping.fromJira, true),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
plugin.settings.fieldMapping.fieldMappingsStrings = defaultMappingsStrings;
|
plugin.settings.fieldMapping.fieldMappingsStrings = defaultMappingsStrings;
|
||||||
|
|
@ -131,10 +142,12 @@ export class FieldMappingsComponent implements SettingsComponent {
|
||||||
this.renderExamples(mappingSection);
|
this.renderExamples(mappingSection);
|
||||||
|
|
||||||
// Add security notice
|
// Add security notice
|
||||||
const securityNote = containerEl.createEl("div", { cls: "setting-item-description" });
|
const securityNote = containerEl.createEl('div', {
|
||||||
securityNote.createEl("strong", { text: t("secNote.name") });
|
cls: 'setting-item-description',
|
||||||
|
});
|
||||||
|
securityNote.createEl('strong', { text: t('secNote.name') });
|
||||||
securityNote.createSpan({
|
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
|
* Add a new field mapping item
|
||||||
*/
|
*/
|
||||||
addFieldMapping(fieldName = "", toJira = "", fromJira = ""): FieldMappingItem {
|
addFieldMapping(fieldName = '', toJira = '', fromJira = ''): FieldMappingItem {
|
||||||
const item = new FieldMappingItem({
|
const item = new FieldMappingItem({
|
||||||
container: this.fieldsList,
|
container: this.fieldsList,
|
||||||
fieldName,
|
fieldName,
|
||||||
|
|
@ -186,7 +199,7 @@ export class FieldMappingsComponent implements SettingsComponent {
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.mappingItems.splice(index, 1);
|
this.mappingItems.splice(index, 1);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.mappingItems.push(item);
|
this.mappingItems.push(item);
|
||||||
|
|
@ -206,28 +219,28 @@ export class FieldMappingsComponent implements SettingsComponent {
|
||||||
debugLog(`Loading mapping settings`);
|
debugLog(`Loading mapping settings`);
|
||||||
|
|
||||||
// If we have string representations stored, use those
|
// If we have string representations stored, use those
|
||||||
if (plugin.settings.fieldMapping.fieldMappingsStrings &&
|
if (
|
||||||
Object.keys(plugin.settings.fieldMapping.fieldMappingsStrings).length > 0) {
|
plugin.settings.fieldMapping.fieldMappingsStrings &&
|
||||||
|
Object.keys(plugin.settings.fieldMapping.fieldMappingsStrings).length > 0
|
||||||
|
) {
|
||||||
debugLog('Current mapping (string) is: ', plugin.settings.fieldMapping.fieldMappingsStrings);
|
debugLog('Current mapping (string) is: ', plugin.settings.fieldMapping.fieldMappingsStrings);
|
||||||
|
|
||||||
const savedMappings = plugin.settings.fieldMapping.fieldMappingsStrings;
|
const savedMappings = plugin.settings.fieldMapping.fieldMappingsStrings;
|
||||||
for (const [fieldName, mapping] of Object.entries(savedMappings)) {
|
for (const [fieldName, mapping] of Object.entries(savedMappings)) {
|
||||||
if (mapping && typeof mapping === 'object' && 'toJira' in mapping && 'fromJira' in mapping) {
|
if (mapping && typeof mapping === 'object' && 'toJira' in mapping && 'fromJira' in mapping) {
|
||||||
this.addFieldMapping(
|
this.addFieldMapping(fieldName, mapping.toJira, mapping.fromJira);
|
||||||
fieldName,
|
|
||||||
mapping.toJira,
|
|
||||||
mapping.fromJira
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
plugin.settings.fieldMapping.fieldMappings = await transform_string_to_functions_mappings(
|
plugin.settings.fieldMapping.fieldMappings = await transform_string_to_functions_mappings(
|
||||||
plugin.settings.fieldMapping.fieldMappingsStrings,
|
plugin.settings.fieldMapping.fieldMappingsStrings,
|
||||||
plugin.settings.fieldMapping.enableFieldValidation
|
plugin.settings.fieldMapping.enableFieldValidation,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Otherwise, try to use the function mappings and convert them to strings
|
// Otherwise, try to use the function mappings and convert them to strings
|
||||||
else if (plugin.settings.fieldMapping.fieldMappings &&
|
else if (
|
||||||
Object.keys(plugin.settings.fieldMapping.fieldMappings).length > 0) {
|
plugin.settings.fieldMapping.fieldMappings &&
|
||||||
|
Object.keys(plugin.settings.fieldMapping.fieldMappings).length > 0
|
||||||
|
) {
|
||||||
debugLog('Current mapping is: ', plugin.settings.fieldMapping.fieldMappings);
|
debugLog('Current mapping is: ', plugin.settings.fieldMapping.fieldMappings);
|
||||||
|
|
||||||
const existingMappings = plugin.settings.fieldMapping.fieldMappings;
|
const existingMappings = plugin.settings.fieldMapping.fieldMappings;
|
||||||
|
|
@ -236,7 +249,7 @@ export class FieldMappingsComponent implements SettingsComponent {
|
||||||
this.addFieldMapping(
|
this.addFieldMapping(
|
||||||
fieldName,
|
fieldName,
|
||||||
jiraFunctionToString(mapping.toJira, false),
|
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> {
|
async saveFieldMappings(): Promise<void> {
|
||||||
const { plugin } = this.props;
|
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
|
// Collect mappings from UI
|
||||||
const stringMappings = await collectFieldMappingsFromUI(this.fieldsList);
|
const stringMappings = await collectFieldMappingsFromUI(this.fieldsList);
|
||||||
|
|
@ -260,7 +275,7 @@ export class FieldMappingsComponent implements SettingsComponent {
|
||||||
// Process mappings
|
// Process mappings
|
||||||
const { stringMappings: newStringMappings, functionMappings } = await processMappings(
|
const { stringMappings: newStringMappings, functionMappings } = await processMappings(
|
||||||
stringMappings,
|
stringMappings,
|
||||||
plugin.settings.fieldMapping.enableFieldValidation
|
plugin.settings.fieldMapping.enableFieldValidation,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update settings
|
// Update settings
|
||||||
|
|
@ -276,45 +291,47 @@ export class FieldMappingsComponent implements SettingsComponent {
|
||||||
* Render example mappings
|
* Render example mappings
|
||||||
*/
|
*/
|
||||||
private renderExamples(containerEl: HTMLElement): void {
|
private renderExamples(containerEl: HTMLElement): void {
|
||||||
const examplesSection = containerEl.createDiv({ cls: "mapping-examples" });
|
const examplesSection = containerEl.createDiv({
|
||||||
examplesSection.createEl("h4", { text: t("example.name") });
|
cls: 'mapping-examples',
|
||||||
|
});
|
||||||
|
examplesSection.createEl('h4', { text: t('example.name') });
|
||||||
|
|
||||||
const examplesList = examplesSection.createEl("ul");
|
const examplesList = examplesSection.createEl('ul');
|
||||||
|
|
||||||
const examples = [
|
const examples = [
|
||||||
{
|
{
|
||||||
name: "summary",
|
name: 'summary',
|
||||||
toJira: "value",
|
toJira: 'value',
|
||||||
fromJira: "issue.fields.summary"
|
fromJira: 'issue.fields.summary',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "reporter",
|
name: 'reporter',
|
||||||
toJira: "null",
|
toJira: 'null',
|
||||||
fromJira: "issue.fields.reporter.name"
|
fromJira: 'issue.fields.reporter.name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "project",
|
name: 'project',
|
||||||
toJira: "({ key: value })",
|
toJira: '({ key: value })',
|
||||||
fromJira: "issue.fields.project ? issue.fields.project.key : \"\""
|
fromJira: 'issue.fields.project ? issue.fields.project.key : ""',
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
examples.forEach(example => {
|
examples.forEach((example) => {
|
||||||
const item = examplesList.createEl("li");
|
const item = examplesList.createEl('li');
|
||||||
item.createEl("strong", { text: example.name });
|
item.createEl('strong', { text: example.name });
|
||||||
|
|
||||||
// Add button to use this example
|
// Add button to use this example
|
||||||
const useBtn = item.createEl("button", {
|
const useBtn = item.createEl('button', {
|
||||||
text: t("example.use"),
|
text: t('example.use'),
|
||||||
cls: "use-example-btn"
|
cls: 'use-example-btn',
|
||||||
});
|
});
|
||||||
|
|
||||||
item.createEl("br");
|
item.createEl('br');
|
||||||
item.createSpan({ text: `toJira: ${example.toJira}` });
|
item.createSpan({ text: `toJira: ${example.toJira}` });
|
||||||
item.createEl("br");
|
item.createEl('br');
|
||||||
item.createSpan({ text: `fromJira: ${example.fromJira}` });
|
item.createSpan({ text: `fromJira: ${example.fromJira}` });
|
||||||
|
|
||||||
useBtn.addEventListener("click", () => {
|
useBtn.addEventListener('click', () => {
|
||||||
this.addFieldMapping(example.name, example.toJira, example.fromJira);
|
this.addFieldMapping(example.name, example.toJira, example.fromJira);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { App, AbstractInputSuggest, TFolder } from "obsidian";
|
import { App, AbstractInputSuggest, TFolder } from 'obsidian';
|
||||||
import {debugLog} from "../../tools/debugLogging";
|
import { debugLog } from '../../tools/debugLogging';
|
||||||
|
|
||||||
interface FolderOption {
|
interface FolderOption {
|
||||||
path: string;
|
path: string;
|
||||||
|
|
@ -27,7 +27,7 @@ export class FolderSuggest extends AbstractInputSuggest<FolderOption> {
|
||||||
if (this.cacheValid) return;
|
if (this.cacheValid) return;
|
||||||
|
|
||||||
this.allFolders = [];
|
this.allFolders = [];
|
||||||
this.scanFolderForFolders(this.app.vault.getRoot(), "");
|
this.scanFolderForFolders(this.app.vault.getRoot(), '');
|
||||||
|
|
||||||
// Sort by display name
|
// Sort by display name
|
||||||
this.allFolders.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
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 {
|
private scanFolderForFolders(folder: TFolder, prefix: string): void {
|
||||||
// Add root folder
|
// Add root folder
|
||||||
if (prefix === "") {
|
if (prefix === '') {
|
||||||
this.allFolders.push({
|
this.allFolders.push({
|
||||||
path: "",
|
path: '',
|
||||||
displayName: "/ (root)"
|
displayName: '/ (root)',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ export class FolderSuggest extends AbstractInputSuggest<FolderOption> {
|
||||||
const displayName = prefix ? `${prefix}/${child.name}` : child.name;
|
const displayName = prefix ? `${prefix}/${child.name}` : child.name;
|
||||||
this.allFolders.push({
|
this.allFolders.push({
|
||||||
path: child.path,
|
path: child.path,
|
||||||
displayName: displayName
|
displayName: displayName,
|
||||||
});
|
});
|
||||||
// Recursively scan subfolders
|
// Recursively scan subfolders
|
||||||
this.scanFolderForFolders(child, displayName);
|
this.scanFolderForFolders(child, displayName);
|
||||||
|
|
@ -82,22 +82,22 @@ export class FolderSuggest extends AbstractInputSuggest<FolderOption> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast filtering - match both display name and path
|
// Fast filtering - match both display name and path
|
||||||
return this.allFolders.filter(option =>
|
return this.allFolders.filter(
|
||||||
option.displayName.toLowerCase().includes(query) ||
|
(option) => option.displayName.toLowerCase().includes(query) || option.path.toLowerCase().includes(query),
|
||||||
option.path.toLowerCase().includes(query)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSuggestion(folder: FolderOption, el: HTMLElement): void {
|
renderSuggestion(folder: FolderOption, el: HTMLElement): void {
|
||||||
el.setText(folder.displayName);
|
el.setText(folder.displayName);
|
||||||
el.setAttr("title", folder.path || "Root folder");
|
el.setAttr('title', folder.path || 'Root folder');
|
||||||
}
|
}
|
||||||
|
|
||||||
selectSuggestion(folder: FolderOption): void {
|
selectSuggestion(folder: FolderOption): void {
|
||||||
debugLog("selected Issues Folder", folder);
|
debugLog('selected Issues Folder', folder);
|
||||||
this.setValue(folder.path);
|
this.setValue(folder.path);
|
||||||
this.onChange && this.onChange(folder.path);
|
if (this.onChange) {
|
||||||
|
this.onChange(folder.path);
|
||||||
|
}
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { App, Setting, normalizePath } from "obsidian";
|
import { App, Setting, normalizePath } from 'obsidian';
|
||||||
import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes";
|
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
|
||||||
import { TemplateSuggest } from "./TemplateSuggest";
|
import { TemplateSuggest } from './TemplateSuggest';
|
||||||
import { FolderSuggest } from "./FolderSuggest";
|
import { FolderSuggest } from './FolderSuggest';
|
||||||
import {useTranslations} from "../../localization/translator";
|
import { useTranslations } from '../../localization/translator';
|
||||||
|
|
||||||
const t = useTranslations("settings.general").t;
|
const t = useTranslations('settings.general').t;
|
||||||
|
|
||||||
interface TemplatePluginInfo {
|
interface TemplatePluginInfo {
|
||||||
coreTemplatesEnabled: boolean;
|
coreTemplatesEnabled: boolean;
|
||||||
|
|
@ -24,17 +24,15 @@ export class GeneralSettingsComponent implements SettingsComponent {
|
||||||
const { plugin } = this.props;
|
const { plugin } = this.props;
|
||||||
|
|
||||||
// Issues folder setting with native search
|
// Issues folder setting with native search
|
||||||
const folderSetting = new Setting(containerEl)
|
const folderSetting = new Setting(containerEl).setName(t('folder.name')).setDesc(t('folder.desc'));
|
||||||
.setName(t("folder.name"))
|
|
||||||
.setDesc(t("folder.desc"));
|
|
||||||
|
|
||||||
folderSetting.addSearch((search) => {
|
folderSetting.addSearch((search) => {
|
||||||
const onChange = async (value: string) => {
|
const onChange = async (value: string) => {
|
||||||
plugin.settings.global.issuesFolder = value;
|
plugin.settings.global.issuesFolder = value;
|
||||||
await plugin.saveSettings();
|
await plugin.saveSettings();
|
||||||
}
|
};
|
||||||
search
|
search
|
||||||
.setPlaceholder(t("folder.placeholder"))
|
.setPlaceholder(t('folder.placeholder'))
|
||||||
.setValue(plugin.settings.global.issuesFolder)
|
.setValue(plugin.settings.global.issuesFolder)
|
||||||
.onChange(onChange);
|
.onChange(onChange);
|
||||||
new FolderSuggest(plugin.app, search.inputEl, onChange);
|
new FolderSuggest(plugin.app, search.inputEl, onChange);
|
||||||
|
|
@ -43,16 +41,14 @@ export class GeneralSettingsComponent implements SettingsComponent {
|
||||||
// Template path setting with native search
|
// Template path setting with native search
|
||||||
const templateInfo = this.detectTemplatePlugins(plugin.app);
|
const templateInfo = this.detectTemplatePlugins(plugin.app);
|
||||||
|
|
||||||
const setting = new Setting(containerEl)
|
const setting = new Setting(containerEl).setName(t('template.name')).setDesc(t('template.desc'));
|
||||||
.setName(t("template.name"))
|
|
||||||
.setDesc(t("template.desc"));
|
|
||||||
|
|
||||||
// Add warning if no template plugins are enabled
|
// Add warning if no template plugins are enabled
|
||||||
if (templateInfo.warningMessage) {
|
if (templateInfo.warningMessage) {
|
||||||
setting.descEl.createDiv({}, (div) => {
|
setting.descEl.createDiv({}, (div) => {
|
||||||
div.createEl("small", {
|
div.createEl('small', {
|
||||||
text: templateInfo.warningMessage,
|
text: templateInfo.warningMessage,
|
||||||
cls: "mod-warning"
|
cls: 'mod-warning',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -62,16 +58,15 @@ export class GeneralSettingsComponent implements SettingsComponent {
|
||||||
const onChange = async (value: string) => {
|
const onChange = async (value: string) => {
|
||||||
plugin.settings.global.templatePath = value ? normalizePath(value) : '';
|
plugin.settings.global.templatePath = value ? normalizePath(value) : '';
|
||||||
await plugin.saveSettings();
|
await plugin.saveSettings();
|
||||||
}
|
};
|
||||||
search
|
search
|
||||||
.setPlaceholder(t("template.selector.placeholder"))
|
.setPlaceholder(t('template.selector.placeholder'))
|
||||||
.setValue(plugin.settings.global.templatePath || "")
|
.setValue(plugin.settings.global.templatePath || '')
|
||||||
.onChange(onChange);
|
.onChange(onChange);
|
||||||
new TemplateSuggest(plugin.app, search.inputEl, templateInfo.templateDirectory, onChange);
|
new TemplateSuggest(plugin.app, search.inputEl, templateInfo.templateDirectory, onChange);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private detectTemplatePlugins(app: App): TemplatePluginInfo {
|
private detectTemplatePlugins(app: App): TemplatePluginInfo {
|
||||||
const coreTemplates = (app as any).internalPlugins?.plugins?.templates;
|
const coreTemplates = (app as any).internalPlugins?.plugins?.templates;
|
||||||
const templaterPlugin = (app as any).plugins?.plugins?.['templater-obsidian'];
|
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;
|
const templaterEnabled = (app as any).plugins?.enabledPlugins?.has('templater-obsidian') || false;
|
||||||
|
|
||||||
let templateDirectory: string | null = null;
|
let templateDirectory: string | null = null;
|
||||||
let warningMessage = "";
|
let warningMessage = '';
|
||||||
|
|
||||||
if (coreTemplatesEnabled && coreTemplates.instance?.options?.folder) {
|
if (coreTemplatesEnabled && coreTemplates.instance?.options?.folder) {
|
||||||
templateDirectory = coreTemplates.instance.options.folder;
|
templateDirectory = coreTemplates.instance.options.folder;
|
||||||
|
|
@ -89,14 +84,14 @@ export class GeneralSettingsComponent implements SettingsComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!coreTemplatesEnabled && !templaterEnabled) {
|
if (!coreTemplatesEnabled && !templaterEnabled) {
|
||||||
warningMessage = t("template.warning");
|
warningMessage = t('template.warning');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
coreTemplatesEnabled,
|
coreTemplatesEnabled,
|
||||||
templaterEnabled,
|
templaterEnabled,
|
||||||
templateDirectory,
|
templateDirectory,
|
||||||
warningMessage
|
warningMessage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import {fetchCountIssuesByJQL, fetchIssuesByJQLRaw} from "../../api";
|
import { fetchCountIssuesByJQL, fetchIssuesByJQLRaw } from '../../api';
|
||||||
import JiraPlugin from "../../main";
|
import JiraPlugin from '../../main';
|
||||||
import {useTranslations} from "../../localization/translator";
|
import { useTranslations } from '../../localization/translator';
|
||||||
|
|
||||||
const t = useTranslations("modals.jql_search").t;
|
const t = useTranslations('modals.jql_search').t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable JQL Preview component
|
* Reusable JQL Preview component
|
||||||
|
|
@ -23,49 +23,55 @@ export class JQLPreview {
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupContainer() {
|
private setupContainer() {
|
||||||
this.containerEl.addClass("jql-preview-container");
|
this.containerEl.addClass('jql-preview-container');
|
||||||
this.renderSkeleton();
|
this.renderSkeleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSkeleton() {
|
private renderSkeleton() {
|
||||||
this.containerEl.empty();
|
this.containerEl.empty();
|
||||||
|
|
||||||
this.loadingEl = this.containerEl.createDiv({ cls: "jql-preview-loading" });
|
this.loadingEl = this.containerEl.createDiv({
|
||||||
this.loadingEl.createDiv({
|
cls: 'jql-preview-loading',
|
||||||
cls: "jql-preview-loading-icon",
|
|
||||||
text: "🔍"
|
|
||||||
});
|
});
|
||||||
this.loadingEl.createDiv({
|
this.loadingEl.createDiv({
|
||||||
cls: "jql-preview-loading-text",
|
cls: 'jql-preview-loading-icon',
|
||||||
text: t("preview.loading")
|
text: '🔍',
|
||||||
|
});
|
||||||
|
this.loadingEl.createDiv({
|
||||||
|
cls: 'jql-preview-loading-text',
|
||||||
|
text: t('preview.loading'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderEmpty() {
|
private renderEmpty() {
|
||||||
this.containerEl.empty();
|
this.containerEl.empty();
|
||||||
|
|
||||||
const emptyEl = this.containerEl.createDiv({ cls: "jql-preview-empty" });
|
const emptyEl = this.containerEl.createDiv({
|
||||||
emptyEl.createDiv({
|
cls: 'jql-preview-empty',
|
||||||
cls: "jql-preview-empty-icon",
|
|
||||||
text: "📝"
|
|
||||||
});
|
});
|
||||||
emptyEl.createDiv({
|
emptyEl.createDiv({
|
||||||
cls: "jql-preview-empty-text",
|
cls: 'jql-preview-empty-icon',
|
||||||
text: t("preview.empty_input")
|
text: '📝',
|
||||||
|
});
|
||||||
|
emptyEl.createDiv({
|
||||||
|
cls: 'jql-preview-empty-text',
|
||||||
|
text: t('preview.empty_input'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderError(error: string) {
|
private renderError(error: string) {
|
||||||
this.containerEl.empty();
|
this.containerEl.empty();
|
||||||
|
|
||||||
const errorEl = this.containerEl.createDiv({ cls: "jql-preview-error" });
|
const errorEl = this.containerEl.createDiv({
|
||||||
errorEl.createDiv({
|
cls: 'jql-preview-error',
|
||||||
cls: "jql-preview-error-icon",
|
|
||||||
text: "⚠️"
|
|
||||||
});
|
});
|
||||||
errorEl.createDiv({
|
errorEl.createDiv({
|
||||||
cls: "jql-preview-error-text",
|
cls: 'jql-preview-error-icon',
|
||||||
text: t("preview.error", { error })
|
text: '⚠️',
|
||||||
|
});
|
||||||
|
errorEl.createDiv({
|
||||||
|
cls: 'jql-preview-error-text',
|
||||||
|
text: t('preview.error', { error }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,217 +80,257 @@ export class JQLPreview {
|
||||||
|
|
||||||
// Header with total count
|
// Header with total count
|
||||||
if (this.show_header) {
|
if (this.show_header) {
|
||||||
const headerEl = this.containerEl.createDiv({ cls: "jql-preview-header" });
|
const headerEl = this.containerEl.createDiv({
|
||||||
const totalEl = headerEl.createDiv({ cls: "jql-preview-total" });
|
cls: 'jql-preview-header',
|
||||||
totalEl.createSpan({ cls: "jql-preview-total-text", text: t("preview.total", { total }) });
|
});
|
||||||
|
const totalEl = headerEl.createDiv({ cls: 'jql-preview-total' });
|
||||||
|
totalEl.createSpan({
|
||||||
|
cls: 'jql-preview-total-text',
|
||||||
|
text: t('preview.total', { total }),
|
||||||
|
});
|
||||||
|
|
||||||
if (total > this.limit) {
|
if (total > this.limit) {
|
||||||
totalEl.createSpan({
|
totalEl.createSpan({
|
||||||
cls: "jql-preview-showing",
|
cls: 'jql-preview-showing',
|
||||||
text: t("preview.showing", { limit: this.limit })
|
text: t('preview.showing', { limit: this.limit }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Issues table
|
// Issues table
|
||||||
if (issues.length > 0) {
|
if (issues.length > 0) {
|
||||||
const tableWrapperEl = this.containerEl.createDiv({ cls: "jql-preview-table-wrapper" });
|
const tableWrapperEl = this.containerEl.createDiv({
|
||||||
const tableEl = tableWrapperEl.createEl("table", { cls: "jql-preview-table" });
|
cls: 'jql-preview-table-wrapper',
|
||||||
|
});
|
||||||
|
const tableEl = tableWrapperEl.createEl('table', {
|
||||||
|
cls: 'jql-preview-table',
|
||||||
|
});
|
||||||
|
|
||||||
// Table header
|
// Table header
|
||||||
const theadEl = tableEl.createEl("thead");
|
const theadEl = tableEl.createEl('thead');
|
||||||
const headerRowEl = theadEl.createEl("tr");
|
const headerRowEl = theadEl.createEl('tr');
|
||||||
|
|
||||||
// Header columns
|
// Header columns
|
||||||
headerRowEl.createEl("th", {
|
headerRowEl.createEl('th', {
|
||||||
cls: "jql-preview-th-key",
|
cls: 'jql-preview-th-key',
|
||||||
text: "Key"
|
text: 'Key',
|
||||||
});
|
});
|
||||||
headerRowEl.createEl("th", {
|
headerRowEl.createEl('th', {
|
||||||
cls: "jql-preview-th-summary",
|
cls: 'jql-preview-th-summary',
|
||||||
text: "Summary"
|
text: 'Summary',
|
||||||
});
|
});
|
||||||
headerRowEl.createEl("th", {
|
headerRowEl.createEl('th', {
|
||||||
cls: "jql-preview-th-icon",
|
cls: 'jql-preview-th-icon',
|
||||||
text: "T"
|
text: 'T',
|
||||||
});
|
});
|
||||||
headerRowEl.createEl("th", {
|
headerRowEl.createEl('th', {
|
||||||
cls: "jql-preview-th-date",
|
cls: 'jql-preview-th-date',
|
||||||
text: "Created"
|
text: 'Created',
|
||||||
});
|
});
|
||||||
headerRowEl.createEl("th", {
|
headerRowEl.createEl('th', {
|
||||||
cls: "jql-preview-th-date",
|
cls: 'jql-preview-th-date',
|
||||||
text: "Updated"
|
text: 'Updated',
|
||||||
});
|
});
|
||||||
headerRowEl.createEl("th", {
|
headerRowEl.createEl('th', {
|
||||||
cls: "jql-preview-th-icon",
|
cls: 'jql-preview-th-icon',
|
||||||
text: "R"
|
text: 'R',
|
||||||
});
|
});
|
||||||
headerRowEl.createEl("th", {
|
headerRowEl.createEl('th', {
|
||||||
cls: "jql-preview-th-icon",
|
cls: 'jql-preview-th-icon',
|
||||||
text: "A"
|
text: 'A',
|
||||||
});
|
});
|
||||||
headerRowEl.createEl("th", {
|
headerRowEl.createEl('th', {
|
||||||
cls: "jql-preview-th-icon",
|
cls: 'jql-preview-th-icon',
|
||||||
text: "P"
|
text: 'P',
|
||||||
});
|
});
|
||||||
headerRowEl.createEl("th", {
|
headerRowEl.createEl('th', {
|
||||||
cls: "jql-preview-th-status",
|
cls: 'jql-preview-th-status',
|
||||||
text: "Status"
|
text: 'Status',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Table body
|
// Table body
|
||||||
const tbodyEl = tableEl.createEl("tbody");
|
const tbodyEl = tableEl.createEl('tbody');
|
||||||
|
const conn = this.plugin.getCurrentConnection();
|
||||||
|
const jiraUrl = conn?.jiraUrl || '';
|
||||||
|
|
||||||
issues.forEach((issue, _) => {
|
issues.forEach((issue) => {
|
||||||
const rowEl = tbodyEl.createEl("tr", { cls: "jql-preview-row" });
|
const rowEl = tbodyEl.createEl('tr', {
|
||||||
|
cls: 'jql-preview-row',
|
||||||
|
});
|
||||||
|
|
||||||
// Key column
|
// Key column
|
||||||
const keyCellEl = rowEl.createEl("td", { cls: "jql-preview-cell-key" });
|
const keyCellEl = rowEl.createEl('td', {
|
||||||
const keyLinkEl = keyCellEl.createEl("a", {
|
cls: 'jql-preview-cell-key',
|
||||||
cls: "jql-preview-key-link",
|
});
|
||||||
text: issue.key || "N/A",
|
keyCellEl.createEl('a', {
|
||||||
|
cls: 'jql-preview-key-link',
|
||||||
|
text: issue.key || 'N/A',
|
||||||
attr: {
|
attr: {
|
||||||
href: `${this.plugin.settings.connection.jiraUrl}/browse/${issue.key}`,
|
href: `${jiraUrl}/browse/${issue.key}`,
|
||||||
target: "_blank",
|
target: '_blank',
|
||||||
rel: "noopener noreferrer"
|
rel: 'noopener noreferrer',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Summary column (S)
|
// Summary column (S)
|
||||||
const summaryCellEl = rowEl.createEl("td", { cls: "jql-preview-cell-summary" });
|
const summaryCellEl = rowEl.createEl('td', {
|
||||||
const summary = issue.fields?.summary || "No summary available";
|
cls: 'jql-preview-cell-summary',
|
||||||
summaryCellEl.setAttr("title", summary); // Show full text on hover
|
});
|
||||||
summaryCellEl.setText(summary.length > 80 ? summary.substring(0, 80) + "..." : 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)
|
// 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;
|
const issueType = issue.fields?.issuetype;
|
||||||
if (issueType && issueType.iconUrl) {
|
if (issueType && issueType.iconUrl) {
|
||||||
const typeIconEl = typeCellEl.createEl("img", {
|
typeCellEl.createEl('img', {
|
||||||
attr: { src: issueType.iconUrl, alt: issueType.name },
|
attr: { src: issueType.iconUrl, alt: issueType.name },
|
||||||
cls: "jql-preview-type-icon"
|
cls: 'jql-preview-type-icon',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Fallback icon for unknown type
|
// Fallback icon for unknown type
|
||||||
const fallbackIconEl = typeCellEl.createEl("div", {
|
typeCellEl.createEl('div', {
|
||||||
cls: "jql-preview-status-indicator status-undefined"
|
cls: 'jql-preview-status-indicator status-undefined',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Created date column
|
// 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;
|
const createdDate = issue.fields?.created;
|
||||||
if (createdDate) {
|
if (createdDate) {
|
||||||
const date = new Date(createdDate);
|
const date = new Date(createdDate);
|
||||||
createdCellEl.setText(date.toLocaleDateString("en-US", {
|
createdCellEl.setText(
|
||||||
month: "numeric",
|
date.toLocaleDateString('en-US', {
|
||||||
day: "numeric",
|
month: 'numeric',
|
||||||
year: "numeric"
|
day: 'numeric',
|
||||||
}));
|
year: 'numeric',
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
createdCellEl.setText("N/A");
|
createdCellEl.setText('N/A');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated date column
|
// 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;
|
const updatedDate = issue.fields?.updated;
|
||||||
if (updatedDate) {
|
if (updatedDate) {
|
||||||
const date = new Date(updatedDate);
|
const date = new Date(updatedDate);
|
||||||
updatedCellEl.setText(date.toLocaleDateString("en-US", {
|
updatedCellEl.setText(
|
||||||
month: "numeric",
|
date.toLocaleDateString('en-US', {
|
||||||
day: "numeric",
|
month: 'numeric',
|
||||||
year: "numeric"
|
day: 'numeric',
|
||||||
}));
|
year: 'numeric',
|
||||||
|
}),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
updatedCellEl.setText("N/A");
|
updatedCellEl.setText('N/A');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reporter column (R)
|
// 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;
|
const reporter = issue.fields?.reporter;
|
||||||
if (reporter) {
|
if (reporter) {
|
||||||
const reporterLinkEl = reporterCellEl.createEl("a", {
|
const reporterLinkEl = reporterCellEl.createEl('a', {
|
||||||
attr: {
|
attr: {
|
||||||
href: `${this.plugin.settings.connection.jiraUrl}/secure/ViewProfile.jspa?name=${reporter.name}`,
|
href: `${jiraUrl}/secure/ViewProfile.jspa?name=${reporter.name}`,
|
||||||
target: "_blank",
|
target: '_blank',
|
||||||
rel: "noopener noreferrer",
|
rel: 'noopener noreferrer',
|
||||||
title: reporter.displayName || reporter.name
|
title: reporter.displayName || reporter.name,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if (reporter.avatarUrls && reporter.avatarUrls["16x16"]) {
|
if (reporter.avatarUrls && reporter.avatarUrls['16x16']) {
|
||||||
const avatarEl = reporterLinkEl.createEl("img", {
|
reporterLinkEl.createEl('img', {
|
||||||
attr: { src: reporter.avatarUrls["16x16"], alt: reporter.displayName },
|
attr: {
|
||||||
cls: "jql-preview-user-avatar"
|
src: reporter.avatarUrls['16x16'],
|
||||||
|
alt: reporter.displayName,
|
||||||
|
},
|
||||||
|
cls: 'jql-preview-user-avatar',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const initialEl = reporterLinkEl.createEl("div", {
|
reporterLinkEl.createEl('div', {
|
||||||
cls: "jql-preview-user-initial",
|
cls: 'jql-preview-user-initial',
|
||||||
text: reporter.displayName ? reporter.displayName.charAt(0).toUpperCase() : "?"
|
text: reporter.displayName ? reporter.displayName.charAt(0).toUpperCase() : '?',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const unknownEl = reporterCellEl.createEl("div", {
|
reporterCellEl.createEl('div', {
|
||||||
cls: "jql-preview-status-indicator status-undefined"
|
cls: 'jql-preview-status-indicator status-undefined',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assignee column (A)
|
// 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;
|
const assignee = issue.fields?.assignee;
|
||||||
if (assignee) {
|
if (assignee) {
|
||||||
const assigneeLinkEl = assigneeCellEl.createEl("a", {
|
const assigneeLinkEl = assigneeCellEl.createEl('a', {
|
||||||
attr: {
|
attr: {
|
||||||
href: `${this.plugin.settings.connection.jiraUrl}/secure/ViewProfile.jspa?name=${assignee.name}`,
|
href: `${jiraUrl}/secure/ViewProfile.jspa?name=${assignee.name}`,
|
||||||
target: "_blank",
|
target: '_blank',
|
||||||
rel: "noopener noreferrer",
|
rel: 'noopener noreferrer',
|
||||||
title: assignee.displayName || assignee.name
|
title: assignee.displayName || assignee.name,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if (assignee.avatarUrls && assignee.avatarUrls["16x16"]) {
|
if (assignee.avatarUrls && assignee.avatarUrls['16x16']) {
|
||||||
const avatarEl = assigneeLinkEl.createEl("img", {
|
assigneeLinkEl.createEl('img', {
|
||||||
attr: { src: assignee.avatarUrls["16x16"], alt: assignee.displayName },
|
attr: {
|
||||||
cls: "jql-preview-user-avatar"
|
src: assignee.avatarUrls['16x16'],
|
||||||
|
alt: assignee.displayName,
|
||||||
|
},
|
||||||
|
cls: 'jql-preview-user-avatar',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const initialEl = assigneeLinkEl.createEl("div", {
|
assigneeLinkEl.createEl('div', {
|
||||||
cls: "jql-preview-user-initial",
|
cls: 'jql-preview-user-initial',
|
||||||
text: assignee.displayName ? assignee.displayName.charAt(0).toUpperCase() : "?"
|
text: assignee.displayName ? assignee.displayName.charAt(0).toUpperCase() : '?',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const unknownEl = assigneeCellEl.createEl("div", {
|
assigneeCellEl.createEl('div', {
|
||||||
cls: "jql-preview-status-indicator status-undefined"
|
cls: 'jql-preview-status-indicator status-undefined',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority column (P)
|
// 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;
|
const priority = issue.fields?.priority;
|
||||||
if (priority && priority.iconUrl) {
|
if (priority && priority.iconUrl) {
|
||||||
const priorityIconEl = priorityCellEl.createEl("img", {
|
priorityCellEl.createEl('img', {
|
||||||
attr: { src: priority.iconUrl, alt: priority.name },
|
attr: { src: priority.iconUrl, alt: priority.name },
|
||||||
cls: "jql-preview-priority-icon"
|
cls: 'jql-preview-priority-icon',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const unknownEl = priorityCellEl.createEl("div", {
|
priorityCellEl.createEl('div', {
|
||||||
cls: "jql-preview-status-indicator status-undefined"
|
cls: 'jql-preview-status-indicator status-undefined',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status column
|
// 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;
|
const status = issue.fields?.status;
|
||||||
if (status) {
|
if (status) {
|
||||||
const statusBadgeEl = statusCellEl.createEl("span", {
|
statusCellEl.createEl('span', {
|
||||||
cls: `jql-preview-status-badge status-${status.statusCategory?.key || 'default'}`,
|
cls: `jql-preview-status-badge status-${status.statusCategory?.key || 'default'}`,
|
||||||
text: status.name
|
text: status.name,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const statusBadgeEl = statusCellEl.createEl("span", {
|
statusCellEl.createEl('span', {
|
||||||
cls: "jql-preview-status-badge status-default",
|
cls: 'jql-preview-status-badge status-default',
|
||||||
text: "Unknown"
|
text: 'Unknown',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -306,15 +352,25 @@ export class JQLPreview {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Enhanced fields for better preview (matching Jira table columns)
|
// 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);
|
const res = await fetchIssuesByJQLRaw(this.plugin, trimmedJql, this.limit, fields);
|
||||||
|
|
||||||
let total = 0;
|
let total = 0;
|
||||||
switch (this.plugin.settings.connection.apiVersion) {
|
switch (this.plugin.getCurrentConnection()?.apiVersion) {
|
||||||
case "2":
|
case '2':
|
||||||
total = res.total;
|
total = res.total;
|
||||||
break;
|
break;
|
||||||
case "3":
|
case '3':
|
||||||
total = await fetchCountIssuesByJQL(this.plugin, trimmedJql);
|
total = await fetchCountIssuesByJQL(this.plugin, trimmedJql);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { App, AbstractInputSuggest, TFile, TFolder } from "obsidian";
|
import { App, AbstractInputSuggest, TFile, TFolder } from 'obsidian';
|
||||||
import {debugLog} from "../../tools/debugLogging";
|
import { debugLog } from '../../tools/debugLogging';
|
||||||
|
|
||||||
interface TemplateOption {
|
interface TemplateOption {
|
||||||
path: string;
|
path: string;
|
||||||
|
|
@ -15,7 +15,12 @@ export class TemplateSuggest extends AbstractInputSuggest<TemplateOption> {
|
||||||
private cacheValid: boolean = false;
|
private cacheValid: boolean = false;
|
||||||
private onChange?: (value: string) => void;
|
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);
|
super(app, inputEl);
|
||||||
this.templateDirectory = templateDirectory;
|
this.templateDirectory = templateDirectory;
|
||||||
this.onChange = onChange;
|
this.onChange = onChange;
|
||||||
|
|
@ -34,9 +39,9 @@ export class TemplateSuggest extends AbstractInputSuggest<TemplateOption> {
|
||||||
: this.app.vault.getRoot();
|
: this.app.vault.getRoot();
|
||||||
|
|
||||||
if (searchFolder instanceof TFolder) {
|
if (searchFolder instanceof TFolder) {
|
||||||
this.scanFolderForTemplates(searchFolder, "");
|
this.scanFolderForTemplates(searchFolder, '');
|
||||||
} else {
|
} else {
|
||||||
this.scanFolderForTemplates(this.app.vault.getRoot(), "");
|
this.scanFolderForTemplates(this.app.vault.getRoot(), '');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.allTemplates.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
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;
|
const displayName = prefix ? `${prefix}/${child.basename}` : child.basename;
|
||||||
this.allTemplates.push({
|
this.allTemplates.push({
|
||||||
path: child.path,
|
path: child.path,
|
||||||
displayName: displayName
|
displayName: displayName,
|
||||||
});
|
});
|
||||||
} else if (child instanceof TFolder) {
|
} else if (child instanceof TFolder) {
|
||||||
const newPrefix = prefix ? `${prefix}/${child.name}` : child.name;
|
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
|
// Fast filtering - only filter if we have a query
|
||||||
return this.allTemplates.filter(option =>
|
return this.allTemplates.filter(
|
||||||
option.displayName.toLowerCase().includes(query) ||
|
(option) => option.displayName.toLowerCase().includes(query) || option.path.toLowerCase().includes(query),
|
||||||
option.path.toLowerCase().includes(query)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSuggestion(template: TemplateOption, el: HTMLElement): void {
|
renderSuggestion(template: TemplateOption, el: HTMLElement): void {
|
||||||
el.setText(template.displayName);
|
el.setText(template.displayName);
|
||||||
el.setAttr("title", template.path);
|
el.setAttr('title', template.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectSuggestion(template: TemplateOption): void {
|
selectSuggestion(template: TemplateOption): void {
|
||||||
debugLog("selected Templates Folder", template);
|
debugLog('selected Templates Folder', template);
|
||||||
this.setValue(template.path);
|
this.setValue(template.path);
|
||||||
this.onChange && this.onChange(template.path);
|
if (this.onChange) {
|
||||||
|
this.onChange(template.path);
|
||||||
|
}
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,139 +1,157 @@
|
||||||
import { SettingsComponent, SettingsComponentProps } from "../../interfaces/settingsTypes";
|
import { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
|
||||||
import debounce from "lodash/debounce";
|
import debounce from 'lodash/debounce';
|
||||||
import { safeStringToFunction } from "../../tools/convertFunctionString";
|
import { safeStringToFunction } from '../../tools/convertFunctionString';
|
||||||
import {useTranslations} from "../../localization/translator";
|
import { useTranslations } from '../../localization/translator';
|
||||||
import {setIcon} from "obsidian";
|
import { setIcon } from 'obsidian';
|
||||||
|
|
||||||
const t = useTranslations("settings.tfm").t;
|
const t = useTranslations('settings.tfm').t;
|
||||||
|
|
||||||
export class TestFieldMappingsComponent implements SettingsComponent {
|
export class TestFieldMappingsComponent implements SettingsComponent {
|
||||||
private props: SettingsComponentProps;
|
private props: SettingsComponentProps;
|
||||||
private testMappingsContainer: HTMLElement | null = null;
|
private testMappingsContainer: HTMLElement | null = null;
|
||||||
private currentIssueData: any = null;
|
private currentIssueData: any = null;
|
||||||
private getCurrentIssue: (() => any) | null = null;
|
private getCurrentIssue: (() => any) | null = null;
|
||||||
private _testMappingItemsContainer: HTMLElement | null = null;
|
private _testMappingItemsContainer: HTMLElement | null = null;
|
||||||
|
|
||||||
constructor(props: SettingsComponentProps & { getCurrentIssue?: () => any }) {
|
constructor(props: SettingsComponentProps & { getCurrentIssue?: () => any }) {
|
||||||
this.props = props;
|
this.props = props;
|
||||||
if (props.getCurrentIssue) {
|
if (props.getCurrentIssue) {
|
||||||
this.getCurrentIssue = props.getCurrentIssue;
|
this.getCurrentIssue = props.getCurrentIssue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render(containerEl: HTMLElement): void {
|
render(containerEl: HTMLElement): void {
|
||||||
// Add section header
|
// Add section header
|
||||||
this.testMappingsContainer = containerEl.createDiv({
|
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"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Container for all test mapping items
|
this.testMappingsContainer.createEl('p', {
|
||||||
this._testMappingItemsContainer = this.testMappingsContainer.createDiv({ cls: "test-mapping-items-list" });
|
text: t('desc'),
|
||||||
|
cls: 'test-mappings-desc',
|
||||||
// Add first test mapping item
|
|
||||||
this.addTestMappingItem();
|
|
||||||
|
|
||||||
const buttonView = this.testMappingsContainer.createDiv({ cls: "button-view" });
|
|
||||||
|
|
||||||
// Add new mapping button (only once, at the bottom)
|
|
||||||
const addBtn = buttonView.createEl("button", {
|
|
||||||
text: t("add_mapping") || "Add Mapping",
|
|
||||||
cls: "add-field-mapping-btn",
|
|
||||||
attr: { 'data-tooltip': t("add_mapping_tooltip") || "Add new field mapping" }
|
|
||||||
});
|
});
|
||||||
setIcon(addBtn, "circle-plus");
|
|
||||||
addBtn.addEventListener("click", () => {
|
|
||||||
this.addTestMappingItem();
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetBtn = buttonView.createEl("button", {
|
// Container for all test mapping items
|
||||||
text: t("reset") || "Reset All",
|
this._testMappingItemsContainer = this.testMappingsContainer.createDiv({
|
||||||
cls: "reset-field-mappings-btn",
|
cls: 'test-mapping-items-list',
|
||||||
attr: { 'data-tooltip': t("reset_tooltip") || "Clear all field mappings" }
|
|
||||||
});
|
});
|
||||||
setIcon(resetBtn, "refresh-cw");
|
|
||||||
resetBtn.addEventListener("click", () => {
|
// Add first test mapping item
|
||||||
|
this.addTestMappingItem();
|
||||||
|
|
||||||
|
const buttonView = this.testMappingsContainer.createDiv({
|
||||||
|
cls: 'button-view',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add new mapping button (only once, at the bottom)
|
||||||
|
const addBtn = buttonView.createEl('button', {
|
||||||
|
text: t('add_mapping') || 'Add Mapping',
|
||||||
|
cls: 'add-field-mapping-btn',
|
||||||
|
attr: {
|
||||||
|
'data-tooltip': t('add_mapping_tooltip') || 'Add new field mapping',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setIcon(addBtn, 'circle-plus');
|
||||||
|
addBtn.addEventListener('click', () => {
|
||||||
|
this.addTestMappingItem();
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetBtn = buttonView.createEl('button', {
|
||||||
|
text: t('reset') || 'Reset All',
|
||||||
|
cls: 'reset-field-mappings-btn',
|
||||||
|
attr: {
|
||||||
|
'data-tooltip': t('reset_tooltip') || 'Clear all field mappings',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setIcon(resetBtn, 'refresh-cw');
|
||||||
|
resetBtn.addEventListener('click', () => {
|
||||||
this._testMappingItemsContainer?.empty();
|
this._testMappingItemsContainer?.empty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private addTestMappingItem(): void {
|
private addTestMappingItem(): void {
|
||||||
// Use the dedicated items container
|
// Use the dedicated items container
|
||||||
const itemsContainer = this._testMappingItemsContainer as HTMLElement;
|
const itemsContainer = this._testMappingItemsContainer as HTMLElement;
|
||||||
if (!itemsContainer) return;
|
if (!itemsContainer) return;
|
||||||
|
|
||||||
const itemContainer = itemsContainer.createDiv({
|
const itemContainer = itemsContainer.createDiv({
|
||||||
cls: "test-mapping-item"
|
cls: 'test-mapping-item',
|
||||||
});
|
});
|
||||||
|
|
||||||
// From Jira expression input
|
// From Jira expression input
|
||||||
const fromJiraContainer = itemContainer.createDiv({ cls: "from-jira-container" });
|
const fromJiraContainer = itemContainer.createDiv({
|
||||||
fromJiraContainer.createEl("span", { text: t("from_jira")+":", cls: "field-mapping-label" });
|
cls: 'from-jira-container',
|
||||||
|
});
|
||||||
|
fromJiraContainer.createEl('span', {
|
||||||
|
text: t('from_jira') + ':',
|
||||||
|
cls: 'field-mapping-label',
|
||||||
|
});
|
||||||
|
|
||||||
const fromJiraInput = fromJiraContainer.createEl("textarea", {
|
const fromJiraInput = fromJiraContainer.createEl('textarea', {
|
||||||
cls: "from-jira-input"
|
cls: 'from-jira-input',
|
||||||
});
|
});
|
||||||
fromJiraInput.rows = 1;
|
fromJiraInput.rows = 1;
|
||||||
fromJiraInput.placeholder = "(issue) => null";
|
fromJiraInput.placeholder = '(issue) => null';
|
||||||
|
|
||||||
// Value display
|
// Value display
|
||||||
const valueContainer = itemContainer.createDiv({ cls: "value-container" });
|
const valueContainer = itemContainer.createDiv({
|
||||||
valueContainer.createEl("span", { text: t("value")+":", cls: "field-mapping-label" });
|
cls: 'value-container',
|
||||||
const valueDisplay = valueContainer.createEl("div", {
|
});
|
||||||
cls: "value-display",
|
valueContainer.createEl('span', {
|
||||||
attr: {
|
text: t('value') + ':',
|
||||||
style: "user-select: text; -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text;"
|
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;',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Debounced evaluation function
|
// Debounced evaluation function
|
||||||
const debouncedEvaluate = debounce(async () => {
|
const debouncedEvaluate = debounce(async () => {
|
||||||
const issueData = this.getCurrentIssue ? this.getCurrentIssue() : this.currentIssueData;
|
const issueData = this.getCurrentIssue ? this.getCurrentIssue() : this.currentIssueData;
|
||||||
if (!issueData) {
|
if (!issueData) {
|
||||||
valueDisplay.setText(t("no_data"));
|
valueDisplay.setText(t('no_data'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fromJiraFn = await safeStringToFunction(fromJiraInput.value, 'fromJira', false);
|
const fromJiraFn = await safeStringToFunction(fromJiraInput.value, 'fromJira', false);
|
||||||
if (fromJiraFn) {
|
if (fromJiraFn) {
|
||||||
const result = fromJiraFn(issueData, null);
|
const result = fromJiraFn(issueData, null);
|
||||||
valueDisplay.setText(JSON.stringify(result, null, 2));
|
valueDisplay.setText(JSON.stringify(result, null, 2));
|
||||||
} else {
|
} else {
|
||||||
valueDisplay.setText(t("invalid_exp"));
|
valueDisplay.setText(t('invalid_exp'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
valueDisplay.setText(`Error: ${error.message}`);
|
valueDisplay.setText(`Error: ${(error as Error).message}`);
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
// Add event listeners
|
// Add event listeners
|
||||||
fromJiraInput.addEventListener("input", debouncedEvaluate);
|
fromJiraInput.addEventListener('input', debouncedEvaluate);
|
||||||
|
|
||||||
// Add remove button
|
// Add remove button
|
||||||
const removeBtn = itemContainer.createEl("button", {
|
const removeBtn = itemContainer.createEl('button', {
|
||||||
text: "✕",
|
text: '✕',
|
||||||
cls: "remove-field-btn"
|
cls: 'remove-field-btn',
|
||||||
});
|
});
|
||||||
removeBtn.addEventListener("click", () => {
|
removeBtn.addEventListener('click', () => {
|
||||||
itemContainer.remove();
|
itemContainer.remove();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally, allow setting the current issue externally
|
// Optionally, allow setting the current issue externally
|
||||||
setCurrentIssue(issue: any) {
|
setCurrentIssue(issue: any) {
|
||||||
this.currentIssueData = issue;
|
this.currentIssueData = issue;
|
||||||
// Trigger re-evaluation of all test mappings
|
// Trigger re-evaluation of all test mappings
|
||||||
this.testMappingsContainer?.querySelectorAll(".from-jira-input").forEach((input: HTMLTextAreaElement) => {
|
this.testMappingsContainer?.querySelectorAll('.from-jira-input').forEach((input) => {
|
||||||
input.dispatchEvent(new Event("input"));
|
(input as HTMLTextAreaElement).dispatchEvent(new Event('input'));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
hide(): void {
|
hide(): void {
|
||||||
// No persistent state to clear
|
// No persistent state to clear
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { SettingsComponent, SettingsComponentProps } from '../../interfaces/settingsTypes';
|
||||||
import { processWorkLogBatch } from "../../commands";
|
import { processWorkLogBatch } from '../../commands';
|
||||||
import {useTranslations} from "../../localization/translator";
|
import { useTranslations } from '../../localization/translator';
|
||||||
import {debugLog} from "../../tools/debugLogging";
|
import { debugLog } from '../../tools/debugLogging';
|
||||||
|
import {
|
||||||
|
parseTimeBlocks,
|
||||||
|
processTimeEntries,
|
||||||
|
ProcessedEntry,
|
||||||
|
GroupedEntries,
|
||||||
|
TimeTrackerEntry,
|
||||||
|
} from '../../tools/timeTracker';
|
||||||
|
|
||||||
type TimeRangeType = 'days' | 'weeks' | 'months' | 'custom';
|
type TimeRangeType = 'days' | 'weeks' | 'months' | 'custom';
|
||||||
type SendComment = 'no' | 'last_block' | 'block_path';
|
type SendComment = 'no' | 'last_block' | 'block_path';
|
||||||
|
|
@ -12,40 +19,7 @@ interface Option {
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Entry {
|
const t = useTranslations('settings.statistics').t;
|
||||||
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;
|
|
||||||
|
|
||||||
export class TimekeepSettingsComponent implements SettingsComponent {
|
export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
private props: SettingsComponentProps;
|
private props: SettingsComponentProps;
|
||||||
|
|
@ -81,30 +55,30 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
const settingsContainer = containerEl.createDiv('timekeep-settings');
|
const settingsContainer = containerEl.createDiv('timekeep-settings');
|
||||||
|
|
||||||
new Setting(settingsContainer)
|
new Setting(settingsContainer)
|
||||||
.setName(t("settings.send_comments.name"))
|
.setName(t('settings.send_comments.name'))
|
||||||
.setDesc(t("settings.send_comments.description"))
|
.setDesc(t('settings.send_comments.description'))
|
||||||
.addDropdown(dropdown => {
|
.addDropdown((dropdown) => {
|
||||||
dropdown.addOption('no', t("settings.send_comments.options.no"));
|
dropdown.addOption('no', t('settings.send_comments.options.no'));
|
||||||
dropdown.addOption('last_block', t("settings.send_comments.options.last_block"));
|
dropdown.addOption('last_block', t('settings.send_comments.options.last_block'));
|
||||||
dropdown.addOption('block_path', t("settings.send_comments.options.block_path"));
|
dropdown.addOption('block_path', t('settings.send_comments.options.block_path'));
|
||||||
dropdown.setValue(this.props.plugin.settings.timekeep.sendComments);
|
dropdown.setValue(this.props.plugin.settings.timekeep.sendComments);
|
||||||
dropdown.onChange(async (value: SendComment) => {
|
dropdown.onChange(async (value: string) => {
|
||||||
this.props.plugin.settings.timekeep.sendComments = value;
|
this.props.plugin.settings.timekeep.sendComments = value as SendComment;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Time range selector
|
// Time range selector
|
||||||
new Setting(settingsContainer)
|
new Setting(settingsContainer)
|
||||||
.setName(t("settings.time_range.name"))
|
.setName(t('settings.time_range.name'))
|
||||||
.setDesc(t("settings.time_range.description"))
|
.setDesc(t('settings.time_range.description'))
|
||||||
.addDropdown(dropdown => {
|
.addDropdown((dropdown) => {
|
||||||
dropdown.addOption('days', t("settings.time_range.options.days"));
|
dropdown.addOption('days', t('settings.time_range.options.days'));
|
||||||
dropdown.addOption('weeks', t("settings.time_range.options.weeks"));
|
dropdown.addOption('weeks', t('settings.time_range.options.weeks'));
|
||||||
dropdown.addOption('months', t("settings.time_range.options.months"));
|
dropdown.addOption('months', t('settings.time_range.options.months'));
|
||||||
dropdown.addOption('custom', t("settings.time_range.options.custom"));
|
dropdown.addOption('custom', t('settings.time_range.options.custom'));
|
||||||
dropdown.setValue(this.props.plugin.settings.timekeep.statisticsTimeType);
|
dropdown.setValue(this.props.plugin.settings.timekeep.statisticsTimeType);
|
||||||
dropdown.onChange(async (value: TimeRangeType) => {
|
dropdown.onChange(async (value: string) => {
|
||||||
this.props.plugin.settings.timekeep.statisticsTimeType = value;
|
this.props.plugin.settings.timekeep.statisticsTimeType = value as TimeRangeType;
|
||||||
this.toggleCustomDateInputs();
|
this.toggleCustomDateInputs();
|
||||||
await this.refreshData();
|
await this.refreshData();
|
||||||
});
|
});
|
||||||
|
|
@ -112,10 +86,11 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
|
|
||||||
// Max items slider (hidden for custom range)
|
// Max items slider (hidden for custom range)
|
||||||
this.maxItemsSetting = new Setting(settingsContainer)
|
this.maxItemsSetting = new Setting(settingsContainer)
|
||||||
.setName(t("settings.max_items.name"))
|
.setName(t('settings.max_items.name'))
|
||||||
.setDesc(t("settings.max_items.description"))
|
.setDesc(t('settings.max_items.description'))
|
||||||
.addSlider(slider => {
|
.addSlider((slider) => {
|
||||||
slider.setLimits(1, 20, 1)
|
slider
|
||||||
|
.setLimits(1, 20, 1)
|
||||||
.setValue(this.props.plugin.settings.timekeep.maxItemsToShow)
|
.setValue(this.props.plugin.settings.timekeep.maxItemsToShow)
|
||||||
.setDynamicTooltip()
|
.setDynamicTooltip()
|
||||||
.onChange(async (value) => {
|
.onChange(async (value) => {
|
||||||
|
|
@ -128,56 +103,59 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
this.customDatesContainer = settingsContainer.createDiv('custom-dates-container');
|
this.customDatesContainer = settingsContainer.createDiv('custom-dates-container');
|
||||||
|
|
||||||
new Setting(this.customDatesContainer)
|
new Setting(this.customDatesContainer)
|
||||||
.setName(t("settings.date_from.name"))
|
.setName(t('settings.date_from.name'))
|
||||||
.setDesc(t("settings.date_from.description"))
|
.setDesc(t('settings.date_from.description'))
|
||||||
.addText(text => {
|
.addText((text) => {
|
||||||
text.setPlaceholder(t("settings.date_from.placeholder"));
|
text.setPlaceholder(t('settings.date_from.placeholder'));
|
||||||
text.setValue(this.props.plugin.settings.timekeep.customDateRange.start);
|
text.setValue(this.props.plugin.settings.timekeep.customDateRange.start);
|
||||||
text.onChange(async (value) => {
|
text.onChange(async (value) => {
|
||||||
this.props.plugin.settings.timekeep.customDateRange.start = 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();
|
await this.refreshData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
new Setting(this.customDatesContainer)
|
new Setting(this.customDatesContainer)
|
||||||
.setName(t("settings.date_to.name"))
|
.setName(t('settings.date_to.name'))
|
||||||
.setDesc(t("settings.date_to.description"))
|
.setDesc(t('settings.date_to.description'))
|
||||||
.addText(text => {
|
.addText((text) => {
|
||||||
text.setPlaceholder(t("settings.date_to.placeholder"));
|
text.setPlaceholder(t('settings.date_to.placeholder'));
|
||||||
text.setValue(this.props.plugin.settings.timekeep.customDateRange.end);
|
text.setValue(this.props.plugin.settings.timekeep.customDateRange.end);
|
||||||
text.onChange(async (value) => {
|
text.onChange(async (value) => {
|
||||||
this.props.plugin.settings.timekeep.customDateRange.end = 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();
|
await this.refreshData();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
new Setting(settingsContainer)
|
new Setting(settingsContainer)
|
||||||
.setName(t("display.select_period"))
|
.setName(t('display.select_period'))
|
||||||
.setClass('period-selector')
|
.setClass('period-selector')
|
||||||
.addDropdown(dropdown => {
|
.addDropdown((dropdown) => {
|
||||||
this.periodDropdown = dropdown;
|
this.periodDropdown = dropdown;
|
||||||
this.populateTimeRangeOptions(dropdown, []);
|
this.populateTimeRangeOptions(dropdown, []);
|
||||||
dropdown
|
dropdown.setValue(this.props.plugin.settings.timekeep.sendComments).onChange(async (selectedPeriod) => {
|
||||||
.setValue(this.props.plugin.settings.timekeep.sendComments)
|
this.selectedPeriodData = this.groupedByPeriod[selectedPeriod] || [];
|
||||||
.onChange(async (selectedPeriod) => {
|
if (this.sendButton) {
|
||||||
this.selectedPeriodData = this.groupedByPeriod[selectedPeriod] || [];
|
this.sendButton.setDisabled(false);
|
||||||
this.sendButton? this.sendButton.setDisabled(false) : null;
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.addButton(
|
.addButton((button) => {
|
||||||
button => {
|
this.sendButton = button;
|
||||||
this.sendButton = button;
|
button.setDisabled(true);
|
||||||
button.setDisabled(true);
|
button.setButtonText(t('actions.send_to_jira'));
|
||||||
button.setButtonText(t("actions.send_to_jira"));
|
button.onClick(() => this.sendWorkLogToJira());
|
||||||
button.onClick(() => this.sendWorkLogToJira());
|
button.setClass('timekeep-btn');
|
||||||
button.setClass('timekeep-btn');
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private toggleCustomDateInputs(): void {
|
private toggleCustomDateInputs(): void {
|
||||||
|
|
@ -186,7 +164,7 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
const isCustom = this.props.plugin.settings.timekeep.statisticsTimeType === 'custom';
|
const isCustom = this.props.plugin.settings.timekeep.statisticsTimeType === 'custom';
|
||||||
|
|
||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
this.maxItemsSetting.settingEl.hide()
|
this.maxItemsSetting.settingEl.hide();
|
||||||
this.customDatesContainer.show();
|
this.customDatesContainer.show();
|
||||||
} else {
|
} else {
|
||||||
this.maxItemsSetting.settingEl.show();
|
this.maxItemsSetting.settingEl.show();
|
||||||
|
|
@ -203,10 +181,14 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
try {
|
try {
|
||||||
await this.processFiles();
|
await this.processFiles();
|
||||||
this.updateDisplay();
|
this.updateDisplay();
|
||||||
if (!disableNotification) new Notice(t("messages.refresh_success"));
|
if (!disableNotification) new Notice(t('messages.refresh_success'));
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Error refreshing timekeep data:', error);
|
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 {
|
} finally {
|
||||||
// Убираем индикатор загрузки
|
// Убираем индикатор загрузки
|
||||||
this.containerEl.removeClass('timekeep-loading');
|
this.containerEl.removeClass('timekeep-loading');
|
||||||
|
|
@ -222,8 +204,8 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
|
|
||||||
if (Object.keys(this.groupedByPeriod).length === 0) {
|
if (Object.keys(this.groupedByPeriod).length === 0) {
|
||||||
this.containerEl.createEl('div', {
|
this.containerEl.createEl('div', {
|
||||||
text: t("display.no_entries"),
|
text: t('display.no_entries'),
|
||||||
cls: 'timekeep-no-data'
|
cls: 'timekeep-no-data',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -234,8 +216,8 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
private renderAllPeriodsDisplay(container: HTMLElement): void {
|
private renderAllPeriodsDisplay(container: HTMLElement): void {
|
||||||
const display = container.createDiv('all-periods-display jira-copyable timekeep-fade-in');
|
const display = container.createDiv('all-periods-display jira-copyable timekeep-fade-in');
|
||||||
display.createEl('h3', {
|
display.createEl('h3', {
|
||||||
text: t("display.all_periods"),
|
text: t('display.all_periods'),
|
||||||
cls: 'timekeep-section-title'
|
cls: 'timekeep-section-title',
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedPeriods = Object.keys(this.groupedByPeriod).sort().reverse();
|
const sortedPeriods = Object.keys(this.groupedByPeriod).sort().reverse();
|
||||||
|
|
@ -253,25 +235,25 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
const headerContainer = periodDiv.createDiv('timekeep-period-header');
|
const headerContainer = periodDiv.createDiv('timekeep-period-header');
|
||||||
headerContainer.createEl('h4', {
|
headerContainer.createEl('h4', {
|
||||||
text: this.formatPeriodLabel(period),
|
text: this.formatPeriodLabel(period),
|
||||||
cls: 'timekeep-period-title'
|
cls: 'timekeep-period-title',
|
||||||
});
|
});
|
||||||
headerContainer.createEl('span', {
|
headerContainer.createEl('span', {
|
||||||
text: this.parseMsToDuration(totalMs),
|
text: this.parseMsToDuration(totalMs),
|
||||||
cls: 'timekeep-period-total'
|
cls: 'timekeep-period-total',
|
||||||
});
|
});
|
||||||
|
|
||||||
this.createEntriesTable(periodDiv, this.groupedByPeriod[period]);
|
this.createEntriesTable(periodDiv, this.groupedByPeriod[period]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private groupAndSummarizeEntries(entries: ProcessedEntry[]):
|
private groupAndSummarizeEntries(
|
||||||
Record<string, Record<string, { entries: ProcessedEntry[], totalMs: number }>>
|
entries: ProcessedEntry[],
|
||||||
{
|
): Record<string, Record<string, { entries: ProcessedEntry[]; totalMs: number }>> {
|
||||||
const grouped: ReturnType<typeof this.groupAndSummarizeEntries> = {};
|
const grouped: ReturnType<typeof this.groupAndSummarizeEntries> = {};
|
||||||
|
|
||||||
entries.forEach(entry => {
|
entries.forEach((entry) => {
|
||||||
const file = entry.file;
|
const file = entry.file;
|
||||||
const issueKey = entry.issueKey || "no-issue";
|
const issueKey = entry.issueKey || 'no-issue';
|
||||||
|
|
||||||
if (!grouped[file]) grouped[file] = {};
|
if (!grouped[file]) grouped[file] = {};
|
||||||
if (!grouped[file][issueKey]) {
|
if (!grouped[file][issueKey]) {
|
||||||
|
|
@ -311,37 +293,43 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Group title
|
// 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 });
|
groupTitle.createEl('span', { text: file });
|
||||||
|
|
||||||
// Group meta (issue key and duration)
|
// 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') {
|
if (issueKey && issueKey !== 'no-issue') {
|
||||||
const issueKeySpan = groupMeta.createEl('a', {
|
const issueKeySpan = groupMeta.createEl('a', {
|
||||||
text: issueKey,
|
text: issueKey,
|
||||||
cls: 'timekeep-group-issue-key',
|
cls: 'timekeep-group-issue-key',
|
||||||
attr: {
|
attr: {
|
||||||
href: `${this.props.plugin.settings.connection.jiraUrl}/browse/${issueKey}`,
|
href: `${jiraUrl}/browse/${issueKey}`,
|
||||||
target: "_blank",
|
target: '_blank',
|
||||||
rel: "noopener noreferrer"
|
rel: 'noopener noreferrer',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
issueKeySpan.addEventListener('click', (e) => {
|
issueKeySpan.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
debugLog(`${this.props.plugin.settings.connection.jiraUrl}/browse/${issueKey}`)
|
debugLog(`${jiraUrl}/browse/${issueKey}`);
|
||||||
}
|
}
|
||||||
groupMeta.createEl('span', {
|
groupMeta.createEl('span', {
|
||||||
text: this.parseMsToDuration(group.totalMs),
|
text: this.parseMsToDuration(group.totalMs),
|
||||||
cls: 'timekeep-group-duration'
|
cls: 'timekeep-group-duration',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collapse icon
|
// Collapse icon
|
||||||
const chevron = groupMeta.createEl('span', {
|
const chevron = groupMeta.createEl('span', {
|
||||||
cls: 'jira-collapse-icon',
|
cls: 'jira-collapse-icon',
|
||||||
});
|
});
|
||||||
setIcon(chevron, "chevron-down");
|
setIcon(chevron, 'chevron-down');
|
||||||
|
|
||||||
// Create entries container
|
// Create entries container
|
||||||
const entriesContainer = groupCard.createDiv('timekeep-entries-container');
|
const entriesContainer = groupCard.createDiv('timekeep-entries-container');
|
||||||
|
|
@ -350,16 +338,16 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
if (group.entries.length > 0) {
|
if (group.entries.length > 0) {
|
||||||
const labelsDiv = entriesContainer.createDiv('timekeep-entry-labels');
|
const labelsDiv = entriesContainer.createDiv('timekeep-entry-labels');
|
||||||
labelsDiv.createEl('div', {
|
labelsDiv.createEl('div', {
|
||||||
text: t("display.table.task"),
|
text: t('display.table.task'),
|
||||||
cls: 'timekeep-entry-label-task'
|
cls: 'timekeep-entry-label-task',
|
||||||
});
|
});
|
||||||
labelsDiv.createEl('div', {
|
labelsDiv.createEl('div', {
|
||||||
text: t("display.table.start_time"),
|
text: t('display.table.start_time'),
|
||||||
cls: 'timekeep-entry-label-start'
|
cls: 'timekeep-entry-label-start',
|
||||||
});
|
});
|
||||||
labelsDiv.createEl('div', {
|
labelsDiv.createEl('div', {
|
||||||
text: t("display.table.duration"),
|
text: t('display.table.duration'),
|
||||||
cls: 'timekeep-entry-label-duration'
|
cls: 'timekeep-entry-label-duration',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -371,21 +359,21 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
entryDiv.createEl('div', {
|
entryDiv.createEl('div', {
|
||||||
text: entry.blockPath,
|
text: entry.blockPath,
|
||||||
cls: 'timekeep-entry-block-path',
|
cls: 'timekeep-entry-block-path',
|
||||||
title: entry.blockPath
|
title: entry.blockPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start time
|
// Start time
|
||||||
entryDiv.createEl('div', {
|
entryDiv.createEl('div', {
|
||||||
text: entry.startTime,
|
text: entry.startTime,
|
||||||
cls: 'timekeep-entry-start-time',
|
cls: 'timekeep-entry-start-time',
|
||||||
title: entry.startTime
|
title: entry.startTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Duration
|
// Duration
|
||||||
entryDiv.createEl('div', {
|
entryDiv.createEl('div', {
|
||||||
text: entry.duration,
|
text: entry.duration,
|
||||||
cls: 'timekeep-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> {
|
private async sendWorkLogToJira(): Promise<void> {
|
||||||
if (this.selectedPeriodData.length === 0) {
|
if (this.selectedPeriodData.length === 0) {
|
||||||
new Notice(t("messages.select_period_first"));
|
new Notice(t('messages.select_period_first'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,13 +396,13 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
case 'last_block':
|
case 'last_block':
|
||||||
data_to_send = this.selectedPeriodData.map((entry) => ({
|
data_to_send = this.selectedPeriodData.map((entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
comment: entry.lastBlockName
|
comment: entry.lastBlockName,
|
||||||
}));
|
}));
|
||||||
break;
|
break;
|
||||||
case 'block_path':
|
case 'block_path':
|
||||||
data_to_send = this.selectedPeriodData.map((entry) => ({
|
data_to_send = this.selectedPeriodData.map((entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
comment: entry.blockPath
|
comment: entry.blockPath,
|
||||||
}));
|
}));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
@ -422,15 +410,17 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await processWorkLogBatch(this.props.plugin, data_to_send);
|
await processWorkLogBatch(this.props.plugin, data_to_send);
|
||||||
new Notice(t("messages.send_success"));
|
new Notice(t('messages.send_success'));
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
console.error('Error sending work log to Jira:', error);
|
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 {
|
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 {
|
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]}`;
|
return `${date.toISOString().split('T')[0]} – ${weekEnd.toISOString().split('T')[0]}`;
|
||||||
}
|
}
|
||||||
case 'months':
|
case 'months':
|
||||||
return date.toLocaleDateString(undefined, { year: 'numeric', month: 'long' });
|
return date.toLocaleDateString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
});
|
||||||
default:
|
default:
|
||||||
return date.toISOString().split('T')[0];
|
return date.toISOString().split('T')[0];
|
||||||
}
|
}
|
||||||
|
|
@ -463,9 +456,10 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
switch (this.props.plugin.settings.timekeep.statisticsTimeType) {
|
switch (this.props.plugin.settings.timekeep.statisticsTimeType) {
|
||||||
case 'days':
|
case 'days':
|
||||||
return date.toISOString().split('T')[0];
|
return date.toISOString().split('T')[0];
|
||||||
case 'weeks':
|
case 'weeks': {
|
||||||
const weekStart = this.getWeekStartDate(date);
|
const weekStart = this.getWeekStartDate(date);
|
||||||
return `week-${weekStart.toISOString().split('T')[0]}`;
|
return `week-${weekStart.toISOString().split('T')[0]}`;
|
||||||
|
}
|
||||||
case 'months':
|
case 'months':
|
||||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||||
case 'custom':
|
case 'custom':
|
||||||
|
|
@ -478,7 +472,11 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
private isDateInRange(date: Date): boolean {
|
private isDateInRange(date: Date): boolean {
|
||||||
if (this.props.plugin.settings.timekeep.statisticsTimeType !== 'custom') return true;
|
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 from = new Date(this.props.plugin.settings.timekeep.customDateRange.start);
|
||||||
const to = new Date(this.props.plugin.settings.timekeep.customDateRange.end);
|
const to = new Date(this.props.plugin.settings.timekeep.customDateRange.end);
|
||||||
|
|
@ -490,8 +488,11 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
private validateCustomRange(): boolean {
|
private validateCustomRange(): boolean {
|
||||||
if (this.props.plugin.settings.timekeep.statisticsTimeType !== 'custom') return true;
|
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) {
|
if (
|
||||||
new Notice(t("messages.custom_range_required"));
|
!this.props.plugin.settings.timekeep.customDateRange.start ||
|
||||||
|
!this.props.plugin.settings.timekeep.customDateRange.end
|
||||||
|
) {
|
||||||
|
new Notice(t('messages.custom_range_required'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -499,7 +500,7 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
const to = new Date(this.props.plugin.settings.timekeep.customDateRange.end);
|
const to = new Date(this.props.plugin.settings.timekeep.customDateRange.end);
|
||||||
|
|
||||||
if (from > to) {
|
if (from > to) {
|
||||||
new Notice(t("messages.invalid_date_range"));
|
new Notice(t('messages.invalid_date_range'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -517,11 +518,11 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
|
|
||||||
private parseMsToDuration(ms: number): string {
|
private parseMsToDuration(ms: number): string {
|
||||||
const units: { label: string; ms: number }[] = [
|
const units: { label: string; ms: number }[] = [
|
||||||
{ label: "w", ms: 1000 * 60 * 60 * 24 * 7 },
|
{ label: 'w', ms: 1000 * 60 * 60 * 24 * 7 },
|
||||||
{ label: "d", ms: 1000 * 60 * 60 * 24 },
|
{ label: 'd', ms: 1000 * 60 * 60 * 24 },
|
||||||
{ label: "h", ms: 1000 * 60 * 60 },
|
{ label: 'h', ms: 1000 * 60 * 60 },
|
||||||
{ label: "m", ms: 1000 * 60 },
|
{ label: 'm', ms: 1000 * 60 },
|
||||||
{ label: "s", ms: 1000 }
|
{ label: 's', ms: 1000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
let remainingMs = ms;
|
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 {
|
private parseDurationToMs(duration: string): number {
|
||||||
if (duration === 'ongoing') return 0;
|
if (duration === 'ongoing') return 0;
|
||||||
|
|
||||||
const units: { label: string; ms: number }[] = [
|
const units: { label: string; ms: number }[] = [
|
||||||
{ label: "w", ms: 1000 * 60 * 60 * 24 * 7 },
|
{ label: 'w', ms: 1000 * 60 * 60 * 24 * 7 },
|
||||||
{ label: "d", ms: 1000 * 60 * 60 * 24 },
|
{ label: 'd', ms: 1000 * 60 * 60 * 24 },
|
||||||
{ label: "h", ms: 1000 * 60 * 60 },
|
{ label: 'h', ms: 1000 * 60 * 60 },
|
||||||
{ label: "m", ms: 1000 * 60 },
|
{ label: 'm', ms: 1000 * 60 },
|
||||||
{ label: "s", ms: 1000 }
|
{ label: 's', ms: 1000 },
|
||||||
];
|
];
|
||||||
|
|
||||||
let ms = 0;
|
let ms = 0;
|
||||||
|
|
@ -576,108 +577,65 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
private processEntries(
|
private processEntries(
|
||||||
entries: Entry[] | undefined,
|
entries: TimeTrackerEntry[] | undefined,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
issueKey: string | undefined,
|
issueKey: string | undefined,
|
||||||
parentPath: string,
|
parentPath: string,
|
||||||
groupedEntries: GroupedEntries
|
groupedEntries: GroupedEntries,
|
||||||
): void {
|
): void {
|
||||||
if (!entries || !Array.isArray(entries)) return;
|
processTimeEntries(
|
||||||
|
entries,
|
||||||
for (const entry of entries) {
|
fileName,
|
||||||
const currentPath = parentPath ? `${parentPath} > ${entry.name}` : entry.name;
|
issueKey,
|
||||||
|
parentPath,
|
||||||
if (entry.startTime) {
|
groupedEntries,
|
||||||
const startTime = new Date(entry.startTime);
|
(date) => this.isDateInRange(date),
|
||||||
|
(date) => this.getPeriodKey(date),
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async processFiles(): Promise<void> {
|
private async processFiles(): Promise<void> {
|
||||||
if (!this.validateCustomRange()) return;
|
if (!this.validateCustomRange()) return;
|
||||||
|
|
||||||
const timekeepBlocks: TimekeepBlock[] = [];
|
const allBlocks: ReturnType<typeof parseTimeBlocks> = [];
|
||||||
const files = this.props.app.vault.getMarkdownFiles().filter(
|
const files = this.props.app.vault
|
||||||
file => file.path.startsWith(`${this.props.plugin.settings.global.issuesFolder}/`)
|
.getMarkdownFiles()
|
||||||
|
.filter((file) => file.path.startsWith(`${this.props.plugin.settings.global.issuesFolder}/`));
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
files.map(async (file) => {
|
||||||
|
const content = await this.props.app.vault.read(file);
|
||||||
|
if (!content.includes('```timekeep') && !content.includes('```simple-time-tracker')) return;
|
||||||
|
|
||||||
|
const metadata = this.props.app.metadataCache.getFileCache(file);
|
||||||
|
const issueKey = metadata?.frontmatter?.key;
|
||||||
|
|
||||||
|
const blocks = parseTimeBlocks(content, file, issueKey);
|
||||||
|
allBlocks.push(...blocks);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(files.map(async file => {
|
|
||||||
const content = await this.props.app.vault.read(file);
|
|
||||||
if (!content.includes("```timekeep")) return;
|
|
||||||
|
|
||||||
const metadata = this.props.app.metadataCache.getFileCache(file);
|
|
||||||
const issueKey = metadata?.frontmatter?.key;
|
|
||||||
|
|
||||||
const timekeepMatches = content.match(/```timekeep([\s\S]*?)```/g);
|
|
||||||
timekeepMatches?.forEach(block => {
|
|
||||||
try {
|
|
||||||
const jsonContent = block.slice(11, -3).trim();
|
|
||||||
const data = JSON.parse(jsonContent);
|
|
||||||
|
|
||||||
timekeepBlocks.push({
|
|
||||||
file: file.name,
|
|
||||||
path: file.path,
|
|
||||||
issueKey,
|
|
||||||
data
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error parsing timekeep block in ${file.name}:`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
this.groupedByPeriod = {};
|
this.groupedByPeriod = {};
|
||||||
|
|
||||||
timekeepBlocks.forEach(block => {
|
allBlocks.forEach((block) => {
|
||||||
this.processEntries(block.data.entries, block.file, block.issueKey, "", this.groupedByPeriod);
|
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);
|
this.trimOldEntries(this.groupedByPeriod);
|
||||||
|
|
||||||
const sortedPeriods = Object.keys(this.groupedByPeriod).sort().reverse(); // Most recent first
|
const sortedPeriods = Object.keys(this.groupedByPeriod).sort().reverse(); // Most recent first
|
||||||
const options = sortedPeriods.map(period => ({
|
const options = sortedPeriods.map((period) => ({
|
||||||
value: period,
|
value: period,
|
||||||
label: this.formatPeriodLabel(period)
|
label: this.formatPeriodLabel(period),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.populateTimeRangeOptions(this.periodDropdown, options);
|
this.populateTimeRangeOptions(this.periodDropdown, options);
|
||||||
this.periodDropdown?.setValue("");
|
this.periodDropdown?.setValue('');
|
||||||
this.sendButton ? this.sendButton.disabled = true : null;
|
if (this.sendButton) {
|
||||||
|
this.sendButton.disabled = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private populateTimeRangeOptions(dropdown: DropdownComponent | null, options: Option[]): void {
|
private populateTimeRangeOptions(dropdown: DropdownComponent | null, options: Option[]): void {
|
||||||
|
|
@ -687,16 +645,17 @@ export class TimekeepSettingsComponent implements SettingsComponent {
|
||||||
dropdown.selectEl.empty();
|
dropdown.selectEl.empty();
|
||||||
|
|
||||||
// Add new options based on current state
|
// Add new options based on current state
|
||||||
const record_options = options.reduce((acc, curr) => {
|
const record_options = options.reduce(
|
||||||
acc[curr.value] = curr.label;
|
(acc, curr) => {
|
||||||
return acc;
|
acc[curr.value] = curr.label;
|
||||||
}, {} as Record<string, string>);
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, string>,
|
||||||
|
);
|
||||||
dropdown.addOptions(record_options);
|
dropdown.addOptions(record_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private toLocalIso(date: Date): string {
|
private toLocalIso(date: Date): string {
|
||||||
return date.toISOString().slice(0, 19).replace("T", " ");
|
return date.toISOString().slice(0, 19).replace('T', ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import {FieldMapping} from "../default/obsidianJiraFieldsMapping";
|
import { FieldMapping } from '../default/obsidianJiraFieldsMapping';
|
||||||
|
|
||||||
|
|
||||||
export interface ConnectionSettingsInterface {
|
export interface ConnectionSettingsInterface {
|
||||||
|
name: string;
|
||||||
jiraUrl: string;
|
jiraUrl: string;
|
||||||
apiVersion: "2" | "3";
|
apiVersion: '2' | '3';
|
||||||
authMethod: "bearer" | "basic" | "session";
|
authMethod: 'bearer' | 'basic' | 'session';
|
||||||
apiToken: string;
|
apiToken: string;
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|
@ -29,7 +29,7 @@ export interface FetchIssueInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimekeepSettingsInterface {
|
export interface TimekeepSettingsInterface {
|
||||||
sendComments: "no" | "last_block" | "block_path";
|
sendComments: 'no' | 'last_block' | 'block_path';
|
||||||
statisticsTimeType: string;
|
statisticsTimeType: string;
|
||||||
maxItemsToShow: number;
|
maxItemsToShow: number;
|
||||||
customDateRange: { start: string; end: string };
|
customDateRange: { start: string; end: string };
|
||||||
|
|
@ -47,7 +47,8 @@ export interface CollapsedSections {
|
||||||
export interface JiraSettingsInterface {
|
export interface JiraSettingsInterface {
|
||||||
collapsedSections: CollapsedSections;
|
collapsedSections: CollapsedSections;
|
||||||
|
|
||||||
connection: ConnectionSettingsInterface;
|
connections: ConnectionSettingsInterface[];
|
||||||
|
currentConnectionIndex: number;
|
||||||
global: GlobalSettingsInterface;
|
global: GlobalSettingsInterface;
|
||||||
fieldMapping: FieldMappingSettingsInterface;
|
fieldMapping: FieldMappingSettingsInterface;
|
||||||
fetchIssue: FetchIssueInterface;
|
fetchIssue: FetchIssueInterface;
|
||||||
|
|
@ -67,19 +68,23 @@ export const DEFAULT_SETTINGS: JiraSettingsInterface = {
|
||||||
statistics: false,
|
statistics: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
connection: {
|
connections: [
|
||||||
authMethod: "bearer",
|
{
|
||||||
apiToken: "",
|
name: 'Connection 1',
|
||||||
username: "",
|
authMethod: 'bearer',
|
||||||
email: "",
|
apiToken: '',
|
||||||
password: "",
|
username: '',
|
||||||
jiraUrl: "",
|
email: '',
|
||||||
apiVersion: "2",
|
password: '',
|
||||||
},
|
jiraUrl: '',
|
||||||
|
apiVersion: '2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
currentConnectionIndex: 0,
|
||||||
|
|
||||||
global: {
|
global: {
|
||||||
issuesFolder: "jira-issues",
|
issuesFolder: 'jira-issues',
|
||||||
templatePath: "",
|
templatePath: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
fieldMapping: {
|
fieldMapping: {
|
||||||
|
|
@ -89,19 +94,18 @@ export const DEFAULT_SETTINGS: JiraSettingsInterface = {
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchIssue: {
|
fetchIssue: {
|
||||||
filenameTemplate: "{summary} ({key})",
|
filenameTemplate: '{summary} ({key})',
|
||||||
fields: ["*all"],
|
fields: ['*all'],
|
||||||
expand: [],
|
expand: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
timekeep: {
|
timekeep: {
|
||||||
sendComments: "no",
|
sendComments: 'no',
|
||||||
statisticsTimeType: "weeks",
|
statisticsTimeType: 'weeks',
|
||||||
maxItemsToShow: 10,
|
maxItemsToShow: 10,
|
||||||
customDateRange: { start: "", end: "" },
|
customDateRange: { start: '', end: '' },
|
||||||
},
|
},
|
||||||
|
|
||||||
sessionCookieName: "JSESSIONID",
|
sessionCookieName: 'JSESSIONID',
|
||||||
issueKeyToFilePathCache: {}
|
issueKeyToFilePathCache: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { Notice } from "obsidian";
|
import { Notice } from 'obsidian';
|
||||||
import { ValidationResult } from "../../interfaces/settingsTypes";
|
import { ValidationResult } from '../../interfaces/settingsTypes';
|
||||||
import { validateFunctionString } from "../../tools/convertFunctionString";
|
import { validateFunctionString } from '../../tools/convertFunctionString';
|
||||||
import { debugLog } from "../../tools/debugLogging";
|
import { debugLog } from '../../tools/debugLogging';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate a field and update its visual state
|
* Validate a field and update its visual state
|
||||||
|
|
@ -10,7 +10,7 @@ export async function validateField(
|
||||||
input: HTMLInputElement | HTMLTextAreaElement,
|
input: HTMLInputElement | HTMLTextAreaElement,
|
||||||
requireValidating: boolean = true,
|
requireValidating: boolean = true,
|
||||||
type: string = 'string',
|
type: string = 'string',
|
||||||
validateParams: Array<string> = []
|
validateParams: Array<string> = [],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const validatorFunction = async (event?: Event) => {
|
const validatorFunction = async (event?: Event) => {
|
||||||
const consoleOutput = !!event;
|
const consoleOutput = !!event;
|
||||||
|
|
@ -22,10 +22,10 @@ export async function validateField(
|
||||||
setupValidatorProperty(input, validatorFunction);
|
setupValidatorProperty(input, validatorFunction);
|
||||||
|
|
||||||
if (requireValidating) {
|
if (requireValidating) {
|
||||||
input.addEventListener("change", (input as any)._validatorFunction);
|
input.addEventListener('change', (input as any)._validatorFunction);
|
||||||
await validatorFunction(); // Run initial validation
|
await validatorFunction(); // Run initial validation
|
||||||
} else {
|
} else {
|
||||||
input.removeEventListener("change", (input as any)._validatorFunction);
|
input.removeEventListener('change', (input as any)._validatorFunction);
|
||||||
updateFieldAppearance({ isValid: true }, input, false); // Clear any validation errors
|
updateFieldAppearance({ isValid: true }, input, false); // Clear any validation errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -36,12 +36,12 @@ export async function validateField(
|
||||||
async function getValidationResult(
|
async function getValidationResult(
|
||||||
value: string,
|
value: string,
|
||||||
type: string = 'string',
|
type: string = 'string',
|
||||||
validateParams: Array<string> = []
|
validateParams: Array<string> = [],
|
||||||
): Promise<ValidationResult> {
|
): Promise<ValidationResult> {
|
||||||
switch(type) {
|
switch (type) {
|
||||||
case 'string':
|
case 'string':
|
||||||
return value.length === 0
|
return value.length === 0
|
||||||
? { isValid: false, errorMessage: "Field name cannot be empty" }
|
? { isValid: false, errorMessage: 'Field name cannot be empty' }
|
||||||
: { isValid: true };
|
: { isValid: true };
|
||||||
case 'function':
|
case 'function':
|
||||||
return await validateFunctionString(value, validateParams);
|
return await validateFunctionString(value, validateParams);
|
||||||
|
|
@ -56,16 +56,16 @@ async function getValidationResult(
|
||||||
export function updateFieldAppearance(
|
export function updateFieldAppearance(
|
||||||
validation: ValidationResult,
|
validation: ValidationResult,
|
||||||
element: HTMLInputElement | HTMLTextAreaElement,
|
element: HTMLInputElement | HTMLTextAreaElement,
|
||||||
consoleOutput: boolean
|
consoleOutput: boolean,
|
||||||
): void {
|
): void {
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
element.classList.add("invalid");
|
element.classList.add('invalid');
|
||||||
if (consoleOutput && validation.errorMessage) {
|
if (consoleOutput && validation.errorMessage) {
|
||||||
debugLog(validation.errorMessage);
|
debugLog(validation.errorMessage);
|
||||||
new Notice(validation.errorMessage);
|
new Notice(validation.errorMessage);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
element.classList.remove("invalid");
|
element.classList.remove('invalid');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,16 +74,16 @@ export function updateFieldAppearance(
|
||||||
*/
|
*/
|
||||||
function setupValidatorProperty(
|
function setupValidatorProperty(
|
||||||
input: HTMLInputElement | HTMLTextAreaElement,
|
input: HTMLInputElement | HTMLTextAreaElement,
|
||||||
validatorFunction: (event?: Event) => Promise<void>
|
validatorFunction: (event?: Event) => Promise<void>,
|
||||||
): void {
|
): void {
|
||||||
if (!input.hasOwnProperty('_validatorFunction')) {
|
if (!Object.prototype.hasOwnProperty.call(input, '_validatorFunction')) {
|
||||||
Object.defineProperty(input, '_validatorFunction', {
|
Object.defineProperty(input, '_validatorFunction', {
|
||||||
value: validatorFunction,
|
value: validatorFunction,
|
||||||
writable: true,
|
writable: true,
|
||||||
configurable: true
|
configurable: true,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
input.removeEventListener("change", (input as any)._validatorFunction);
|
input.removeEventListener('change', (input as any)._validatorFunction);
|
||||||
(input as any)._validatorFunction = validatorFunction;
|
(input as any)._validatorFunction = validatorFunction;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { JiraFieldMapping, JiraFieldMappingString } from "../../interfaces/settingsTypes";
|
import { JiraFieldMapping, JiraFieldMappingString } from '../../interfaces/settingsTypes';
|
||||||
import {
|
import {
|
||||||
jiraFunctionToString,
|
jiraFunctionToString,
|
||||||
transform_string_to_functions_mappings,
|
transform_string_to_functions_mappings,
|
||||||
// validateFunctionString
|
// validateFunctionString
|
||||||
} from "../../tools/convertFunctionString";
|
} from '../../tools/convertFunctionString';
|
||||||
import { debugLog } from "../../tools/debugLogging";
|
import { debugLog } from '../../tools/debugLogging';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert function mappings to their string representation
|
* Convert function mappings to their string representation
|
||||||
*/
|
*/
|
||||||
export function convertFunctionMappingsToStrings(
|
export function convertFunctionMappingsToStrings(
|
||||||
mappings: Record<string, JiraFieldMapping>
|
mappings: Record<string, JiraFieldMapping>,
|
||||||
): Record<string, JiraFieldMappingString> {
|
): Record<string, JiraFieldMappingString> {
|
||||||
const result: 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) {
|
if (mapping && typeof mapping === 'object' && 'toJira' in mapping && 'fromJira' in mapping) {
|
||||||
result[fieldName] = {
|
result[fieldName] = {
|
||||||
toJira: jiraFunctionToString(mapping.toJira, false),
|
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
|
// enableValidation: boolean
|
||||||
): Promise<Record<string, JiraFieldMappingString>> {
|
): Promise<Record<string, JiraFieldMappingString>> {
|
||||||
const mappings: 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 => {
|
fieldItems.forEach((item) => {
|
||||||
const fieldNameInput = item.querySelector(".field-name-input");
|
const fieldNameInput = item.querySelector('.field-name-input');
|
||||||
const toJiraInput = item.querySelector(".to-jira-input");
|
const toJiraInput = item.querySelector('.to-jira-input');
|
||||||
const fromJiraInput = item.querySelector(".from-jira-input");
|
const fromJiraInput = item.querySelector('.from-jira-input');
|
||||||
|
|
||||||
if (!fieldNameInput || !toJiraInput || !fromJiraInput) {
|
if (!fieldNameInput || !toJiraInput || !fromJiraInput) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -53,7 +53,7 @@ export async function collectFieldMappingsFromUI(
|
||||||
if (fieldName) {
|
if (fieldName) {
|
||||||
mappings[fieldName] = {
|
mappings[fieldName] = {
|
||||||
toJira: toJira,
|
toJira: toJira,
|
||||||
fromJira: fromJira
|
fromJira: fromJira,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -67,19 +67,16 @@ export async function collectFieldMappingsFromUI(
|
||||||
*/
|
*/
|
||||||
export async function processMappings(
|
export async function processMappings(
|
||||||
stringMappings: Record<string, JiraFieldMappingString>,
|
stringMappings: Record<string, JiraFieldMappingString>,
|
||||||
enableValidation: boolean
|
enableValidation: boolean,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
stringMappings: Record<string, JiraFieldMappingString>,
|
stringMappings: Record<string, JiraFieldMappingString>;
|
||||||
functionMappings: Record<string, JiraFieldMapping>
|
functionMappings: Record<string, JiraFieldMapping>;
|
||||||
}> {
|
}> {
|
||||||
// Convert string mappings to function mappings
|
// Convert string mappings to function mappings
|
||||||
const functionMappings = await transform_string_to_functions_mappings(
|
const functionMappings = await transform_string_to_functions_mappings(stringMappings, enableValidation);
|
||||||
stringMappings,
|
|
||||||
enableValidation
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stringMappings,
|
stringMappings,
|
||||||
functionMappings
|
functionMappings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,26 @@
|
||||||
import {TextComponent} from "obsidian";
|
import { TextComponent } from 'obsidian';
|
||||||
|
|
||||||
export function setupArrayTextSetting(
|
export function setupArrayTextSetting(
|
||||||
text: TextComponent,
|
text: TextComponent,
|
||||||
initialArray: string[],
|
initialArray: string[],
|
||||||
onChange: (array: string[]) => Promise<void>
|
onChange: (array: string[]) => Promise<void>,
|
||||||
) {
|
) {
|
||||||
text
|
text.setValue(initialArray.join(', ')).onChange(async (value) => {
|
||||||
.setValue(initialArray.join(", "))
|
const array = value
|
||||||
.onChange(async (value) => {
|
.split(',')
|
||||||
const array = value
|
.map((field) => field.trim())
|
||||||
.split(",")
|
.filter((field) => field.length > 0);
|
||||||
.map(field => field.trim())
|
|
||||||
.filter(field => field.length > 0);
|
|
||||||
|
|
||||||
await onChange(array);
|
await onChange(array);
|
||||||
});
|
});
|
||||||
|
|
||||||
text.inputEl.addEventListener("blur", () => {
|
text.inputEl.addEventListener('blur', () => {
|
||||||
const currentValue = text.inputEl.value;
|
const currentValue = text.inputEl.value;
|
||||||
const cleaned = currentValue
|
const cleaned = currentValue
|
||||||
.split(",")
|
.split(',')
|
||||||
.map(field => field.trim())
|
.map((field) => field.trim())
|
||||||
.filter(field => field.length > 0)
|
.filter((field) => field.length > 0)
|
||||||
.join(", ");
|
.join(', ');
|
||||||
|
|
||||||
if (currentValue !== cleaned) {
|
if (currentValue !== cleaned) {
|
||||||
text.setValue(cleaned);
|
text.setValue(cleaned);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
export function createLimiter(concurrency: number) {
|
export function createLimiter(concurrency: number) {
|
||||||
let activeCount = 0;
|
let activeCount = 0;
|
||||||
const queue: (() => void)[] = [];
|
const queue: (() => void)[] = [];
|
||||||
|
|
@ -13,7 +12,7 @@ export function createLimiter(concurrency: number) {
|
||||||
|
|
||||||
return async function limit<T>(fn: () => Promise<T>): Promise<T> {
|
return async function limit<T>(fn: () => Promise<T>): Promise<T> {
|
||||||
if (activeCount >= concurrency) {
|
if (activeCount >= concurrency) {
|
||||||
await new Promise<void>(resolve => queue.push(resolve));
|
await new Promise<void>((resolve) => queue.push(resolve));
|
||||||
}
|
}
|
||||||
activeCount++;
|
activeCount++;
|
||||||
try {
|
try {
|
||||||
|
|
@ -25,7 +24,5 @@ export function createLimiter(concurrency: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function chunkArray<T>(arr: T[], size: number): T[][] {
|
export function chunkArray<T>(arr: T[], size: number): T[][] {
|
||||||
return Array.from({length: Math.ceil(arr.length / size)}, (_, i) =>
|
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size));
|
||||||
arr.slice(i * size, i * size + size)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import { TFile, TFolder } from "obsidian";
|
import { TFile, TFolder } from 'obsidian';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility functions for managing the issue key to file path cache
|
* Utility functions for managing the issue key to file path cache
|
||||||
|
|
@ -11,7 +11,7 @@ import { TFile, TFolder } from "obsidian";
|
||||||
*/
|
*/
|
||||||
export async function buildCacheFromFilesystem(plugin: JiraPlugin): Promise<void> {
|
export async function buildCacheFromFilesystem(plugin: JiraPlugin): Promise<void> {
|
||||||
const issuesFolder = plugin.app.vault.getAbstractFileByPath(plugin.settings.global.issuesFolder);
|
const issuesFolder = plugin.app.vault.getAbstractFileByPath(plugin.settings.global.issuesFolder);
|
||||||
|
|
||||||
if (!issuesFolder || !(issuesFolder instanceof TFolder)) {
|
if (!issuesFolder || !(issuesFolder instanceof TFolder)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -42,16 +42,16 @@ async function scanFolderForIssueKeys(plugin: JiraPlugin, folder: TFolder): Prom
|
||||||
*/
|
*/
|
||||||
export async function validateCache(plugin: JiraPlugin): Promise<void> {
|
export async function validateCache(plugin: JiraPlugin): Promise<void> {
|
||||||
const entriesToRemove: string[] = [];
|
const entriesToRemove: string[] = [];
|
||||||
|
|
||||||
for (const [issueKey, filePath] of plugin.getAllIssueKeysMap().entries()) {
|
for (const [issueKey, filePath] of plugin.getAllIssueKeysMap().entries()) {
|
||||||
const file = plugin.app.vault.getFileByPath(filePath);
|
const file = plugin.app.vault.getFileByPath(filePath);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
entriesToRemove.push(issueKey);
|
entriesToRemove.push(issueKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove invalid entries
|
// Remove invalid entries
|
||||||
entriesToRemove.forEach(issueKey => {
|
entriesToRemove.forEach((issueKey) => {
|
||||||
plugin.removeIssueKeyFromCache(issueKey);
|
plugin.removeIssueKeyFromCache(issueKey);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {validateSettings} from "../api";
|
import { validateSettings } from '../api';
|
||||||
import {Notice} from "obsidian";
|
import { Notice } from 'obsidian';
|
||||||
// import {debugLog} from "./debugLogging";
|
// import {debugLog} from "./debugLogging";
|
||||||
|
|
||||||
export function checkCommandCallback(
|
export function checkCommandCallback(
|
||||||
|
|
@ -8,7 +8,7 @@ export function checkCommandCallback(
|
||||||
checking: boolean,
|
checking: boolean,
|
||||||
functionToExecute: (...args: any[]) => Promise<void>,
|
functionToExecute: (...args: any[]) => Promise<void>,
|
||||||
frontmatterChecks: string[],
|
frontmatterChecks: string[],
|
||||||
functionArgs: string[] = []
|
functionArgs: string[] = [],
|
||||||
): boolean {
|
): boolean {
|
||||||
const settings_are_valid = validateSettings(plugin);
|
const settings_are_valid = validateSettings(plugin);
|
||||||
const file = plugin.app.workspace.getActiveFile();
|
const file = plugin.app.workspace.getActiveFile();
|
||||||
|
|
@ -19,8 +19,10 @@ export function checkCommandCallback(
|
||||||
}
|
}
|
||||||
|
|
||||||
const frontmatter = plugin.app.metadataCache.getFileCache(file)?.frontmatter;
|
const frontmatter = plugin.app.metadataCache.getFileCache(file)?.frontmatter;
|
||||||
const hasChecks = frontmatterChecks.every(key => frontmatter?.[key] !== undefined && frontmatter?.[key] !== null && frontmatter?.[key] !== '');
|
const hasChecks = frontmatterChecks.every(
|
||||||
const functionArgsValues = functionArgs.map(arg => frontmatter?.[arg]);
|
(key) => frontmatter?.[key] !== undefined && frontmatter?.[key] !== null && frontmatter?.[key] !== '',
|
||||||
|
);
|
||||||
|
const functionArgsValues = functionArgs.map((arg) => frontmatter?.[arg]);
|
||||||
|
|
||||||
// debugLog('Function checked:', settings_are_valid && hasChecks,
|
// debugLog('Function checked:', settings_are_valid && hasChecks,
|
||||||
// 'for function to execute:', functionToExecute.name,
|
// 'for function to execute:', functionToExecute.name,
|
||||||
|
|
@ -29,12 +31,10 @@ export function checkCommandCallback(
|
||||||
|
|
||||||
if (settings_are_valid && hasChecks) {
|
if (settings_are_valid && hasChecks) {
|
||||||
if (!checking) {
|
if (!checking) {
|
||||||
functionToExecute(plugin, file, ...functionArgsValues).catch(
|
functionToExecute(plugin, file, ...functionArgsValues).catch((error) => {
|
||||||
(error) => {
|
new Notice(`Error when doing ${functionToExecute.name}: ` + (error.message || 'Unknown error'));
|
||||||
new Notice(`Error when doing ${functionToExecute.name}: ` + (error.message || "Unknown error"));
|
console.error(error);
|
||||||
console.error(error);
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,20 @@
|
||||||
import { Notice } from "obsidian";
|
import { Notice } from 'obsidian';
|
||||||
import { JiraIssue } from "../interfaces";
|
import { JiraIssue } from '../interfaces';
|
||||||
import { parse } from "acorn";
|
import { parse } from 'acorn';
|
||||||
import {debugLog} from "./debugLogging";
|
import { debugLog } from './debugLogging';
|
||||||
import {FieldMapping} from "../default/obsidianJiraFieldsMapping";
|
import { FieldMapping } from '../default/obsidianJiraFieldsMapping';
|
||||||
import {defaultIssue} from "../default/defaultIssue";
|
import { defaultIssue } from '../default/defaultIssue';
|
||||||
import {jiraToMarkdown, markdownToJira} from "./markdownHtml";
|
import { jiraToMarkdown, markdownToJira } from './markdownHtml';
|
||||||
|
|
||||||
// Constants for validation and error messages
|
// Constants for validation and error messages
|
||||||
const FORBIDDEN_PATTERNS = ["document", "window", "eval", "Function", "fetch", "setTimeout", "globalThis"];
|
const FORBIDDEN_PATTERNS = ['document', 'window', 'eval', 'Function', 'fetch', 'setTimeout', 'globalThis'];
|
||||||
const SYNTAX_KEYWORDS = ["return", "if", "else", "for", "while", "switch", "try", "catch"];
|
const SYNTAX_KEYWORDS = ['return', 'if', 'else', 'for', 'while', 'switch', 'try', 'catch'];
|
||||||
const SAFE_GLOBALS = {
|
const SAFE_GLOBALS = {
|
||||||
jiraToMarkdown,
|
jiraToMarkdown,
|
||||||
markdownToJira,
|
markdownToJira,
|
||||||
JSON: {
|
JSON: {
|
||||||
parse: JSON.parse,
|
parse: JSON.parse,
|
||||||
stringify: JSON.stringify
|
stringify: JSON.stringify,
|
||||||
},
|
},
|
||||||
Math: Math,
|
Math: Math,
|
||||||
Date: Date,
|
Date: Date,
|
||||||
|
|
@ -22,88 +22,111 @@ const SAFE_GLOBALS = {
|
||||||
Number: Number,
|
Number: Number,
|
||||||
Boolean: Boolean,
|
Boolean: Boolean,
|
||||||
Array: Array,
|
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) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
parse(fnString, { ecmaVersion: "latest" });
|
parse(fnString, { ecmaVersion: 'latest' });
|
||||||
} catch (error) {
|
} 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");
|
const iframe = document.createElement('iframe');
|
||||||
iframe.style.display = "none";
|
iframe.style.display = 'none';
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
const iframeWindow = iframe.contentWindow as Window;
|
const iframeWindow = iframe.contentWindow as Window;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let testFnString = fnString;
|
let testFnString = fnString;
|
||||||
|
|
||||||
if (isSimpleExpression(fnString) || (fnString.startsWith("{") && fnString.endsWith("}") && !fnString.includes("return"))) {
|
if (
|
||||||
const varDeclarations = approved_vars.map(varName => {
|
isSimpleExpression(fnString) ||
|
||||||
if (varName === 'issue') return `${varName} = ${JSON.stringify(defaultIssue)}`;
|
(fnString.startsWith('{') && fnString.endsWith('}') && !fnString.includes('return'))
|
||||||
if (varName === 'value') return `${varName} = "test value"`;
|
) {
|
||||||
if (varName === 'data_source') return `${varName} = {}`;
|
const varDeclarations = approved_vars
|
||||||
return `${varName} = {}`;
|
.map((varName) => {
|
||||||
}).join(', ');
|
if (varName === 'issue') return `${varName} = ${JSON.stringify(defaultIssue)}`;
|
||||||
|
if (varName === 'value') return `${varName} = "test value"`;
|
||||||
|
if (varName === 'data_source') return `${varName} = {}`;
|
||||||
|
return `${varName} = {}`;
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
testFnString = `(${varDeclarations}) => ${fnString}`;
|
testFnString = `(${varDeclarations}) => ${fnString}`;
|
||||||
}
|
}
|
||||||
debugLog(`checking function: ${testFnString.substring(0, 100)}`);
|
debugLog(`checking function: ${testFnString.substring(0, 100)}`);
|
||||||
|
|
||||||
const context: Record<string, any> = {};
|
const context: Record<string, any> = {};
|
||||||
Object.keys(SAFE_GLOBALS).forEach(key => {
|
Object.keys(SAFE_GLOBALS).forEach((key) => {
|
||||||
context[key] = (SAFE_GLOBALS as any)[key];
|
context[key] = (SAFE_GLOBALS as any)[key];
|
||||||
});
|
});
|
||||||
|
|
||||||
const safeBuiltIns = ['Infinity', 'NaN', 'undefined', 'isFinite', 'isNaN',
|
const safeBuiltIns = [
|
||||||
'parseFloat', 'parseInt', 'decodeURI', 'decodeURIComponent',
|
'Infinity',
|
||||||
'encodeURI', 'encodeURIComponent'];
|
'NaN',
|
||||||
safeBuiltIns.forEach(key => {
|
'undefined',
|
||||||
|
'isFinite',
|
||||||
|
'isNaN',
|
||||||
|
'parseFloat',
|
||||||
|
'parseInt',
|
||||||
|
'decodeURI',
|
||||||
|
'decodeURIComponent',
|
||||||
|
'encodeURI',
|
||||||
|
'encodeURIComponent',
|
||||||
|
];
|
||||||
|
safeBuiltIns.forEach((key) => {
|
||||||
context[key] = (iframeWindow as any)[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 === 'issue') return defaultIssue;
|
||||||
if (varName === 'value') return "test value";
|
if (varName === 'value') return 'test value';
|
||||||
if (varName === 'data_source') return {};
|
if (varName === 'data_source') return {};
|
||||||
return {};
|
return {};
|
||||||
});
|
});
|
||||||
|
|
||||||
const contextKeys = Object.keys(context);
|
const contextKeys = Object.keys(context);
|
||||||
const contextValues = contextKeys.map(key => context[key]);
|
const contextValues = contextKeys.map((key) => context[key]);
|
||||||
|
|
||||||
const func = new Function(
|
const func = new Function(...approved_vars, ...contextKeys, `return (${fnString});`);
|
||||||
...approved_vars,
|
|
||||||
...contextKeys,
|
|
||||||
`return (${fnString});`
|
|
||||||
);
|
|
||||||
|
|
||||||
func(...testArgs, ...contextValues);
|
func(...testArgs, ...contextValues);
|
||||||
resolve({ isValid: true });
|
resolve({ isValid: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugLog('Not valid')
|
debugLog('Not valid');
|
||||||
resolve({ isValid: false, errorMessage: `Runtime error: ${(error as Error).message}` });
|
resolve({
|
||||||
|
isValid: false,
|
||||||
|
errorMessage: `Runtime error: ${(error as Error).message}`,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
document.body.removeChild(iframe);
|
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
|
// Check for null, undefined or empty string
|
||||||
if (!fnString || fnString.trim().length === 0) {
|
if (!fnString || fnString.trim().length === 0) {
|
||||||
return { isValid: true };
|
return { isValid: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for forbidden patterns (security)
|
// 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) {
|
if (foundForbidden) {
|
||||||
return {
|
return {
|
||||||
isValid: false,
|
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
|
// 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
|
// 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
|
// 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;
|
return !hasComplexSyntax && !hasCodeBlock;
|
||||||
}
|
}
|
||||||
|
|
@ -145,14 +168,17 @@ function isSimpleExpression(fnString: string): boolean {
|
||||||
export async function safeStringToFunction(
|
export async function safeStringToFunction(
|
||||||
exprString: string,
|
exprString: string,
|
||||||
type: 'toJira' | 'fromJira' = 'toJira',
|
type: 'toJira' | 'fromJira' = 'toJira',
|
||||||
extraValidate: boolean = true
|
extraValidate: boolean = true,
|
||||||
): Promise<Function | null> {
|
): Promise<((...args: any[]) => any) | null> {
|
||||||
try {
|
try {
|
||||||
if (exprString.trim().length === 0) {
|
if (exprString.trim().length === 0) {
|
||||||
return () => null;
|
return () => null;
|
||||||
}
|
}
|
||||||
if (extraValidate) {
|
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) {
|
if (!validation.isValid) {
|
||||||
console.warn(`Invalid function: ${validation.errorMessage}`);
|
console.warn(`Invalid function: ${validation.errorMessage}`);
|
||||||
// new Notice(`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);
|
const arrowFnMatch = exprString.match(/^\s*\((.*?)\)\s*=>\s*(.*)$/s);
|
||||||
let body = arrowFnMatch ? arrowFnMatch[2].trim() : exprString.trim();
|
let body = arrowFnMatch ? arrowFnMatch[2].trim() : exprString.trim();
|
||||||
|
|
||||||
|
|
@ -168,7 +193,7 @@ export async function safeStringToFunction(
|
||||||
body = `return ${body};`;
|
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)} }`;
|
body = `{ return ${body.slice(1, -1)} }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,13 +207,15 @@ export async function safeStringToFunction(
|
||||||
Number,
|
Number,
|
||||||
Boolean,
|
Boolean,
|
||||||
Array,
|
Array,
|
||||||
Object
|
Object,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type === 'toJira') {
|
if (type === 'toJira') {
|
||||||
return function(value: any) {
|
return function (value: any) {
|
||||||
|
const fn = new Function(
|
||||||
const fn = new Function('value', 'context', `
|
'value',
|
||||||
|
'context',
|
||||||
|
`
|
||||||
with (context) {
|
with (context) {
|
||||||
try {
|
try {
|
||||||
${body}
|
${body}
|
||||||
|
|
@ -197,14 +224,19 @@ export async function safeStringToFunction(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`);
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
return fn.call(context, value, context);
|
return fn.call(context, value, context);
|
||||||
};
|
};
|
||||||
} else { // fromJira
|
} else {
|
||||||
return function(issue: JiraIssue, data_source: Record<string, any> | null) {
|
// fromJira
|
||||||
|
return function (issue: JiraIssue, data_source: Record<string, any> | null) {
|
||||||
const fn = new Function('issue', 'data_source', 'context', `
|
const fn = new Function(
|
||||||
|
'issue',
|
||||||
|
'data_source',
|
||||||
|
'context',
|
||||||
|
`
|
||||||
with (context) {
|
with (context) {
|
||||||
try {
|
try {
|
||||||
${body}
|
${body}
|
||||||
|
|
@ -213,19 +245,20 @@ export async function safeStringToFunction(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`);
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
return fn.call(context, issue, data_source, context);
|
return fn.call(context, issue, data_source, context);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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}`);
|
new Notice(`Failed to create function: ${(error as Error).message}`);
|
||||||
return null;
|
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);
|
const baseStr = functionToExpressionString(fn);
|
||||||
|
|
||||||
if (!baseStr) return baseStr;
|
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*\(([^)]*)\))/);
|
const paramMatch = fnStr.match(/^\s*(?:\(?([^)]*)\)?\s*=>|\s*function\s*\(([^)]*)\))/);
|
||||||
if (paramMatch) {
|
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;
|
let resultStr = baseStr;
|
||||||
|
|
||||||
|
|
@ -267,7 +300,7 @@ export function jiraFunctionToString(fn: Function, isFromJira: boolean = false):
|
||||||
return baseStr;
|
return baseStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function functionToExpressionString(fn: Function): string {
|
export function functionToExpressionString(fn: (...args: any[]) => any): string {
|
||||||
try {
|
try {
|
||||||
const fnStr = fn.toString().trim();
|
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
|
// If we can't parse it properly, return empty string
|
||||||
console.error("Failed to extract expression from function:", fnStr);
|
console.error('Failed to extract expression from function:', fnStr);
|
||||||
new Notice("Failed to extract expression from function");
|
new Notice('Failed to extract expression from function');
|
||||||
return "";
|
return '';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error converting function to expression string:", error);
|
console.error('Error converting function to expression string:', error);
|
||||||
return "";
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function transform_string_to_functions_mappings (
|
export async function transform_string_to_functions_mappings(
|
||||||
mappings: Record<string, { toJira: string; fromJira: string }>, extraValidate: boolean = true) {
|
mappings: Record<string, { toJira: string; fromJira: string }>,
|
||||||
|
extraValidate: boolean = true,
|
||||||
|
) {
|
||||||
// Also convert to functions for runtime use
|
// Also convert to functions for runtime use
|
||||||
const transformedMappings: Record<string, FieldMapping> = {};
|
const transformedMappings: Record<string, FieldMapping> = {};
|
||||||
for (const [fieldName, { toJira, fromJira }] of Object.entries(mappings)) {
|
for (const [fieldName, { toJira, fromJira }] of Object.entries(mappings)) {
|
||||||
|
|
@ -309,12 +344,12 @@ export async function transform_string_to_functions_mappings (
|
||||||
|
|
||||||
if (toJiraFn && fromJiraFn) {
|
if (toJiraFn && fromJiraFn) {
|
||||||
transformedMappings[fieldName] = {
|
transformedMappings[fieldName] = {
|
||||||
toJira: await toJiraFn as (value: any) => any,
|
toJira: (await toJiraFn) as (value: any) => any,
|
||||||
fromJira: await fromJiraFn as (issue: JiraIssue, data_source: Record<string, any>) => any,
|
fromJira: (await fromJiraFn) as (issue: JiraIssue, data_source: Record<string, any> | null) => any,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.warn(`Invalid function in field: ${fieldName}`);
|
console.warn(`Invalid function in field: ${fieldName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return transformedMappings
|
return transformedMappings;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// Create a debug utility module (debug.ts)
|
|
||||||
export const DEBUG_MODE = process.env.NODE_ENV !== 'production';
|
export const DEBUG_MODE = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
export function debugLog(...args: any[]): void {
|
export function debugLog(...args: any[]): void {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the issues folder exists
|
* Ensure the issues folder exists
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import {JiraIssue} from "../interfaces";
|
import { JiraIssue } from '../interfaces';
|
||||||
import {jiraToMarkdown} from "./markdownHtml";
|
import { jiraToMarkdown } from './markdownHtml';
|
||||||
import {Notice, TFile} from "obsidian";
|
import { Notice, TFile } from 'obsidian';
|
||||||
import JiraPlugin from "../main";
|
import JiraPlugin from '../main';
|
||||||
import {extractAllJiraSyncValuesFromContent, updateJiraSyncContent} from "./sectionTools";
|
import { extractAllJiraSyncValuesFromContent, updateJiraSyncContent } from './sectionTools';
|
||||||
import {FieldMapping, obsidianJiraFieldMappings} from "../default/obsidianJiraFieldsMapping";
|
import { FieldMapping, obsidianJiraFieldMappings } from '../default/obsidianJiraFieldsMapping';
|
||||||
import {debugLog} from "./debugLogging";
|
import { debugLog } from './debugLogging';
|
||||||
|
|
||||||
|
|
||||||
export function localToJiraFields(
|
export function localToJiraFields(
|
||||||
data_source: Record<string, any>, customFieldMappings: Record<string, FieldMapping>
|
data_source: Record<string, any>,
|
||||||
|
customFieldMappings: Record<string, FieldMapping>,
|
||||||
): Record<string, any> {
|
): Record<string, any> {
|
||||||
const jiraFields: Record<string, any> = {};
|
const jiraFields: Record<string, any> = {};
|
||||||
|
|
||||||
|
|
@ -31,7 +31,6 @@ export function localToJiraFields(
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error mapping for ${key}: ${e}`);
|
console.error(`Error mapping for ${key}: ${e}`);
|
||||||
new Notice(`Error mapping for ${key}: ${e}`);
|
new Notice(`Error mapping for ${key}: ${e}`);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle custom fields
|
// Handle custom fields
|
||||||
|
|
@ -43,15 +42,14 @@ export function localToJiraFields(
|
||||||
return jiraFields;
|
return jiraFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateJiraToLocal(
|
export async function updateJiraToLocal(plugin: JiraPlugin, file: TFile, issue: JiraIssue): Promise<void> {
|
||||||
plugin: JiraPlugin,
|
|
||||||
file: TFile,
|
|
||||||
issue: JiraIssue
|
|
||||||
): Promise<void> {
|
|
||||||
// First, update the frontmatter using processFrontMatter
|
// First, update the frontmatter using processFrontMatter
|
||||||
await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
await plugin.app.fileManager.processFrontMatter(file, (frontmatter) => {
|
||||||
// Update frontmatter with Jira data
|
// 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
|
// Then, process the file content to update sync sections
|
||||||
|
|
@ -60,7 +58,10 @@ export async function updateJiraToLocal(
|
||||||
const syncSections = extractAllJiraSyncValuesFromContent(fileContent);
|
const syncSections = extractAllJiraSyncValuesFromContent(fileContent);
|
||||||
|
|
||||||
// Update sync sections with Jira data
|
// 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
|
// Update content sections from Jira fields
|
||||||
let updatedContent = fileContent;
|
let updatedContent = fileContent;
|
||||||
|
|
@ -79,7 +80,7 @@ export async function updateJiraToLocal(
|
||||||
export function applyJiraDataToLocal(
|
export function applyJiraDataToLocal(
|
||||||
localData: Record<string, any>,
|
localData: Record<string, any>,
|
||||||
issue: JiraIssue,
|
issue: JiraIssue,
|
||||||
fieldMappings: Record<string, FieldMapping>
|
fieldMappings: Record<string, FieldMapping>,
|
||||||
): void {
|
): void {
|
||||||
// Process existing fields in local data
|
// Process existing fields in local data
|
||||||
for (const key of Object.keys(localData)) {
|
for (const key of Object.keys(localData)) {
|
||||||
|
|
@ -99,7 +100,7 @@ function updateFieldFromJira(
|
||||||
key: string,
|
key: string,
|
||||||
targetObject: Record<string, any>,
|
targetObject: Record<string, any>,
|
||||||
issue: JiraIssue,
|
issue: JiraIssue,
|
||||||
fieldMappings: Record<string, FieldMapping>
|
fieldMappings: Record<string, FieldMapping>,
|
||||||
): void {
|
): void {
|
||||||
try {
|
try {
|
||||||
let value = issue.fields[key];
|
let value = issue.fields[key];
|
||||||
|
|
@ -116,28 +117,24 @@ function updateFieldFromJira(
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Log available mappings for debugging
|
// Log available mappings for debugging
|
||||||
logMappingDebugInfo(key, e, fieldMappings);
|
logMappingDebugInfo(key, e as Error, fieldMappings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log debug information for mapping errors
|
* Log debug information for mapping errors
|
||||||
*/
|
*/
|
||||||
function logMappingDebugInfo(
|
function logMappingDebugInfo(key: string, error: Error, fieldMappings: Record<string, FieldMapping>): void {
|
||||||
key: string,
|
|
||||||
error: Error,
|
|
||||||
fieldMappings: Record<string, FieldMapping>
|
|
||||||
): void {
|
|
||||||
console.error(`Error mapping for ${key}: ${error}`);
|
console.error(`Error mapping for ${key}: ${error}`);
|
||||||
new Notice(`Error mapping for ${key}: ${error}`);
|
new Notice(`Error mapping for ${key}: ${error}`);
|
||||||
|
|
||||||
// Create debug info about available mappings
|
// 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)) {
|
for (const mappingKey of Object.keys(fieldMappings)) {
|
||||||
mappingInfo[mappingKey] = {
|
mappingInfo[mappingKey] = {
|
||||||
hasToJira: typeof fieldMappings[mappingKey].toJira,
|
hasToJira: typeof fieldMappings[mappingKey].toJira,
|
||||||
hasFromJira: typeof fieldMappings[mappingKey].fromJira
|
hasFromJira: typeof fieldMappings[mappingKey].fromJira,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,16 @@ export function jiraToMarkdown(str: any): string {
|
||||||
if (str === null || str === undefined) return '';
|
if (str === null || str === undefined) return '';
|
||||||
|
|
||||||
// Initial normalization to string
|
// Initial normalization to string
|
||||||
let content: string = "";
|
let content: string = '';
|
||||||
if (typeof str === "string") content = str;
|
if (typeof str === 'string') content = str;
|
||||||
else if (typeof str === "number") content = str.toString();
|
else if (typeof str === 'number') content = str.toString();
|
||||||
else if (typeof str === "object") content = JSON.stringify(str);
|
else if (typeof str === 'object') content = JSON.stringify(str);
|
||||||
else content = String(str);
|
else content = String(str);
|
||||||
|
|
||||||
// URL Protection: Store original URLs
|
// URL Protection: Store original URLs
|
||||||
const urlMap: Map<string, string> = new Map();
|
const urlMap: Map<string, string> = new Map();
|
||||||
// Regex to find URLs
|
// Regex to find URLs
|
||||||
const urlRegex = /(https?:\/\/[^\s\(\)\[\]\{\}]+)/g;
|
const urlRegex = /(https?:\/\/[^\s()[\]{}]+)/g;
|
||||||
let urlCount = 0;
|
let urlCount = 0;
|
||||||
|
|
||||||
content = content.replace(urlRegex, (match) => {
|
content = content.replace(urlRegex, (match) => {
|
||||||
|
|
@ -59,7 +59,7 @@ export function jiraToMarkdown(str: any): string {
|
||||||
// Code Blocks
|
// Code Blocks
|
||||||
.replace(
|
.replace(
|
||||||
/\{code(:([a-z]+))?([:|]?(title|borderStyle|borderColor|borderWidth|bgColor|titleBGColor)=.+?)*\}([^]*?)\n?\{code\}/gm,
|
/\{code(:([a-z]+))?([:|]?(title|borderStyle|borderColor|borderWidth|bgColor|titleBGColor)=.+?)*\}([^]*?)\n?\{code\}/gm,
|
||||||
'```$2$5\n```'
|
'```$2$5\n```',
|
||||||
)
|
)
|
||||||
.replace(/{noformat}/g, '```')
|
.replace(/{noformat}/g, '```')
|
||||||
// Images
|
// Images
|
||||||
|
|
@ -85,11 +85,10 @@ export function jiraToMarkdown(str: any): string {
|
||||||
});
|
});
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
|
|
||||||
} catch (e) {
|
} 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
|
// 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 {
|
export function markdownToJira(str: string): string {
|
||||||
if (!str) return '';
|
if (!str) return '';
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
del: "-",
|
del: '-',
|
||||||
ins: "+",
|
ins: '+',
|
||||||
sup: "^",
|
sup: '^',
|
||||||
sub: "~",
|
sub: '~',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -112,106 +111,82 @@ export function markdownToJira(str: string): string {
|
||||||
// Tables
|
// Tables
|
||||||
.replace(
|
.replace(
|
||||||
/^(\|[^\n]+\|\r?\n)((?:\|\s*:?[-]+:?\s*)+\|)(\n(?:\|[^\n]+\|\r?\n?)*)?$/gm,
|
/^(\|[^\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 headers = headerLine.match(/[^|]+(?=\|)/g) || [];
|
||||||
const separators =
|
const separators = separatorLine.match(/[^|]+(?=\|)/g) || [];
|
||||||
separatorLine.match(/[^|]+(?=\|)/g) || [];
|
|
||||||
if (headers.length !== separators.length) return match;
|
if (headers.length !== separators.length) return match;
|
||||||
|
|
||||||
const rows = rowstr.split("\n");
|
const rows = rowstr.split('\n');
|
||||||
if (rows.length === 2 && headers.length === 1)
|
if (rows.length === 2 && headers.length === 1)
|
||||||
// Panel
|
// Panel
|
||||||
return `{panel:title=${headers[0].trim()}}\n${rowstr
|
return `{panel:title=${headers[0].trim()}}\n${rowstr
|
||||||
.replace(/^\|(.*)[ \t]*\|/, "$1")
|
.replace(/^\|(.*)[ \t]*\|/, '$1')
|
||||||
.trim()}\n{panel}\n`;
|
.trim()}\n{panel}\n`;
|
||||||
|
|
||||||
return `||${headers.join("||")}||${rowstr}`;
|
return `||${headers.join('||')}||${rowstr}`;
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
// Bold, Italic, and Combined (bold+italic)
|
// Bold, Italic, and Combined (bold+italic)
|
||||||
.replace(
|
.replace(/([*_]+)(\S.*?)\1/g, (match: string, wrapper: string, content: string) => {
|
||||||
/([*_]+)(\S.*?)\1/g,
|
switch (wrapper.length) {
|
||||||
(match: string, wrapper: string, content: string) => {
|
case 1:
|
||||||
switch (wrapper.length) {
|
return `_${content}_`;
|
||||||
case 1:
|
case 2:
|
||||||
return `_${content}_`;
|
return `*${content}*`;
|
||||||
case 2:
|
case 3:
|
||||||
return `*${content}*`;
|
return `_*${content}*_`;
|
||||||
case 3:
|
default:
|
||||||
return `_*${content}*_`;
|
return wrapper + content + wrapper;
|
||||||
default:
|
|
||||||
return wrapper + content + wrapper;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
// All Headers (# format)
|
// All Headers (# format)
|
||||||
.replace(
|
.replace(/^([#]+)(.*?)$/gm, (match: string, level: string, content: string) => {
|
||||||
/^([#]+)(.*?)$/gm,
|
return `h${level.length}.${content}`;
|
||||||
(match: string, level: string, content: string) => {
|
})
|
||||||
return `h${level.length}.${content}`;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Headers (H1 and H2 underlines)
|
// Headers (H1 and H2 underlines)
|
||||||
.replace(
|
.replace(/^(.*?)\n([=-]+)$/gm, (match: string, content: string, level: string) => {
|
||||||
/^(.*?)\n([=-]+)$/gm,
|
return `h${level[0] === '=' ? 1 : 2}. ${content}`;
|
||||||
(match: string, content: string, level: string) => {
|
})
|
||||||
return `h${level[0] === "=" ? 1 : 2}. ${content}`;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Ordered lists
|
// Ordered lists
|
||||||
.replace(
|
.replace(/^([ \t]*)\d+\.\s+/gm, (match: string, spaces: string) => {
|
||||||
/^([ \t]*)\d+\.\s+/gm,
|
return `${Array(Math.floor(spaces.length / 3) + 1)
|
||||||
(match: string, spaces: string) => {
|
.fill('#')
|
||||||
return `${Array(Math.floor(spaces.length / 3) + 1)
|
.join('')} `;
|
||||||
.fill("#")
|
})
|
||||||
.join("")} `;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Un-Ordered Lists
|
// Un-Ordered Lists
|
||||||
.replace(
|
.replace(/^([ \t]*)\*\s+/gm, (match: string, spaces: string) => {
|
||||||
/^([ \t]*)\*\s+/gm,
|
return `${Array(Math.floor(spaces.length / 2 + 1))
|
||||||
(match: string, spaces: string) => {
|
.fill('*')
|
||||||
return `${Array(Math.floor(spaces.length / 2 + 1))
|
.join('')} `;
|
||||||
.fill("*")
|
})
|
||||||
.join("")} `;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// Headers (h1 or h2) (lines "underlined" by ---- or =====)
|
// Headers (h1 or h2) (lines "underlined" by ---- or =====)
|
||||||
// Citations, Inserts, Subscripts, Superscripts, and Strikethroughs
|
// Citations, Inserts, Subscripts, Superscripts, and Strikethroughs
|
||||||
.replace(
|
.replace(
|
||||||
new RegExp(`<(${Object.keys(map).join("|")})>(.*?)</\\1>`, "g"),
|
new RegExp(`<(${Object.keys(map).join('|')})>(.*?)</\\1>`, 'g'),
|
||||||
(match: string, from: string, content: string) => {
|
(match: string, from: string, content: string) => {
|
||||||
const to = map[from];
|
const to = map[from];
|
||||||
return to + content + to;
|
return to + content + to;
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
// Other kind of strikethrough
|
// Other kind of strikethrough
|
||||||
.replace(/(\s+)~~(.*?)~~(\s+)/g, "$1-$2-$3")
|
.replace(/(\s+)~~(.*?)~~(\s+)/g, '$1-$2-$3')
|
||||||
// Named/Un-Named Code Block
|
// Named/Un-Named Code Block
|
||||||
.replace(
|
.replace(/```(.+\n)?((?:.|\n)*?)```/g, (match: string, synt: string, content: string) => {
|
||||||
/```(.+\n)?((?:.|\n)*?)```/g,
|
let code = '{code}';
|
||||||
(match: string, synt: string, content: string) => {
|
if (synt) {
|
||||||
let code = "{code}";
|
code = `{code:${synt.replace(/\n/g, '')}}\n`;
|
||||||
if (synt) {
|
|
||||||
code = `{code:${synt.replace(/\n/g, "")}}\n`;
|
|
||||||
}
|
|
||||||
return `${code}${content}{code}`;
|
|
||||||
}
|
}
|
||||||
)
|
return `${code}${content}{code}`;
|
||||||
|
})
|
||||||
// Inline-Preformatted Text
|
// Inline-Preformatted Text
|
||||||
.replace(/`([^`]+)`/g, "{{$1}}")
|
.replace(/`([^`]+)`/g, '{{$1}}')
|
||||||
// Images
|
// Images
|
||||||
.replace(/!\[[^\]]*\]\(([^)]+)\)/g, "!$1!")
|
.replace(/!\[[^\]]*\]\(([^)]+)\)/g, '!$1!')
|
||||||
// Named Link
|
// Named Link
|
||||||
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "[$1|$2]")
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[$1|$2]')
|
||||||
// Un-Named Link
|
// Un-Named Link
|
||||||
.replace(/<([^>]+)>/g, "[$1]")
|
.replace(/<([^>]+)>/g, '[$1]')
|
||||||
// Single Paragraph Blockquote
|
// Single Paragraph Blockquote
|
||||||
.replace(/^>/gm, "bq.")
|
.replace(/^>/gm, 'bq.')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@ import {
|
||||||
ConnectionSettingsInterface,
|
ConnectionSettingsInterface,
|
||||||
FieldMappingSettingsInterface,
|
FieldMappingSettingsInterface,
|
||||||
GlobalSettingsInterface,
|
GlobalSettingsInterface,
|
||||||
TimekeepSettingsInterface
|
TimekeepSettingsInterface,
|
||||||
} from "../settings/default";
|
} 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;
|
if (!data || typeof data !== 'object') return data;
|
||||||
|
|
||||||
const result = { ...data };
|
const result = { ...data };
|
||||||
|
|
@ -13,7 +13,13 @@ export function checkMigrateSettings(data: any, saveSettings: ()=>void): any {
|
||||||
|
|
||||||
// Migrate root-level connection fields
|
// Migrate root-level connection fields
|
||||||
const connectionFields: (keyof ConnectionSettingsInterface)[] = [
|
const connectionFields: (keyof ConnectionSettingsInterface)[] = [
|
||||||
'jiraUrl', 'apiVersion', 'authMethod', 'apiToken', 'username', 'email', 'password'
|
'jiraUrl',
|
||||||
|
'apiVersion',
|
||||||
|
'authMethod',
|
||||||
|
'apiToken',
|
||||||
|
'username',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
];
|
];
|
||||||
for (const field of connectionFields) {
|
for (const field of connectionFields) {
|
||||||
if (field in result) {
|
if (field in result) {
|
||||||
|
|
@ -37,7 +43,9 @@ export function checkMigrateSettings(data: any, saveSettings: ()=>void): any {
|
||||||
|
|
||||||
// Migrate root-level field mapping fields
|
// Migrate root-level field mapping fields
|
||||||
const fieldMappingFields: (keyof FieldMappingSettingsInterface)[] = [
|
const fieldMappingFields: (keyof FieldMappingSettingsInterface)[] = [
|
||||||
'fieldMappings', 'fieldMappingsStrings', 'enableFieldValidation'
|
'fieldMappings',
|
||||||
|
'fieldMappingsStrings',
|
||||||
|
'enableFieldValidation',
|
||||||
];
|
];
|
||||||
for (const field of fieldMappingFields) {
|
for (const field of fieldMappingFields) {
|
||||||
if (field in result) {
|
if (field in result) {
|
||||||
|
|
@ -50,7 +58,10 @@ export function checkMigrateSettings(data: any, saveSettings: ()=>void): any {
|
||||||
|
|
||||||
// Migrate root-level timekeep fields
|
// Migrate root-level timekeep fields
|
||||||
const timekeepFields: (keyof TimekeepSettingsInterface)[] = [
|
const timekeepFields: (keyof TimekeepSettingsInterface)[] = [
|
||||||
'sendComments', 'statisticsTimeType', 'maxItemsToShow', 'customDateRange'
|
'sendComments',
|
||||||
|
'statisticsTimeType',
|
||||||
|
'maxItemsToShow',
|
||||||
|
'customDateRange',
|
||||||
];
|
];
|
||||||
for (const field of timekeepFields) {
|
for (const field of timekeepFields) {
|
||||||
if (field in result) {
|
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) {
|
if (data_changed) {
|
||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitize a file name to ensure it's valid
|
* Sanitize a file name to ensure it's valid
|
||||||
* @param fileName The original file name
|
* @param fileName The original file name
|
||||||
* @returns Sanitized file name
|
* @returns Sanitized file name
|
||||||
*/
|
*/
|
||||||
export function sanitizeFileName(fileName: string): string {
|
export function sanitizeFileName(fileName: string): string {
|
||||||
return fileName.replace(/[\\/:*?"<>|]/g, "-");
|
return fileName.replace(/[\\/:*?"<>|]/g, '-');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
interface ParsedBlock {
|
||||||
type: "section" | "line" | "inline" | "block";
|
type: 'section' | 'line' | 'inline' | 'block';
|
||||||
name: string;
|
name: string;
|
||||||
content: string;
|
content: string;
|
||||||
startIndex: number;
|
startIndex: number;
|
||||||
|
|
@ -20,37 +21,37 @@ export function parseFileContent(fileContent: string): ParsedBlock[] {
|
||||||
|
|
||||||
while ((match = UNIFIED_REGEX.exec(fileContent)) !== null) {
|
while ((match = UNIFIED_REGEX.exec(fileContent)) !== null) {
|
||||||
const [fullMatch, typeRaw, name, rawContent] = match;
|
const [fullMatch, typeRaw, name, rawContent] = match;
|
||||||
let type: ParsedBlock["type"];
|
let type: ParsedBlock['type'];
|
||||||
let extractedContent: string;
|
let extractedContent: string;
|
||||||
let actualEndIndex: number;
|
let actualEndIndex: number;
|
||||||
|
|
||||||
// Determine actual type and extract content
|
// Determine actual type and extract content
|
||||||
if (typeRaw === "section") {
|
if (typeRaw === 'section') {
|
||||||
type = "section";
|
type = 'section';
|
||||||
// Cut at first heading or another jira-sync-*
|
// Cut at first heading or another jira-sync-*
|
||||||
const headingMatch = rawContent.match(/\n#+? /);
|
const headingMatch = rawContent.match(/\n#+? /);
|
||||||
const contentBeforeHeading = headingMatch
|
const contentBeforeHeading = headingMatch
|
||||||
? rawContent.substring(0, (headingMatch.index||0)+1)
|
? rawContent.substring(0, (headingMatch.index || 0) + 1)
|
||||||
: rawContent;
|
: rawContent;
|
||||||
actualEndIndex = match.index + fullMatch.length - (rawContent.length - contentBeforeHeading.length);
|
actualEndIndex = match.index + fullMatch.length - (rawContent.length - contentBeforeHeading.length);
|
||||||
extractedContent = contentBeforeHeading.trim();
|
extractedContent = contentBeforeHeading.trim();
|
||||||
} else if (typeRaw === "line") {
|
} else if (typeRaw === 'line') {
|
||||||
type = "line";
|
type = 'line';
|
||||||
// Content is on the same line only
|
// 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);
|
actualEndIndex = match.index + fullMatch.length - (rawContent.length - firstLine.length);
|
||||||
extractedContent = firstLine.trim();
|
extractedContent = firstLine.trim();
|
||||||
} else if (typeRaw === "inline-start") {
|
} else if (typeRaw === 'inline-start') {
|
||||||
type = "inline";
|
type = 'inline';
|
||||||
// Content between inline-start and inline-end (markers included in fullMatch)
|
// Content between inline-start and inline-end (markers included in fullMatch)
|
||||||
actualEndIndex = match.index + fullMatch.length;
|
actualEndIndex = match.index + fullMatch.length;
|
||||||
extractedContent = rawContent.trim();
|
extractedContent = rawContent.trim();
|
||||||
} else if (typeRaw === "block-start") {
|
} else if (typeRaw === 'block-start') {
|
||||||
type = "block";
|
type = 'block';
|
||||||
// Content between block-start and block-end
|
// Content between block-start and block-end
|
||||||
const lines = rawContent.split("\n");
|
const lines = rawContent.split('\n');
|
||||||
const contentLines = lines.slice(lines[0] === "" ? 1 : 0, -1);
|
const contentLines = lines.slice(lines[0] === '' ? 1 : 0, -1);
|
||||||
const joinedContent = contentLines.join("\n");
|
const joinedContent = contentLines.join('\n');
|
||||||
actualEndIndex = match.index + fullMatch.length;
|
actualEndIndex = match.index + fullMatch.length;
|
||||||
extractedContent = joinedContent.trim();
|
extractedContent = joinedContent.trim();
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -64,16 +65,14 @@ export function parseFileContent(fileContent: string): ParsedBlock[] {
|
||||||
startIndex: match.index,
|
startIndex: match.index,
|
||||||
endIndex: actualEndIndex,
|
endIndex: actualEndIndex,
|
||||||
// indexesContent: fileContent.substring(match.index, actualEndIndex),
|
// indexesContent: fileContent.substring(match.index, actualEndIndex),
|
||||||
fullMatch
|
fullMatch,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return blocks;
|
return blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractAllJiraSyncValuesFromContent(
|
export function extractAllJiraSyncValuesFromContent(fileContent: string): Record<string, string> {
|
||||||
fileContent: string
|
|
||||||
): Record<string, string> {
|
|
||||||
const blocks = parseFileContent(fileContent);
|
const blocks = parseFileContent(fileContent);
|
||||||
const result: Record<string, string> = {};
|
const result: Record<string, string> = {};
|
||||||
|
|
||||||
|
|
@ -85,10 +84,7 @@ export function extractAllJiraSyncValuesFromContent(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateJiraSyncContent(
|
export function updateJiraSyncContent(fileContent: string, updates: Record<string, string>): string {
|
||||||
fileContent: string,
|
|
||||||
updates: Record<string, string>
|
|
||||||
): string {
|
|
||||||
const blocks = parseFileContent(fileContent);
|
const blocks = parseFileContent(fileContent);
|
||||||
|
|
||||||
// Sort blocks in reverse order to replace from end to start
|
// Sort blocks in reverse order to replace from end to start
|
||||||
|
|
@ -106,16 +102,16 @@ export function updateJiraSyncContent(
|
||||||
let newBlock: string;
|
let newBlock: string;
|
||||||
|
|
||||||
switch (block.type) {
|
switch (block.type) {
|
||||||
case "section":
|
case 'section':
|
||||||
newBlock = `\`jira-sync-section-${block.name}\`\n${newContent}\n`;
|
newBlock = `\`jira-sync-section-${block.name}\`\n${newContent}\n`;
|
||||||
break;
|
break;
|
||||||
case "line":
|
case 'line':
|
||||||
newBlock = `\`jira-sync-line-${block.name}\`${newContent.split("\n")[0]}`;
|
newBlock = `\`jira-sync-line-${block.name}\`${newContent.split('\n')[0]}`;
|
||||||
break;
|
break;
|
||||||
case "inline":
|
case 'inline':
|
||||||
newBlock = `\`jira-sync-inline-start-${block.name}\`${newContent}\`jira-sync-end\``;
|
newBlock = `\`jira-sync-inline-start-${block.name}\`${newContent}\`jira-sync-end\``;
|
||||||
break;
|
break;
|
||||||
case "block":
|
case 'block':
|
||||||
newBlock = `\`jira-sync-block-start-${block.name}\`\n${newContent}\n\`jira-sync-end\``;
|
newBlock = `\`jira-sync-block-start-${block.name}\`\n${newContent}\n\`jira-sync-end\``;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
173
src/tools/timeTracker.ts
Normal file
173
src/tools/timeTracker.ts
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
133
styles.css
133
styles.css
|
|
@ -18,7 +18,6 @@
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Template Selector Styles */
|
/* Template Selector Styles */
|
||||||
.suggest-select-container {
|
.suggest-select-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -53,83 +52,81 @@
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.mod-warning {
|
.mod-warning {
|
||||||
color: var(--text-warning);
|
color: var(--text-warning);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.test-mappings-header {
|
.test-mappings-header {
|
||||||
margin-bottom: 0.2em;
|
margin-bottom: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-mappings-desc {
|
.test-mappings-desc {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-mapping-items-list {
|
.test-mapping-items-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.2em;
|
gap: 1.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-mapping-item {
|
.test-mapping-item {
|
||||||
background: var(--background-secondary-alt);
|
background: var(--background-secondary-alt);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1em 1em 1em 1.2em;
|
padding: 1em 1em 1em 1.2em;
|
||||||
position: relative;
|
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 {
|
.value-container {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.value-display {
|
.value-display {
|
||||||
background: var(--background-secondary);
|
background: var(--background-secondary);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.5em 0.8em;
|
padding: 0.5em 0.8em;
|
||||||
/*font-family: var(--font-monospace);*/
|
/*font-family: var(--font-monospace);*/
|
||||||
font-size: 0.98em;
|
font-size: 0.98em;
|
||||||
min-height: 2em;
|
min-height: 2em;
|
||||||
margin-top: 0.2em;
|
margin-top: 0.2em;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
border: 1px solid var(--background-modifier-border);
|
border: 1px solid var(--background-modifier-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-mapping-item .from-jira-container {
|
.test-mapping-item .from-jira-container {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.test-mapping-item .from-jira-input {
|
.test-mapping-item .from-jira-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 2.2em;
|
min-height: 2.2em;
|
||||||
/*font-family: var(--font-monospace);*/
|
/*font-family: var(--font-monospace);*/
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
margin-top: 0.2em;
|
margin-top: 0.2em;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid var(--background-modifier-border);
|
border: 1px solid var(--background-modifier-border);
|
||||||
background: var(--background-secondary);
|
background: var(--background-secondary);
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
padding: 0.3em 0.5em;
|
padding: 0.3em 0.5em;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-test-mapping-btn {
|
.add-test-mapping-btn {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 1.5em auto 0 auto;
|
margin: 1.5em auto 0 auto;
|
||||||
padding: 0.5em 1.5em;
|
padding: 0.5em 1.5em;
|
||||||
background: var(--interactive-accent);
|
background: var(--interactive-accent);
|
||||||
color: var(--text-on-accent);
|
color: var(--text-on-accent);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-mappings-list {
|
.field-mappings-list {
|
||||||
|
|
@ -168,7 +165,6 @@
|
||||||
border-bottom: 2px solid var(--background-modifier-border);
|
border-bottom: 2px solid var(--background-modifier-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.field-mapping-label {
|
.field-mapping-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-normal);
|
color: var(--text-normal);
|
||||||
|
|
@ -217,7 +213,6 @@
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.to-jira-input,
|
.to-jira-input,
|
||||||
.from-jira-input {
|
.from-jira-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -402,10 +397,10 @@
|
||||||
pre code.hljs {
|
pre code.hljs {
|
||||||
display: block;
|
display: block;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
padding: 1em
|
padding: 1em;
|
||||||
}
|
}
|
||||||
code.hljs {
|
code.hljs {
|
||||||
padding: 3px 5px
|
padding: 3px 5px;
|
||||||
}
|
}
|
||||||
/*!
|
/*!
|
||||||
Theme: GitHub
|
Theme: GitHub
|
||||||
|
|
@ -419,7 +414,7 @@ code.hljs {
|
||||||
*/
|
*/
|
||||||
.hljs {
|
.hljs {
|
||||||
color: #24292e;
|
color: #24292e;
|
||||||
background: #ffffff
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
.hljs-doctag,
|
.hljs-doctag,
|
||||||
.hljs-keyword,
|
.hljs-keyword,
|
||||||
|
|
@ -429,88 +424,87 @@ code.hljs {
|
||||||
.hljs-type,
|
.hljs-type,
|
||||||
.hljs-variable.language_ {
|
.hljs-variable.language_ {
|
||||||
/* prettylights-syntax-keyword */
|
/* prettylights-syntax-keyword */
|
||||||
color: #f6cc42
|
color: #f6cc42;
|
||||||
}
|
}
|
||||||
.hljs-title,
|
.hljs-title,
|
||||||
.hljs-title.class_,
|
.hljs-title.class_,
|
||||||
.hljs-title.class_.inherited__,
|
.hljs-title.class_.inherited__,
|
||||||
.hljs-title.function_ {
|
.hljs-title.function_ {
|
||||||
/* prettylights-syntax-entity */
|
/* prettylights-syntax-entity */
|
||||||
color: #6f42c1
|
color: #6f42c1;
|
||||||
}
|
}
|
||||||
.hljs-attr,
|
.hljs-attr,
|
||||||
.hljs-attribute,
|
.hljs-attribute,
|
||||||
.hljs-literal,
|
.hljs-literal,
|
||||||
.hljs-meta,
|
.hljs-meta,
|
||||||
|
|
||||||
.hljs-operator,
|
.hljs-operator,
|
||||||
.hljs-variable,
|
.hljs-variable,
|
||||||
.hljs-selector-attr,
|
.hljs-selector-attr,
|
||||||
.hljs-selector-class,
|
.hljs-selector-class,
|
||||||
.hljs-selector-id {
|
.hljs-selector-id {
|
||||||
/* prettylights-syntax-constant */
|
/* prettylights-syntax-constant */
|
||||||
color: #6ecbfa
|
color: #6ecbfa;
|
||||||
}
|
}
|
||||||
.hljs-number {
|
.hljs-number {
|
||||||
/* prettylights-syntax-numeric-literal */
|
/* prettylights-syntax-numeric-literal */
|
||||||
color: #93cda7
|
color: #93cda7;
|
||||||
}
|
}
|
||||||
.hljs-regexp,
|
.hljs-regexp,
|
||||||
.hljs-string,
|
.hljs-string,
|
||||||
.hljs-meta .hljs-string {
|
.hljs-meta .hljs-string {
|
||||||
/* prettylights-syntax-string */
|
/* prettylights-syntax-string */
|
||||||
color: #cd905c
|
color: #cd905c;
|
||||||
}
|
}
|
||||||
.hljs-built_in,
|
.hljs-built_in,
|
||||||
.hljs-symbol {
|
.hljs-symbol {
|
||||||
/* prettylights-syntax-variable */
|
/* prettylights-syntax-variable */
|
||||||
color: #e36209
|
color: #e36209;
|
||||||
}
|
}
|
||||||
.hljs-comment,
|
.hljs-comment,
|
||||||
.hljs-code,
|
.hljs-code,
|
||||||
.hljs-formula {
|
.hljs-formula {
|
||||||
/* prettylights-syntax-comment */
|
/* prettylights-syntax-comment */
|
||||||
color: #6a737d
|
color: #6a737d;
|
||||||
}
|
}
|
||||||
.hljs-name,
|
.hljs-name,
|
||||||
.hljs-quote,
|
.hljs-quote,
|
||||||
.hljs-selector-tag,
|
.hljs-selector-tag,
|
||||||
.hljs-selector-pseudo {
|
.hljs-selector-pseudo {
|
||||||
/* prettylights-syntax-entity-tag */
|
/* prettylights-syntax-entity-tag */
|
||||||
color: #22863a
|
color: #22863a;
|
||||||
}
|
}
|
||||||
.hljs-subst {
|
.hljs-subst {
|
||||||
/* prettylights-syntax-storage-modifier-import */
|
/* prettylights-syntax-storage-modifier-import */
|
||||||
color: #24292e
|
color: #24292e;
|
||||||
}
|
}
|
||||||
.hljs-section {
|
.hljs-section {
|
||||||
/* prettylights-syntax-markup-heading */
|
/* prettylights-syntax-markup-heading */
|
||||||
color: #005cc5;
|
color: #005cc5;
|
||||||
font-weight: bold
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.hljs-bullet {
|
.hljs-bullet {
|
||||||
/* prettylights-syntax-markup-list */
|
/* prettylights-syntax-markup-list */
|
||||||
color: #735c0f
|
color: #735c0f;
|
||||||
}
|
}
|
||||||
.hljs-emphasis {
|
.hljs-emphasis {
|
||||||
/* prettylights-syntax-markup-italic */
|
/* prettylights-syntax-markup-italic */
|
||||||
color: #24292e;
|
color: #24292e;
|
||||||
font-style: italic
|
font-style: italic;
|
||||||
}
|
}
|
||||||
.hljs-strong {
|
.hljs-strong {
|
||||||
/* prettylights-syntax-markup-bold */
|
/* prettylights-syntax-markup-bold */
|
||||||
color: #24292e;
|
color: #24292e;
|
||||||
font-weight: bold
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.hljs-addition {
|
.hljs-addition {
|
||||||
/* prettylights-syntax-markup-inserted */
|
/* prettylights-syntax-markup-inserted */
|
||||||
color: #22863a;
|
color: #22863a;
|
||||||
background-color: #f0fff4
|
background-color: #f0fff4;
|
||||||
}
|
}
|
||||||
.hljs-deletion {
|
.hljs-deletion {
|
||||||
/* prettylights-syntax-markup-deleted */
|
/* prettylights-syntax-markup-deleted */
|
||||||
color: #b31d28;
|
color: #b31d28;
|
||||||
background-color: #ffeef0
|
background-color: #ffeef0;
|
||||||
}
|
}
|
||||||
/* These highlight.js classes are intentionally left unstyled */
|
/* These highlight.js classes are intentionally left unstyled */
|
||||||
|
|
||||||
|
|
@ -1343,7 +1337,8 @@ code.hljs {
|
||||||
|
|
||||||
/* Animations */
|
/* Animations */
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,18 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"inlineSourceMap": true,
|
||||||
"inlineSourceMap": true,
|
"inlineSources": true,
|
||||||
"inlineSources": true,
|
"module": "node16",
|
||||||
"module": "node16",
|
"target": "ES2018",
|
||||||
"target": "ES2018",
|
"allowJs": true,
|
||||||
"allowJs": true,
|
"resolveJsonModule": true,
|
||||||
"resolveJsonModule": true,
|
"esModuleInterop": true,
|
||||||
"esModuleInterop": true,
|
"noImplicitAny": true,
|
||||||
"noImplicitAny": true,
|
"importHelpers": true,
|
||||||
"moduleResolution": "node16",
|
"isolatedModules": false,
|
||||||
"importHelpers": true,
|
"strictNullChecks": true,
|
||||||
"isolatedModules": false,
|
"lib": ["DOM", "ES2017", "ES2018", "ES2019", "ES2020"],
|
||||||
"strictNullChecks": true,
|
"types": ["node"]
|
||||||
"lib": [
|
},
|
||||||
"DOM",
|
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"]
|
||||||
"ES5",
|
|
||||||
"ES6",
|
|
||||||
"ES7"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"**/*.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { readFileSync, writeFileSync } from "fs";
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
|
||||||
const targetVersion = process.env.npm_package_version;
|
const targetVersion = process.env.npm_package_version;
|
||||||
|
|
||||||
// read minAppVersion from manifest.json and bump version to target 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;
|
const { minAppVersion } = manifest;
|
||||||
manifest.version = targetVersion;
|
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
|
// 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;
|
versions[targetVersion] = minAppVersion;
|
||||||
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
|
writeFileSync('versions.json', JSON.stringify(versions, null, '\t'));
|
||||||
|
|
|
||||||
876
yarn.lock
876
yarn.lock
|
|
@ -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"
|
|
||||||
Loading…
Reference in a new issue