feat(registratura): auto-detect {proiect} placeholder in subject and switch to template mode
Typing {proiect}, {nr}, {an}, {detalii} or {text} in the subject field
now auto-transforms to template mode with the appropriate input widgets
(project dropdown, number fields, etc.).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
|||||||
filterSubjects,
|
filterSubjects,
|
||||||
getRecommendedTemplates,
|
getRecommendedTemplates,
|
||||||
getSubjectPlaceholder,
|
getSubjectPlaceholder,
|
||||||
|
createDynamicTemplate,
|
||||||
} from "../services/subject-template-service";
|
} from "../services/subject-template-service";
|
||||||
import type { SubjectTemplate } from "../services/subject-template-service";
|
import type { SubjectTemplate } from "../services/subject-template-service";
|
||||||
import { SubjectTemplateInput } from "./subject-template-input";
|
import { SubjectTemplateInput } from "./subject-template-input";
|
||||||
@@ -912,6 +913,7 @@ export function RegistryEntryForm({
|
|||||||
<li>Contract nr. 15/2026 — Proiect X</li>
|
<li>Contract nr. 15/2026 — Proiect X</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-muted-foreground">Selecteaza un sablon sau tasteaza liber.</p>
|
<p className="text-muted-foreground">Selecteaza un sablon sau tasteaza liber.</p>
|
||||||
|
<p className="text-muted-foreground mt-1">Tip: scrie <span className="font-mono text-foreground">{"{proiect}"}</span> pentru dropdown de proiecte.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-xs space-y-1">
|
<div className="text-xs space-y-1">
|
||||||
@@ -923,6 +925,7 @@ export function RegistryEntryForm({
|
|||||||
<li>Notificare incepere lucrari — Proiect X</li>
|
<li>Notificare incepere lucrari — Proiect X</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p className="text-muted-foreground">Selecteaza un sablon sau tasteaza liber.</p>
|
<p className="text-muted-foreground">Selecteaza un sablon sau tasteaza liber.</p>
|
||||||
|
<p className="text-muted-foreground mt-1">Tip: scrie <span className="font-mono text-foreground">{"{proiect}"}</span> pentru dropdown de proiecte.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
@@ -949,8 +952,29 @@ export function RegistryEntryForm({
|
|||||||
<Input
|
<Input
|
||||||
value={subjectQuery}
|
value={subjectQuery}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSubjectQuery(e.target.value);
|
const val = e.target.value;
|
||||||
setSubject(e.target.value);
|
setSubjectQuery(val);
|
||||||
|
setSubject(val);
|
||||||
|
|
||||||
|
// Auto-detect {proiect}, {nr}, {an}, {detalii}, {text} placeholders
|
||||||
|
// and switch to template mode when a closing } is typed
|
||||||
|
if (val.includes("{") && val.includes("}")) {
|
||||||
|
const dynTemplate = createDynamicTemplate(val);
|
||||||
|
if (dynTemplate) {
|
||||||
|
// Pre-fill default values (e.g. current year for {an})
|
||||||
|
const prefill: Record<string, string> = {};
|
||||||
|
for (const field of dynTemplate.fields) {
|
||||||
|
if (field.defaultValue) {
|
||||||
|
prefill[field.id] = field.defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setActiveTemplate(dynTemplate);
|
||||||
|
setTemplateFieldValues(prefill);
|
||||||
|
setSubject(assembleSubject(dynTemplate, prefill));
|
||||||
|
setSubjectFocused(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={() => setSubjectFocused(true)}
|
onFocus={() => setSubjectFocused(true)}
|
||||||
onBlur={() => setTimeout(() => setSubjectFocused(false), 200)}
|
onBlur={() => setTimeout(() => setSubjectFocused(false), 200)}
|
||||||
|
|||||||
@@ -589,6 +589,73 @@ export function filterTemplates(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Recognized field placeholder names that can be used in dynamic templates */
|
||||||
|
const DYNAMIC_FIELD_NAMES = new Set(["nr", "an", "detalii", "proiect", "text"]);
|
||||||
|
|
||||||
|
/** Regex to detect at least one valid {fieldName} placeholder */
|
||||||
|
const HAS_PLACEHOLDER_RE = /\{(nr|an|detalii|proiect|text)\}/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a dynamic SubjectTemplate from free text containing `{fieldName}`
|
||||||
|
* placeholders. Recognised names: nr, an, detalii, proiect, text.
|
||||||
|
*
|
||||||
|
* Returns `null` when the text contains no valid placeholder.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* createDynamicTemplate("Cerere CU — {proiect}")
|
||||||
|
* → SubjectTemplate with static "Cerere CU — " + field(proiect)
|
||||||
|
*/
|
||||||
|
export function createDynamicTemplate(
|
||||||
|
pattern: string,
|
||||||
|
): SubjectTemplate | null {
|
||||||
|
if (!HAS_PLACEHOLDER_RE.test(pattern)) return null;
|
||||||
|
|
||||||
|
const tokens: TemplateToken[] = [];
|
||||||
|
const fields: TemplateField[] = [];
|
||||||
|
let fIdx = 0;
|
||||||
|
|
||||||
|
// Split by {fieldName} placeholders (keeping the delimiters)
|
||||||
|
const parts = pattern.split(/(\{[^}]+\})/);
|
||||||
|
for (const part of parts) {
|
||||||
|
if (!part) continue;
|
||||||
|
const fieldMatch = part.match(/^\{(\w+)\}$/);
|
||||||
|
if (fieldMatch) {
|
||||||
|
const name = fieldMatch[1]!;
|
||||||
|
if (!DYNAMIC_FIELD_NAMES.has(name)) {
|
||||||
|
// Unknown placeholder — treat as static text
|
||||||
|
tokens.push({ type: "static", value: part });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ft = resolveFieldType(name);
|
||||||
|
const field: TemplateField = {
|
||||||
|
id: `f${fIdx}`,
|
||||||
|
name,
|
||||||
|
placeholder: fieldPlaceholder(ft),
|
||||||
|
width: fieldWidth(ft),
|
||||||
|
fieldType: ft,
|
||||||
|
defaultValue: ft === "an" ? CURRENT_YEAR : undefined,
|
||||||
|
};
|
||||||
|
fields.push(field);
|
||||||
|
tokens.push({ type: "field", value: "", field });
|
||||||
|
fIdx++;
|
||||||
|
} else {
|
||||||
|
tokens.push({ type: "static", value: part });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.length === 0) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `dyn_${hashPattern(pattern)}`,
|
||||||
|
pattern,
|
||||||
|
tokens,
|
||||||
|
fields,
|
||||||
|
frequency: 0,
|
||||||
|
exampleSubject: pattern,
|
||||||
|
isSeed: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter existing subjects by query (plain text substring match).
|
* Filter existing subjects by query (plain text substring match).
|
||||||
* Returns unique matches, limited to `limit`.
|
* Returns unique matches, limited to `limit`.
|
||||||
|
|||||||
Reference in New Issue
Block a user