Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 53 additions & 26 deletions apps/obsidian/src/components/NodeTypeSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ const FIELD_CONFIGS: Record<EditableFieldKey, BaseFieldConfig> = {
required: true,
type: "text",
validate: (value, nodeType, existingNodes) =>
validateNodeName(value, [nodeType, ...existingNodes]),
validateNodeName({
name: value,
currentNode: nodeType,
allNodes: existingNodes,
}),
placeholder: "Name",
},
format: {
Expand All @@ -42,9 +46,21 @@ const FIELD_CONFIGS: Record<EditableFieldKey, BaseFieldConfig> = {
required: true,
type: "text",
validate: (value, nodeType, existingNodes) =>
validateNodeFormat(value, [nodeType, ...existingNodes]),
validateNodeFormat({
format: value,
currentNode: nodeType,
allNodes: existingNodes,
}),
placeholder: "Format (e.g., CLM - {content})",
},
description: {
key: "description",
label: "Description",
description: "A brief description of what this node type represents",
required: false,
type: "text",
placeholder: "Enter a description",
},
template: {
key: "template",
label: "Template",
Expand Down Expand Up @@ -342,7 +358,7 @@ const NodeTypeSettings = () => {
handleNodeTypeChange(fieldConfig.key, newValue);

return (
<FieldWrapper fieldConfig={fieldConfig} error={error}>
<FieldWrapper fieldConfig={fieldConfig} error={error} key={fieldConfig.key}>
{fieldConfig.key === "template" ? (
<TemplateField
value={value}
Expand Down Expand Up @@ -371,32 +387,43 @@ const NodeTypeSettings = () => {
{nodeTypes.map((nodeType, index) => (
<div
key={nodeType.id}
className="node-type-item hover:bg-secondary-lt flex cursor-pointer items-center justify-between p-2"
className="node-type-item hover:bg-secondary-lt flex cursor-pointer flex-col gap-1 p-2"
onClick={() => startEditing(index)}
>
<span>{nodeType.name}</span>
<div className="flex gap-2">
<button
className="icon-button"
onClick={(e) => {
e.stopPropagation();
startEditing(index);
}}
aria-label="Edit node type"
>
<div className="icon" ref={(el) => el && setIcon(el, "pencil")} />
</button>
<button
className="icon-button mod-warning"
onClick={(e) => {
e.stopPropagation();
confirmDeleteNodeType(index);
}}
aria-label="Delete node type"
>
<div className="icon" ref={(el) => el && setIcon(el, "trash")} />
</button>
<div className="flex items-center justify-between">
<span className="font-medium">{nodeType.name}</span>
<div className="flex gap-2">
<button
className="icon-button"
onClick={(e) => {
e.stopPropagation();
startEditing(index);
}}
aria-label="Edit node type"
>
<div
className="icon"
ref={(el) => el && setIcon(el, "pencil")}
/>
</button>
<button
className="icon-button mod-warning"
onClick={(e) => {
e.stopPropagation();
confirmDeleteNodeType(index);
}}
aria-label="Delete node type"
>
<div
className="icon"
ref={(el) => el && setIcon(el, "trash")}
/>
</button>
</div>
</div>
{nodeType.description && (
<span className="text-muted text-sm">{nodeType.description}</span>
)}
</div>
))}
</div>
Expand Down
1 change: 1 addition & 0 deletions apps/obsidian/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type DiscourseNode = {
name: string;
format: string;
template?: string;
description?: string;
shortcut?: string;
color?: string;
};
Expand Down
69 changes: 38 additions & 31 deletions apps/obsidian/src/utils/validateNodeType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ type ValidationResult = {
error?: string;
};

export function validateNodeFormat(
format: string,
nodeTypes: DiscourseNode[],
): ValidationResult {
export const validateNodeFormat = ({
format,
currentNode,
allNodes,
}: {
format: string;
currentNode: DiscourseNode;
allNodes: DiscourseNode[];
}): ValidationResult => {
if (!format) {
return {
isValid: false,
Expand All @@ -35,13 +40,17 @@ export function validateNodeFormat(
return invalidCharsResult;
}

const uniquenessResult = validateFormatUniqueness(nodeTypes);
if (!uniquenessResult.isValid) {
return uniquenessResult;
const otherNodes = allNodes.filter((node) => node.id !== currentNode.id);
const isDuplicate = otherNodes.some((node) => node.format === format);
if (isDuplicate) {
return {
isValid: false,
error: "Format must be unique across all node types",
};
}

return { isValid: true };
}
};

export const checkInvalidChars = (format: string): ValidationResult => {
const INVALID_FILENAME_CHARS_REGEX = /[#^\[\]|]/;
Expand All @@ -56,31 +65,21 @@ export const checkInvalidChars = (format: string): ValidationResult => {
return { isValid: true };
};

const validateFormatUniqueness = (
nodeTypes: DiscourseNode[],
): ValidationResult => {
const isDuplicate =
new Set(nodeTypes.map((nodeType) => nodeType.format)).size !==
nodeTypes.length;

if (isDuplicate) {
return { isValid: false, error: "Format must be unique" };
}

return { isValid: true };
};

export const validateNodeName = (
name: string,
nodeTypes: DiscourseNode[],
): ValidationResult => {
export const validateNodeName = ({
name,
currentNode,
allNodes,
}: {
name: string;
currentNode: DiscourseNode;
allNodes: DiscourseNode[];
}): ValidationResult => {
if (!name || name.trim() === "") {
return { isValid: false, error: "Name is required" };
}

const isDuplicate =
new Set(nodeTypes.map((nodeType) => nodeType.name)).size !==
nodeTypes.length;
const otherNodes = allNodes.filter((node) => node.id !== currentNode.id);
const isDuplicate = otherNodes.some((node) => node.name === name);

if (isDuplicate) {
return { isValid: false, error: "Name must be unique" };
Expand All @@ -101,14 +100,22 @@ export const validateAllNodes = (
return;
}

const formatValidation = validateNodeFormat(nodeType.format, nodeTypes);
const formatValidation = validateNodeFormat({
format: nodeType.format,
currentNode: nodeType,
allNodes: nodeTypes,
});
if (!formatValidation.isValid) {
errorMap[index] = formatValidation.error || "Invalid format";
hasErrors = true;
return;
}

const nameValidation = validateNodeName(nodeType.name, nodeTypes);
const nameValidation = validateNodeName({
name: nodeType.name,
currentNode: nodeType,
allNodes: nodeTypes,
});
if (!nameValidation.isValid) {
errorMap[index] = nameValidation.error || "Invalid name";
hasErrors = true;
Expand Down