release: 1.1.1

This commit is contained in:
qf3l3k 2026-03-03 08:29:38 +01:00
parent c1e004818d
commit 70f472f631
10 changed files with 562 additions and 116 deletions

View file

@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
## [1.1.1] - 2026-03-03
### Added
- Added compact endpoint list view in settings with columns for name, type, URL, headers count, and row actions.
- Added endpoint editor modal for create/edit flows to keep advanced endpoint details out of the main settings list.
- Added endpoint row actions: Edit, Duplicate, and Delete.
### Changed
- Updated endpoint settings layout for better readability when many aliases exist.
- Reduced endpoint list font size and tightened spacing to better match Obsidian settings styling and avoid clipped action buttons.
## [1.1.0] - 2026-03-02
### Added

110
RELEASE_CHECKLIST.md Normal file
View file

@ -0,0 +1,110 @@
# Release Checklist (Public GitHub)
Use this flow when publishing `1.0.7`, `1.0.8`, `1.0.9`, etc. to the public repo without force-pushing `main`.
## 1) Pick release version
- Set a target version, for example: `1.0.7`.
- Ensure these files match that version:
- `manifest.json`
- `versions.json` (must include the target entry)
- `package.json`
- `package-lock.json`
## 2) Validate locally
```powershell
cmd /c npx tsc -noEmit -skipLibCheck
cmd /c npm run build
```
## 3) Create release commit/tag on internal main
```powershell
git checkout main
git pull origin main
git status --short
git add manifest.json versions.json package.json package-lock.json CHANGELOG.md README.md
git commit -m "release: 1.0.7"
git tag 1.0.7
git push origin main --tags
```
## 4) Prepare public PR branch from `public/main`
```powershell
git fetch public --tags
git checkout -B public-fix-1.0.7 public/main
```
## 5) Copy release metadata from tag to PR branch
```powershell
git checkout 1.0.7 -- manifest.json versions.json package.json package-lock.json
```
## 6) Sanity check diff (must be version metadata only)
```powershell
git diff -- manifest.json versions.json package.json package-lock.json
git status --short
```
## 7) Commit and push PR branch
```powershell
git add manifest.json versions.json package.json package-lock.json
git commit -m "public: bump release metadata to 1.0.7"
git push public public-fix-1.0.7
```
## 8) Open and merge PR
- Open:
- `https://github.com/qf3l3k/obsidian-data-fetcher/pull/new/public-fix-1.0.7`
- Merge into `public/main`.
## 9) Publish GitHub release assets for tag `1.0.7`
- Upload files built from the same tag:
- `manifest.json`
- `main.js`
- `styles.css`
## 10) Verify end-user update path
- In Obsidian:
- Check plugin version visible in Community Plugins.
- Run update from previous version and confirm it upgrades to target.
---
## Quick Command Template
Replace `1.0.7` with your target version before running:
```powershell
# 0) Variables
$V = "1.0.7"
# 1) Internal release
git checkout main
git pull origin main
cmd /c npx tsc -noEmit -skipLibCheck
cmd /c npm run build
git add manifest.json versions.json package.json package-lock.json CHANGELOG.md README.md
git commit -m "release: $V"
git tag $V
git push origin main --tags
# 2) Public PR branch
git fetch public --tags
git checkout -B "public-fix-$V" public/main
git checkout $V -- manifest.json versions.json package.json package-lock.json
git add manifest.json versions.json package.json package-lock.json
git commit -m "public: bump release metadata to $V"
git push public "public-fix-$V"
# 3) Open PR URL manually
Write-Output "Open: https://github.com/qf3l3k/obsidian-data-fetcher/pull/new/public-fix-$V"
```

347
main.ts
View file

@ -1,5 +1,5 @@
import { App, Editor, EventRef, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
import { DataFetcherSettings, DEFAULT_SETTINGS } from './src/settings';
import { DataFetcherSettings, DEFAULT_SETTINGS, EndpointConfig } from './src/settings';
import { parseDataQuery, executeQuery, QueryParams, QueryResult } from './src/queryEngine';
import { CacheManager } from './src/cacheManager';
@ -827,113 +827,117 @@ class DataFetcherSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName('Endpoint aliases')
.setHeading();
const endpointList = containerEl.createEl('div', {cls: 'endpoint-list'});
// Create UI for each existing endpoint
this.plugin.settings.endpoints.forEach((endpoint, index) => {
this.createEndpointSetting(endpointList, endpoint, index);
});
// Add new endpoint button
this.renderEndpointTable(containerEl);
new Setting(containerEl)
.addButton(button => button
.setButtonText('Add endpoint')
.onClick(async () => {
this.plugin.settings.endpoints.push({
alias: '',
url: '',
method: 'GET',
type: 'rest',
headers: {}
});
await this.plugin.saveSettings();
this.display();
}));
}
createEndpointSetting(container: HTMLElement, endpoint: any, index: number) {
const endpointEl = container.createEl('div', {cls: 'endpoint-item'});
new Setting(endpointEl)
.setName('Alias')
.addText(text => text
.setPlaceholder('api-name')
.setValue(endpoint.alias)
.onChange(async (value) => {
this.plugin.settings.endpoints[index].alias = value;
await this.plugin.saveSettings();
}));
new Setting(endpointEl)
.setName('Type')
.addDropdown(dropdown => dropdown
.addOption('rest', 'REST')
.addOption('graphql', 'GraphQL')
.addOption('grpc', 'gRPC')
.addOption('rpc', 'RPC')
.setValue(endpoint.type)
.onChange(async (value) => {
const validTypes = ["rest", "graphql", "grpc", "rpc"];
if (validTypes.includes(value)) {
this.plugin.settings.endpoints[index].type = value as "rest" | "graphql" | "grpc" | "rpc";
} else {
// Default to REST if invalid type
this.plugin.settings.endpoints[index].type = "rest";
}
await this.plugin.saveSettings();
this.display();
}));
new Setting(endpointEl)
.setName('URL')
.addText(text => text
.setPlaceholder('https://api.example.com')
.setValue(endpoint.url)
.onChange(async (value) => {
this.plugin.settings.endpoints[index].url = value;
await this.plugin.saveSettings();
}));
// Add different fields based on endpoint type
if (endpoint.type === 'rest' || endpoint.type === 'rpc') {
new Setting(endpointEl)
.setName('Method')
.addDropdown(dropdown => dropdown
.addOption('GET', 'GET')
.addOption('POST', 'POST')
.addOption('PUT', 'PUT')
.addOption('DELETE', 'DELETE')
.setValue(endpoint.method)
.onChange(async (value) => {
this.plugin.settings.endpoints[index].method = value;
await this.plugin.saveSettings();
}));
}
// Add headers section
new Setting(endpointEl)
.setName('Headers')
.setDesc('Add headers for this endpoint')
.addButton(button => button
.setButtonText('Manage headers')
.setCta()
.onClick(() => {
new HeadersModal(this.app, endpoint.headers, async (headers) => {
this.plugin.settings.endpoints[index].headers = headers;
const newEndpoint = this.buildDefaultEndpoint();
new EndpointEditorModal(this.app, newEndpoint, async (savedEndpoint) => {
this.plugin.settings.endpoints.push(savedEndpoint);
await this.plugin.saveSettings();
this.display();
}).open();
}));
// Delete button
new Setting(endpointEl)
.addButton(button => button
.setButtonText('Delete')
.setClass('data-fetcher-delete-btn')
.onClick(async () => {
this.plugin.settings.endpoints.splice(index, 1);
}
private buildDefaultEndpoint(): EndpointConfig {
return {
alias: '',
url: '',
method: 'GET',
type: 'rest',
headers: {}
};
}
private endpointTypeLabel(type: EndpointConfig['type']): string {
if (type === 'rest') return 'REST';
if (type === 'graphql') return 'GraphQL';
if (type === 'grpc') return 'gRPC';
return 'RPC';
}
private truncateUrl(url: string, maxLen = 72): string {
if (!url) return '-';
if (url.length <= maxLen) return url;
return `${url.substring(0, maxLen - 1)}...`;
}
private renderEndpointTable(containerEl: HTMLElement): void {
const table = containerEl.createEl('div', { cls: 'data-fetcher-endpoint-table' });
const header = table.createEl('div', { cls: 'data-fetcher-endpoint-row data-fetcher-endpoint-row-header' });
header.createEl('div', { text: 'Name', cls: 'data-fetcher-endpoint-col data-fetcher-endpoint-col-alias' });
header.createEl('div', { text: 'Type', cls: 'data-fetcher-endpoint-col data-fetcher-endpoint-col-type' });
header.createEl('div', { text: 'URL', cls: 'data-fetcher-endpoint-col data-fetcher-endpoint-col-url' });
header.createEl('div', { text: 'Headers', cls: 'data-fetcher-endpoint-col data-fetcher-endpoint-col-headers' });
header.createEl('div', { text: 'Actions', cls: 'data-fetcher-endpoint-col data-fetcher-endpoint-col-actions' });
if (this.plugin.settings.endpoints.length === 0) {
const empty = table.createEl('div', { cls: 'data-fetcher-endpoint-empty' });
empty.setText('No endpoints configured yet.');
return;
}
this.plugin.settings.endpoints.forEach((endpoint, index) => {
const row = table.createEl('div', { cls: 'data-fetcher-endpoint-row' });
row.createEl('div', {
text: endpoint.alias?.trim() || '(unnamed)',
cls: 'data-fetcher-endpoint-col data-fetcher-endpoint-col-alias'
});
row.createEl('div', {
text: this.endpointTypeLabel(endpoint.type),
cls: 'data-fetcher-endpoint-col data-fetcher-endpoint-col-type'
});
row.createEl('div', {
text: this.truncateUrl(endpoint.url),
cls: 'data-fetcher-endpoint-col data-fetcher-endpoint-col-url'
});
row.createEl('div', {
text: String(Object.keys(endpoint.headers || {}).length),
cls: 'data-fetcher-endpoint-col data-fetcher-endpoint-col-headers'
});
const actions = row.createEl('div', {
cls: 'data-fetcher-endpoint-col data-fetcher-endpoint-col-actions data-fetcher-endpoint-actions'
});
actions.createEl('button', { text: 'Edit' }).addEventListener('click', () => {
const draft = { ...endpoint, headers: { ...(endpoint.headers || {}) } };
new EndpointEditorModal(this.app, draft, async (savedEndpoint) => {
this.plugin.settings.endpoints[index] = savedEndpoint;
await this.plugin.saveSettings();
this.display();
}));
}).open();
});
actions.createEl('button', { text: 'Duplicate' }).addEventListener('click', () => {
const duplicateAlias = endpoint.alias ? `${endpoint.alias}-copy` : '';
const draft: EndpointConfig = {
...endpoint,
alias: duplicateAlias,
headers: { ...(endpoint.headers || {}) }
};
new EndpointEditorModal(this.app, draft, async (savedEndpoint) => {
this.plugin.settings.endpoints.push(savedEndpoint);
await this.plugin.saveSettings();
this.display();
}).open();
});
actions.createEl('button', { text: 'Delete', cls: 'mod-warning' }).addEventListener('click', async () => {
const alias = endpoint.alias?.trim() || 'this endpoint';
const shouldDelete = window.confirm(`Delete ${alias}?`);
if (!shouldDelete) {
return;
}
this.plugin.settings.endpoints.splice(index, 1);
await this.plugin.saveSettings();
this.display();
});
});
}
async updateCacheInfo(containerEl: HTMLElement) {
@ -952,6 +956,151 @@ class DataFetcherSettingTab extends PluginSettingTab {
}
}
class EndpointEditorModal extends Modal {
private endpoint: EndpointConfig;
private onSubmit: (endpoint: EndpointConfig) => void;
private methodSettingEl: HTMLElement | null = null;
private headersSummaryEl: HTMLElement | null = null;
constructor(app: App, endpoint: EndpointConfig, onSubmit: (endpoint: EndpointConfig) => void) {
super(app);
this.endpoint = {
...endpoint,
headers: { ...(endpoint.headers || {}) },
method: endpoint.method || 'GET'
};
this.onSubmit = onSubmit;
}
private updateHeadersSummary(): void {
if (!this.headersSummaryEl) {
return;
}
const count = Object.keys(this.endpoint.headers || {}).length;
this.headersSummaryEl.setText(`${count} header${count === 1 ? '' : 's'} configured`);
}
private renderMethodSetting(containerEl: HTMLElement): void {
if (this.methodSettingEl) {
this.methodSettingEl.remove();
this.methodSettingEl = null;
}
if (this.endpoint.type !== 'rest' && this.endpoint.type !== 'rpc') {
return;
}
this.methodSettingEl = containerEl.createDiv();
new Setting(this.methodSettingEl)
.setName('Method')
.setDesc('HTTP method for REST/RPC requests')
.addDropdown(dropdown => dropdown
.addOption('GET', 'GET')
.addOption('POST', 'POST')
.addOption('PUT', 'PUT')
.addOption('DELETE', 'DELETE')
.setValue(this.endpoint.method || 'GET')
.onChange(value => {
this.endpoint.method = value;
}));
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
this.modalEl.addClass('data-fetcher-endpoint-editor-modal');
contentEl.addClass('data-fetcher-endpoint-editor');
new Setting(contentEl)
.setName('Endpoint editor')
.setHeading();
new Setting(contentEl)
.setName('Alias')
.setDesc('Endpoint reference name used in notes, e.g. @my-api')
.addText(text => text
.setPlaceholder('my-api')
.setValue(this.endpoint.alias || '')
.onChange(value => {
this.endpoint.alias = value.trim();
}));
new Setting(contentEl)
.setName('Type')
.addDropdown(dropdown => dropdown
.addOption('rest', 'REST')
.addOption('graphql', 'GraphQL')
.addOption('grpc', 'gRPC')
.addOption('rpc', 'RPC')
.setValue(this.endpoint.type)
.onChange(value => {
const validTypes: EndpointConfig['type'][] = ['rest', 'graphql', 'grpc', 'rpc'];
this.endpoint.type = validTypes.includes(value as EndpointConfig['type'])
? (value as EndpointConfig['type'])
: 'rest';
this.renderMethodSetting(contentEl);
}));
new Setting(contentEl)
.setName('URL')
.setDesc('Endpoint URL')
.addText(text => text
.setPlaceholder('https://api.example.com')
.setValue(this.endpoint.url || '')
.onChange(value => {
this.endpoint.url = value.trim();
}));
this.renderMethodSetting(contentEl);
new Setting(contentEl)
.setName('Headers')
.setDesc('Authentication and custom request headers')
.addButton(button => button
.setButtonText('Manage headers')
.onClick(() => {
new HeadersModal(this.app, this.endpoint.headers || {}, (headers) => {
this.endpoint.headers = headers;
this.updateHeadersSummary();
}).open();
}));
this.headersSummaryEl = contentEl.createEl('div', { cls: 'data-fetcher-endpoint-header-summary' });
this.updateHeadersSummary();
const actions = contentEl.createEl('div', { cls: 'data-fetcher-endpoint-editor-actions' });
actions.createEl('button', { text: 'Cancel' }).addEventListener('click', () => this.close());
actions.createEl('button', { text: 'Save', cls: 'mod-cta' }).addEventListener('click', () => {
if (!this.endpoint.alias?.trim()) {
new Notice('Alias is required.');
return;
}
if (!this.endpoint.url?.trim()) {
new Notice('URL is required.');
return;
}
if (this.endpoint.type === 'graphql' || this.endpoint.type === 'grpc') {
this.endpoint.method = 'POST';
}
this.onSubmit({
alias: this.endpoint.alias.trim(),
type: this.endpoint.type,
url: this.endpoint.url.trim(),
method: this.endpoint.method || 'GET',
headers: { ...(this.endpoint.headers || {}) }
});
this.close();
});
}
onClose() {
this.modalEl.removeClass('data-fetcher-endpoint-editor-modal');
this.contentEl.empty();
}
}
class CacheBrowserModal extends Modal {
private cacheManager: CacheManager;
private entriesContainer: HTMLElement;

View file

@ -1,7 +1,7 @@
{
"id": "data-fetcher",
"name": "Data Fetcher",
"version": "1.1.0",
"version": "1.1.1",
"minAppVersion": "1.8.3",
"description": "Fetch data from multiple sources (REST APIs, RPC, gRPC, GraphQL) and insert results into notes",
"author": "qf3l3k",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-api-fetcher",
"version": "1.1.0",
"version": "1.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-api-fetcher",
"version": "1.1.0",
"version": "1.1.1",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-api-fetcher",
"version": "1.1.0",
"version": "1.1.1",
"description": "Fetch data from multiple sources (REST APIs, RPC, gRPC, GraphQL) and insert results into notes",
"main": "main.js",
"scripts": {

View file

@ -0,0 +1,52 @@
param(
[Parameter(Mandatory = $true)]
[string]$Version
)
$ErrorActionPreference = "Stop"
function Require-CleanTree {
$status = git status --porcelain
if ($LASTEXITCODE -ne 0) {
throw "Failed to read git status."
}
$dirty = $status | Where-Object { $_ -notmatch '^\?\?\s+DEV_NOTES\.md$' }
if ($dirty) {
throw "Working tree is not clean. Commit/stash changes before running release script."
}
}
function Require-Ref([string]$RefName) {
git rev-parse --verify $RefName *> $null
if ($LASTEXITCODE -ne 0) {
throw "Missing required ref: $RefName"
}
}
Write-Host "==> Releasing version $Version"
Require-CleanTree
Write-Host "==> Internal release on origin/main"
git checkout main
git pull origin main
cmd /c npx tsc -noEmit -skipLibCheck
cmd /c npm run build
git add manifest.json versions.json package.json package-lock.json CHANGELOG.md README.md
git commit -m "release: $Version"
git tag $Version
git push origin main --tags
Write-Host "==> Preparing PR branch from public/main"
git fetch public --tags
Require-Ref "public/main"
Require-Ref $Version
$branch = "public-fix-$Version"
git checkout -B $branch public/main
git checkout $Version -- manifest.json versions.json package.json package-lock.json
git add manifest.json versions.json package.json package-lock.json
git commit -m "public: bump release metadata to $Version"
git push public $branch
Write-Host "==> Open PR:"
Write-Host "https://github.com/qf3l3k/obsidian-data-fetcher/pull/new/$branch"

55
scripts/release-public.sh Normal file
View file

@ -0,0 +1,55 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "Usage: $0 <version>" >&2
echo "Example: $0 1.0.7" >&2
exit 1
fi
V="$1"
require_clean_tree() {
local dirty
dirty="$(git status --porcelain | grep -vE '^\?\? DEV_NOTES\.md$' || true)"
if [[ -n "$dirty" ]]; then
echo "Working tree is not clean. Commit/stash changes before running release script." >&2
exit 1
fi
}
require_ref() {
local ref="$1"
if ! git rev-parse --verify "$ref" >/dev/null 2>&1; then
echo "Missing required ref: $ref" >&2
exit 1
fi
}
echo "==> Releasing version $V"
require_clean_tree
echo "==> Internal release on origin/main"
git checkout main
git pull origin main
npx tsc -noEmit -skipLibCheck
npm run build
git add manifest.json versions.json package.json package-lock.json CHANGELOG.md README.md
git commit -m "release: $V"
git tag "$V"
git push origin main --tags
echo "==> Preparing PR branch from public/main"
git fetch public --tags
require_ref "public/main"
require_ref "$V"
BRANCH="public-fix-$V"
git checkout -B "$BRANCH" public/main
git checkout "$V" -- manifest.json versions.json package.json package-lock.json
git add manifest.json versions.json package.json package-lock.json
git commit -m "public: bump release metadata to $V"
git push public "$BRANCH"
echo "==> Open PR:"
echo "https://github.com/qf3l3k/obsidian-data-fetcher/pull/new/$BRANCH"

View file

@ -129,18 +129,6 @@
}
/* Settings styles */
.endpoint-item {
margin-bottom: 20px;
padding: 15px;
border: 1px solid var(--background-modifier-border);
border-radius: 5px;
background-color: var(--background-secondary);
}
.endpoint-list {
margin-bottom: 20px;
}
.header-row {
display: flex;
margin-bottom: 8px;
@ -151,6 +139,86 @@
margin-right: 8px;
}
.data-fetcher-endpoint-table {
border: 1px solid var(--background-modifier-border);
border-radius: 8px;
overflow: hidden;
margin-bottom: 14px;
font-size: var(--font-ui-smaller);
}
.data-fetcher-endpoint-row {
display: grid;
grid-template-columns: minmax(84px, 1fr) 72px minmax(140px, 2fr) 64px minmax(150px, 1.2fr);
gap: 6px;
align-items: center;
padding: 7px 9px;
border-bottom: 1px solid var(--background-modifier-border);
}
.data-fetcher-endpoint-row:last-child {
border-bottom: none;
}
.data-fetcher-endpoint-row-header {
background-color: var(--background-secondary);
font-weight: 600;
}
.data-fetcher-endpoint-col {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.data-fetcher-endpoint-col-alias {
font-family: var(--font-monospace);
}
.data-fetcher-endpoint-actions {
display: flex;
gap: 4px;
justify-content: flex-start;
flex-wrap: wrap;
}
.data-fetcher-endpoint-col-actions {
overflow: visible;
text-overflow: clip;
white-space: normal;
}
.data-fetcher-endpoint-actions button {
font-size: var(--font-ui-smaller);
padding: 2px 6px;
}
.data-fetcher-endpoint-empty {
color: var(--text-muted);
padding: 12px;
font-style: italic;
}
.data-fetcher-endpoint-editor-modal {
width: min(760px, 92vw);
max-width: min(760px, 92vw);
}
.data-fetcher-endpoint-editor-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 14px;
}
.data-fetcher-endpoint-header-summary {
margin-top: -4px;
margin-bottom: 10px;
color: var(--text-muted);
font-size: 0.85em;
}
.data-fetcher-delete-btn {
color: var(--text-error);
}

View file

@ -4,5 +4,6 @@
"1.0.7": "1.8.3",
"1.0.8": "1.8.3",
"1.0.9": "1.8.3",
"1.1.0": "1.8.3"
"1.1.0": "1.8.3",
"1.1.1": "1.8.3"
}