Skip to content

Commit d26a05b

Browse files
committed
Merge branch 'LF-3050-fix-invalid-source-in-enable-when' into 'master'
LF-3050 - validate invalid source in enable when See merge request lfor/formbuilder!114
2 parents 2e102b9 + 5faf11c commit d26a05b

15 files changed

Lines changed: 797 additions & 107 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This project follows [Semantic Versioning](http://semver.org/).
55
## [10.3.12] 2025-12-06
66
### Fixed
77
- Fixed issue with variable deletion not updating the list.
8+
- Fix invalid source item in enableWhen condition.
89

910
## [10.3.11] 2025-12-05
1011
### Fixed

cypress/e2e/item-level/item-level.cy.ts

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2015,6 +2015,31 @@ describe('Home page', () => {
20152015
.scrollIntoView()
20162016
.should('have.value', '');
20172017
});
2018+
2019+
/*
2020+
* Verifies that when a new item is created after a focused item that has expanded children, the new item's
2021+
* linkId is properly populated. This ensures that the fix correctly assigns a linkId even when the previous
2022+
* node is expanded and has children.
2023+
*/
2024+
it('should populate linkId when creating a new item after a focused item with expanded children', () => {
2025+
// Click on '2 Family member health history'
2026+
cy.getTreeNode('Family member health history').click();
2027+
2028+
cy.toggleTreeNodeExpansion('Family member health history');
2029+
2030+
// Click the 'Add new item'
2031+
cy.contains('button', 'Add new item').click();
2032+
// Click on the new added item
2033+
cy.getTreeNode('New item 1').click();
2034+
2035+
// Go to the link id section
2036+
cy.editableLinkId()
2037+
.scrollIntoView()
2038+
.should('be.visible')
2039+
.invoke('val')
2040+
.should('not.be.empty');
2041+
});
2042+
20182043
});
20192044

20202045
describe('Test descendant items and display/group type changes', () => {
@@ -2103,28 +2128,23 @@ describe('Home page', () => {
21032128
cy.get('lfb-node-dialog').find('#moveTarget1').click().clear().type('Display Data Type');
21042129
cy.get('lfb-node-dialog').find('button.dropdown-item').should('exist').should('have.length', 1).click();
21052130

2106-
cy.get('lfb-node-dialog').find('ul').within(() => {
2107-
cy.get('li').should('have.length', 2);
2108-
cy.get('li').eq(0).should('contain.text', 'After the target item.');
2109-
cy.get('li').eq(1).should('contain.text', 'Before the target item.');
2110-
});
2111-
2112-
// Clear the target again. 3 drop locations should be presented.
2113-
cy.get('lfb-node-dialog').find('#moveTarget1').click().clear();
2114-
cy.get('lfb-node-dialog form').click();
21152131
cy.get('lfb-node-dialog').find('ul').within(() => {
21162132
cy.get('li').should('have.length', 3);
2117-
cy.get('li').eq(0).should('contain.text', 'After the target item.');
2118-
cy.get('li').eq(1).should('contain.text', 'Before the target item.');
2119-
cy.get('li').eq(2).should('contain.text', 'As a child of target item.');
2120-
// Select the 'As a child of target item.' option.
21212133
cy.get('li').eq(2).should('contain.text', 'As a child of target item.')
21222134
.find('input[type="radio"').check();
21232135

21242136
});
2125-
// The 'Display Data Type' item should be excluded from the target item list.
2126-
cy.get('lfb-node-dialog').find('#moveTarget1').click().clear().type('Display Data Type');
2127-
cy.get('lfb-node-dialog').find('button.dropdown-item').should('not.exist');
2137+
2138+
cy.get('lfb-node-dialog').contains('button', 'Move').as('moveBtn').click();
2139+
2140+
// The modal is displayed.
2141+
cy.get('lfb-confirm-dlg > div.modal-header').should('contain.text', 'Move Not Allowed');
2142+
cy.get('lfb-confirm-dlg > div.modal-body')
2143+
.invoke('text')
2144+
.should('match', /Cannot drop into item 'Display Data Type' \(linkId: [^)]+\) of type 'display' because it cannot contain children\./);
2145+
2146+
// Close the modal by clicking the close button
2147+
cy.get('lfb-confirm-dlg').contains('button', 'Close').click();
21282148
});
21292149

21302150
it('should not be able to insert a new child item to an item of type "display"', () => {
@@ -2149,6 +2169,43 @@ describe('Home page', () => {
21492169
// Due to the data type 'display', the option 'Insert a new child item.' should be hidden.
21502170
cy.get('div.dropdown-menu.show').should('not.contain', 'Insert a new child item');
21512171
});
2172+
2173+
it('should show "display" data type if the last child is removed from the item', () => {
2174+
cy.toggleTreeNodeExpansion('Family member health history');
2175+
cy.getTreeNode('Race').click();
2176+
2177+
// Add a new item under the 'Race' item.
2178+
cy.contains('Add new item').scrollIntoView().click();
2179+
cy.getItemTextField().clear().type('Item with child');
2180+
cy.getItemTypeField().as('dataTypes');
2181+
// Confirm that the data type 'display' is included in the list.
2182+
cy.get('@dataTypes').find('option').should('contain.text', 'display');
2183+
2184+
// Create a chid item.
2185+
cy.getTreeNode('Item with child').as('contextNode');
2186+
cy.get('@contextNode').find('button.dropdown-toggle').click();
2187+
cy.get('div.dropdown-menu.show').contains('button.dropdown-item', 'Insert a new child item').click();
2188+
cy.getItemTextField().should('have.value', 'New item 2');
2189+
2190+
// Click back to the parent item 'Item with child'.
2191+
cy.getTreeNode('Item with child').click();
2192+
// The data type 'display' should no longer be on the list.
2193+
cy.getItemTypeField().as('dataTypes');
2194+
cy.get('@dataTypes').find('option').should('not.contain.text', 'display');
2195+
2196+
// Delete the child item
2197+
cy.getTreeNode('New item 2').as('childItem').click();
2198+
cy.get('@childItem').find('button.dropdown-toggle').click();
2199+
cy.get('div.dropdown-menu.show').contains('button.dropdown-item', 'Remove this item').click();
2200+
cy.contains('button', 'Yes').click();
2201+
2202+
// The parent item 'Item with child' should now be the focused node.
2203+
cy.get('#text').should('contain.value', 'Item with child');
2204+
// As the item 'Item with child' no longer has any children, the data type 'display' should once again be available.
2205+
cy.getItemTypeField().as('dataTypes');
2206+
cy.get('@dataTypes').find('option').should('contain.text', 'display');
2207+
});
2208+
21522209
});
21532210
});
21542211

cypress/e2e/item-level/units.cy.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,6 @@ describe('Home page', () => {
371371
expect(qJson.item[0].extension[0].valueCoding.display).equal('[Ampere/kilogram]*stere');
372372
});
373373
});
374-
375374
it('should support lookup display string that contains spaces between words', () => {
376375
cy.get('[id^="units"]').should('not.exist');
377376
cy.selectDataType('decimal');
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
{
2+
"resourceType": "Questionnaire",
3+
"meta": {
4+
"profile": [
5+
"http://hl7.org/fhir/4.0/StructureDefinition/Questionnaire"
6+
]
7+
},
8+
"title": "enableWhen-dependency-validation-sample",
9+
"status": "draft",
10+
"item": [
11+
{
12+
"item": [
13+
{
14+
"item": [
15+
{
16+
"type": "string",
17+
"linkId": "790053845245",
18+
"text": "New item 2",
19+
"item": [
20+
{
21+
"type": "string",
22+
"linkId": "543640098457",
23+
"text": "New item 3",
24+
"item": [
25+
{
26+
"type": "string",
27+
"linkId": "793172287454",
28+
"text": "New item 4"
29+
}
30+
]
31+
}
32+
]
33+
}
34+
],
35+
"type": "string",
36+
"linkId": "269578934615",
37+
"text": "New item 1"
38+
}
39+
],
40+
"type": "string",
41+
"linkId": "587234105213",
42+
"text": "Item 0"
43+
},
44+
{
45+
"item": [
46+
{
47+
"type": "string",
48+
"linkId": "863989562511",
49+
"text": "New item 6",
50+
"enableWhen": [
51+
{
52+
"question": "790053845245",
53+
"operator": "exists",
54+
"answerBoolean": true
55+
}
56+
]
57+
}
58+
],
59+
"type": "string",
60+
"linkId": "587234105333",
61+
"text": "New item 5"
62+
},
63+
{
64+
"item": [
65+
{
66+
"type": "string",
67+
"linkId": "863989565554",
68+
"text": "New item 8"
69+
}
70+
],
71+
"type": "string",
72+
"linkId": "587234105553",
73+
"text": "New item 7",
74+
"enableWhen": [
75+
{
76+
"question": "790053845245",
77+
"operator": "exists",
78+
"answerBoolean": true
79+
}
80+
]
81+
},
82+
{
83+
"type": "string",
84+
"linkId": "587234105444",
85+
"text": "Invalid item with enableWhen references question id that does not exist",
86+
"enableWhen": [
87+
{
88+
"question": "000000000000",
89+
"operator": "exists",
90+
"answerBoolean": true
91+
}
92+
]
93+
},
94+
{
95+
"type": "string",
96+
"linkId": "963433838378",
97+
"text": "Invalid item with enableWhen question references a child item",
98+
"enableWhen": [
99+
{
100+
"question": "677718877639",
101+
"operator": "exists",
102+
"answerBoolean": true
103+
}
104+
],
105+
"item": [
106+
{
107+
"type": "string",
108+
"linkId": "677718877639",
109+
"text": "New item 7"
110+
}
111+
]
112+
},
113+
{
114+
"type": "display",
115+
"linkId": "969848586908",
116+
"text": "Display item"
117+
},
118+
{
119+
"type": "string",
120+
"linkId": "587234105444",
121+
"text": "Invalid item with enableWhen references display item",
122+
"enableWhen": [
123+
{
124+
"question": "969848586908",
125+
"operator": "exists",
126+
"answerBoolean": true
127+
}
128+
]
129+
},
130+
{
131+
"type": "group",
132+
"linkId": "967213897865",
133+
"text": "Group item"
134+
},
135+
{
136+
"type": "string",
137+
"linkId": "587234105555",
138+
"text": "Invalid item with enableWhen references group item",
139+
"enableWhen": [
140+
{
141+
"question": "967213897865",
142+
"operator": "exists",
143+
"answerBoolean": true
144+
}
145+
]
146+
}
147+
]
148+
}

cypress/support/commands.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -714,14 +714,18 @@ Cypress.Commands.add('selectAutocompleteOptions',
714714
cy.wrap(autocompleteElement)
715715
.parentsUntil('div.query-select')
716716
.parent()
717-
.find('span.autocomp_selected > ul > li')
717+
.find('span.autocomp_selected', { timeout: 5000 })
718+
.should('exist')
719+
.find('ul > li')
718720
.should('have.length', 0);
719721
} else if (Array.isArray(expectedResults)) {
720722
// Existing positive case
721723
cy.wrap(autocompleteElement)
722724
.parentsUntil('div.query-select')
723725
.parent()
724-
.find('span.autocomp_selected > ul > li')
726+
.find('span.autocomp_selected', { timeout: 5000 })
727+
.should('exist')
728+
.find('ul > li')
725729
.should(($lis) => {
726730
expect($lis.length, 'Number of results returned by the search').to.equal(expectedResults.length);
727731
expectedResults.forEach((text, idx) => {

src/app/item/item.component.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,41 @@
1+
/* Style the internal drop slot for correct drag-over effect */
2+
:host ::ng-deep .node-drop-slot {
3+
height: 4px !important;
4+
width: 100% !important;
5+
display: flex !important;
6+
align-items: center !important;
7+
justify-content: center !important;
8+
cursor: pointer;
9+
border-radius: 4px;
10+
margin-top: 0;
11+
margin-bottom: 4px;
12+
position: relative;
13+
}
14+
15+
/* Hide a tree-node-drop-slot element that is:
16+
- not at level 1 (root)
17+
- is the last child (:last-of-type)
18+
*/
19+
:host ::ng-deep [class*="tree-node-level-"]:not(.tree-node-level-1) tree-node-drop-slot:last-of-type {
20+
height: 0px !important;
21+
display: none !important;
22+
}
23+
24+
:host ::ng-deep .node-drop-slot.is-dragging-over {
25+
height: 30px !important;
26+
background: #ddffee;
27+
border: 2px dotted #888;
28+
}
29+
30+
/* Show a message during the `drag-over` event at the root level (level 1) */
31+
:host ::ng-deep [class*="tree-node-level-1"] .node-drop-slot.is-dragging-over::after {
32+
content: "Drop at root level";
33+
}
34+
35+
/* Show a message during the `drag-over` event at the child level (level > 1) */
36+
:host ::ng-deep [class*="tree-node-level-"]:not(.tree-node-level-1) .node-drop-slot.is-dragging-over::after {
37+
content: "Drop at child level";
38+
}
139

240
.node-content-wrapper {
341
width: 100%;
@@ -99,3 +137,8 @@
99137
white-space: nowrap;
100138
z-index: 1500 !important;
101139
}
140+
141+
.drop-target {
142+
background-color: #d4f8e8;
143+
border: 1px dashed #111111;
144+
}

src/app/item/item.component.html

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
</div>
4242
</ng-template>
4343
<ng-template #treeNodeTemplate let-node let-index="index">
44-
<div [ngClass]="node.nodeClass" class="d-flex" #tooltip="matTooltip" [matTooltip]="node.displayField">
44+
<div [ngClass]="node.nodeClass" class="d-flex" #tooltip="matTooltip" [matTooltip]="node.displayField"
45+
(dragover)="onDragOver(node)" (dragleave)="onDragLeave(node)"
46+
[ngClass]="{ 'drop-target': node.data?.__$isHovered }" >
4547
<span class="node-display-prefix" aria-hidden="true">{{getIndexPath(node).join('.')}}</span>
4648
<span aria-hidden="true" class="text-truncate flex-fill node-display-field" [ngClass]="node.nodeClass">{{node.displayField}}</span>
4749
<fa-icon id="error" *ngIf="hasError(node)" [icon]="errorIcon" style="color: red" aria-hidden="!!errors"></fa-icon>&nbsp;
@@ -67,7 +69,11 @@
6769
The questions are organized in hierarchical tree on the left side. Select the desired item to edit their properties here.
6870
</li>
6971
<li *ngIf="(errors$ | async) || isTreeNodeError" class="text-danger list-group-item-warning"
70-
role="region" aria-live="assertive">{{errorMessage}}</li>
72+
role="region" aria-live="assertive">{{errorMessage}}
73+
<ul>
74+
<li *ngFor="let errorMsg of getNodeErrors()">{{errorMsg}}</li>
75+
</ul>
76+
</li>
7177
<li *ngIf="isChildTreeNodeError" class="text-danger list-group-item-warning"
7278
role="region" aria-live="assertive">{{childErrorMessage}}</li>
7379
</ul>

0 commit comments

Comments
 (0)