Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x | /**
* FormInspector — debug panel showing live form state, replacing @hookform/devtools.
*
* Renders a side panel (styled like PropertyEditor) with collapsible sections for:
* - Fields — per-field dirty/touched/error badges (via useFormState from react-hook-form)
* - Values — current form values (via useWatch — live on every keystroke)
* - Table selections (from FormStateContext — not tracked by react-hook-form)
* - State — read-only / loading flags (from FormStateContext — not tracked by react-hook-form)
*
* More sections can be added in future by following the Section pattern below.
*
* The component must be rendered inside a <Form> so it can read FormContext and
* FormStateContext. FormInspector guards against null context and renders nothing
* when used outside a Form.
*/
import type {ReactNode} from 'react';
import {useState} from 'react';
import type {Control} from 'react-hook-form';
import {useFormState, useWatch} from 'react-hook-form';
import {useBlongForm, useBlongFormState} from '../components/Form/FormContext.js';
import {Json} from '../components/Json/Json.js';
// ── Section (collapsible) ────────────────────────────────────────────────────
interface ISectionProps {
title: string;
value?: unknown;
defaultOpen?: boolean;
children?: ReactNode;
}
function Section({title, value, defaultOpen = false, children}: ISectionProps) {
const [open, setOpen] = useState(defaultOpen);
return (
<div className="blong-inspector__section">
<button
type="button"
className="blong-inspector__toggle"
onClick={() => setOpen(o => !o)}
>
<span
className={`pi pi-chevron-${open ? 'down' : 'right'} blong-inspector__chevron`}
/>
{title}
</button>
{open && (
<div className="blong-inspector__body">{children ?? <Json value={value} />}</div>
)}
</div>
);
}
// ── Fields section ───────────────────────────────────────────────────────────
/**
* Renders one row per field showing dirty / touched / error status.
* Uses useFormState (subscribes to dirty/touched/errors) and useWatch (subscribes
* to values) separately so each only triggers when its own slice changes.
*/
function FieldsSection({
dirtyFields,
touchedFields,
errors,
}: Pick<ReturnType<typeof useFormState>, 'dirtyFields' | 'touchedFields' | 'errors'>) {
const allFields = new Set([
...Object.keys(dirtyFields),
...Object.keys(touchedFields),
...Object.keys(errors),
]);
if (allFields.size === 0) {
return <p className="blong-inspector__empty">No dirty / touched / error fields yet.</p>;
}
return (
<dl className="blong-inspector__fields">
{[...allFields].map(field => {
const dirty = !!dirtyFields[field];
const touched = !!touchedFields[field];
const error = errors[field];
return (
<div
key={field}
className="blong-inspector__field-row"
>
<dt
className={`blong-inspector__field-name${dirty ? ' blong-inspector__field-name--dirty' : ''}`}
>
{field}
</dt>
<dd className="blong-inspector__field-flags">
{dirty && (
<span className="blong-inspector__badge blong-inspector__badge--dirty">
D
</span>
)}
{touched && (
<span className="blong-inspector__badge blong-inspector__badge--touched">
T
</span>
)}
{error && (
<span
className="blong-inspector__badge blong-inspector__badge--error"
title={error.message as string | undefined}
>
{String(error.type ?? 'E')}
</span>
)}
{error?.message && (
<span className="blong-inspector__error-msg">
{error.message as string}
</span>
)}
</dd>
</div>
);
})}
</dl>
);
}
// ── Main inspector ───────────────────────────────────────────────────────────
interface IFormInspectorInnerProps {
control: Control<Record<string, unknown>>;
}
function FormInspectorInner({control}: IFormInspectorInnerProps) {
const stateCtx = useBlongFormState();
// Subscribe to all field value changes so the Values section stays live.
const values = useWatch({control});
const {dirtyFields, touchedFields, errors, isDirty, isLoading, submitCount, isValid} =
useFormState({control});
return (
<div className="blong-property-editor blong-inspector p-component m-2">
<div className="blong-property-editor__title">
<i className="pi pi-info-circle blong-inspector__icon" />
Form Inspector
</div>
<Section
title="Fields"
defaultOpen
>
<FieldsSection
dirtyFields={dirtyFields}
touchedFields={touchedFields}
errors={errors}
/>
</Section>
<Section
title="Values"
value={values}
defaultOpen
/>
<Section
title="Table Selections"
value={stateCtx?.tableSelections ?? {}}
defaultOpen
/>
<Section
title="State"
value={{
editorMode: stateCtx?.editorMode,
editorLayout: stateCtx?.editorLayout,
readOnly: stateCtx?.readOnly,
loading: stateCtx?.loading,
isDirty,
isValid,
submitCount,
isLoading,
}}
defaultOpen
/>
</div>
);
}
/**
* FormInspector — render inside a <Form> to display live form state in debug mode.
* Returns null when rendered outside a Form context (guards against both FormContext
* and FormStateContext being unavailable).
*/
export function FormInspector() {
const formCtx = useBlongForm();
if (!formCtx) return null;
return <FormInspectorInner control={formCtx.control} />;
}
|