Skip to content

Commit fe72c5a

Browse files
alopezoclaude
andcommitted
Improve ECL builder resilience and bindings sandbox UX
- Fall back to < 762705008 attribute range and << 138875005 value range when MRCM is unavailable; show a subtle info hint instead of silently failing - Retry ecl-string-to-model and ecl-model-to-string against the central Snowstorm server when the configured server lacks those util endpoints - Change answer options ECL field to a 3-row textarea - Widen properties panel to 1/3 of the workspace layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 88d48b0 commit fe72c5a

6 files changed

Lines changed: 60 additions & 19 deletions

File tree

src/app/bindings-sandbox/bindings-sandbox.component.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
.workspace-layout {
4545
position: relative;
4646
display: grid;
47-
grid-template-columns: minmax(0, 1fr) 420px;
47+
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
4848
gap: var(--bs-space-4);
4949
align-items: start;
5050
}

src/app/bindings-sandbox/bindings-sandbox.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ <h4>Answer</h4>
260260
@if (newBindingForm.controls.type.value != 'Section header' && newBindingForm.controls.type.value != 'Text box' && newBindingForm.controls.type.value != 'Checkbox' && newBindingForm.controls.type.value != 'Integer' && newBindingForm.controls.type.value != 'Decimal') {
261261
<mat-form-field class="input-field">
262262
<mat-label>Answer options: ECL Expression</mat-label>
263-
<input matInput type="text" formControlName="ecl" placeholder="Enter ECL...">
263+
<textarea matInput rows="3" formControlName="ecl" placeholder="Enter ECL..."></textarea>
264264
<a href="javascript:void(0)" class="field-builder-link" (click)="openEclBuilder(newBindingForm.get('ecl')?.value, 'ecl')">ECL Builder</a>
265265
@if (newBindingForm.get('ecl')?.value) {
266266
<button matSuffix mat-icon-button aria-label="Clear" (click)="newBindingForm.get('ecl')?.reset()">

src/app/bindings/ecl-builder/attribute-editor/attribute-editor.component.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@
5151
color: rgba(0, 0, 0, 0.7);
5252
}
5353

54+
.mrcm-fallback-hint {
55+
display: flex;
56+
align-items: center;
57+
gap: 4px;
58+
font-size: 12px;
59+
color: var(--mat-sys-outline);
60+
}
61+
62+
.mrcm-fallback-hint .hint-icon {
63+
font-size: 14px;
64+
width: 14px;
65+
height: 14px;
66+
}
67+
5468
@media (max-width: 900px) {
5569
.field-grid {
5670
grid-template-columns: 1fr;

src/app/bindings/ecl-builder/attribute-editor/attribute-editor.component.html

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
</div>
1414
}
1515

16+
@if (mrcmFailed) {
17+
<div class="mrcm-fallback-hint">
18+
<mat-icon class="hint-icon">info_outline</mat-icon>
19+
<span>MRCM unavailable — showing all attributes</span>
20+
</div>
21+
}
22+
1623
<div class="field-grid">
1724
<app-constraint-operator-select
1825
class="attribute-operator-cell"
@@ -25,7 +32,7 @@
2532

2633
<app-multi-prefix-select-binding
2734
class="attribute-name-cell"
28-
[binding]="{ title: 'Attribute name', note: domainAttributesEcl ? 'Restricted by MRCM' : '', ecl: domainAttributesEcl, type: 'Select' }"
35+
[binding]="{ title: 'Attribute name', note: mrcmFailed ? '' : (domainAttributesEcl ? 'Restricted by MRCM' : ''), ecl: domainAttributesEcl, type: 'Select' }"
2936
[term]="attributeNameSelection"
3037
[readonly]="readonly"
3138
[appearance]="'outline'"
@@ -57,7 +64,7 @@
5764
<app-concept-picker
5865
class="value-cell"
5966
[label]="'Value'"
60-
[note]="getAttributeRangeEcl() ? 'Constrained by attribute range' : ''"
67+
[note]="mrcmFailed ? 'Any SNOMED concept' : (getAttributeRangeEcl() ? 'Constrained by attribute range' : '')"
6168
[ecl]="getAttributeRangeEcl()"
6269
[conceptId]="valueConceptId"
6370
[readonly]="readonly"

src/app/bindings/ecl-builder/attribute-editor/attribute-editor.component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class AttributeEditorComponent implements OnChanges {
1919
domainAttributes: EclDomainAttribute[] = [];
2020
domainAttributesEcl = '';
2121
loadingDomainAttributes = false;
22+
mrcmFailed = false;
2223
attributeNameSelection: { code: string; display: string } | null = null;
2324

2425
readonly comparisonOperators: EclComparisonOperator[] = ['=', '!='];
@@ -96,6 +97,7 @@ export class AttributeEditorComponent implements OnChanges {
9697
}
9798

9899
getAttributeRangeEcl(): string {
100+
if (this.mrcmFailed) return '<< 138875005';
99101
const conceptId = this.extractConceptId(this.attribute.attributeName.conceptId);
100102
const matching = this.domainAttributes.find((item) => item.conceptId === conceptId);
101103
return matching?.attributeRange?.[0]?.rangeConstraint || '';
@@ -109,6 +111,7 @@ export class AttributeEditorComponent implements OnChanges {
109111
return;
110112
}
111113

114+
this.mrcmFailed = false;
112115
this.loadingDomainAttributes = true;
113116
this.eclBuilderService.getDomainAttributes(conceptId).subscribe({
114117
next: (attributes) => {
@@ -119,8 +122,9 @@ export class AttributeEditorComponent implements OnChanges {
119122
this.loadingDomainAttributes = false;
120123
},
121124
error: () => {
125+
this.mrcmFailed = true;
122126
this.domainAttributes = [];
123-
this.domainAttributesEcl = '';
127+
this.domainAttributesEcl = '< 762705008';
124128
this.loadingDomainAttributes = false;
125129
}
126130
});

src/app/bindings/ecl-builder/ecl-builder.service.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Injectable } from '@angular/core';
22
import { HttpClient, HttpHeaders } from '@angular/common/http';
3-
import { Observable, map, of } from 'rxjs';
3+
import { Observable, catchError, map, of } from 'rxjs';
44
import { TerminologyService } from '../../services/terminology.service';
55
import {
66
EclAttribute,
@@ -26,27 +26,43 @@ export class EclBuilderService {
2626
return of(this.transformIn({ wildcard: true, returnAllMemberFields: false }));
2727
}
2828

29+
const headers = new HttpHeaders({ 'Content-Type': 'text/plain' });
30+
const options = { headers, responseType: 'json' as const };
31+
2932
return this.http
30-
.post<any>(`${this.getSnowstormBaseUrl()}/util/ecl-string-to-model`, normalized, {
31-
headers: new HttpHeaders({
32-
'Content-Type': 'text/plain'
33-
}),
34-
responseType: 'json' as const
35-
})
36-
.pipe(map((model) => this.transformIn(model)));
33+
.post<any>(`${this.getSnowstormBaseUrl()}/util/ecl-string-to-model`, normalized, options)
34+
.pipe(
35+
catchError(() =>
36+
this.http.post<any>(
37+
`${this.fallbackSnowstormBase}/util/ecl-string-to-model`,
38+
normalized,
39+
options
40+
)
41+
),
42+
map((model) => this.transformIn(model))
43+
);
3744
}
3845

46+
private readonly fallbackSnowstormBase = 'https://snowstorm.ihtsdotools.org/snowstorm/snomed-ct';
47+
3948
modelToString(model: EclExpressionConstraint): Observable<string> {
4049
const clonedModel = this.deepClone(model);
4150
this.transformOut(clonedModel);
4251

52+
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
53+
4354
return this.http
44-
.post<any>(`${this.getSnowstormBaseUrl()}/util/ecl-model-to-string`, clonedModel, {
45-
headers: new HttpHeaders({
46-
'Content-Type': 'application/json'
47-
})
48-
})
49-
.pipe(map((response) => response?.eclString ?? ''));
55+
.post<any>(`${this.getSnowstormBaseUrl()}/util/ecl-model-to-string`, clonedModel, { headers })
56+
.pipe(
57+
catchError(() =>
58+
this.http.post<any>(
59+
`${this.fallbackSnowstormBase}/util/ecl-model-to-string`,
60+
clonedModel,
61+
{ headers }
62+
)
63+
),
64+
map((response) => response?.eclString ?? '')
65+
);
5066
}
5167

5268
getDomainAttributes(parentConceptId: string): Observable<EclDomainAttribute[]> {

0 commit comments

Comments
 (0)