Skip to content

Commit d7b2a9c

Browse files
authored
LEGLINK-18: Normalization - Add extension removal support - UI (#1643)
* Add extension removal operation * Add unit and integration tests * Add UI support for new operation * Document new operation * Support ad hoc testing of new operation * Disallow leading/trailing whitespace
1 parent 1e9f032 commit d7b2a9c

13 files changed

Lines changed: 603 additions & 4 deletions

File tree

DotNet/Normalization/Application/Services/Operations/OperationServiceHelper.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,12 @@ private static void SetPropertyValue(Base parentPoco, PropertyInfo property, obj
842842
{
843843
return (false, "RemoveExtensionsOperation.ExtensionUrls must not contain null or empty entries.");
844844
}
845+
846+
var paddedUrls = op.ExtensionUrls.Where(u => u != u.Trim()).ToList();
847+
if (paddedUrls.Any())
848+
{
849+
return (false, "RemoveExtensionsOperation.ExtensionUrls must not contain entries with leading or trailing whitespace.");
850+
}
845851
}
846852
else
847853
{

Web/Admin.UI/src/app/components/normalization/operations/operation-dialog/operation-dialog.component.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ <h1 mat-dialog-title class="dialog-form-header">{{dialogTitle}}</h1>
1212
@if (operationType === OperationType.CopyLocation) {
1313
<app-copy-location #copylocation [operation]="operation" [formMode]="formMode" [viewOnly]="viewOnly" (formValueChanged)="onFormValueChanged($event)" (submittedConfiguration)="onSubmittedConfiguration($event)"></app-copy-location>
1414
}
15+
@if (operationType === OperationType.RemoveExtensions) {
16+
<app-remove-extensions [operation]="operation" [formMode]="formMode" [viewOnly]="viewOnly" (formValueChanged)="onFormValueChanged($event)" (submittedConfiguration)="onSubmittedConfiguration($event)"></app-remove-extensions>
17+
}
1518
</div>
1619
<div mat-dialog-actions align="end">
1720
@if (!viewOnly) {

Web/Admin.UI/src/app/components/normalization/operations/operation-dialog/operation-dialog.component.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ import {OperationType} from "../../../../interfaces/normalization/operation-type
1313
import {ConditionalTransformationComponent} from "../conditional-transformation/conditional-transformation.component";
1414
import {CodeMapComponent} from "../code-map/code-map.component";
1515
import {CopyLocationComponent} from "../copy-location/copy-location.component";
16+
import {RemoveExtensionsComponent} from "../remove-extensions/remove-extensions.component";
1617

1718
@Component({
1819
selector: 'app-normalization-dialog',
1920
standalone: true,
20-
imports: [MatDialogModule, MatButtonModule, MatIconModule, CopyPropertyComponent, ConditionalTransformationComponent, CodeMapComponent, CopyLocationComponent],
21+
imports: [MatDialogModule, MatButtonModule, MatIconModule, CopyPropertyComponent, ConditionalTransformationComponent, CodeMapComponent, CopyLocationComponent, RemoveExtensionsComponent],
2122
templateUrl: './operation-dialog.component.html',
2223
styleUrl: './operation-dialog.component.scss'
2324
})
@@ -27,6 +28,7 @@ export class OperationDialogComponent implements OnInit {
2728
@ViewChild(ConditionalTransformationComponent) conditionalTransformForm!: ConditionalTransformationComponent;
2829
@ViewChild(CodeMapComponent) codeMapForm!: CodeMapComponent;
2930
@ViewChild(CopyLocationComponent) copyLocationForm!: CopyLocationComponent;
31+
@ViewChild(RemoveExtensionsComponent) removeExtensionsForm!: RemoveExtensionsComponent;
3032

3133
dialogTitle: string = '';
3234
viewOnly: boolean = false;
@@ -91,6 +93,9 @@ export class OperationDialogComponent implements OnInit {
9193
case OperationType.CopyLocation:
9294
this.copyLocationForm?.submitConfiguration();
9395
break;
96+
case OperationType.RemoveExtensions:
97+
this.removeExtensionsForm?.submitConfiguration();
98+
break;
9499
default:
95100
console.warn('Unknown operation type:', this.operationType);
96101
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<mat-card-content>
2+
3+
@if (errorMessage) {
4+
<div class="error-snackbar" #errorDiv>
5+
<mat-icon color="warn">error_outline</mat-icon>
6+
{{ errorMessage }}
7+
</div>
8+
}
9+
<form [formGroup]="form" class="operation-form">
10+
<h2 class="section-title">Operation Details</h2>
11+
12+
<!-- Facility Id -->
13+
@if (!isVendorMode) {
14+
<mat-form-field appearance="outline" class="full-width">
15+
<mat-label>Facility Id</mat-label>
16+
<input matInput formControlName="facilityId" [disabled]="true" placeholder="Facility Id"/>
17+
</mat-form-field>
18+
}
19+
20+
<!-- Vendor Id -->
21+
@if (isVendorMode) {
22+
<mat-form-field appearance="outline" class="full-width">
23+
<mat-label>Select Vendor</mat-label>
24+
<mat-select formControlName="selectedVendor" [disabled]="viewOnly" multiple>
25+
@for (vendor of vendors; track vendor) {
26+
<mat-option [value]="vendor.id">{{ vendor.name }}</mat-option>
27+
}
28+
</mat-select>
29+
</mat-form-field>
30+
}
31+
32+
@if (showFacilityOrVendorError) {
33+
<div class="validation-error">
34+
Vendor is <strong>required</strong>
35+
</div>
36+
}
37+
38+
<!-- Name -->
39+
<mat-form-field appearance="fill" class="full-width">
40+
<mat-label>Name</mat-label>
41+
<input matInput formControlName="name" [readonly]="viewOnly"/>
42+
@if (nameControl.value && !viewOnly) {
43+
<button matSuffix mat-icon-button aria-label="Clear" (click)="clearName()">
44+
<mat-icon>close</mat-icon>
45+
</button>
46+
}
47+
@if (nameControl.hasError('required') && (nameControl.touched || nameControl.dirty)) {
48+
<mat-error>Name is <strong>required</strong></mat-error>
49+
}
50+
</mat-form-field>
51+
52+
<!-- Description -->
53+
<mat-form-field appearance="fill" class="full-width">
54+
<mat-label>Description</mat-label>
55+
<input matInput formControlName="description" [readonly]="viewOnly"/>
56+
@if (descriptionControl.value && !viewOnly) {
57+
<button matSuffix mat-icon-button aria-label="Clear" (click)="clearDescription()">
58+
<mat-icon>close</mat-icon>
59+
</button>
60+
}
61+
</mat-form-field>
62+
63+
<!-- Resource Types -->
64+
@if (selectedResourceTypesControl.value?.length > 0) {
65+
<div class="selected-resource-types">
66+
<strong>Selected Resource Types: </strong> {{ selectedResourceTypesControl.value.join(', ') }}
67+
</div>
68+
}
69+
70+
<mat-form-field appearance="outline" class="full-width">
71+
<mat-label>Select Resource Types</mat-label>
72+
<input
73+
matInput
74+
placeholder="Start typing to search"
75+
[formControl]="resourceTypeControl"
76+
[matAutocomplete]="auto"
77+
(keydown)="onInputKeydown($event)"
78+
(mousedown)="userClicked = true"
79+
[readonly]="viewOnly"
80+
(focus)="openAutocompletePanel()"/>
81+
<mat-autocomplete #auto="matAutocomplete" autoActiveFirstOption="">
82+
@for (type of filteredResourceTypes; track type) {
83+
<mat-option [value]="type">
84+
<mat-checkbox [checked]="selectedResourceTypesControl.value?.includes(type)"
85+
(click)="$event.stopPropagation()"
86+
(change)="toggleSelection(type, $event.source.checked)">
87+
{{ type }}
88+
</mat-checkbox>
89+
</mat-option>
90+
}
91+
</mat-autocomplete>
92+
</mat-form-field>
93+
94+
@if (selectedResourceTypesControl.hasError('required') && (resourceTypeControl.touched || resourceTypeControl.dirty)) {
95+
<div class="validation-error">Resource Type is <strong>required</strong></div>
96+
}
97+
98+
<!-- Extension URLs -->
99+
<h2 class="section-title">Extension URLs to Remove</h2>
100+
101+
<div formArrayName="extensionUrls">
102+
@for (ctrl of extensionUrlsArray.controls; track ctrl; let i = $index) {
103+
<div class="url-row">
104+
<mat-form-field appearance="fill" class="full-width">
105+
<mat-label>Extension URL {{ i + 1 }}</mat-label>
106+
<input matInput [formControlName]="i" [readonly]="viewOnly" placeholder="http://example.com/fhir/extension/..."/>
107+
@if (ctrl.hasError('required') && (ctrl.touched || ctrl.dirty)) {
108+
<mat-error>URL is <strong>required</strong></mat-error>
109+
}
110+
@if (ctrl.hasError('whitespace') && (ctrl.touched || ctrl.dirty)) {
111+
<mat-error>URL must not have leading or trailing whitespace</mat-error>
112+
}
113+
</mat-form-field>
114+
@if (!viewOnly) {
115+
<button mat-icon-button color="warn" type="button" aria-label="Remove URL" (click)="removeExtensionUrl(i)">
116+
<mat-icon>close</mat-icon>
117+
</button>
118+
}
119+
</div>
120+
}
121+
</div>
122+
123+
@if (showUrlListError) {
124+
<div class="validation-error">At least one extension URL is <strong>required</strong></div>
125+
}
126+
127+
@if (!viewOnly) {
128+
<div>
129+
<button mat-stroked-button type="button" (click)="addExtensionUrl()">
130+
<mat-icon>add</mat-icon>
131+
Add URL
132+
</button>
133+
</div>
134+
}
135+
136+
<!-- Enable/Disable checkbox -->
137+
<mat-checkbox formControlName="isEnabled">Enabled</mat-checkbox>
138+
</form>
139+
</mat-card-content>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
@use '../../../../../styles/core.scss';
2+
3+
$primary: #398eb4;
4+
5+
.operation-form {
6+
display: flex;
7+
flex-direction: column;
8+
gap: 16px;
9+
}
10+
11+
.section-title {
12+
margin-top: 24px;
13+
font-size: 20px;
14+
font-weight: 600;
15+
color: $primary;
16+
border-bottom: 2px solid #e0e0e0;
17+
padding-bottom: 4px;
18+
}
19+
20+
.full-width {
21+
width: 100%;
22+
}
23+
24+
.validation-error {
25+
color: #ba1a1a;
26+
font-size: 12px;
27+
margin-top: -8px;
28+
margin-left: 4px;
29+
}
30+
31+
.selected-resource-types {
32+
margin-bottom: 12px;
33+
}
34+
35+
.url-row {
36+
display: flex;
37+
align-items: center;
38+
gap: 8px;
39+
40+
mat-form-field {
41+
flex: 1;
42+
}
43+
}

0 commit comments

Comments
 (0)