feat: expose form semantic documentation subjects
Module Package Release / publish-packages (push) Successful in 12s

This commit is contained in:
2026-08-21 15:44:42 +02:00
parent 5bc651a8e7
commit b9132d076d
9 changed files with 959 additions and 13 deletions
@@ -1,4 +1,4 @@
import { ArrowDown, ArrowUp, Eye, Languages, Plus, Trash2 } from "lucide-react";
import { ArrowDown, ArrowUp, BookOpen, Eye, Languages, Plus, Trash2 } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { FormGrid,
ActionToolbar,
@@ -14,7 +14,8 @@ import { FormGrid,
usePlatformLanguage,
useUnsavedChanges,
useUnsavedDraftGuard,
type ApiSettings
type ApiSettings,
type DocumentationHelpReference
} from "@govoplan/core-webui";
import {
saveFormDefinition,
@@ -80,6 +81,14 @@ export default function FormDefinitionDialog({
setConfirmLifecycle(false);
}, [definition, open, tenantId]);
useEffect(() => {
if (!open || !definition || !window.location.hash) return;
const targetId = decodeURIComponent(window.location.hash.slice(1));
window.requestAnimationFrame(() => {
document.getElementById(targetId)?.scrollIntoView({ block: "center" });
});
}, [definition, open]);
const valid = useMemo(() => Boolean(
draft.title.trim()
&& draft.key.trim()
@@ -197,7 +206,15 @@ export default function FormDefinitionDialog({
</>
}>
<div className="form-definition-editor">
<div className="form-definition-help"><DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} /></div>
<div className="form-definition-help">
<DocumentationHelpLink reference={FORMS_FIELD_DOCUMENTATION} />
{definition && <>
<DocumentationHelpLink reference={semanticFormDocumentation(definition)} />
<a className="btn btn-secondary" href={semanticAuthoringHref(definition)} target="_blank" rel="noreferrer">
<BookOpen size={16} aria-hidden="true" /> Document form meaning
</a>
</>}
</div>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
<FormGrid columns={2} gap="small" collapseAt="narrow">
<Field label="Title">
@@ -271,13 +288,18 @@ export default function FormDefinitionDialog({
</ActionToolbar>
<div className="form-field-editor-list">
{draft.fields.map((field, index) =>
<div className="form-field-editor-row" key={`${index}:${field.key}`}>
<div className="form-field-editor-row" id={`field-${field.key}`} key={`${index}:${field.key}`}>
<div className="form-field-order">
<IconButton label={`Move ${field.label || "field"} up`} icon={<ArrowUp size={15} />} disabled={busy || index === 0} onClick={() => moveField(index, -1)} />
<IconButton label={`Move ${field.label || "field"} down`} icon={<ArrowDown size={15} />} disabled={busy || index === draft.fields.length - 1} onClick={() => moveField(index, 1)} />
</div>
<Field label="Key"><input value={field.key} disabled={busy} onChange={(event) => patchField(index, { key: event.target.value })} /></Field>
<Field label="Label"><input value={field.label} disabled={busy} onChange={(event) => patchField(index, { label: event.target.value })} /></Field>
<Field
label="Label"
documentation={definition && definition.fields.some((item) => item.key === field.key) ? semanticFieldDocumentation(definition, field.key) : FORMS_FIELD_DOCUMENTATION}
>
<input value={field.label} disabled={busy} onChange={(event) => patchField(index, { label: event.target.value })} />
</Field>
<Field label="Type">
<select value={field.value_type} disabled={busy} onChange={(event) => patchField(index, { value_type: event.target.value as FormValueType, options: isChoice(event.target.value) ? field.options : [] })}>
{VALUE_TYPES.map((type) => <option key={type.value} value={type.value}>{type.label}</option>)}
@@ -304,6 +326,16 @@ export default function FormDefinitionDialog({
disabledReason={busy ? FORMS_I18N.busy : draft.fields.length === 1 ? FORMS_I18N.oneField : undefined}
onClick={() => setDraft(removeField(draft, index))}
/>
{definition && definition.fields.some((item) => item.key === field.key) && (
<a
className="btn btn-secondary"
href={semanticAuthoringHref(definition, `field-${field.key}`)}
target="_blank"
rel="noreferrer"
>
<BookOpen size={15} aria-hidden="true" /> Document meaning
</a>
)}
</div>
)}
</div>
@@ -701,3 +733,43 @@ function constraintValue(value: unknown): number | "" {
function humanize(value: string): string {
return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
}
function semanticHelpContext(definition: FormDefinition, routeAnchor?: string): string {
return [
"semantic",
"forms",
"form_definition",
definition.reference.object_id,
routeAnchor
].filter(Boolean).join(".");
}
function semanticFormDocumentation(definition: FormDefinition): DocumentationHelpReference {
return {
contextId: semanticHelpContext(definition),
documentationType: "user"
};
}
function semanticFieldDocumentation(
definition: FormDefinition,
fieldKey: string
): DocumentationHelpReference {
return {
contextId: semanticHelpContext(definition, `field-${fieldKey}`),
documentationType: "user"
};
}
function semanticAuthoringHref(
definition: FormDefinition,
routeAnchor?: string
): string {
const params = new URLSearchParams({
module: "forms",
subjectKind: "form_definition",
subjectId: definition.reference.object_id
});
if (routeAnchor) params.set("routeAnchor", routeAnchor);
return `/docs/semantic?${params}`;
}
+13
View File
@@ -1,5 +1,6 @@
import { Download, Pencil, Plus, Search, Upload } from "lucide-react";
import { useCallback, useEffect, useRef, useState, type FormEvent } from "react";
import { useSearchParams } from "react-router";
import { ActionBlockerHint,
Button,
Dialog,
@@ -32,6 +33,7 @@ import { FORMS_DOCUMENTATION, FORMS_FIELD_DOCUMENTATION, FORMS_I18N } from "./in
export default function FormsPage({ settings, auth }: PlatformRouteContext) {
const [searchParams, setSearchParams] = useSearchParams();
const [query, setQuery] = useState("");
const [submittedQuery, setSubmittedQuery] = useState("");
const [state, setState] = useState("");
@@ -75,6 +77,17 @@ export default function FormsPage({ settings, auth }: PlatformRouteContext) {
return () => controller.abort();
}, [load]);
useEffect(() => {
const formId = searchParams.get("formId");
if (!formId || editing || loading) return;
const requested = items.find((item) => item.reference.object_id === formId);
if (!requested) return;
setEditing(requested);
const next = new URLSearchParams(searchParams);
next.delete("formId");
setSearchParams(next, { replace: true });
}, [editing, items, loading, searchParams, setSearchParams]);
function search(event: FormEvent) {
event.preventDefault();
setSubmittedQuery(query.trim());
+2 -2
View File
@@ -9,9 +9,9 @@ const FormsPage = lazy(() => import("./features/forms/FormsPage"));
export const formsModule: PlatformWebModule = {
id: "forms",
label: "i18n:govoplan-forms.form_definitions",
version: "0.1.14",
version: "0.1.19",
dependencies: ["access"],
optionalDependencies: ["forms_runtime", "portal", "workflow_engine", "cases", "policy"],
optionalDependencies: ["forms_runtime", "portal", "workflow_engine", "cases", "policy", "docs"],
translations: generatedTranslations,
routes: [
{