diff --git a/apps/obsidian/src/utils/__tests__/validateNodeType.test.ts b/apps/obsidian/src/utils/__tests__/validateNodeType.test.ts index e85e90334..5c1e9f714 100644 --- a/apps/obsidian/src/utils/__tests__/validateNodeType.test.ts +++ b/apps/obsidian/src/utils/__tests__/validateNodeType.test.ts @@ -1,8 +1,10 @@ import { describe, expect, it } from "vitest"; import { decorateTitle } from "@repo/database/lib/decorateTitle"; +import type { DiscourseNode } from "~/types"; import { checkInvalidChars, normalizeImportedNodeFormat, + validateNodeFormat, } from "~/utils/validateNodeType"; describe("normalizeImportedNodeFormat", () => { @@ -55,3 +57,49 @@ describe("normalizeImportedNodeFormat", () => { expect(normalizeImportedNodeFormat(format)).toBe(format); }); }); + +describe("validateNodeFormat", () => { + const currentNode: DiscourseNode = { + id: "node-1", + name: "Evidence", + format: "", + created: 0, + modified: 0, + }; + const validate = (format: string): ReturnType => + validateNodeFormat({ format, currentNode, allNodes: [currentNode] }); + + it.each([ + [ + "EVD - {content} - {Source}", + "Format contains unsupported placeholder: {Source}. Only {content} is supported.", + ], + [ + "{Author}: {content}", + "Format contains unsupported placeholder: {Author}. Only {content} is supported.", + ], + [ + "{Source} - {content} - {Author} - {Source}", + "Format contains unsupported placeholders: {Source}, {Author}. Only {content} is supported.", + ], + ])("rejects the unsupported placeholders in %s", (format, error) => { + expect(validate(format)).toEqual({ isValid: false, error }); + }); + + it.each(["CLM - {content}", "{content}", "@{content}"])( + "accepts the content-only format %s", + (format) => { + expect(validate(format)).toEqual({ isValid: true }); + }, + ); + + it.each(["CLM - title", "EVD - {Source}"])( + "reports the missing {content} for %s", + (format) => { + expect(validate(format)).toEqual({ + isValid: false, + error: 'Format must include the placeholder "{content}"', + }); + }, + ); +}); diff --git a/apps/obsidian/src/utils/validateNodeFormat.ts b/apps/obsidian/src/utils/validateNodeFormat.ts deleted file mode 100644 index 5e03281b8..000000000 --- a/apps/obsidian/src/utils/validateNodeFormat.ts +++ /dev/null @@ -1,30 +0,0 @@ -export const validateNodeFormat = ( - format: string, -): { - isValid: boolean; - error?: string; -} => { - if (!format) { - return { - isValid: false, - error: "Format cannot be empty", - }; - } - - if (format.includes("[[") || format.includes("]]")) { - return { - isValid: false, - error: "Format should not contain double brackets [[ or ]]", - }; - } - - const hasVariable = /{[a-zA-Z]+}/.test(format); - if (!hasVariable) { - return { - isValid: false, - error: "Format must contain at least one variable in {varName} format", - }; - } - - return { isValid: true }; -}; diff --git a/apps/obsidian/src/utils/validateNodeType.ts b/apps/obsidian/src/utils/validateNodeType.ts index f577e7125..da98c8a32 100644 --- a/apps/obsidian/src/utils/validateNodeType.ts +++ b/apps/obsidian/src/utils/validateNodeType.ts @@ -1,3 +1,7 @@ +import { + CONTENT_PLACEHOLDER, + FORMAT_PLACEHOLDER, +} from "@repo/database/lib/decorateTitle"; import { DiscourseNode } from "~/types"; type ValidationResult = { @@ -35,6 +39,21 @@ export const validateNodeFormat = ({ }; } + // Same placeholder rule as decorateTitle, which can't build titles from other placeholders. + const unsupportedPlaceholders = [ + ...new Set( + (format.match(FORMAT_PLACEHOLDER) ?? []).filter( + (placeholder) => placeholder.toLowerCase() !== CONTENT_PLACEHOLDER, + ), + ), + ]; + if (unsupportedPlaceholders.length > 0) { + return { + isValid: false, + error: `Format contains unsupported placeholder${unsupportedPlaceholders.length > 1 ? "s" : ""}: ${unsupportedPlaceholders.join(", ")}. Only ${CONTENT_PLACEHOLDER} is supported.`, + }; + } + const invalidCharsResult = checkInvalidChars(format); if (!invalidCharsResult.isValid) { return invalidCharsResult;