diff --git a/package-lock.json b/package-lock.json index 8d6f0bdc..93ff5d65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "obsidian-confluence", + "name": "obsidian-confluence-root", "version": "3.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "obsidian-confluence", + "name": "obsidian-confluence-root", "version": "3.3.0", "workspaces": [ "packages/*" @@ -17049,7 +17049,7 @@ }, "packages/lib": { "name": "@markdown-confluence/lib", - "version": "3.1.0", + "version": "3.3.0", "license": "Apache 2.0", "dependencies": { "@atlaskit/adf-utils": "^17.1.4", @@ -17109,47 +17109,14 @@ }, "packages/mermaid-electron-renderer": { "name": "@markdown-confluence/mermaid-electron-renderer", - "version": "3.1.0", + "version": "3.3.0", "license": "Apache 2.0", "dependencies": { - "@atlaskit/adf-utils": "^17.1.4", - "@atlaskit/editor-common": "^72.4.0", - "@atlaskit/editor-confluence-transformer": "^8.1.33", - "@atlaskit/editor-json-transformer": "^8.8.3", - "@atlaskit/renderer": "^107.3.2", "@electron/remote": "^2.0.9", - "@markdown-confluence/lib": "3.1.0", - "@types/prosemirror-state": "^1.3.0", - "@types/prosemirror-transform": "^1.4.2", - "@types/prosemirror-view": "^1.23.1", - "confluence.js": "^1.6.3", - "deep-equal": "^2.2.0", - "formdata-node": "^5.0.0", - "gray-matter": "^4.0.3", - "lodash": "^4.17.21", - "markdown-it-table": "^2.0.4", - "mermaid": "^10.1.0", - "mime-types": "^2.1.35", - "prosemirror-markdown": "^1.10.1", - "prosemirror-model": "1.14.3", - "punycode": "^1.4.1", - "react": "^16.14.0", - "react-dom": "^16.14.0", - "react-intl-next": "npm:react-intl@^5.18.1", - "sort-any": "^4.0.5", - "spark-md5": "^3.0.2", - "uuid": "^9.0.0" + "@markdown-confluence/lib": "3.3.0", + "mermaid": "^10.1.0" }, "devDependencies": { - "@jest/globals": "^29.5.0", - "@types/deep-equal": "^1.0.1", - "@types/lodash": "^4.14.194", - "@types/markdown-it": "^12.2.3", - "@types/mime-types": "^2.1.1", - "@types/node": "^16.11.6", - "@types/prosemirror-model": "1.16.2", - "@types/react-dom": "^18.0.11", - "@types/spark-md5": "^3.0.2", "@typescript-eslint/eslint-plugin": "^5.29.0", "@typescript-eslint/parser": "^5.58.0", "builtin-modules": "3.3.0", @@ -17157,12 +17124,8 @@ "esbuild-node-externals": "^1.7.0", "eslint": "^8.38.0", "eslint-config-prettier": "^8.8.0", - "husky": "^8.0.3", - "jest": "^29.5.0", "lint-staged": "^13.2.1", - "markdown-it": "^13.0.1", "prettier": "2.8.7", - "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "2.5.0", "typescript": "4.7.4" @@ -17170,7 +17133,7 @@ }, "packages/obsidian": { "name": "obsidian-confluence", - "version": "3.1.0", + "version": "3.3.0", "license": "Apache 2.0", "dependencies": { "@atlaskit/adf-utils": "^17.1.4", @@ -17179,8 +17142,8 @@ "@atlaskit/editor-json-transformer": "^8.8.3", "@atlaskit/renderer": "^107.3.2", "@electron/remote": "^2.0.9", - "@markdown-confluence/lib": "3.1.0", - "@markdown-confluence/mermaid-electron-renderer": "3.1.0", + "@markdown-confluence/lib": "3.3.0", + "@markdown-confluence/mermaid-electron-renderer": "3.3.0", "@types/prosemirror-state": "^1.3.0", "@types/prosemirror-transform": "^1.4.2", "@types/prosemirror-view": "^1.23.1", @@ -20421,58 +20384,21 @@ "@markdown-confluence/mermaid-electron-renderer": { "version": "file:packages/mermaid-electron-renderer", "requires": { - "@atlaskit/adf-utils": "^17.1.4", - "@atlaskit/editor-common": "^72.4.0", - "@atlaskit/editor-confluence-transformer": "^8.1.33", - "@atlaskit/editor-json-transformer": "^8.8.3", - "@atlaskit/renderer": "^107.3.2", "@electron/remote": "^2.0.9", - "@jest/globals": "^29.5.0", - "@markdown-confluence/lib": "3.1.0", - "@types/deep-equal": "^1.0.1", - "@types/lodash": "^4.14.194", - "@types/markdown-it": "^12.2.3", - "@types/mime-types": "^2.1.1", - "@types/node": "^16.11.6", - "@types/prosemirror-model": "1.16.2", - "@types/prosemirror-state": "^1.3.0", - "@types/prosemirror-transform": "^1.4.2", - "@types/prosemirror-view": "^1.23.1", - "@types/react-dom": "^18.0.11", - "@types/spark-md5": "^3.0.2", + "@markdown-confluence/lib": "3.3.0", "@typescript-eslint/eslint-plugin": "^5.29.0", "@typescript-eslint/parser": "^5.58.0", "builtin-modules": "3.3.0", - "confluence.js": "^1.6.3", - "deep-equal": "^2.2.0", "esbuild": "0.17.16", "esbuild-node-externals": "^1.7.0", "eslint": "^8.38.0", "eslint-config-prettier": "^8.8.0", - "formdata-node": "^5.0.0", - "gray-matter": "^4.0.3", - "husky": "^8.0.3", - "jest": "^29.5.0", "lint-staged": "^13.2.1", - "lodash": "^4.17.21", - "markdown-it": "^13.0.1", - "markdown-it-table": "^2.0.4", "mermaid": "^10.1.0", - "mime-types": "^2.1.35", "prettier": "2.8.7", - "prosemirror-markdown": "^1.10.1", - "prosemirror-model": "1.14.3", - "punycode": "^1.4.1", - "react": "^16.14.0", - "react-dom": "^16.14.0", - "react-intl-next": "npm:react-intl@^5.18.1", - "sort-any": "^4.0.5", - "spark-md5": "^3.0.2", - "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "2.5.0", - "typescript": "4.7.4", - "uuid": "^9.0.0" + "typescript": "4.7.4" } }, "@nodelib/fs.scandir": { @@ -27306,8 +27232,8 @@ "@atlaskit/renderer": "^107.3.2", "@electron/remote": "^2.0.9", "@jest/globals": "^29.5.0", - "@markdown-confluence/lib": "3.1.0", - "@markdown-confluence/mermaid-electron-renderer": "3.1.0", + "@markdown-confluence/lib": "3.3.0", + "@markdown-confluence/mermaid-electron-renderer": "3.3.0", "@types/deep-equal": "^1.0.1", "@types/lodash": "^4.14.194", "@types/markdown-it": "^12.2.3", diff --git a/packages/lib/src/ConniePageConfig.ts b/packages/lib/src/ConniePageConfig.ts index 4af34d50..a5a3a336 100644 --- a/packages/lib/src/ConniePageConfig.ts +++ b/packages/lib/src/ConniePageConfig.ts @@ -2,29 +2,51 @@ import { MarkdownFile } from "./adaptors"; export type PageContentType = "page" | "blogpost"; -type ConfluencePerPageConfig = { - pageTitle: FrontmatterConfig; - frontmatterToPublish: FrontmatterConfig; - tags: FrontmatterConfig; - pageId: FrontmatterConfig; - dontChangeParentPageId: FrontmatterConfig; - blogPostDate: FrontmatterConfig; - contentType: FrontmatterConfig; +export type ConfluencePerPageConfig = { + publish: FrontmatterConfig; + pageTitle: FrontmatterConfig; + frontmatterToPublish: FrontmatterConfig; + tags: FrontmatterConfig; + pageId: FrontmatterConfig; + dontChangeParentPageId: FrontmatterConfig; + blogPostDate: FrontmatterConfig; + contentType: FrontmatterConfig; }; -interface FrontmatterConfig { +export type InputType = "text" | "array-text" | "boolean" | "options"; +export type InputValidator = (value: IN) => { + valid: boolean; + errors: Error[]; +}; + +interface FrontmatterConfigBase { key: string; default: OUT; alwaysProcess?: boolean; process: ProcessFunction; + inputType: InputType; + inputValidator: InputValidator; } +type FrontmatterConfigOptions = T extends "options" + ? { selectOptions: OUT[] } + : unknown; + +export type FrontmatterConfig< + OUT, + T extends InputType +> = FrontmatterConfigBase & FrontmatterConfigOptions; + type ProcessFunction = ( value: IN, markdownFile: MarkdownFile, alreadyParsed: Partial ) => OUT | Error; +export type ConfluencePerPageAllValues = { + [K in keyof ConfluencePerPageConfig]: ConfluencePerPageConfig[K]["default"]; // TODO: Accumlate Errors +}; + type excludedProperties = "frontmatterToPublish"; export type ConfluencePerPageValues = Omit< @@ -34,100 +56,43 @@ export type ConfluencePerPageValues = Omit< excludedProperties >; -export function processConniePerPageConfig( - markdownFile: MarkdownFile -): ConfluencePerPageValues { - const result: Partial = {}; - const config = conniePerPageConfig; - - for (const propertyKey in config) { - const { - process, - default: defaultValue, - key, - alwaysProcess, - } = config[propertyKey as keyof ConfluencePerPageConfig]; - if (key in markdownFile.frontmatter || alwaysProcess) { - const frontmatterValue = markdownFile.frontmatter[key]; - result[propertyKey as keyof ConfluencePerPageValues] = process( - frontmatterValue as never, - markdownFile, - result - ) as never; - } else { - result[propertyKey as keyof ConfluencePerPageValues] = - defaultValue as never; - } - } - return preventOverspreading(result, "frontmatterToPublish"); -} - -function preventOverspreading( - source: Partial, - ...keysToOmit: excludedProperties[] -): T { - const result: Partial = {}; - - for (const key in source) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if (!keysToOmit.includes(key as any)) { - result[key as keyof T] = source[key]; - } - } - - return result as T; -} - -type ValidationResult = { - valid: boolean; - reasons?: string[]; -}; - -function validateDate(dateString: string): ValidationResult { - const reasons: string[] = []; - - // Regular expression to check the format: YYYY-MM-DD - const regex = /^\d{4}-\d{2}-\d{2}$/; - - if (!regex.test(dateString)) { - reasons.push("Invalid format"); - } else { - const parts = dateString.split("-"); - const year = parseInt(parts[0], 10); - const month = parseInt(parts[1], 10) - 1; // Date months are 0-based - const day = parseInt(parts[2], 10); - - if (year < 0 || year > 9999) { - reasons.push("Invalid year"); - } - - if (month < 0 || month > 11) { - reasons.push("Invalid month"); - } - - const date = new Date(year, month, day); - - if ( - date.getFullYear() !== year || - date.getMonth() !== month || - date.getDate() !== day - ) { - reasons.push("Invalid day"); - } - } - - if (reasons.length > 0) { - return { valid: false, reasons }; - } else { - return { valid: true }; - } -} - export const conniePerPageConfig: ConfluencePerPageConfig = { + publish: { + key: "connie-publish", + default: false, + inputType: "boolean", + inputValidator: (value) => { + switch (typeof value) { + case "boolean": + return { valid: true, errors: [] }; + default: + return { + valid: false, + errors: [new Error("Publish should be a boolean.")], + }; + } + }, + process: (publish) => (typeof publish === "boolean" ? publish : false), + }, pageTitle: { key: "connie-title", default: "", alwaysProcess: true, + inputType: "text", + inputValidator: (value) => { + switch (typeof value) { + case "string": + return { + valid: true, + errors: [], + }; + default: + return { + valid: false, + errors: [new Error("Title needs to be a string.")], + }; + } + }, process: (yamlValue, markdownFile) => { return typeof yamlValue === "string" ? yamlValue @@ -136,7 +101,14 @@ export const conniePerPageConfig: ConfluencePerPageConfig = { }, frontmatterToPublish: { key: "connie-frontmatter-to-publish", - default: undefined, + default: [], + inputType: "array-text", + inputValidator: (value) => { + return { + valid: true, + errors: [], + }; + }, process: (yamlValue, markdownFile) => { if (yamlValue && Array.isArray(yamlValue)) { let frontmatterHeader = @@ -153,12 +125,19 @@ export const conniePerPageConfig: ConfluencePerPageConfig = { markdownFile.contents = frontmatterHeader + markdownFile.contents; } - return undefined; + return []; }, }, tags: { key: "tags", default: [], + inputType: "array-text", + inputValidator: (value) => { + return { + valid: true, + errors: [], + }; + }, process: (yamlValue, markdownFile) => { const tags: string[] = []; if (Array.isArray(yamlValue)) { @@ -174,6 +153,21 @@ export const conniePerPageConfig: ConfluencePerPageConfig = { pageId: { key: "connie-page-id", default: undefined, + inputType: "text", + inputValidator: (value) => { + const digitRegex = /^\d+$/; + if (typeof value === "string" && digitRegex.test(value)) { + return { valid: true, errors: [] }; + } + return { + valid: false, + errors: [ + new Error( + "Page ID needs to be a string and only can contain numbers" + ), + ], + }; + }, process: (yamlValue, markdownFile) => { let pageId: string | undefined; switch (typeof yamlValue) { @@ -190,6 +184,13 @@ export const conniePerPageConfig: ConfluencePerPageConfig = { dontChangeParentPageId: { key: "connie-dont-change-parent-page", default: false, + inputType: "boolean", + inputValidator: (value) => { + return { + valid: true, + errors: [], + }; + }, process: (dontChangeParentPageId) => typeof dontChangeParentPageId === "boolean" ? dontChangeParentPageId @@ -198,6 +199,32 @@ export const conniePerPageConfig: ConfluencePerPageConfig = { blogPostDate: { key: "connie-blog-post-date", default: undefined, + inputType: "text", + inputValidator: (yamlValue) => { + if (typeof yamlValue !== "string") { + return { + valid: false, + errors: [ + new Error( + `Blog post date needs to be a string in the format of "YYYY-MM-DD".` + ), + ], + }; + } + const blogPostDateValidation = validateDate(yamlValue); + if (blogPostDateValidation.valid) { + return { valid: true, errors: [] }; + } else { + return { + valid: false, + errors: [ + new Error( + `Blog post date error. ${blogPostDateValidation.reasons?.join()}` + ), + ], + }; + } + }, process: (yamlValue: string) => { const blogPostDateValidation = validateDate(yamlValue); if (blogPostDateValidation.valid) { @@ -213,6 +240,14 @@ export const conniePerPageConfig: ConfluencePerPageConfig = { key: "connie-content-type", default: "page", alwaysProcess: true, + inputType: "options", + selectOptions: ["page", "blogpost"], + inputValidator: (value) => { + return { + valid: true, + errors: [], + }; + }, process: (yamlValue, markdownFile, alreadyParsed) => { if (yamlValue !== undefined && typeof yamlValue !== "string") { return Error(`Provided "connie-content-type" isn't a string.`); @@ -253,3 +288,93 @@ export const conniePerPageConfig: ConfluencePerPageConfig = { }, }, }; + +export function processConniePerPageConfig( + markdownFile: MarkdownFile +): ConfluencePerPageValues { + const result: Partial = {}; + const config = conniePerPageConfig; + + for (const propertyKey in config) { + const { + process, + default: defaultValue, + key, + alwaysProcess, + } = config[propertyKey as keyof ConfluencePerPageConfig]; + if (key in markdownFile.frontmatter || alwaysProcess) { + const frontmatterValue = markdownFile.frontmatter[key]; + result[propertyKey as keyof ConfluencePerPageValues] = process( + frontmatterValue as never, + markdownFile, + result + ) as never; + } else { + result[propertyKey as keyof ConfluencePerPageValues] = + defaultValue as never; + } + } + + return preventOverspreading(result, "frontmatterToPublish"); +} + +function preventOverspreading( + source: Partial, + ...keysToOmit: excludedProperties[] +): T { + const result: Partial = {}; + + for (const key in source) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if (!keysToOmit.includes(key as any)) { + result[key as keyof T] = source[key]; + } + } + + return result as T; +} + +type ValidationResult = { + valid: boolean; + reasons?: string[]; +}; + +function validateDate(dateString: string): ValidationResult { + const reasons: string[] = []; + + // Regular expression to check the format: YYYY-MM-DD + const regex = /^\d{4}-\d{2}-\d{2}$/; + + if (!regex.test(dateString)) { + reasons.push("Invalid format"); + } else { + const parts = dateString.split("-"); + const year = parseInt(parts[0], 10); + const month = parseInt(parts[1], 10) - 1; // Date months are 0-based + const day = parseInt(parts[2], 10); + + if (year < 0 || year > 9999) { + reasons.push("Invalid year"); + } + + if (month < 0 || month > 11) { + reasons.push("Invalid month"); + } + + const date = new Date(year, month, day); + + if ( + date.getFullYear() !== year || + date.getMonth() !== month || + date.getDate() !== day + ) { + reasons.push("Invalid day"); + } + } + + if (reasons.length > 0) { + return { valid: false, reasons }; + } else { + return { valid: true }; + } +} diff --git a/packages/lib/src/Publisher.test.ts b/packages/lib/src/Publisher.test.ts index c1ea42ec..e464c12e 100644 --- a/packages/lib/src/Publisher.test.ts +++ b/packages/lib/src/Publisher.test.ts @@ -9,6 +9,7 @@ import { MarkdownFile, } from "./adaptors"; import { orderMarks } from "./AdfEqual"; +import { ConfluencePerPageAllValues } from "./ConniePageConfig"; import { ChartData, MermaidRenderer } from "./mermaid_renderers"; import { Publisher } from "./Publisher"; import { ConfluenceSettings } from "./Settings"; @@ -224,11 +225,11 @@ class InMemoryAdaptor implements LoaderAdaptor { constructor(inMemoryFiles: MarkdownFile[]) { this.inMemoryFiles = inMemoryFiles; } - - async updateMarkdownPageId( + async updateMarkdownValues( absoluteFilePath: string, - id: string + values: Partial ): Promise {} + async loadMarkdownFile(absoluteFilePath: string): Promise { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.inMemoryFiles.find( diff --git a/packages/lib/src/TreeConfluence.ts b/packages/lib/src/TreeConfluence.ts index ea85c1bd..21f969a1 100644 --- a/packages/lib/src/TreeConfluence.ts +++ b/packages/lib/src/TreeConfluence.ts @@ -141,6 +141,11 @@ async function ensurePageExists( throw new Error("Missing Space Key"); } + await adaptor.updateMarkdownValues(file.absoluteFilePath, { + publish: true, + pageId: contentById.id, + }); + return { id: contentById.id, title: file.pageTitle, @@ -174,10 +179,10 @@ async function ensurePageExists( ); } - await adaptor.updateMarkdownPageId( - file.absoluteFilePath, - currentPage.id - ); + await adaptor.updateMarkdownValues(file.absoluteFilePath, { + publish: true, + pageId: currentPage.id, + }); return { id: currentPage.id, title: file.pageTitle, @@ -208,10 +213,10 @@ async function ensurePageExists( creatingBlankPageRequest ); - await adaptor.updateMarkdownPageId( - file.absoluteFilePath, - pageDetails.id - ); + await adaptor.updateMarkdownValues(file.absoluteFilePath, { + publish: true, + pageId: pageDetails.id, + }); return { id: pageDetails.id, title: file.pageTitle, diff --git a/packages/lib/src/__snapshots__/MdToADF.test.ts.snap b/packages/lib/src/__snapshots__/MdToADF.test.ts.snap index cb7c664d..6b15683e 100644 --- a/packages/lib/src/__snapshots__/MdToADF.test.ts.snap +++ b/packages/lib/src/__snapshots__/MdToADF.test.ts.snap @@ -48,6 +48,7 @@ exports[`parses blockquotes.md 1`] = ` }, "pageId": undefined, "pageTitle": "Blockquotes", + "publish": false, "tags": [], } `; @@ -143,6 +144,7 @@ exports[`parses blog.md 1`] = ` }, "pageId": undefined, "pageTitle": "Blog", + "publish": false, "tags": [], } `; @@ -198,6 +200,7 @@ exports[`parses code.md 1`] = ` }, "pageId": undefined, "pageTitle": "Code", + "publish": false, "tags": [], } `; @@ -312,6 +315,7 @@ exports[`parses emphasis.md 1`] = ` }, "pageId": undefined, "pageTitle": "Emphasis", + "publish": false, "tags": [], } `; @@ -354,6 +358,7 @@ exports[`parses escaping.md 1`] = ` }, "pageId": undefined, "pageTitle": "Escaping", + "publish": false, "tags": [], } `; @@ -559,6 +564,7 @@ exports[`parses file1.md 1`] = ` }, "pageId": "12345", "pageTitle": "Custom Title", + "publish": false, "tags": [ "test", "example", @@ -718,6 +724,7 @@ exports[`parses file2.md 1`] = ` }, "pageId": "67890", "pageTitle": "Another Custom Title", + "publish": false, "tags": [ "demo", ], @@ -826,6 +833,7 @@ exports[`parses file3.md 1`] = ` }, "pageId": "qwerty", "pageTitle": "Third Test Page", + "publish": false, "tags": [ "sample", "test", @@ -925,6 +933,7 @@ exports[`parses headers.md 1`] = ` }, "pageId": undefined, "pageTitle": "Headers", + "publish": false, "tags": [], } `; @@ -958,6 +967,7 @@ exports[`parses horizontal_rules.md 1`] = ` }, "pageId": undefined, "pageTitle": "Horizontal Rules", + "publish": false, "tags": [], } `; @@ -1012,6 +1022,7 @@ exports[`parses images.md 1`] = ` }, "pageId": undefined, "pageTitle": "Images", + "publish": false, "tags": [], } `; @@ -1045,6 +1056,7 @@ exports[`parses inline_html.md 1`] = ` }, "pageId": undefined, "pageTitle": "Inline HTML", + "publish": false, "tags": [], } `; @@ -1103,6 +1115,7 @@ exports[`parses links.md 1`] = ` }, "pageId": undefined, "pageTitle": "Links", + "publish": false, "tags": [], } `; @@ -1196,6 +1209,7 @@ exports[`parses lists.md 1`] = ` }, "pageId": undefined, "pageTitle": "Lists", + "publish": false, "tags": [], } `; @@ -1322,6 +1336,7 @@ exports[`parses tables.md 1`] = ` }, "pageId": undefined, "pageTitle": "Tables", + "publish": false, "tags": [], } `; diff --git a/packages/lib/src/adaptors/index.ts b/packages/lib/src/adaptors/index.ts index 1cdd4d4f..f0587bdc 100644 --- a/packages/lib/src/adaptors/index.ts +++ b/packages/lib/src/adaptors/index.ts @@ -1,4 +1,5 @@ import { Api } from "confluence.js"; +import { ConfluencePerPageAllValues } from "../ConniePageConfig"; export type FilesToUpload = Array; export interface MarkdownFile { @@ -20,7 +21,10 @@ export interface BinaryFile { } export interface LoaderAdaptor { - updateMarkdownPageId(absoluteFilePath: string, id: string): Promise; + updateMarkdownValues( + absoluteFilePath: string, + values: Partial + ): Promise; loadMarkdownFile(absoluteFilePath: string): Promise; getMarkdownFilesToUpload(): Promise; readBinary( diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts index e322e223..fc1e762f 100644 --- a/packages/lib/src/index.ts +++ b/packages/lib/src/index.ts @@ -3,3 +3,4 @@ export * from "./MdToADF"; export * from "./adaptors"; export * as ConfluenceUploadSettings from "./Settings"; export * from "./mermaid_renderers"; +export * as ConfluencePageConfig from "./ConniePageConfig"; diff --git a/packages/obsidian/src/CompletedModal.tsx b/packages/obsidian/src/CompletedModal.tsx index effb3e4d..8dfb5528 100644 --- a/packages/obsidian/src/CompletedModal.tsx +++ b/packages/obsidian/src/CompletedModal.tsx @@ -150,6 +150,7 @@ export class CompletedModal extends Modal { onClose() { const { contentEl } = this; + ReactDOM.unmountComponentAtNode(contentEl); contentEl.empty(); } } diff --git a/packages/obsidian/src/ConfluencePerPageForm.tsx b/packages/obsidian/src/ConfluencePerPageForm.tsx new file mode 100644 index 00000000..fd5789b2 --- /dev/null +++ b/packages/obsidian/src/ConfluencePerPageForm.tsx @@ -0,0 +1,523 @@ +import { Modal, App, FrontMatterCache } from "obsidian"; +import ReactDOM from "react-dom"; +import React, { useState, ChangeEvent } from "react"; +import { ConfluencePageConfig } from "@markdown-confluence/lib"; +import { Property } from "csstype"; + +export type ConfluencePerPageUIValues = { + [K in keyof ConfluencePageConfig.ConfluencePerPageConfig]: { + value?: ConfluencePageConfig.ConfluencePerPageConfig[K]["default"]; + isSet: boolean; + }; +}; + +export function mapFrontmatterToConfluencePerPageUIValues( + frontmatter: FrontMatterCache | undefined +): ConfluencePerPageUIValues { + const config = ConfluencePageConfig.conniePerPageConfig; + const result: Partial = {}; + + if (!frontmatter) { + throw new Error("Missing frontmatter"); + } + + for (const propertyKey in config) { + if (config.hasOwnProperty(propertyKey)) { + const { + key, + inputType, + default: defaultValue, + } = config[ + propertyKey as keyof ConfluencePageConfig.ConfluencePerPageConfig + ]; + const frontmatterValue = frontmatter[key]; + + if (frontmatterValue !== undefined) { + result[propertyKey as keyof ConfluencePerPageUIValues] = { + value: frontmatterValue, + isSet: true, + }; + } else { + switch (inputType) { + case "options": + case "array-text": + result[propertyKey as keyof ConfluencePerPageUIValues] = + { value: defaultValue as never, isSet: false }; + break; + case "boolean": + case "text": + result[propertyKey as keyof ConfluencePerPageUIValues] = + { value: undefined, isSet: false }; + break; + default: + throw new Error("Missing case for inputType"); + } + } + } + } + return result as ConfluencePerPageUIValues; +} + +interface FormProps { + config: ConfluencePageConfig.ConfluencePerPageConfig; + initialValues: ConfluencePerPageUIValues; + onSubmit: (values: ConfluencePerPageUIValues) => void; +} + +interface ModalProps { + config: ConfluencePageConfig.ConfluencePerPageConfig; + initialValues: ConfluencePerPageUIValues; + onSubmit: (values: ConfluencePerPageUIValues, close: () => void) => void; +} + +const handleChange = ( + key: string, + value: unknown, + inputValidator: ConfluencePageConfig.InputValidator, + setValues: React.Dispatch>, + setErrors: React.Dispatch>>, + isSetValue: boolean +) => { + const validationResult = inputValidator(value); + + setValues((prevValues) => ({ + ...prevValues, + [key]: { + ...prevValues[key as keyof ConfluencePerPageUIValues], + ...(isSetValue ? { isSet: value } : { value }), + }, + })); + setErrors((prevErrors) => ({ + ...prevErrors, + [key]: validationResult.valid ? [] : validationResult.errors, + })); +}; + +const styles = { + errorTd: { + columnSpan: "all" as Property.ColumnSpan, + color: "red", + }, +}; + +const renderTextInput = ( + key: string, + config: ConfluencePageConfig.FrontmatterConfig, + values: ConfluencePerPageUIValues, + errors: Record, + setValues: React.Dispatch>, + setErrors: React.Dispatch>> +) => ( + <> + + + + + + ) => + handleChange( + key, + e.target.value, + config.inputValidator, + setValues, + setErrors, + false + ) + } + /> + + + ) => + handleChange( + key, + e.target.checked, + config.inputValidator, + setValues, + setErrors, + true + ) + } + /> + + + + {errors[key]?.length > 0 && ( + +
+ {errors[key].map((error) => ( +

{error.message}

+ ))} +
+ + )} + + +); + +const renderArrayText = ( + key: string, + config: ConfluencePageConfig.FrontmatterConfig, + values: ConfluencePerPageUIValues, + errors: Record, + setValues: React.Dispatch>, + setErrors: React.Dispatch>> +) => ( + <> + + + + + + {( + values[key as keyof ConfluencePerPageUIValues] + .value as unknown as string[] + ).map((value, index) => ( + ) => { + const newArray = [ + ...(values[ + key as keyof ConfluencePerPageUIValues + ].value as unknown as string[]), + ]; + newArray[index] = e.target.value; + handleChange( + key, + newArray, + config.inputValidator, + setValues, + setErrors, + false + ); + }} + /> + ))} + + + + ) => + handleChange( + key, + e.target.checked, + config.inputValidator, + setValues, + setErrors, + true + ) + } + /> + + + + {errors[key]?.length > 0 && ( + +
+ {errors[key].map((error) => ( +

{error.message}

+ ))} +
+ + )} + + +); + +const renderBoolean = ( + key: string, + config: ConfluencePageConfig.FrontmatterConfig, + values: ConfluencePerPageUIValues, + errors: Record, + setValues: React.Dispatch>, + setErrors: React.Dispatch>> +) => ( + <> + + + + + + ) => + handleChange( + key, + e.target.checked, + config.inputValidator, + setValues, + setErrors, + false + ) + } + /> + + + ) => + handleChange( + key, + e.target.checked, + config.inputValidator, + setValues, + setErrors, + true + ) + } + /> + + + + {errors[key]?.length > 0 && ( + +
+ {errors[key].map((error) => ( +

{error.message}

+ ))} +
+ + )} + + +); +const renderOptions = ( + key: string, + config: ConfluencePageConfig.FrontmatterConfig< + ConfluencePageConfig.PageContentType, + "options" + >, + values: ConfluencePerPageUIValues, + errors: Record, + setValues: React.Dispatch>, + setErrors: React.Dispatch>> +) => ( + <> + + + + + + + + + ) => + handleChange( + key, + e.target.checked, + config.inputValidator, + setValues, + setErrors, + true + ) + } + /> + + + + {errors[key]?.length > 0 && ( + +
+ {errors[key].map((error) => ( +

{error.message}

+ ))} +
+ + )} + + +); + +const ConfluenceForm: React.FC = ({ + config, + initialValues, + onSubmit, +}) => { + const [values, setValues] = + useState(initialValues); + const [errors, setErrors] = useState>({}); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit(values as ConfluencePerPageUIValues); + }; + + return ( +
+

Update Confluence Page Settings

+ + + + + + + + + + {Object.entries(config).map(([key, config]) => { + switch (config.inputType) { + case "text": + return renderTextInput( + key, + config as ConfluencePageConfig.FrontmatterConfig< + string, + "text" + >, + values, + errors, + setValues, + setErrors + ); + case "array-text": + return renderArrayText( + key, + config as ConfluencePageConfig.FrontmatterConfig< + string[], + "array-text" + >, + values, + errors, + setValues, + setErrors + ); + case "boolean": + return renderBoolean( + key, + config as ConfluencePageConfig.FrontmatterConfig< + boolean, + "boolean" + >, + values, + errors, + setValues, + setErrors + ); + case "options": + return renderOptions( + key, + config as ConfluencePageConfig.FrontmatterConfig< + ConfluencePageConfig.PageContentType, + "options" + >, + values, + errors, + setValues, + setErrors + ); + default: + return null; + } + })} + +
YAML KeyValueUpdate
+ +
+ ); +}; + +export class ConfluencePerPageForm extends Modal { + modalProps: ModalProps; + + constructor(app: App, modalProps: ModalProps) { + super(app); + this.modalProps = modalProps; + } + + onOpen() { + const { contentEl } = this; + const test: FormProps = { + ...this.modalProps, + onSubmit: (values) => { + const boundClose = this.close.bind(this); + this.modalProps.onSubmit(values, boundClose); + }, + }; + ReactDOM.render(React.createElement(ConfluenceForm, test), contentEl); + } + + onClose() { + const { contentEl } = this; + ReactDOM.unmountComponentAtNode(contentEl); + contentEl.empty(); + } +} diff --git a/packages/obsidian/src/adaptors/obsidian.ts b/packages/obsidian/src/adaptors/obsidian.ts index 430a43af..908655f3 100644 --- a/packages/obsidian/src/adaptors/obsidian.ts +++ b/packages/obsidian/src/adaptors/obsidian.ts @@ -5,6 +5,7 @@ import { FilesToUpload, LoaderAdaptor, MarkdownFile, + ConfluencePageConfig, } from "@markdown-confluence/lib"; import { lookup } from "mime-types"; @@ -114,15 +115,31 @@ export default class ObsidianAdaptor implements LoaderAdaptor { return false; } - - async updateMarkdownPageId( + async updateMarkdownValues( absoluteFilePath: string, - pageId: string + values: Partial ): Promise { + const config = ConfluencePageConfig.conniePerPageConfig; const file = this.app.vault.getAbstractFileByPath(absoluteFilePath); if (file instanceof TFile) { this.app.fileManager.processFrontMatter(file, (fm) => { - fm["connie-page-id"] = pageId; + for (const propertyKey in config) { + if (!config.hasOwnProperty(propertyKey)) { + continue; + } + + const { key } = + config[ + propertyKey as keyof ConfluencePageConfig.ConfluencePerPageConfig + ]; + const value = + values[ + propertyKey as keyof ConfluencePageConfig.ConfluencePerPageAllValues + ]; + if (propertyKey in values) { + fm[key] = value; + } + } }); } } diff --git a/packages/obsidian/src/main.ts b/packages/obsidian/src/main.ts index 52ec93d6..59ba9105 100644 --- a/packages/obsidian/src/main.ts +++ b/packages/obsidian/src/main.ts @@ -6,13 +6,22 @@ import { WorkspaceLeaf, Workspace, } from "obsidian"; -import { ConfluenceUploadSettings, Publisher } from "@markdown-confluence/lib"; +import { + ConfluenceUploadSettings, + Publisher, + ConfluencePageConfig, +} from "@markdown-confluence/lib"; import { ElectronMermaidRenderer } from "@markdown-confluence/mermaid-electron-renderer"; import { ConfluenceSettingTab } from "./ConfluenceSettingTab"; import ObsidianAdaptor from "./adaptors/obsidian"; import { CompletedModal } from "./CompletedModal"; import { CustomConfluenceClient } from "./MyBaseClient"; import AdfView, { ADF_VIEW_TYPE } from "./AdfView"; +import { + ConfluencePerPageForm, + ConfluencePerPageUIValues, + mapFrontmatterToConfluencePerPageUIValues, +} from "./ConfluencePerPageForm"; export default class ConfluencePlugin extends Plugin { settings: ConfluenceUploadSettings.ConfluenceSettings; @@ -308,6 +317,51 @@ export default class ConfluencePlugin extends Plugin { }, }); + this.addCommand({ + id: "page-settings", + name: "Update Confluence Page Settings", + editorCallback: (editor: Editor, view: MarkdownView) => { + const frontMatter = this.app.metadataCache.getCache( + view.file.path + )?.frontmatter; + + const file = view.file; + + new ConfluencePerPageForm(this.app, { + config: ConfluencePageConfig.conniePerPageConfig, + initialValues: + mapFrontmatterToConfluencePerPageUIValues(frontMatter), + onSubmit: (values, close) => { + const valuesToSet: Partial = + {}; + for (const propertyKey in values) { + if ( + Object.prototype.hasOwnProperty.call( + values, + propertyKey + ) + ) { + const element = + values[ + propertyKey as keyof ConfluencePerPageUIValues + ]; + if (element.isSet) { + valuesToSet[ + propertyKey as keyof ConfluencePerPageUIValues + ] = element.value as never; + } + } + } + this.adaptor.updateMarkdownValues( + file.path, + valuesToSet + ); + close(); + }, + }).open(); + }, + }); + this.addSettingTab(new ConfluenceSettingTab(this.app, this)); }