Skip to content

Commit 0c2ec5a

Browse files
authored
[ENG-926] Add tag as a field in setting (Obsidian) (#475)
* add tag as a field in setting * address PR comment * make the placeholder dynamic * address PR comments * Revert pnpm-lock.yaml to main * change observer and validating rule * change the setting
1 parent 510ac31 commit 0c2ec5a

File tree

4 files changed

+81
-17
lines changed

4 files changed

+81
-17
lines changed

apps/obsidian/src/components/NodeTypeSettings.tsx

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ import { DiscourseNode } from "~/types";
77
import { ConfirmationModal } from "./ConfirmationModal";
88
import { getTemplateFiles, getTemplatePluginInfo } from "~/utils/templates";
99

10+
const generateTagPlaceholder = (format: string, nodeName?: string): string => {
11+
if (!format) return "Enter tag (e.g., clm-candidate or #clm-candidate)";
12+
13+
// Extract the prefix before " - {content}" or " -{content}" or " -{content}" etc.
14+
const match = format.match(/^([A-Z]+)\s*-\s*\{content\}/i);
15+
if (match && match[1]) {
16+
const prefix = match[1].toLowerCase();
17+
return `Enter tag (e.g., ${prefix}-candidate)`;
18+
}
19+
20+
if (nodeName && nodeName.length >= 3) {
21+
const prefix = nodeName.substring(0, 3).toLowerCase();
22+
return `Enter tag (e.g., ${prefix}-candidate)`;
23+
}
24+
25+
return "Enter tag (e.g., clm-candidate)";
26+
};
27+
1028
type EditableFieldKey = keyof Omit<DiscourseNode, "id" | "shortcut">;
1129

1230
type BaseFieldConfig = {
@@ -75,6 +93,29 @@ const FIELD_CONFIGS: Record<EditableFieldKey, BaseFieldConfig> = {
7593
type: "color",
7694
required: false,
7795
},
96+
tag: {
97+
key: "tag",
98+
label: "Node tag",
99+
description: "Tags that signal a line is a node candidate",
100+
type: "text",
101+
required: false,
102+
validate: (value: string) => {
103+
if (!value.trim()) return { isValid: true };
104+
if (/\s/.test(value)) {
105+
return { isValid: false, error: "Tag cannot contain spaces" };
106+
}
107+
const invalidTagChars = /[^a-zA-Z0-9-]/;
108+
const invalidCharMatch = value.match(invalidTagChars);
109+
if (invalidCharMatch) {
110+
return {
111+
isValid: false,
112+
error: `Tag contains invalid character: ${invalidCharMatch[0]}. Tags can only contain letters, numbers, and dashes.`,
113+
};
114+
}
115+
116+
return { isValid: true };
117+
},
118+
},
78119
};
79120

80121
const FIELD_CONFIG_ARRAY = Object.values(FIELD_CONFIGS);
@@ -84,20 +125,32 @@ const TextField = ({
84125
value,
85126
error,
86127
onChange,
128+
nodeType,
87129
}: {
88130
fieldConfig: BaseFieldConfig;
89131
value: string;
90132
error?: string;
91133
onChange: (value: string) => void;
92-
}) => (
93-
<input
94-
type="text"
95-
value={value || ""}
96-
onChange={(e) => onChange(e.target.value)}
97-
placeholder={fieldConfig.placeholder}
98-
className={`w-full ${error ? "input-error" : ""}`}
99-
/>
100-
);
134+
nodeType?: DiscourseNode;
135+
}) => {
136+
// Generate dynamic placeholder for tag field based on node format and name
137+
const getPlaceholder = (): string => {
138+
if (fieldConfig.key === "tag" && nodeType?.format) {
139+
return generateTagPlaceholder(nodeType.format, nodeType.name);
140+
}
141+
return fieldConfig.placeholder || "";
142+
};
143+
144+
return (
145+
<input
146+
type="text"
147+
value={value || ""}
148+
onChange={(e) => onChange(e.target.value)}
149+
placeholder={getPlaceholder()}
150+
className={`w-full ${error ? "input-error" : ""}`}
151+
/>
152+
);
153+
};
101154

102155
const ColorField = ({
103156
value,
@@ -163,8 +216,12 @@ const FieldWrapper = ({
163216
<div className="setting-item-description">{fieldConfig.description}</div>
164217
</div>
165218
<div className="setting-item-control">
166-
{children}
167-
{error && <div className="text-error mt-1 text-xs">{error}</div>}
219+
<div className="flex flex-col">
220+
{children}
221+
<div className="mt-1 min-h-[1rem] text-xs">
222+
{error && <div className="text-error">{error}</div>}
223+
</div>
224+
</div>
168225
</div>
169226
</div>
170227
);
@@ -256,6 +313,7 @@ const NodeTypeSettings = () => {
256313
name: "",
257314
format: "",
258315
template: "",
316+
tag: "",
259317
};
260318
setEditingNodeType(newNodeType);
261319
setSelectedNodeIndex(nodeTypes.length);
@@ -403,6 +461,7 @@ const NodeTypeSettings = () => {
403461
value={value}
404462
error={error}
405463
onChange={handleChange}
464+
nodeType={editingNodeType}
406465
/>
407466
)}
408467
</FieldWrapper>

apps/obsidian/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ export const DEFAULT_NODE_TYPES: Record<string, DiscourseNode> = {
1313
name: "Claim",
1414
format: "CLM - {content}",
1515
color: "#7DA13E",
16+
tag: "#clm-candidate",
1617
},
1718
Evidence: {
1819
id: generateUid("node"),
1920
name: "Evidence",
2021
format: "EVD - {content}",
2122
color: "#DB134A",
23+
tag: "#evd-candidate",
2224
},
2325
};
2426
export const DEFAULT_RELATION_TYPES: Record<string, DiscourseRelationType> = {

apps/obsidian/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export type DiscourseNode = {
88
description?: string;
99
shortcut?: string;
1010
color?: string;
11+
tag?: string;
1112
};
1213

1314
export type DiscourseRelationType = {

apps/obsidian/src/utils/tagNodeHandler.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
12
/* eslint-disable @typescript-eslint/naming-convention */
23
import { App, Editor, Notice, MarkdownView } from "obsidian";
34
import { DiscourseNode } from "~/types";
@@ -152,15 +153,17 @@ export class TagNodeHandler {
152153
}
153154

154155
this.plugin.settings.nodeTypes.forEach((nodeType) => {
155-
const nodeTypeName = nodeType.name.toLowerCase();
156-
const tagSelector = `.cm-tag-${nodeTypeName}`;
156+
if (!nodeType.tag) {
157+
return;
158+
}
159+
160+
const tag = nodeType.tag as string;
161+
const tagSelector = `.cm-tag-${tag}`;
157162

158-
// Check if the element itself matches
159163
if (element.matches(tagSelector)) {
160164
this.applyDiscourseTagStyling(element, nodeType);
161165
}
162166

163-
// Check all children
164167
const childTags = element.querySelectorAll(tagSelector);
165168
childTags.forEach((tagEl) => {
166169
if (tagEl instanceof HTMLElement) {
@@ -257,7 +260,7 @@ export class TagNodeHandler {
257260
}
258261

259262
const cleanText = sanitizeTitle(
260-
extractedData.fullLineContent.replace(/#\w+/g, ""),
263+
extractedData.fullLineContent.replace(/#[^\s]+/g, ""),
261264
);
262265

263266
new CreateNodeModal(this.app, {
@@ -428,7 +431,6 @@ export class TagNodeHandler {
428431

429432
const hideTooltip = () => {
430433
if (this.currentTooltip) {
431-
console.log("Removing tooltip");
432434
this.currentTooltip.remove();
433435
this.currentTooltip = null;
434436
}

0 commit comments

Comments
 (0)