feat: expose form semantic documentation subjects
Module Package Release / publish-packages (push) Successful in 12s
Module Package Release / publish-packages (push) Successful in 12s
This commit is contained in:
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user