/** * Placement + serialization for a control added to an AxFormExtension. * * A form extension expresses a new control in one of TWO mutually exclusive * shapes, and which one is correct depends entirely on WHERE the parent lives: * * 1. Parent is a control of the BASE FORM → an * envelope in the extension's ROOT , carrying its own wrapper * , the real control under , and a reference: * * ← root, child of AxFormExtension * * FormExtensionControlfse38xiwz * … * Grid * * * * 2. Parent is a container THE EXTENSION ITSELF DEFINES → a bare * in that container's NESTED . No * envelope and no : the nesting already encodes parentage, so a * element there is meaningless. * * * QualityOrders * ← nested * … * * QualityOrders * * * The previous implementation built shape 1 unconditionally and spliced it in * with `content.replace('', …)`. A string pattern replaces the FIRST * occurrence, and when the extension defines its own container the nested * closes first — so the envelope landed inside the nested * collection, which is typed to AxFormControl. `parentControl` was never * resolved to a node at all; it only supplied the text. The result was * well-formed XML in the wrong collection, reported as a success. * * This module resolves the parent against the extension's own control tree and * derives BOTH the representation and the insertion offset from where that * parent turns out to live. Pure and side-effect-free so it is trivially * testable — the file I/O stays in the caller. */ import { type XmlNode } from './xmlNodeTree.js'; export interface FormExtPlacementProblem { /** The misplaced element. */ element: string; /** 1-based line in the supplied XML. */ line: number; detail: string; } /** * Check that every control element sits in a collection typed to hold it. * * This exists because of how the platform actually behaves, measured 2026-08-12 * by compiling the malformed file: an inside a nested * does NOT fail the build. xppc returns 0 errors — the deserializer * silently DISCARDS the node. The control never reaches the form, and the only * trace is a metadata WARNING, and only when the parent happens to be * -bound so there are two field sets to compare: * * Metadata Warning: …/Controls/FormExtensionControl…/…/DataGroup: The form * control has different fields from the field group '…' it is bound to. * Use restore on the form control. * * That warning names neither the malformed node nor the control, and it arrived * among 52 pre-existing warnings. For a parent that is NOT DataGroup-bound there * is nothing to compare, so the discard is expected to be entirely silent. * * A compiler that stays quiet is the whole problem: nothing downstream will ever * catch this, so the check has to happen here, before the write. Name-based * validation (formExtensionShapeValidator) cannot see it — every element in the * malformed file is spelled correctly; only its POSITION is wrong. */ export declare function findFormExtensionPlacementProblems(xml: string): FormExtPlacementProblem[]; /** * The subtrees the D365FO deserializer discards, for a writer that only needs to * know WHICH controls are dead rather than why. * * The removal writer asks this: a name that resolves to a control inside a * discarded element is not the control on the form, and cutting it out while * reporting "removed" would tell the caller their button is gone when it never * arrived. Safe to call on an AxForm as well — nothing there is misplaced in * these two ways, so it answers with an empty list. */ export declare function discardedControlRoots(xml: string, root: XmlNode): XmlNode[]; /** Render placement problems as a blocking, self-explaining error. */ export declare function buildFormExtensionPlacementError(objectName: string, problems: FormExtPlacementProblem[]): string; export interface FormExtensionControlSpec { controlName: string; parentControl: string; /** Element name emitted as i:type, e.g. "AxFormCheckBoxControl". */ iType: string; /** value, e.g. "CheckBox". */ typeValue: string; dataSource?: string; dataField?: string; label?: string; /** * Wrapper for the envelope shape. Injected rather than generated here so * this module stays deterministic; ignored for the nested shape, which has no * wrapper. */ wrapperName: string; /** * Name of the existing sibling to place the control after. * * Nested shape: the control is spliced directly after that sibling in the * parent's . Envelope shape: position among the BASE FORM's children * is not expressible by splice order, so it is written as * AfterItem…. */ previousSibling?: string; /** "AfterItem" (needs previousSibling), "Begin", or "End" (default). Envelope shape only. */ positionType?: string; } export type InsertFormExtensionControlResult = /** Control written. `representation` says which of the two shapes was emitted. */ { kind: 'inserted'; xml: string; representation: 'envelope' | 'nested'; notes: string[]; } /** A control with this name is already present — nothing to do. */ | { kind: 'exists'; } /** Understood the file, but writing would be wrong. `message` is caller-facing. */ | { kind: 'refused'; message: string; } /** Not a shape this writer recognises; the caller should fall through. */ | { kind: 'unsupported'; }; /** * Insert a control into an AxFormExtension, choosing shape AND location from * where `parentControl` actually lives. */ export declare function insertFormExtensionControl(xml: string, spec: FormExtensionControlSpec): InsertFormExtensionControlResult; /** * Where the control sits among its parent's existing children, as the two * elements the envelope carries for it. * * Only `AfterItem` and `Begin` appear in shipped metadata (765 and 182 * occurrences across the 1088 AxFormExtension files in PackagesLocalDirectory; * no other value occurs), so those are the two that get written. `End` is * spelled by omitting both elements — 1594 shipped envelopes carry no * at all — and anything else is refused rather than guessed at, * because an unknown enum value is exactly the kind of thing that deserializes * to a discarded node under a silent build. */ export type ResolvedPosition = { kind: 'none'; note?: string; } | { kind: 'placed'; positionType: 'AfterItem' | 'Begin'; previousSibling?: string; note?: string; } | { kind: 'invalid'; message: string; }; export declare function resolvePosition(spec: Pick): ResolvedPosition; /** * The message for output this writer refuses to persist. * * It takes the DOCUMENT and derives the object name itself rather than accepting * a pre-picked string, because picking it at the call site is what went wrong: * the control name was passed into a parameter named `objectName`, so the banner * read `form-extension "NewCtl"` and never named the file the very next sentence * asks the reader to attach. Two names are in play and only one identifies the * object; deriving it here leaves the call site nothing to get wrong. * * Exported for tests. Ordinary input cannot reach it — the writer's output is * clean on all 1088 shipped extensions, and escaping closed the one route that * made it reachable — so a caller-level test cannot cover it. */ export declare function buildAbandonedWriteMessage(xml: string, controlName: string, introduced: FormExtPlacementProblem[]): string; //# sourceMappingURL=formExtensionControlXml.d.ts.map