mirror of
https://github.com/stephenkall/obsidian-confluence-vault-uploader.git
synced 2026-07-22 07:43:48 +00:00
fix: v0.0.30 — marketplace review fixes + build script cleanup
- Delete dead src/page-browser-modal.ts (inline style violations, never imported)
- settings.ts: replace createEl('h2') with setHeading(), remove (text as any) cast
- main.ts: add typed Confluence API interfaces, make requestConfluence generic,
remove unused App import, fix catch bindings, show failureCount in final notice
- build-and-deploy.sh + update-plugin.bat: read OBSIDIAN_PLUGIN_DIR from .env
- Add .env.example so vault path is never hardcoded in tracked files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d1ad21a0fe
commit
b743ca3f80
7 changed files with 97 additions and 262 deletions
5
.env.example
Normal file
5
.env.example
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Copy this file to .env and fill in your local vault plugin path.
|
||||
# .env is gitignored and never committed.
|
||||
|
||||
# Absolute path to the plugin folder inside your Obsidian vault
|
||||
OBSIDIAN_PLUGIN_DIR="C:/Users/YOU/path/to/YourVault/.obsidian/plugins/confluence-vault-uploader"
|
||||
|
|
@ -1,8 +1,19 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Setup PATH
|
||||
# Setup PATH for Node.js (Git Bash on Windows)
|
||||
export PATH="/c/Program Files/nodejs:$PATH"
|
||||
|
||||
# Load local vault path from .env (copy .env.example → .env to configure)
|
||||
if [ -f .env ]; then
|
||||
# shellcheck disable=SC1091
|
||||
source .env
|
||||
fi
|
||||
|
||||
if [ -z "$OBSIDIAN_PLUGIN_DIR" ]; then
|
||||
echo "❌ OBSIDIAN_PLUGIN_DIR not set. Copy .env.example → .env and fill in the path."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get version from package.json
|
||||
VERSION=$(grep '"version"' package.json | head -1 | sed 's/.*"version": "\([^"]*\)".*/\1/')
|
||||
echo "📦 Current version: $VERSION"
|
||||
|
|
@ -16,15 +27,12 @@ echo "🏗️ Building plugin..."
|
|||
npm run build
|
||||
|
||||
# Deploy to Obsidian plugins folder
|
||||
PLUGIN_DEST="C:/Users/zhuvi1/Mars Inc/WMS Non-ECC - General/Applications/PDLS/PDLS Application code/PDLS_Vault/.obsidian/plugins/obsidian-confluence-vault-uploader"
|
||||
echo "📁 Copying to: $OBSIDIAN_PLUGIN_DIR"
|
||||
mkdir -p "$OBSIDIAN_PLUGIN_DIR"
|
||||
|
||||
echo "📁 Copying to: $PLUGIN_DEST"
|
||||
mkdir -p "$PLUGIN_DEST"
|
||||
|
||||
# Copy compiled files
|
||||
cp main.js "$PLUGIN_DEST/"
|
||||
cp main.js.map "$PLUGIN_DEST/"
|
||||
cp manifest.json "$PLUGIN_DEST/"
|
||||
cp main.js "$OBSIDIAN_PLUGIN_DIR/"
|
||||
cp main.js.map "$OBSIDIAN_PLUGIN_DIR/"
|
||||
cp manifest.json "$OBSIDIAN_PLUGIN_DIR/"
|
||||
|
||||
echo "✅ Build complete! Version $VERSION deployed."
|
||||
echo "🔄 Reload plugin in Obsidian (Settings → Community plugins → Reload or Ctrl+Shift+P → Reload)"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "confluence-vault-uploader",
|
||||
"name": "Confluence Vault Uploader",
|
||||
"version": "0.0.29",
|
||||
"version": "0.0.30",
|
||||
"minAppVersion": "1.4.0",
|
||||
"description": "Sync your vault to Confluence, preserving folder structure as a page hierarchy with working cross-page links.",
|
||||
"author": "stephenkall",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "confluence-vault-uploader",
|
||||
"version": "0.0.29",
|
||||
"version": "0.0.30",
|
||||
"description": "Obsidian plugin to upload an entire vault to Confluence.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
114
src/main.ts
114
src/main.ts
|
|
@ -1,4 +1,4 @@
|
|||
import { App, Notice, Plugin, requestUrl, TFile, TFolder } from 'obsidian';
|
||||
import { Notice, Plugin, RequestUrlParam, requestUrl, TFile, TFolder } from 'obsidian';
|
||||
import { marked } from 'marked';
|
||||
import { ConfluenceVaultUploaderSettingTab, ConfluenceVaultUploaderSettings, DEFAULT_SETTINGS } from './settings';
|
||||
|
||||
|
|
@ -9,6 +9,34 @@ interface SyncState {
|
|||
timestamp: number;
|
||||
}
|
||||
|
||||
interface SavedData extends ConfluenceVaultUploaderSettings {
|
||||
syncState?: SyncState;
|
||||
}
|
||||
|
||||
interface ConfluencePageResponse {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
version: { number: number };
|
||||
body?: { storage?: { value: string } };
|
||||
ancestors?: Array<{ id: string }>;
|
||||
}
|
||||
|
||||
interface ConfluenceSearchResponse {
|
||||
results: ConfluencePageResponse[];
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface ConfluenceSpacesResponse {
|
||||
results: Array<{ id: string; key: string; name: string }>;
|
||||
}
|
||||
|
||||
interface RequestError {
|
||||
status?: number;
|
||||
message?: string;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
||||
settings: ConfluenceVaultUploaderSettings = DEFAULT_SETTINGS;
|
||||
private pageCache: Record<string, { id: string; version: number }> = {};
|
||||
|
|
@ -119,12 +147,12 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
for (const [title, pageId] of Object.entries(this.pageMap)) {
|
||||
try {
|
||||
const url = `${this.getConfluenceBaseUrl()}/api/v2/pages/${pageId}`;
|
||||
const response = await this.requestConfluence(url, 'GET');
|
||||
const response = await this.requestConfluence<ConfluencePageResponse>(url, 'GET');
|
||||
if (response && response.id) {
|
||||
validPageMap[title] = pageId;
|
||||
console.log(`[Confluence Sync] ✓ Page still exists: ${title}`);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
console.warn(`[Confluence Sync] ✗ Page deleted or inaccessible: ${title} (${pageId})`);
|
||||
this.processedFiles.delete(title); // Re-sync this file
|
||||
}
|
||||
|
|
@ -192,11 +220,12 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
if (successCount % 10 === 0) {
|
||||
await this.saveSyncState();
|
||||
}
|
||||
} catch (error: any) {
|
||||
const status = error?.status ?? error?.response?.status ?? '';
|
||||
const detail = error?.message ?? String(error);
|
||||
} catch (rawError) {
|
||||
const error = rawError as RequestError;
|
||||
const status = error.status ?? '';
|
||||
const detail = rawError instanceof Error ? rawError.message : String(rawError);
|
||||
// Obsidian's RequestUrlError carries the response body in .body (not .message)
|
||||
const responseBody = error?.body ?? '';
|
||||
const responseBody = error.body ?? '';
|
||||
console.error(`[Confluence Sync] ❌ Failed: ${file.path} | status=${status} | ${detail}`);
|
||||
if (responseBody) console.error(`[Confluence Sync] ❌ Response body:`, responseBody.substring(0, 2000));
|
||||
failureCount += 1;
|
||||
|
|
@ -220,7 +249,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
|
||||
await this.saveSyncState();
|
||||
|
||||
const message = `✅ Confluence sync complete: ${successCount} new, ${this.pageMap ? Object.keys(this.pageMap).length : 0} total pages.`;
|
||||
const message = `✅ Confluence sync complete: ${successCount} succeeded, ${failureCount} failed, ${Object.keys(this.pageMap).length} total pages.`;
|
||||
new Notice(message, 5000);
|
||||
console.log(`[Confluence Sync] ${message}`);
|
||||
} finally {
|
||||
|
|
@ -232,7 +261,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
const baseUrl = this.getConfluenceBaseUrl();
|
||||
const url = `${baseUrl}/api/v2/spaces?keys=${this.settings.spaceKey}&limit=1`;
|
||||
|
||||
const response = await this.requestConfluence(url, 'GET');
|
||||
const response = await this.requestConfluence<ConfluenceSpacesResponse>(url, 'GET');
|
||||
if (response?.results && response.results.length > 0) {
|
||||
return response.results[0].id;
|
||||
}
|
||||
|
|
@ -359,7 +388,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
|
||||
console.log(`[findOrCreateFolderPage] Creating new folder: ${folderName}`);
|
||||
const url = `${this.getConfluenceBaseUrl()}/api/v2/pages`;
|
||||
const payload: any = {
|
||||
const payload: Record<string, unknown> = {
|
||||
spaceId: this.spaceId,
|
||||
status: 'current',
|
||||
title: folderName,
|
||||
|
|
@ -367,7 +396,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
};
|
||||
if (parentPageId) payload.parentId = parentPageId;
|
||||
|
||||
const response = await this.requestConfluence(url, 'POST', payload);
|
||||
const response = await this.requestConfluence<ConfluencePageResponse>(url, 'POST', payload);
|
||||
console.log(`[findOrCreateFolderPage] Created folder: ${folderName} (id: ${response.id})`);
|
||||
// Register folder page so Phase 2 can resolve its links
|
||||
this.pageMap[folderName] = response.id;
|
||||
|
|
@ -396,8 +425,8 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
// Convert Obsidian embeds ![[File]] to links — Phase 2 will resolve them to Confluence URLs.
|
||||
// Placeholder uses full vault path so Phase 2 lookup is unambiguous.
|
||||
processedMarkdown = processedMarkdown.replace(/!\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (match, name, alt) => {
|
||||
const fullPath = resolveObsidianPath(name);
|
||||
const display = (alt || name).trim();
|
||||
const fullPath = resolveObsidianPath(name as string);
|
||||
const display = ((alt as string | undefined) || (name as string)).trim();
|
||||
return `[${display}](OBSIDIAN_LINK:${fullPath})`;
|
||||
});
|
||||
|
||||
|
|
@ -417,8 +446,8 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
processedMarkdown = processedMarkdown.replace(
|
||||
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
|
||||
(match, pageName, displayText) => {
|
||||
const fullPath = resolveObsidianPath(pageName);
|
||||
const display = linkDisplay(pageName, displayText);
|
||||
const fullPath = resolveObsidianPath(pageName as string);
|
||||
const display = linkDisplay(pageName as string, displayText as string | undefined);
|
||||
return `[${display}](OBSIDIAN_LINK:${fullPath})`;
|
||||
}
|
||||
);
|
||||
|
|
@ -429,14 +458,14 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
html = html.replace(
|
||||
/<pre><code class="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g,
|
||||
(match, language, code) => {
|
||||
const decoded = code
|
||||
const decoded = (code as string)
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&')
|
||||
.trim();
|
||||
// Escape ]]> so it doesn't terminate the CDATA section prematurely
|
||||
const safe = decoded.replace(/\]\]>/g, ']]]]><![CDATA[>');
|
||||
return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${language}</ac:parameter><ac:plain-text-body><![CDATA[${safe}]]></ac:plain-text-body></ac:structured-macro>`;
|
||||
return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${language as string}</ac:parameter><ac:plain-text-body><![CDATA[${safe}]]></ac:plain-text-body></ac:structured-macro>`;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -444,7 +473,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
html = html.replace(
|
||||
/<pre><code>([\s\S]*?)<\/code><\/pre>/g,
|
||||
(match, code) => {
|
||||
const decoded = code
|
||||
const decoded = (code as string)
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&')
|
||||
|
|
@ -469,10 +498,10 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
html = html.replace(
|
||||
/<blockquote>\s*<p>\[!(note|tip|warning|info|important|caution|danger|error|success|check|question|quote|abstract|summary|todo|bug|example|faq|help|hint|attention)\]([^\n<]*)(?:\n([\s\S]*?))?<\/p>\s*<\/blockquote>/gi,
|
||||
(match, type, titleRaw, bodyRaw) => {
|
||||
const macroType = calloutTypeMap[type.toLowerCase()] || 'info';
|
||||
const title = titleRaw.trim();
|
||||
const macroType = calloutTypeMap[(type as string).toLowerCase()] || 'info';
|
||||
const title = (titleRaw as string).trim();
|
||||
const titleAttr = title ? ` ac:title="${title}"` : '';
|
||||
const body = (bodyRaw || '').trim();
|
||||
const body = ((bodyRaw as string | undefined) || '').trim();
|
||||
const bodyHtml = body ? `<p>${body}</p>` : '';
|
||||
return `\n<ac:structured-macro ac:name="${macroType}"${titleAttr}><ac:rich-text-body>${bodyHtml}</ac:rich-text-body></ac:structured-macro>\n`;
|
||||
}
|
||||
|
|
@ -506,13 +535,13 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
async findPageByTitle(title: string, parentPageId?: string): Promise<{ id: string; version: number } | null> {
|
||||
const url = `${this.getConfluenceBaseUrl()}/rest/api/content?title=${encodeURIComponent(title)}&spaceKey=${encodeURIComponent(this.settings.spaceKey)}&expand=version`;
|
||||
|
||||
const response = await this.requestConfluence(url, 'GET');
|
||||
const response = await this.requestConfluence<ConfluenceSearchResponse>(url, 'GET');
|
||||
if (response && response.results && response.results.length > 0) {
|
||||
let targetPage = response.results[0];
|
||||
|
||||
if (parentPageId && response.results.length > 1) {
|
||||
const childPage = response.results.find((p: any) =>
|
||||
p.ancestors?.some((a: any) => a.id === parentPageId)
|
||||
const childPage = response.results.find(p =>
|
||||
p.ancestors?.some(a => a.id === parentPageId)
|
||||
);
|
||||
if (childPage) {
|
||||
targetPage = childPage;
|
||||
|
|
@ -531,7 +560,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
async createPage(title: string, body: { value: string; representation: string }, parentPageId: string): Promise<string> {
|
||||
const url = `${this.getConfluenceBaseUrl()}/api/v2/pages`;
|
||||
|
||||
const payload: any = {
|
||||
const payload: Record<string, unknown> = {
|
||||
spaceId: this.spaceId,
|
||||
status: 'current',
|
||||
title,
|
||||
|
|
@ -544,11 +573,11 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
|
||||
console.log(`[createPage] POST to ${url}`);
|
||||
console.log(`[createPage] Payload: title="${title}", representation="${body.representation}", bodyLength=${body.value.length}`);
|
||||
const response = await this.requestConfluence(url, 'POST', payload);
|
||||
const response = await this.requestConfluence<ConfluencePageResponse>(url, 'POST', payload);
|
||||
console.log(`[createPage] ✅ Created page ID: ${response.id}`);
|
||||
|
||||
// Track version for future updates
|
||||
this.pageVersions[response.id] = response.version?.number || 1;
|
||||
this.pageVersions[response.id] = response.version.number;
|
||||
|
||||
return response.id;
|
||||
}
|
||||
|
|
@ -556,7 +585,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
async updatePage(pageId: string, currentVersion: number, title: string, body: { value: string; representation: string }, parentPageId: string) {
|
||||
const url = `${this.getConfluenceBaseUrl()}/api/v2/pages/${pageId}`;
|
||||
|
||||
const payload: any = {
|
||||
const payload: Record<string, unknown> = {
|
||||
id: pageId,
|
||||
status: 'current',
|
||||
title,
|
||||
|
|
@ -572,16 +601,16 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
|
||||
console.log(`[updatePage] PUT to ${url}`);
|
||||
console.log(`[updatePage] Payload: title="${title}", version=${currentVersion + 1}, bodyLength=${body.value.length}`);
|
||||
await this.requestConfluence(url, 'PUT', payload);
|
||||
await this.requestConfluence<ConfluencePageResponse>(url, 'PUT', payload);
|
||||
console.log(`[updatePage] ✅ Updated page ID: ${pageId}`);
|
||||
}
|
||||
|
||||
private async updatePageContent(pageId: string, body: { value: string; representation: string }) {
|
||||
try {
|
||||
const url = `${this.getConfluenceBaseUrl()}/api/v2/pages/${pageId}`;
|
||||
const pageData = await this.requestConfluence(url, 'GET');
|
||||
const pageData = await this.requestConfluence<ConfluencePageResponse>(url, 'GET');
|
||||
|
||||
const payload = {
|
||||
const payload: Record<string, unknown> = {
|
||||
id: pageId,
|
||||
status: 'current',
|
||||
title: pageData.title,
|
||||
|
|
@ -591,7 +620,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
}
|
||||
};
|
||||
|
||||
await this.requestConfluence(url, 'PUT', payload);
|
||||
await this.requestConfluence<ConfluencePageResponse>(url, 'PUT', payload);
|
||||
console.log(`[updatePageContent] ✅ Updated content for page ${pageId}`);
|
||||
} catch (error) {
|
||||
console.error(`[updatePageContent] ❌ Failed to update page ${pageId}:`, error);
|
||||
|
|
@ -599,13 +628,13 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async requestConfluence(url: string, method: string, body?: any): Promise<any> {
|
||||
async requestConfluence<T>(url: string, method: string, body?: unknown): Promise<T> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${btoa(`${this.settings.username}:${this.settings.apiToken}`)}`
|
||||
};
|
||||
|
||||
const requestOptions: any = {
|
||||
const requestOptions: RequestUrlParam = {
|
||||
url,
|
||||
method,
|
||||
headers
|
||||
|
|
@ -621,7 +650,6 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
console.error(`[requestConfluence] ERROR ${response.status}`);
|
||||
console.error(`[requestConfluence] statusText:`, (response as any).statusText);
|
||||
console.error(`[requestConfluence] text length:`, response.text?.length);
|
||||
if (response.text) {
|
||||
console.error(`[requestConfluence] text:`, response.text.substring(0, 1000));
|
||||
|
|
@ -644,7 +672,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
console.error(`[requestConfluence] Final error message:`, errorMsg);
|
||||
throw new Error(`Confluence request failed (${response.status}): ${errorMsg}`);
|
||||
}
|
||||
return response.json;
|
||||
return response.json as T;
|
||||
}
|
||||
|
||||
private getConfluenceBaseUrl(): string {
|
||||
|
|
@ -684,11 +712,11 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
}
|
||||
|
||||
private async loadSyncState(): Promise<SyncState | null> {
|
||||
const data = await this.loadData();
|
||||
const data = (await this.loadData()) as SavedData | null;
|
||||
if (data?.syncState) {
|
||||
this.pageMap = data.syncState.pageMap || {};
|
||||
this.pageVersions = data.syncState.pageVersions || {};
|
||||
this.processedFiles = new Set(data.syncState.processedFiles || []);
|
||||
this.pageMap = data.syncState.pageMap ?? {};
|
||||
this.pageVersions = data.syncState.pageVersions ?? {};
|
||||
this.processedFiles = new Set(data.syncState.processedFiles ?? []);
|
||||
console.log(`[Sync State] Loaded state with ${Object.keys(this.pageMap).length} pages, ${this.processedFiles.size} processed files`);
|
||||
return data.syncState;
|
||||
}
|
||||
|
|
@ -717,7 +745,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
try {
|
||||
// Fetch current page content — must request body-format=storage explicitly
|
||||
const url = `${this.getConfluenceBaseUrl()}/api/v2/pages/${pageId}?body-format=storage`;
|
||||
const pageData = await this.requestConfluence(url, 'GET');
|
||||
const pageData = await this.requestConfluence<ConfluencePageResponse>(url, 'GET');
|
||||
|
||||
// Check stop again after async GET (more responsive)
|
||||
if (!this.isSyncing) {
|
||||
|
|
@ -780,7 +808,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
|
||||
// If links were updated, save the page
|
||||
if (hasLinks) {
|
||||
const updatePayload = {
|
||||
const updatePayload: Record<string, unknown> = {
|
||||
id: pageId,
|
||||
status: 'current',
|
||||
title: pageData.title, // use actual Confluence title, not the pageMap key (which is the vault path)
|
||||
|
|
@ -794,7 +822,7 @@ export default class ConfluenceVaultUploaderPlugin extends Plugin {
|
|||
};
|
||||
|
||||
const updateUrl = `${this.getConfluenceBaseUrl()}/api/v2/pages/${pageId}`;
|
||||
await this.requestConfluence(updateUrl, 'PUT', updatePayload);
|
||||
await this.requestConfluence<ConfluencePageResponse>(updateUrl, 'PUT', updatePayload);
|
||||
updated++;
|
||||
console.log(`[updateAllPageLinks] ✅ Updated links in: ${title}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,206 +0,0 @@
|
|||
import { App, Modal, requestUrl, Notice } from 'obsidian';
|
||||
|
||||
export interface ConfluencePage {
|
||||
id: string;
|
||||
title: string;
|
||||
children?: ConfluencePage[];
|
||||
hasChildren?: boolean;
|
||||
}
|
||||
|
||||
export interface PageBrowserOptions {
|
||||
confluenceBaseUrl: string;
|
||||
username: string;
|
||||
apiToken: string;
|
||||
spaceKey: string;
|
||||
onSelect: (page: ConfluencePage) => void;
|
||||
}
|
||||
|
||||
export class PageBrowserModal extends Modal {
|
||||
private options: PageBrowserOptions;
|
||||
private rootPages: ConfluencePage[] = [];
|
||||
private currentLevel: ConfluencePage[] = [];
|
||||
private breadcrumbs: ConfluencePage[] = [];
|
||||
|
||||
constructor(app: App, options: PageBrowserOptions) {
|
||||
super(app);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
this.renderHeader();
|
||||
await this.loadRootPages();
|
||||
this.renderList();
|
||||
}
|
||||
|
||||
private renderHeader() {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
const header = containerEl.createEl('h2', { text: 'Select Confluence Page' });
|
||||
(header as any).style.marginBottom = '12px';
|
||||
|
||||
const breadcrumb = containerEl.createEl('p', { cls: 'setting-item-description' });
|
||||
this.updateBreadcrumb(breadcrumb);
|
||||
}
|
||||
|
||||
private updateBreadcrumb(el: HTMLElement) {
|
||||
const path = this.breadcrumbs.length === 0
|
||||
? 'Root'
|
||||
: this.breadcrumbs.map(p => p.title).join(' > ');
|
||||
el.setText(path);
|
||||
}
|
||||
|
||||
private async loadRootPages() {
|
||||
try {
|
||||
new Notice('Loading pages...');
|
||||
const baseUrl = this.getBaseUrl();
|
||||
const url = `${baseUrl}/rest/api/content?type=page&spaceKey=${encodeURIComponent(this.options.spaceKey)}&expand=ancestors,children.page&limit=250`;
|
||||
|
||||
console.log('Loading from:', url);
|
||||
const response = await this.requestConfluence(url);
|
||||
|
||||
if (response?.results) {
|
||||
// Filter root pages (no ancestors)
|
||||
this.rootPages = response.results
|
||||
.filter((p: any) => !p.ancestors || p.ancestors.length === 0)
|
||||
.map((p: any) => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
hasChildren: p.children?.page?.length > 0
|
||||
}));
|
||||
|
||||
console.log(`Found ${this.rootPages.length} root pages`);
|
||||
this.currentLevel = this.rootPages;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading pages:', error);
|
||||
new Notice(`Error: ${error}`);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadChildPages(page: ConfluencePage) {
|
||||
if (page.children) return;
|
||||
|
||||
try {
|
||||
const baseUrl = this.getBaseUrl();
|
||||
const url = `${baseUrl}/rest/api/content/${page.id}/child/page?limit=250`;
|
||||
|
||||
const response = await this.requestConfluence(url);
|
||||
page.children = (response?.results || []).map((p: any) => ({
|
||||
id: p.id,
|
||||
title: p.title,
|
||||
hasChildren: !!p._links?.childPage?.href
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error loading children:', error);
|
||||
page.children = [];
|
||||
}
|
||||
}
|
||||
|
||||
private renderList() {
|
||||
const { containerEl } = this;
|
||||
|
||||
// Remove old list
|
||||
const oldList = containerEl.querySelector('.page-list-container');
|
||||
if (oldList) oldList.remove();
|
||||
|
||||
const listDiv = containerEl.createDiv({ cls: 'page-list-container' });
|
||||
(listDiv as any).style.border = '1px solid var(--background-tertiary)';
|
||||
(listDiv as any).style.borderRadius = '4px';
|
||||
(listDiv as any).style.maxHeight = '400px';
|
||||
(listDiv as any).style.overflowY = 'auto';
|
||||
(listDiv as any).style.marginBottom = '12px';
|
||||
(listDiv as any).style.backgroundColor = 'var(--background-secondary)';
|
||||
|
||||
this.currentLevel.forEach(page => {
|
||||
const item = listDiv.createEl('div');
|
||||
(item as any).style.padding = '8px 12px';
|
||||
(item as any).style.borderBottom = '1px solid var(--background-tertiary)';
|
||||
(item as any).style.cursor = 'pointer';
|
||||
|
||||
item.onmouseenter = () => {
|
||||
(item as any).style.backgroundColor = 'var(--background-tertiary)';
|
||||
};
|
||||
item.onmouseleave = () => {
|
||||
(item as any).style.backgroundColor = 'transparent';
|
||||
};
|
||||
|
||||
const text = page.hasChildren ? `📁 ${page.title}` : `📄 ${page.title}`;
|
||||
item.setText(text);
|
||||
|
||||
item.onclick = async () => {
|
||||
if (page.hasChildren) {
|
||||
await this.loadChildPages(page);
|
||||
if (page.children && page.children.length > 0) {
|
||||
this.breadcrumbs.push(page);
|
||||
this.currentLevel = page.children;
|
||||
this.renderHeader();
|
||||
this.renderList();
|
||||
this.renderButtons();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.options.onSelect(page);
|
||||
this.close();
|
||||
};
|
||||
});
|
||||
|
||||
this.renderButtons();
|
||||
}
|
||||
|
||||
private renderButtons() {
|
||||
const { containerEl } = this;
|
||||
|
||||
const oldButtons = containerEl.querySelector('.button-group');
|
||||
if (oldButtons) oldButtons.remove();
|
||||
|
||||
const buttonDiv = containerEl.createDiv({ cls: 'button-group' });
|
||||
(buttonDiv as any).style.display = 'flex';
|
||||
(buttonDiv as any).style.gap = '8px';
|
||||
|
||||
if (this.breadcrumbs.length > 0) {
|
||||
const backBtn = buttonDiv.createEl('button', { text: '← Back' });
|
||||
(backBtn as any).style.flex = '1';
|
||||
backBtn.onclick = () => {
|
||||
this.breadcrumbs.pop();
|
||||
this.currentLevel = this.breadcrumbs.length > 0
|
||||
? this.breadcrumbs[this.breadcrumbs.length - 1].children || []
|
||||
: this.rootPages;
|
||||
this.renderHeader();
|
||||
this.renderList();
|
||||
};
|
||||
}
|
||||
|
||||
const cancelBtn = buttonDiv.createEl('button', { text: 'Cancel' });
|
||||
(cancelBtn as any).style.flex = '1';
|
||||
cancelBtn.onclick = () => this.close();
|
||||
}
|
||||
|
||||
private getBaseUrl(): string {
|
||||
let url = this.options.confluenceBaseUrl.trim();
|
||||
if (!url.endsWith('/wiki') && !url.includes('/wiki/')) {
|
||||
url = url.replace(/\/$/, '') + '/wiki';
|
||||
}
|
||||
return url.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
private async requestConfluence(url: string): Promise<any> {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Basic ${btoa(`${this.options.username}:${this.options.apiToken}`)}`
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await requestUrl({ url, method: 'GET', headers });
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
return response.json;
|
||||
} catch (error) {
|
||||
console.error('Request failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ export class ConfluenceVaultUploaderSettingTab extends PluginSettingTab {
|
|||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
containerEl.createEl('h2', { text: 'Confluence Vault Uploader Settings' });
|
||||
new Setting(containerEl).setName('Confluence Vault Uploader Settings').setHeading();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Confluence base URL')
|
||||
|
|
@ -72,7 +72,7 @@ export class ConfluenceVaultUploaderSettingTab extends PluginSettingTab {
|
|||
this.plugin.settings.apiToken = value.trim();
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
(text as any).inputEl.type = 'password';
|
||||
text.inputEl.type = 'password';
|
||||
return text;
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue