From 69fca024d11b9e30f179552ad9fd6734188abe4c Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 7 Sep 2026 23:23:49 -0500 Subject: [PATCH 1/7] Refactor dot-ai-config-detail component for improved UI and functionality - Updated HTML structure for better layout and styling, including adjustments to class names and element properties. - Enhanced loading and error handling with new components for better user feedback. - Improved accessibility and readability by modifying text classes and labels. - Updated icons to use Material Symbols for consistency across the application. - Refactored dynamic field and settings card components for better organization and usability. - Adjusted constants for AI capabilities to use more descriptive icon names. --- ...ot-ai-additional-properties.component.html | 16 +- .../dot-ai-capability-card.component.html | 171 +++++++------- .../dot-ai-dynamic-field.component.html | 16 +- .../dot-ai-settings-card.component.html | 210 +++++++++--------- .../dot-ai-config-detail.component.html | 31 ++- .../dot-ai-config-detail.component.ts | 9 +- .../dot-ai-config.constants.ts | 6 +- .../WEB-INF/messages/Language.properties | 4 +- 8 files changed, 239 insertions(+), 224 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-additional-properties/dot-ai-additional-properties.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-additional-properties/dot-ai-additional-properties.component.html index 17cd78c4f37e..d541bdf08fde 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-additional-properties/dot-ai-additional-properties.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-additional-properties/dot-ai-additional-properties.component.html @@ -1,5 +1,5 @@
- + {{ 'apps.ai.additional-properties.label' | dm }} @@ -10,20 +10,21 @@ + (onClick)="removeProperty(i)"> + close +
} @@ -32,8 +33,9 @@ + (onClick)="addProperty()"> + add + diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html index 2ed003fd1428..9d834d33991a 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html @@ -1,13 +1,15 @@ -
+
-
- +
+ + {{ meta().icon }} +
- {{ meta().title | dm }} + {{ meta().title | dm }}
- {{ meta().description | dm }} + {{ meta().description | dm }}
@if (enabled()) { - -
- - {{ 'apps.ai.provider.label' | dm }} - -
- @for (provider of orderedProviders(); track provider.provider) { - - } -
-
- - - @if (visibleFields().length > 0) { -
- @for (field of visibleFields(); track field.name) { - - } +
+ +
+ + {{ 'apps.ai.provider.label' | dm }} + +
+ @for (provider of orderedProviders(); track provider.provider) { + + } +
- } - - -
- @if (advancedFields().length > 0) { -
- @for (field of advancedFields(); track field.name) { + + @if (visibleFields().length > 0) { +
+
+ @for (field of visibleFields(); track field.name) { }
- } - -
- +
+ } -
- + + +
+ @if (advancedFields().length > 0) { +
+
+ @for (field of advancedFields(); track field.name) { + + } +
+
+ } + +
+
- @if (testResult(); as result) { - - - {{ result.message | dm }} - - } +
+ + + @if (testResult(); as result) { + + + {{ result.success ? 'check_circle' : 'error' }} + + {{ result.message | dm }} + + } +
}
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-dynamic-field/dot-ai-dynamic-field.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-dynamic-field/dot-ai-dynamic-field.component.html index b203d9e29bd0..befe4ac4ca0d 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-dynamic-field/dot-ai-dynamic-field.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-dynamic-field/dot-ai-dynamic-field.component.html @@ -1,6 +1,6 @@
-
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-settings-card/dot-ai-settings-card.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-settings-card/dot-ai-settings-card.component.html index 5f6eabc2f243..2e99a77f7c32 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-settings-card/dot-ai-settings-card.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-settings-card/dot-ai-settings-card.component.html @@ -1,120 +1,124 @@ -
-
- +
+
+ tune
- + {{ 'apps.ai.settings.title' | dm }} - + {{ 'apps.ai.settings.description' | dm }}
-
-
- - - - {{ 'apps.ai.settings.role-prompt.hint' | dm }} - -
- -
+
+
-
-
- - + +
+
+ + +
+
+ + +
-
-
- - -
- +
+ + +
+ - -
-
- @for (settingsField of advancedFields; track settingsField.key) { - @if (settingsField.type === 'checkbox') { -
- - -
- } @else { -
- - @if (settingsField.type === 'number') { - + +
+
+ + @for (settingsField of advancedFields; track settingsField.key) { + @if (settingsField.type === 'checkbox') { +
+ + +
} @else { - - } - @if (settingsField.hint) { - {{ settingsField.hint | dm }} +
+ + @if (settingsField.type === 'number') { + + } @else { + + } + + @if (settingsField.hint) { + {{ settingsField.hint | dm }} + } + +
} -
- } - } - - -
-
+ } + +
+ +
+
+
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html index cd7a9c7eb34f..7b140dd20d55 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html @@ -1,27 +1,23 @@ -
-
+
+
-

+

{{ 'apps.ai.config.title' | dm }}

-

- {{ 'apps.ai.config.subtitle' | dm }} +

+ {{ 'apps.ai.config.first.subtitle' | dm }} +

+

+ {{ 'apps.ai.config.second.subtitle' | dm }}

- @if (siteName()) { -

- {{ 'apps.ai.config.site' | dm: [siteName()] }} -

- }
@if (loading()) { -
- {{ 'apps.ai.loading' | dm }} +
+
} @else if (loadFailed()) { -
- {{ 'apps.ai.error.load' | dm }} -
+ } @else { @for (meta of capabilityMeta; track meta.capability) {
-
+
@if (dirty()) { - {{ 'apps.ai.unsaved.changes' | dm }} + {{ 'apps.ai.unsaved.changes' | dm }} Date: Tue, 8 Sep 2026 09:57:08 -0500 Subject: [PATCH 2/7] Enhance dot-ai-config-detail component UI and functionality - Updated text classes for improved readability and consistency across the component. - Adjusted HTML structure to enhance layout, including margin and padding modifications. - Implemented conditional rendering for hints in dynamic fields and settings card for better user experience. - Standardized text color for additional properties and capability card labels to ensure uniformity. --- .../dot-ai-additional-properties.component.html | 2 +- .../dot-ai-capability-card.component.html | 2 +- .../dot-ai-dynamic-field.component.html | 5 +++-- .../dot-ai-settings-card.component.html | 10 +++++----- .../dot-ai-config-detail.component.html | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-additional-properties/dot-ai-additional-properties.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-additional-properties/dot-ai-additional-properties.component.html index d541bdf08fde..321d12a9cea6 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-additional-properties/dot-ai-additional-properties.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-additional-properties/dot-ai-additional-properties.component.html @@ -1,5 +1,5 @@
- + {{ 'apps.ai.additional-properties.label' | dm }} diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html index 9d834d33991a..3a1a7172bee4 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html @@ -21,7 +21,7 @@
- + {{ 'apps.ai.provider.label' | dm }}
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-dynamic-field/dot-ai-dynamic-field.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-dynamic-field/dot-ai-dynamic-field.component.html index befe4ac4ca0d..9fdaf9cc206d 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-dynamic-field/dot-ai-dynamic-field.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-dynamic-field/dot-ai-dynamic-field.component.html @@ -45,6 +45,7 @@ class="w-full" /> } } - - {{ field().hint }} + @if (field().hint) { + {{ field().hint }} + }
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-settings-card/dot-ai-settings-card.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-settings-card/dot-ai-settings-card.component.html index 2e99a77f7c32..2a455420eb42 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-settings-card/dot-ai-settings-card.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-settings-card/dot-ai-settings-card.component.html @@ -75,7 +75,7 @@ [header]="'apps.ai.settings.advanced.header' | dm: [advancedFields.length.toString()]" transitionOptions="250ms cubic-bezier(0.4, 0, 0.2, 1)">
-
+
@for (settingsField of advancedFields; track settingsField.key) { @if (settingsField.type === 'checkbox') { @@ -107,11 +107,11 @@ [formControlName]="settingsField.key" class="w-full" /> } - - @if (settingsField.hint) { + @if (settingsField.hint) { + {{ settingsField.hint | dm }} - } - + + }
} } diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html index 7b140dd20d55..fdacffea9e5e 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html @@ -1,7 +1,7 @@
-

+

{{ 'apps.ai.config.title' | dm }}

From 84569d72e25fd08e10148997af906fff73a9eccb Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 8 Sep 2026 11:09:15 -0500 Subject: [PATCH 3/7] Refactor dot-ai-config-detail component layout for improved UI consistency - Updated HTML structure across multiple components to enhance layout and styling, including the use of flexbox for better alignment. - Changed class names to ensure uniformity and improved readability in forms and fields. - Enhanced the visual hierarchy by adjusting spacing and grouping of elements within the settings and capability cards. --- .../dot-ai-capability-card.component.html | 4 ++-- .../dot-ai-dynamic-field.component.html | 2 +- .../dot-ai-settings-card.component.html | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html index 3a1a7172bee4..7014db6e2838 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-capability-card/dot-ai-capability-card.component.html @@ -55,7 +55,7 @@ @if (visibleFields().length > 0) { -

+
@for (field of visibleFields(); track field.name) { @@ -76,7 +76,7 @@ transitionOptions="250ms cubic-bezier(0.4, 0, 0.2, 1)">
@if (advancedFields().length > 0) { -
+
@for (field of advancedFields(); track field.name) { +
- -
+ +
@@ -30,7 +30,7 @@
-
+
@@ -41,7 +41,7 @@ class="w-full" [placeholder]="'apps.ai.settings.text-prompt.placeholder' | dm" />
-
+
@@ -54,7 +54,7 @@
-
+
@@ -75,11 +75,11 @@ [header]="'apps.ai.settings.advanced.header' | dm: [advancedFields.length.toString()]" transitionOptions="250ms cubic-bezier(0.4, 0, 0.2, 1)">
-
+
@for (settingsField of advancedFields; track settingsField.key) { @if (settingsField.type === 'checkbox') { -
+
} @else { -
+
From 7b3cfcfb848a972231a7f9def7edc89b65dcbd45 Mon Sep 17 00:00:00 2001 From: Kevin Date: Tue, 8 Sep 2026 23:58:00 -0500 Subject: [PATCH 4/7] Refactor dot-ai-config-detail component for improved text layout - Removed unnecessary max-width classes from subtitle paragraphs to enhance text display. - Simplified HTML structure for better readability and consistency across the component. --- .../dot-ai-config-detail/dot-ai-config-detail.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html index fdacffea9e5e..e465a49d81f9 100644 --- a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/dot-ai-config-detail.component.html @@ -4,10 +4,10 @@

{{ 'apps.ai.config.title' | dm }}

-

+

{{ 'apps.ai.config.first.subtitle' | dm }}

-

+

{{ 'apps.ai.config.second.subtitle' | dm }}

From 75a819e694a2d82e5be3d585ed70ba824c008445 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 9 Sep 2026 00:02:24 -0500 Subject: [PATCH 5/7] Update form field guidelines and styling standards - Removed outdated form markup examples from CLAUDE.md and replaced with a reference to the updated Form Fields documentation. - Expanded the Form Fields section in STYLING_STANDARDS.md to clarify conventions and rules for field markup, including spacing, label handling, and component usage. - Emphasized the importance of adhering to established guidelines to ensure consistency across the application. --- core-web/CLAUDE.md | 18 +++------- docs/frontend/STYLING_STANDARDS.md | 53 +++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/core-web/CLAUDE.md b/core-web/CLAUDE.md index d72e48a9a7c9..66eadb570b80 100644 --- a/core-web/CLAUDE.md +++ b/core-web/CLAUDE.md @@ -83,20 +83,10 @@ spectator.setInput('prop', value); // ALWAYS use setInput ### Form Markup -Always wrap form fields with this structure for consistent styling: - -```html - -
- - -
-
- - -
- -``` +See [Form Fields](docs/frontend/STYLING_STANDARDS.md#form-fields) for the current field-layout, +typography, and hint-handling rules — do not hand-roll a form wrapper from memory or copy an +existing form without checking it first. Different areas of the app (e.g. `edit-content`) have +diverged in the past; the doc is the tie-breaker. ## Portlet Development diff --git a/docs/frontend/STYLING_STANDARDS.md b/docs/frontend/STYLING_STANDARDS.md index 01f4c4ab898d..c57095c5940c 100644 --- a/docs/frontend/STYLING_STANDARDS.md +++ b/docs/frontend/STYLING_STANDARDS.md @@ -11,7 +11,7 @@ ```html
- Title + Title
@@ -24,6 +24,12 @@
...
+ + +
+
...
+
...
+
``` ## When Custom SCSS Is Acceptable @@ -113,6 +119,51 @@ PrimeNG ships two visually similar but semantically different components. Pick b - **Never use `p-chip` for purely informational status** — use `p-tag` with a `severity`. - **Never add Tailwind `!important` color overrides** (`bg-green-100!`, `text-red-700!`, etc.) to PrimeNG components. Rely on native `severity` plus the preset color tokens in `theme.config.ts`. +## Form Fields + +The codebase currently has **more than one field-markup convention** (e.g. the global `.form`/ +`.field` classes in `apps/dotcms-ui/src/style.css`, and `edit-content`'s own +`dot-card-field`/`dot-card-field-label` components). They do not render identically — before +writing a new form, check how the most relevant *existing* surface (usually `edit-content`, the +most actively maintained field UI) actually looks, don't just grep for a `.form`/`.field` example +and copy it. Do not extend the global `.form`/`.field` classes to new features. + +Regardless of which markup you use, these rules apply to every field: + +- **No `text-*` size class on body copy or labels.** Labels and normal UI text inherit the + PrimeNG default size/weight — do not add `text-sm`, `text-base`, `font-medium`, etc. to make a + label "look right." The **only** text allowed `text-sm` is hint/error text under a field, and it + pairs with a muted/semantic color (`text-gray-500` for hints, `text-red-500` for errors). +- **Label-to-control gap is `gap-2`.** A field wrapper is `flex flex-col gap-2` — label, then + control, nothing wider. +- **A field reserves space for its hint only when it has one.** Render the hint conditionally + (`@if (field.hint) { ... }`) — never a permanent + empty slot or a fixed `min-h-*` "for alignment" when there is no hint to show. +- **Required marker is a literal `*`**, shown only `@if (field.required)`, styled `text-red-500`, + placed next to the label. Don't introduce a second, CSS-class-driven required mechanism for new + forms unless you are already extending a surface that uses one. +- **Prefer PrimeNG components over hand-rolled equivalents**: `p-card` (not a bordered div with a + hand-built header bar), `p-panel`/`p-accordion` for collapsible sections, `p-toggleswitch` for + on/off toggles, `p-button` for actions. See the anti-pattern example above. + +```html + +
+ + + @if (hint) { + {{ hint }} + } +
+ + +
+ + + {{ hint }} +
+``` + ## See also - [ANGULAR_STANDARDS.md](./ANGULAR_STANDARDS.md) — Component rules, templates - [docs/frontend/README.md](./README.md) — Index of all frontend docs From 886aefac7f7b28fa4b083c48fdb20d6359c5ae49 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 9 Sep 2026 17:52:25 -0500 Subject: [PATCH 6/7] fix(dotai): address provider-config UI review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Card borders were broken at the corners. A rounded parent does not clip its children, so the header's square bottom corners painted over the arc and erased the border stroke on a not-configured card, which renders header-only. The capability card now clips with `overflow-hidden`; the settings card cannot (its `p-select` overlay renders inline under `overlayAppendTo: 'self'`) so its header takes the border's inner radius instead. Both carry the reason in a comment. Other findings from review: - `p-field-hint` is descendant-scoped to `.form`, which this tree has none of, so all three hints were rendering unstyled. Use the utilities directly. - An icon projected as bare content into `p-button` does not satisfy `hasIcon`, so the remove button had lost `p-button-icon-only` and took the padding of a labelled button. Moved to ``. - `aria-label` on the `` host never reached the real `
} @@ -35,7 +45,10 @@ size="small" [label]="'apps.ai.additional-properties.add.button' | dm" styleClass="self-start" + data-testid="add-property" (onClick)="addProperty()"> - add + + +
diff --git a/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-additional-properties/dot-ai-additional-properties.component.spec.ts b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-additional-properties/dot-ai-additional-properties.component.spec.ts new file mode 100644 index 000000000000..b7d76b44c674 --- /dev/null +++ b/core-web/apps/dotcms-ui/src/app/portlets/dot-apps/components/dot-ai-config-detail/components/dot-ai-additional-properties/dot-ai-additional-properties.component.spec.ts @@ -0,0 +1,119 @@ +import { byTestId, createComponentFactory, Spectator } from '@openng/spectator/jest'; + +import { FormArray, FormControl, FormGroup } from '@angular/forms'; + +import { DotMessageService } from '@dotcms/data-access'; +import { MockDotMessageService } from '@dotcms/utils-testing'; + +import { + DotAiAdditionalPropertiesComponent, + DotAiAdditionalPropertyGroup +} from './dot-ai-additional-properties.component'; + +describe('DotAiAdditionalPropertiesComponent', () => { + let spectator: Spectator; + let properties: FormArray; + + const createComponent = createComponentFactory({ + component: DotAiAdditionalPropertiesComponent, + providers: [{ provide: DotMessageService, useValue: new MockDotMessageService({}) }] + }); + + function propertyRow(key: string, value: string): DotAiAdditionalPropertyGroup { + return new FormGroup({ + key: new FormControl(key, { nonNullable: true }), + value: new FormControl(value, { nonNullable: true }) + }); + } + + /** + * Rows have to exist before the first render. The component is OnPush and `properties` is a + * signal holding the FormArray, so pushing into that array from outside mutates it without + * changing the signal and without marking the component dirty. In the app the rows arrive the + * same way — hydrated into the array by the parent card before it renders. + */ + function setup(rows: DotAiAdditionalPropertyGroup[] = []): void { + properties = new FormArray(rows); + spectator = createComponent({ props: { properties } }); + } + + /** p-button renders its own `