From 327fc1d0f2745e989c9c9d7738d07bf84d453f98 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Wed, 1 Oct 2025 16:40:03 -0400 Subject: [PATCH 1/7] add tag as a field in setting --- apps/obsidian/src/components/NodeTypeSettings.tsx | 10 ++++++++++ apps/obsidian/src/constants.ts | 2 ++ apps/obsidian/src/types.ts | 1 + pnpm-lock.yaml | 2 +- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/obsidian/src/components/NodeTypeSettings.tsx b/apps/obsidian/src/components/NodeTypeSettings.tsx index 7c6ac7c3a..6472a0b2b 100644 --- a/apps/obsidian/src/components/NodeTypeSettings.tsx +++ b/apps/obsidian/src/components/NodeTypeSettings.tsx @@ -75,6 +75,15 @@ const FIELD_CONFIGS: Record = { type: "color", required: false, }, + tag: { + key: "tag", + label: "Node tag", + description: + "Tags that signal a line is a node candidate", + type: "text", + required: false, + placeholder: "Enter tag (e.g., #clm-candidate)", + }, }; const FIELD_CONFIG_ARRAY = Object.values(FIELD_CONFIGS); @@ -256,6 +265,7 @@ const NodeTypeSettings = () => { name: "", format: "", template: "", + tag: "", }; setEditingNodeType(newNodeType); setSelectedNodeIndex(nodeTypes.length); diff --git a/apps/obsidian/src/constants.ts b/apps/obsidian/src/constants.ts index 73258322e..a68a488f4 100644 --- a/apps/obsidian/src/constants.ts +++ b/apps/obsidian/src/constants.ts @@ -13,12 +13,14 @@ export const DEFAULT_NODE_TYPES: Record = { name: "Claim", format: "CLM - {content}", color: "#7DA13E", + tag: "#clm-candidate", }, Evidence: { id: generateUid("node"), name: "Evidence", format: "EVD - {content}", color: "#DB134A", + tag: "#evd-candidate", }, }; export const DEFAULT_RELATION_TYPES: Record = { diff --git a/apps/obsidian/src/types.ts b/apps/obsidian/src/types.ts index 981d5dc2d..9c0041d2e 100644 --- a/apps/obsidian/src/types.ts +++ b/apps/obsidian/src/types.ts @@ -8,6 +8,7 @@ export type DiscourseNode = { description?: string; shortcut?: string; color?: string; + tag?: string; }; export type DiscourseRelationType = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa3e0ec29..9c17f7640 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17883,4 +17883,4 @@ snapshots: zod@3.25.76: {} - zwitch@2.0.4: {} + zwitch@2.0.4: {} \ No newline at end of file From 0aeef4b1a8572f20e8841a91d5b6d9401a8692fa Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Wed, 1 Oct 2025 17:01:28 -0400 Subject: [PATCH 2/7] address PR comment --- apps/obsidian/src/components/NodeTypeSettings.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/obsidian/src/components/NodeTypeSettings.tsx b/apps/obsidian/src/components/NodeTypeSettings.tsx index 6472a0b2b..cd7fd64f4 100644 --- a/apps/obsidian/src/components/NodeTypeSettings.tsx +++ b/apps/obsidian/src/components/NodeTypeSettings.tsx @@ -78,11 +78,20 @@ const FIELD_CONFIGS: Record = { tag: { key: "tag", label: "Node tag", - description: - "Tags that signal a line is a node candidate", + description: "Tags that signal a line is a node candidate", type: "text", required: false, placeholder: "Enter tag (e.g., #clm-candidate)", + validate: (value) => { + if (!value.trim()) return { isValid: true }; + if (!value.startsWith("#")) { + return { isValid: false, error: "Tag must start with #" }; + } + if (/\s/.test(value)) { + return { isValid: false, error: "Tag cannot contain spaces" }; + } + return { isValid: true }; + }, }, }; From 0048ea2c24e222eb403d374cd3d530565ea9aa6d Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Fri, 3 Oct 2025 12:35:50 -0400 Subject: [PATCH 3/7] make the placeholder dynamic --- .../src/components/NodeTypeSettings.tsx | 52 +++++++++++++++---- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/apps/obsidian/src/components/NodeTypeSettings.tsx b/apps/obsidian/src/components/NodeTypeSettings.tsx index cd7fd64f4..41a9f52ca 100644 --- a/apps/obsidian/src/components/NodeTypeSettings.tsx +++ b/apps/obsidian/src/components/NodeTypeSettings.tsx @@ -7,6 +7,24 @@ import { DiscourseNode } from "~/types"; import { ConfirmationModal } from "./ConfirmationModal"; import { getTemplateFiles, getTemplatePluginInfo } from "~/utils/templates"; +const generateTagPlaceholder = (format: string, nodeName?: string): string => { + if (!format) return "Enter tag (e.g., #clm-candidate)"; + + // Extract the prefix before " - {content}" or " -{content}" or " -{content}" etc. + const match = format.match(/^([A-Z]+)\s*-\s*\{content\}/i); + if (match && match[1]) { + const prefix = match[1].toLowerCase(); + return `Enter tag (e.g., #${prefix}-candidate)`; + } + + if (nodeName && nodeName.length >= 3) { + const prefix = nodeName.substring(0, 3).toLowerCase(); + return `Enter tag (e.g., #${prefix}-candidate)`; + } + + return "Enter tag (e.g., #clm-candidate)"; +}; + type EditableFieldKey = keyof Omit; type BaseFieldConfig = { @@ -81,8 +99,7 @@ const FIELD_CONFIGS: Record = { description: "Tags that signal a line is a node candidate", type: "text", required: false, - placeholder: "Enter tag (e.g., #clm-candidate)", - validate: (value) => { + validate: (value: string) => { if (!value.trim()) return { isValid: true }; if (!value.startsWith("#")) { return { isValid: false, error: "Tag must start with #" }; @@ -102,20 +119,32 @@ const TextField = ({ value, error, onChange, + nodeType, }: { fieldConfig: BaseFieldConfig; value: string; error?: string; onChange: (value: string) => void; -}) => ( - onChange(e.target.value)} - placeholder={fieldConfig.placeholder} - className={`w-full ${error ? "input-error" : ""}`} - /> -); + nodeType?: DiscourseNode; +}) => { + // Generate dynamic placeholder for tag field based on node format and name + const getPlaceholder = (): string => { + if (fieldConfig.key === "tag" && nodeType?.format) { + return generateTagPlaceholder(nodeType.format, nodeType.name); + } + return fieldConfig.placeholder || ""; + }; + + return ( + onChange(e.target.value)} + placeholder={getPlaceholder()} + className={`w-full ${error ? "input-error" : ""}`} + /> + ); +}; const ColorField = ({ value, @@ -422,6 +451,7 @@ const NodeTypeSettings = () => { value={value} error={error} onChange={handleChange} + nodeType={editingNodeType} /> )} From b6c65bc8409ee8ffecb95f4201798e676f13a746 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 13 Oct 2025 11:03:27 -0400 Subject: [PATCH 4/7] address PR comments --- apps/obsidian/src/components/NodeTypeSettings.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/obsidian/src/components/NodeTypeSettings.tsx b/apps/obsidian/src/components/NodeTypeSettings.tsx index 41a9f52ca..9a0ad9a87 100644 --- a/apps/obsidian/src/components/NodeTypeSettings.tsx +++ b/apps/obsidian/src/components/NodeTypeSettings.tsx @@ -210,8 +210,12 @@ const FieldWrapper = ({
{fieldConfig.description}
- {children} - {error &&
{error}
} +
+ {children} +
+ {error &&
{error}
} +
+
); From 3f2abb1435a18044489e5a926e7e64623772adb2 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 13 Oct 2025 11:05:57 -0400 Subject: [PATCH 5/7] Revert pnpm-lock.yaml to main --- pnpm-lock.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9c17f7640..fa3e0ec29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17883,4 +17883,4 @@ snapshots: zod@3.25.76: {} - zwitch@2.0.4: {} \ No newline at end of file + zwitch@2.0.4: {} From 097deb3117d7fe34f8eac57bb74c0aa3f6a7c839 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 13 Oct 2025 11:26:20 -0400 Subject: [PATCH 6/7] change observer and validating rule --- apps/obsidian/src/components/NodeTypeSettings.tsx | 11 ++++------- apps/obsidian/src/utils/tagNodeHandler.ts | 13 +++++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/obsidian/src/components/NodeTypeSettings.tsx b/apps/obsidian/src/components/NodeTypeSettings.tsx index 9a0ad9a87..10a160fcc 100644 --- a/apps/obsidian/src/components/NodeTypeSettings.tsx +++ b/apps/obsidian/src/components/NodeTypeSettings.tsx @@ -8,21 +8,21 @@ import { ConfirmationModal } from "./ConfirmationModal"; import { getTemplateFiles, getTemplatePluginInfo } from "~/utils/templates"; const generateTagPlaceholder = (format: string, nodeName?: string): string => { - if (!format) return "Enter tag (e.g., #clm-candidate)"; + if (!format) return "Enter tag (e.g., clm-candidate or #clm-candidate)"; // Extract the prefix before " - {content}" or " -{content}" or " -{content}" etc. const match = format.match(/^([A-Z]+)\s*-\s*\{content\}/i); if (match && match[1]) { const prefix = match[1].toLowerCase(); - return `Enter tag (e.g., #${prefix}-candidate)`; + return `Enter tag (e.g., ${prefix}-candidate)`; } if (nodeName && nodeName.length >= 3) { const prefix = nodeName.substring(0, 3).toLowerCase(); - return `Enter tag (e.g., #${prefix}-candidate)`; + return `Enter tag (e.g., ${prefix}-candidate)`; } - return "Enter tag (e.g., #clm-candidate)"; + return "Enter tag (e.g., clm-candidate)"; }; type EditableFieldKey = keyof Omit; @@ -101,9 +101,6 @@ const FIELD_CONFIGS: Record = { required: false, validate: (value: string) => { if (!value.trim()) return { isValid: true }; - if (!value.startsWith("#")) { - return { isValid: false, error: "Tag must start with #" }; - } if (/\s/.test(value)) { return { isValid: false, error: "Tag cannot contain spaces" }; } diff --git a/apps/obsidian/src/utils/tagNodeHandler.ts b/apps/obsidian/src/utils/tagNodeHandler.ts index 187d46814..b400a5c30 100644 --- a/apps/obsidian/src/utils/tagNodeHandler.ts +++ b/apps/obsidian/src/utils/tagNodeHandler.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/naming-convention */ import { App, Editor, Notice, MarkdownView } from "obsidian"; import { DiscourseNode } from "~/types"; @@ -152,8 +153,13 @@ export class TagNodeHandler { } this.plugin.settings.nodeTypes.forEach((nodeType) => { - const nodeTypeName = nodeType.name.toLowerCase(); - const tagSelector = `.cm-tag-${nodeTypeName}`; + if (!nodeType.tag) { + return; + } + + const tag = nodeType.tag as string; + const tagName = tag.startsWith("#") ? tag.slice(1) : tag; + const tagSelector = `.cm-tag-${tagName}`; // Check if the element itself matches if (element.matches(tagSelector)) { @@ -257,7 +263,7 @@ export class TagNodeHandler { } const cleanText = sanitizeTitle( - extractedData.fullLineContent.replace(/#\w+/g, ""), + extractedData.fullLineContent.replace(/#[^\s]+/g, ""), ); new CreateNodeModal(this.app, { @@ -428,7 +434,6 @@ export class TagNodeHandler { const hideTooltip = () => { if (this.currentTooltip) { - console.log("Removing tooltip"); this.currentTooltip.remove(); this.currentTooltip = null; } From adbac0f70a66c4eb1f581496c813060b72c2df90 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Tue, 14 Oct 2025 17:35:27 -0400 Subject: [PATCH 7/7] change the setting --- apps/obsidian/src/components/NodeTypeSettings.tsx | 9 +++++++++ apps/obsidian/src/utils/tagNodeHandler.ts | 5 +---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/obsidian/src/components/NodeTypeSettings.tsx b/apps/obsidian/src/components/NodeTypeSettings.tsx index 10a160fcc..6f37d1383 100644 --- a/apps/obsidian/src/components/NodeTypeSettings.tsx +++ b/apps/obsidian/src/components/NodeTypeSettings.tsx @@ -104,6 +104,15 @@ const FIELD_CONFIGS: Record = { if (/\s/.test(value)) { return { isValid: false, error: "Tag cannot contain spaces" }; } + const invalidTagChars = /[^a-zA-Z0-9-]/; + const invalidCharMatch = value.match(invalidTagChars); + if (invalidCharMatch) { + return { + isValid: false, + error: `Tag contains invalid character: ${invalidCharMatch[0]}. Tags can only contain letters, numbers, and dashes.`, + }; + } + return { isValid: true }; }, }, diff --git a/apps/obsidian/src/utils/tagNodeHandler.ts b/apps/obsidian/src/utils/tagNodeHandler.ts index b400a5c30..f93a5b3e7 100644 --- a/apps/obsidian/src/utils/tagNodeHandler.ts +++ b/apps/obsidian/src/utils/tagNodeHandler.ts @@ -158,15 +158,12 @@ export class TagNodeHandler { } const tag = nodeType.tag as string; - const tagName = tag.startsWith("#") ? tag.slice(1) : tag; - const tagSelector = `.cm-tag-${tagName}`; + const tagSelector = `.cm-tag-${tag}`; - // Check if the element itself matches if (element.matches(tagSelector)) { this.applyDiscourseTagStyling(element, nodeType); } - // Check all children const childTags = element.querySelectorAll(tagSelector); childTags.forEach((tagEl) => { if (tagEl instanceof HTMLElement) {