diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..350e329 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +name: Release Obsidian plugin + +on: + push: + tags: + - "*" + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: "18.x" + + - name: Build plugin + run: | + npm install + npm run build + + - name: Create release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + tag="${GITHUB_REF#refs/tags/}" + + gh release create "$tag" \ + --title="$tag" \ + --draft \ + main.js manifest.json styles.css \ No newline at end of file diff --git a/LICENSE b/LICENSE index 8aa2645..e172ea7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) [year] [fullname] +Copyright (c) 2025 Owain Williams Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index d845a33..2ed4f31 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -Document Type -# Umbracidian - a plugin for Obsidian +Document Type +# umbPublisher - an Obsidian plugin. -This plugin allows you to push your [Obsidian](https://obsidian.md/) notes to [Umbraco 15+](https://umbraco.com) as a blog post. +This plugin allows you to push your [Obsidian](https://obsidian.md/) notes to [Umbraco 15+](https://umbraco.com) as a content item. -To see how to use this plugin with Obsidian, check out the [Wiki pages](https://github.com/OwainWilliams/Umbracidian/wiki) +To see how to use this plugin with Obsidian, check out the [Wiki pages](https://github.com/OwainWilliams/umbpublisher/wiki) diff --git a/assets/UmbracidianLogo.png b/assets/umbPublisher-Logo.png similarity index 100% rename from assets/UmbracidianLogo.png rename to assets/umbPublisher-Logo.png diff --git a/assets/umbracidianSettings.png b/assets/umbPublisherSettings.png similarity index 100% rename from assets/umbracidianSettings.png rename to assets/umbPublisherSettings.png diff --git a/getLatest.ps1 b/getLatest.ps1 index 66443c4..3424b76 100644 --- a/getLatest.ps1 +++ b/getLatest.ps1 @@ -1,4 +1,4 @@ -$Repo = "OwainWilliams/Umbracidian" # Replace with your actual repository name +$Repo = "OwainWilliams/umbpublisher" # Replace with your actual repository name $ManifestFile = "manifest.json" $JSFile = "main.js" diff --git a/icons/icons.ts b/icons/icons.ts index fdd14a1..c91266b 100644 --- a/icons/icons.ts +++ b/icons/icons.ts @@ -1,9 +1,9 @@ import { addIcon } from "obsidian"; -import umbracoLogo from "./img/umbracidian-logo.svg"; +import umbPublisherLogo from "./img/umbpublisher-logo.svg"; -export class UmbracidianIcons { - private icons = [{ iconId: "umbracidian-logo", svg: umbracoLogo }]; +export class umbpublisherIcons { + private icons = [{ iconId: "umbpublisher-logo", svg: umbPublisherLogo }]; registerIcons = () => { this.icons.forEach(({ iconId, svg }) => { diff --git a/icons/img/umbracidian-logo.svg b/icons/img/umbpublisher-logo.svg similarity index 100% rename from icons/img/umbracidian-logo.svg rename to icons/img/umbpublisher-logo.svg diff --git a/main.ts b/main.ts index f5d31c0..ac45a2b 100644 --- a/main.ts +++ b/main.ts @@ -1,13 +1,13 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import { Editor, MarkdownView, Notice, Plugin, requestUrl } from 'obsidian'; -import { DEFAULT_SETTINGS, UmbracidianSettings } from "./types/index"; +import { DEFAULT_SETTINGS, umbpublisherSettings } from "./types/index"; import { SettingTab } from "./settings"; -import { UmbracidianIcons } from "./icons/icons"; +import { umbpublisherIcons } from "./icons/icons"; import { GetUmbracoDocType } from "./methods/getUmbracoDocType"; import { CallUmbracoApi } from "./methods/callUmbracoApi"; import { GenerateGuid } from 'methods/generateGuid'; -const matter = require("gray-matter"); + interface Frontmatter { @@ -20,10 +20,10 @@ interface Frontmatter { content: string; } -export default class Umbracidian extends Plugin { - settings: UmbracidianSettings; +export default class umbpublisher extends Plugin { + settings: umbpublisherSettings; - private icons = new UmbracidianIcons(); + private icons = new umbpublisherIcons(); private bearerToken: null | string = null; // Initialize bearerToken to null async onload() { @@ -31,7 +31,7 @@ export default class Umbracidian extends Plugin { this.icons.registerIcons(); // This creates an icon in the left ribbon. - this.addRibbonIcon('umbracidian-logo', 'Umbracidian', async (evt: MouseEvent) => { + this.addRibbonIcon('umbpublisher-logo', 'umbpublisher', async (evt: MouseEvent) => { const view = this.app.workspace.getActiveViewOfType(MarkdownView); if (!view) { new Notice('No active Markdown view found.'); @@ -48,18 +48,24 @@ export default class Umbracidian extends Plugin { // This adds an editor command that can perform some operation on the current editor instance this.addCommand({ id: 'push-to-umbraco', - name: 'Push to Umbraco command', - editorCallback: async (editor: Editor) => { - const view = this.app.workspace.getActiveViewOfType(MarkdownView); - if (!view) { + name: 'Push to Umbraco', + editorCheckCallback: (checking: boolean, editor: Editor, view: MarkdownView) => + { + if(checking) return true; + + const value = this.app.workspace.getActiveViewOfType(MarkdownView); + + if(!value){ new Notice('No active Markdown view found.'); return; } + (async () => { this.bearerToken = await this.getBearerToken(); - const umbracoDocType = await GetUmbracoDocType(this.settings.blogDocTypeAlias, this.settings.websiteUrl, this.bearerToken); await this.createObsidianNode(view, umbracoDocType, this.settings.websiteUrl); + })(); } + }); // This adds a settings tab so the user can configure various aspects of the plugin @@ -105,18 +111,16 @@ export default class Umbracidian extends Plugin { body: body.toString(), }); - // Check if the response contains valid JSON + if (response.json) { const data = response.json as { access_token: string }; - // console.log('Bearer token response:', data); - return data.access_token; // Assuming the token is in the "access_token" field + + return data.access_token; } else { - // console.error('Empty or invalid JSON response:', response); new Notice('Failed to fetch bearer token.'); return null; } } catch (error) { - // console.error('Error fetching bearer token:', error); new Notice(`Error fetching bearer token: ${error}`); return null; } @@ -130,9 +134,17 @@ export default class Umbracidian extends Plugin { return null; } - const metaMatter = this.app.metadataCache.getFileCache(noteFile)?.frontmatter; - const pageContent = matter(view.getViewData()); - + const fileCache = this.app.metadataCache.getFileCache(noteFile); + const metaMatter = fileCache?.frontmatter; + const fileContent = await this.app.vault.read(noteFile); + + let content = fileContent; + if (fileCache?.frontmatterPosition) { + const { start, end } = fileCache.frontmatterPosition; + const lines = fileContent.split('\n'); + content = lines.slice(end.line + 1).join('\n'); + } + const frontmatter = { title: metaMatter?.title || view.file?.basename, tags: metaMatter?.tags || [], @@ -140,14 +152,13 @@ export default class Umbracidian extends Plugin { status: metaMatter?.published ? "published" : "draft", excerpt: metaMatter?.excerpt || undefined, feature_image: metaMatter?.feature_image || undefined, - content: pageContent.content, + content: content, }; if (!frontmatter) { new Notice('No frontmatter found.'); return null; } else { - // console.log('Meta matter:', frontmatter); return frontmatter; } diff --git a/manifest.json b/manifest.json index b5ca102..f51dcec 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { - "id": "umbracidian", - "name": "Umbracidian", - "version": "1.0.0", - "minAppVersion": "1.8.10", - "description": "Push notes to Umbraco CMS as blog posts.", + "id": "umbpublisher", + "name": "umbPublisher", + "version": "1.1.0", + "minAppVersion": "1.9.12", + "description": "Push notes to Umbraco CMS as content.", "author": "Owain Williams", "authorUrl": "https://owain.codes", "fundingUrl": "https://buymeacoffee.com/owaincodes", diff --git a/package-lock.json b/package-lock.json index f89b55b..91963a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "Umbracidian", + "name": "umbpublisher", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "Umbracidian", + "name": "umbpublisher", "version": "0.1.0", "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 4c98f99..8e8ffc4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "Umbracidian", - "version": "0.1.0", - "description": "Send Blog Posts to Umbraco CMS via Management API", + "name": "umbpublisher", + "version": "1.1.0", + "description": "Send Obsidian Notes to Umbraco CMS via Management API", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", @@ -9,7 +9,7 @@ "version": "node version-bump.mjs && git add manifest.json versions.json" }, "keywords": [], - "author": "", + "author": "Owain Williams", "license": "MIT", "devDependencies": { "@types/node": "^16.18.126", @@ -32,10 +32,10 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/obsidianmd/obsidian-sample-plugin.git" + "url": "git+https://github.com/OwainWilliams/umbPublisher.git" }, "bugs": { - "url": "https://github.com/obsidianmd/obsidian-sample-plugin/issues" + "url": "https://github.com/OwainWilliams/umbPublisher/issues" }, - "homepage": "https://github.com/obsidianmd/obsidian-sample-plugin#readme" + "homepage": "https://github.com/OwainWilliams/umbPublisher/" } diff --git a/settings/index.ts b/settings/index.ts index c17e0f5..06e7c1c 100644 --- a/settings/index.ts +++ b/settings/index.ts @@ -1,27 +1,93 @@ -import Umbracidian from "main"; -import { App, PluginSettingTab, Setting } from "obsidian"; +import umbpublisher from "main"; +import { App, PluginSettingTab, Setting, requestUrl, Notice } from "obsidian"; + +async function getBearerToken(websiteUrl: string, clientId: string, clientSecret: string): Promise { + const tokenEndpoint = `${websiteUrl}/umbraco/management/api/v1/security/back-office/token`; + const body = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: clientId, + client_secret: clientSecret, + }); + try { + const response = await requestUrl({ + url: tokenEndpoint, + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + }); + return (response.json as any).access_token; + } catch (e) { + new Notice('Failed to fetch bearer token'); + return null; + } +} + +async function fetchContentTree(websiteUrl: string, token: string): Promise { + const endpoint = `${websiteUrl}/umbraco/management/api/v1/tree/document-type/root`; + const response = await requestUrl({ + url: endpoint, + method: 'GET', + headers: { 'Authorization': `Bearer ${token}` }, + }); + return (response.json as any).items || []; +} + +// Recursively fetch all nodes and their children +async function fetchAllContentNodes( + websiteUrl: string, + token: string, + parentId: string | null = null, + depth: number = 0 +): Promise { + const endpoint = parentId + ? `${websiteUrl}/umbraco/management/api/v1/tree/document/children?parentId=${parentId}` + : `${websiteUrl}/umbraco/management/api/v1/tree/document/root?skip=0&take=100&foldersOnly=false`; + + const response = await requestUrl({ + url: endpoint, + method: 'GET', + headers: { 'Authorization': `Bearer ${token}` }, + }); + + const items = (response.json as any).items || []; + let allNodes: any[] = []; + + for (const item of items) { + // Add current node with depth for indentation + allNodes.push({ ...item, depth }); + // Recursively fetch children + const children = await fetchAllContentNodes(websiteUrl, token, item.id, depth + 1); + allNodes = allNodes.concat(children); + } + + return allNodes; +} export class SettingTab extends PluginSettingTab { - plugin: Umbracidian; + plugin: umbpublisher; + private cachedNodes: any[] = []; // Store fetched nodes - constructor(app: App, plugin: Umbracidian) { - super(app, plugin); - this.plugin = plugin; - } + constructor(app: App, plugin: umbpublisher) { + super(app, plugin); + this.plugin = plugin; + } - display(): void { - const { containerEl } = this; + display(): void { + let parentNodeDropdown: HTMLSelectElement | null = null; + let fetchButton: HTMLButtonElement | null = null; + const { containerEl } = this; + containerEl.empty(); - containerEl.empty(); - containerEl.createEl('h2', { text: 'Umbracidian' }); - new Setting(containerEl) - .setName('Website URL') - .setDesc('The URL of the Umbraco website e.g. https://example.com') - .addText(text => text - .setPlaceholder('Enter the website URL') - .setValue(this.plugin.settings.websiteUrl) - .onChange(async (value) => { - this.plugin.settings.websiteUrl = value; + new Setting(containerEl) + .setName('Website URL') + .setDesc('The URL of the Umbraco website e.g. https://example.com') + .addText(text => text + .setPlaceholder('Enter the website URL') + .setValue(this.plugin.settings.websiteUrl) + .onChange(async (value) => { + const match = value.match(/^(https?:\/\/[^\/]+)/i); + const sanitized = match ? match[1] : value.replace(/\/.*$/, ''); + this.plugin.settings.websiteUrl = sanitized; await this.plugin.saveSettings(); })), new Setting(containerEl) @@ -35,25 +101,80 @@ export class SettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })), new Setting(containerEl) - .setName('Client Secret') + .setName('Client secret') .setDesc('The client secret for the Umbraco API') .addText(text => text - .setPlaceholder('Client Secret from Umbraco') + .setPlaceholder('Client secret from Umbraco') .setValue(this.plugin.settings.clientSecret) .onChange(async (value) => { this.plugin.settings.clientSecret = value; await this.plugin.saveSettings(); }).inputEl.setAttribute('type', 'password')), - new Setting(containerEl) - .setName('Blog Parent Node UUID') - .setDesc('The UUID of the parent node for blog posts e.g. 00000000-0000-0000-0000-00000000000, leave empty for root') + new Setting(containerEl) + .setName('Pick content parent node') + .setDesc('Fetch and select a parent node from Umbraco where content will be saved under') + .addButton(button => { + fetchButton = button.buttonEl; + button.setButtonText('Fetch nodes').onClick(async () => { + const { websiteUrl, clientId, clientSecret } = this.plugin.settings; + if (!websiteUrl || !clientId || !clientSecret) { + new Notice('Please enter Website URL, Client Id, and Client Secret first.'); + return; + } + const token = await getBearerToken(websiteUrl, clientId, clientSecret); + if (!token) return; + // Fetch all nodes recursively and cache them + this.cachedNodes = await fetchAllContentNodes(websiteUrl, token); + if (parentNodeDropdown) { + parentNodeDropdown.innerHTML = ''; + const rootOption = document.createElement('option'); + rootOption.value = ''; + rootOption.text = '[Select Node]'; + parentNodeDropdown.appendChild(rootOption); + this.cachedNodes.forEach(node => { + const option = document.createElement('option'); + option.value = node.id; + option.text = `${'—'.repeat(node.depth)} ${node.variants[0].name}`; + parentNodeDropdown?.appendChild(option); + }); + parentNodeDropdown.value = this.plugin.settings.blogParentNodeId || ''; + } + }); + }) + .addDropdown(dropdown => { + parentNodeDropdown = dropdown.selectEl; + // Populate dropdown from cache if available + parentNodeDropdown.innerHTML = ''; + const rootOption = document.createElement('option'); + rootOption.value = ''; + rootOption.text = '[Select Node]'; + parentNodeDropdown.appendChild(rootOption); + if (this.cachedNodes.length > 0) { + this.cachedNodes.forEach(node => { + const option = document.createElement('option'); + option.value = node.id; + option.text = `${'—'.repeat(node.depth)} ${node.variants[0].name}`; + parentNodeDropdown?.appendChild(option); + }); + } + parentNodeDropdown.value = this.plugin.settings.blogParentNodeId || ''; + + dropdown.onChange(async (value) => { + this.plugin.settings.blogParentNodeId = value; + this.display(); + await this.plugin.saveSettings(); + + }); + + }), + new Setting(containerEl) + .setName('Blog parent node UUID') + .setDesc('For reference, this is fetched from the node picker above') .addText(text => text - .setPlaceholder('Enter the parent node UUID') + .setPlaceholder('Fetched from node picker above') .setValue(this.plugin.settings.blogParentNodeId) - .onChange(async (value) => { - this.plugin.settings.blogParentNodeId = value; - await this.plugin.saveSettings(); - })), + .setDisabled(true) + ), new Setting(containerEl) .setName('DocType alias') .setDesc('This is the alias of the DocType you want to use for your blog posts') @@ -71,11 +192,11 @@ export class SettingTab extends PluginSettingTab { .setPlaceholder('Enter the Title alias') .setValue(this.plugin.settings.titleAlias) .onChange(async (value) => { - this.plugin.settings.titleAlias = value; + this.plugin.settings.titleAlias = value; await this.plugin.saveSettings(); })), new Setting(containerEl) - .setName('Blog Content Editor alias') + .setName('Blog content editor alias') .setDesc('This should be an Umbraco.MarkdownEditor property on your page') .addText(text => text .setPlaceholder('Enter the Property alias') @@ -83,6 +204,6 @@ export class SettingTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.blogContentAlias = value; await this.plugin.saveSettings(); - })) + })); } } diff --git a/types/index.ts b/types/index.ts index 4d7baf9..fd3d746 100644 --- a/types/index.ts +++ b/types/index.ts @@ -1,4 +1,4 @@ -export interface UmbracidianSettings { +export interface umbpublisherSettings { mySetting: string; websiteUrl: string; blogParentNodeId: string; @@ -9,7 +9,7 @@ export interface UmbracidianSettings { blogContentAlias: string; } -export const DEFAULT_SETTINGS: UmbracidianSettings = { +export const DEFAULT_SETTINGS: umbpublisherSettings = { mySetting: 'default', blogParentNodeId: 'null', blogDocTypeAlias: 'BlogPost', diff --git a/versions.json b/versions.json index 26382a1..df3d877 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "1.0.0": "0.15.0" + "1.1.0": "0.15.0" }