Skip to content

Commit 47532c0

Browse files
makdenissmakdeniss
authored andcommitted
feat: add delete resource confirmation dialog (#339)
1 parent f9859fe commit 47532c0

File tree

7 files changed

+382
-23
lines changed

7 files changed

+382
-23
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<ui5-dialog #dialog>
2+
<ui5-bar slot="header" design="Header">
3+
<ui5-title slot="startContent">
4+
<ui5-icon name="alert" design="Critical"></ui5-icon>
5+
Delete {{ innerResource()?.metadata?.name?.toLowerCase() }}
6+
</ui5-title>
7+
</ui5-bar>
8+
<section class="content" [formGroup]="form">
9+
<div class="inputs">
10+
<p>Are you sure you want to delete {{ context()?.resourceDefinition?.singular }}
11+
<b>{{ innerResource()?.metadata?.name?.toLowerCase() }}</b>?</p>
12+
<ui5-text style="color: var(--sapCriticalElementColor)">
13+
This action <b>cannot</b> be undone.
14+
</ui5-text>
15+
<p>
16+
Please type <b>{{ innerResource()?.metadata?.name.toLowerCase() }}</b> to confirm:
17+
</p>
18+
<ui5-input
19+
class="input"
20+
placeholder="Type name"
21+
[value]="form.controls.resource.value"
22+
(blur)="onFieldBlur('resource')"
23+
(change)="setFormControlValue($event, 'resource')"
24+
(input)="setFormControlValue($event, 'resource')"
25+
[valueState]="getValueState('resource')"
26+
required
27+
></ui5-input>
28+
</div>
29+
</section>
30+
<ui5-toolbar class="ui5-content-density-compact" slot="footer">
31+
<ui5-toolbar-button
32+
class="dialogCloser"
33+
[disabled]="form.invalid || form.controls.resource.value !== innerResource()?.metadata?.name?.toLowerCase()"
34+
design="Emphasized"
35+
text="Delete"
36+
(click)="delete()"
37+
>
38+
</ui5-toolbar-button>
39+
<ui5-toolbar-button
40+
class="dialogCloser"
41+
design="Transparent"
42+
text="Cancel"
43+
(click)="close()"
44+
>
45+
</ui5-toolbar-button>
46+
</ui5-toolbar>
47+
</ui5-dialog>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.content {
2+
display: flex;
3+
flex-direction: column;
4+
justify-content: space-evenly;
5+
align-items: flex-start;
6+
margin-bottom: 0.5rem;
7+
width: 100%;
8+
}
9+
10+
.input {
11+
width: 100%;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { DeleteResourceModalComponent } from './delete-resource-modal.component';
2+
import { CommonModule } from '@angular/common';
3+
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
4+
import { ComponentFixture, TestBed } from '@angular/core/testing';
5+
import { ReactiveFormsModule } from '@angular/forms';
6+
7+
jest.mock('@ui5/webcomponents-ngx', () => ({}), { virtual: true });
8+
9+
describe('DeleteResourceModalComponent', () => {
10+
let component: DeleteResourceModalComponent;
11+
let fixture: ComponentFixture<DeleteResourceModalComponent>;
12+
let mockDialog: any;
13+
14+
const resource: any = { metadata: { name: 'TestName' } };
15+
16+
beforeEach(async () => {
17+
await TestBed.configureTestingModule({
18+
imports: [CommonModule, ReactiveFormsModule],
19+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
20+
})
21+
.overrideComponent(DeleteResourceModalComponent, {
22+
set: {
23+
imports: [CommonModule, ReactiveFormsModule],
24+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
25+
},
26+
})
27+
.compileComponents();
28+
29+
fixture = TestBed.createComponent(DeleteResourceModalComponent);
30+
component = fixture.componentInstance;
31+
32+
mockDialog = { open: false };
33+
(component as any).dialog = () => mockDialog;
34+
35+
component.ngOnInit();
36+
fixture.detectChanges();
37+
});
38+
39+
it('should create the component', () => {
40+
expect(component).toBeTruthy();
41+
});
42+
43+
it('should initialize the form with "resource" control', () => {
44+
expect(component.form).toBeDefined();
45+
expect(component.form.controls['resource']).toBeDefined();
46+
});
47+
48+
it('should set dialog open and store innerResource', () => {
49+
component.open(resource);
50+
expect(mockDialog.open).toBeTruthy();
51+
expect(component.innerResource()).toBe(resource);
52+
});
53+
54+
it('should set dialog closed when closing', () => {
55+
mockDialog.open = true;
56+
component.close();
57+
expect(mockDialog.open).toBeFalsy();
58+
});
59+
60+
it('should be invalid when empty or mismatched; valid when matches innerResource.name', () => {
61+
component.open(resource);
62+
const control = component.form.controls['resource'];
63+
64+
control.setValue('');
65+
control.markAsTouched();
66+
fixture.detectChanges();
67+
expect(control.invalid).toBeTruthy();
68+
expect(control.hasError('invalidResource')).toBeTruthy();
69+
70+
control.setValue('WrongName');
71+
fixture.detectChanges();
72+
expect(control.invalid).toBeTruthy();
73+
expect(control.hasError('invalidResource')).toBeTruthy();
74+
75+
control.setValue('TestName');
76+
fixture.detectChanges();
77+
expect(control.valid).toBeTruthy();
78+
expect(control.errors).toBeNull();
79+
});
80+
81+
it('should emit the resource and close the dialog when deleting resource', () => {
82+
component.open(resource);
83+
spyOn(component.resource, 'emit');
84+
component.delete();
85+
expect(component.resource.emit).toHaveBeenCalledWith(resource);
86+
expect(mockDialog.open).toBeFalsy();
87+
});
88+
89+
it('should set value and marks touched/dirty', () => {
90+
const control = component.form.controls['resource'];
91+
spyOn(control, 'setValue');
92+
spyOn(control, 'markAsTouched');
93+
spyOn(control, 'markAsDirty');
94+
95+
component.setFormControlValue(
96+
{ target: { value: 'SomeValue' } } as any,
97+
'resource',
98+
);
99+
100+
expect(control.setValue).toHaveBeenCalledWith('SomeValue');
101+
expect(control.markAsTouched).toHaveBeenCalled();
102+
expect(control.markAsDirty).toHaveBeenCalled();
103+
});
104+
105+
it('should return "Negative" for invalid+touched, else "None"', () => {
106+
const control = component.form.controls['resource'];
107+
108+
control.setValue('');
109+
control.markAsTouched();
110+
fixture.detectChanges();
111+
expect(component.getValueState('resource')).toBe('Negative');
112+
113+
component.open(resource);
114+
control.setValue('TestName');
115+
fixture.detectChanges();
116+
expect(component.getValueState('resource')).toBe('None');
117+
118+
control.setValue('');
119+
control.markAsUntouched();
120+
fixture.detectChanges();
121+
expect(component.getValueState('resource')).toBe('None');
122+
});
123+
124+
it('should mark the control as touched', () => {
125+
const control = component.form.controls['resource'];
126+
spyOn(control, 'markAsTouched');
127+
component.onFieldBlur('resource');
128+
expect(control.markAsTouched).toHaveBeenCalled();
129+
});
130+
131+
it('should render title with resource name in lowercase in the header', () => {
132+
component.open(resource);
133+
fixture.detectChanges();
134+
const title = fixture.nativeElement.querySelector('ui5-title');
135+
expect(title?.textContent?.toLowerCase()).toContain('delete testname');
136+
});
137+
138+
it('should render prompt text with resource name and cannot be undone note', () => {
139+
component.open(resource);
140+
(component as any).context = () => ({
141+
resourceDefinition: { singular: 'resource' },
142+
});
143+
fixture.detectChanges();
144+
const content = fixture.nativeElement.querySelector('section.content');
145+
const text = content?.textContent?.toLowerCase() || '';
146+
expect(text).toContain('are you sure you want to delete');
147+
expect(text).toContain('testname');
148+
expect(text).toContain('cannot');
149+
});
150+
151+
it('should bind input value to form control and show Negative valueState when invalid and touched', () => {
152+
component.open(resource);
153+
fixture.detectChanges();
154+
155+
const inputEl: HTMLElement & {
156+
value?: string;
157+
valueState?: string;
158+
dispatchEvent?: any;
159+
} = fixture.nativeElement.querySelector('ui5-input');
160+
expect(inputEl).toBeTruthy();
161+
162+
component.setFormControlValue(
163+
{ target: { value: 'wrong' } } as any,
164+
'resource',
165+
);
166+
component.onFieldBlur('resource');
167+
fixture.detectChanges();
168+
169+
expect(component.form.controls['resource'].invalid).toBeTruthy();
170+
expect(component.getValueState('resource')).toBe('Negative');
171+
});
172+
173+
it('should close dialog when Cancel button clicked', () => {
174+
component.open(resource);
175+
mockDialog.open = true;
176+
fixture.detectChanges();
177+
178+
const cancelBtn: HTMLElement = fixture.nativeElement.querySelector(
179+
'ui5-toolbar-button[design="Transparent"]',
180+
);
181+
expect(cancelBtn).toBeTruthy();
182+
183+
cancelBtn.dispatchEvent(new Event('click'));
184+
fixture.detectChanges();
185+
expect(mockDialog.open).toBeFalsy();
186+
});
187+
});
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
OnInit,
5+
inject,
6+
input,
7+
output,
8+
signal,
9+
viewChild,
10+
} from '@angular/core';
11+
import {
12+
FormBuilder,
13+
FormControl,
14+
FormGroup,
15+
ReactiveFormsModule,
16+
Validators,
17+
} from '@angular/forms';
18+
import { Resource } from '@openmfp/portal-ui-lib';
19+
import {
20+
BarComponent,
21+
DialogComponent,
22+
IconComponent,
23+
InputComponent,
24+
TextComponent,
25+
TitleComponent,
26+
ToolbarButtonComponent,
27+
ToolbarComponent,
28+
} from '@ui5/webcomponents-ngx';
29+
import {ResourceNodeContext} from '@platform-mesh/portal-ui-lib/services';
30+
31+
@Component({
32+
selector: 'delete-resource-modal',
33+
standalone: true,
34+
imports: [
35+
ReactiveFormsModule,
36+
DialogComponent,
37+
TitleComponent,
38+
ToolbarButtonComponent,
39+
ToolbarComponent,
40+
InputComponent,
41+
BarComponent,
42+
IconComponent,
43+
TextComponent,
44+
],
45+
templateUrl: './delete-resource-modal.component.html',
46+
styleUrl: './delete-resource-modal.component.scss',
47+
changeDetection: ChangeDetectionStrategy.OnPush,
48+
})
49+
export class DeleteResourceModalComponent implements OnInit {
50+
context = input<ResourceNodeContext>();
51+
dialog = viewChild<DialogComponent>('dialog');
52+
innerResource = signal<Resource | null>(null);
53+
54+
resource = output<Resource>();
55+
56+
fb = inject(FormBuilder);
57+
form: FormGroup;
58+
59+
ngOnInit(): void {
60+
this.form = this.fb.group(this.createControls());
61+
this.form.controls.resource.valueChanges.subscribe((value) => {
62+
if (!value || this.innerResource()?.metadata?.name !== value) {
63+
this.form.controls.resource.setErrors({ invalidResource: true });
64+
} else {
65+
this.form.controls.resource.setErrors(null);
66+
}
67+
});
68+
}
69+
70+
open(resource: Resource): void {
71+
const dialog = this.dialog();
72+
if (dialog) {
73+
dialog.open = true;
74+
this.innerResource.set(resource);
75+
}
76+
}
77+
78+
close(): void {
79+
const dialog = this.dialog();
80+
if (dialog) {
81+
this.form.controls.resource.setValue(null);
82+
dialog.open = false;
83+
}
84+
}
85+
86+
delete(): void {
87+
const res = this.innerResource();
88+
if (res) {
89+
this.resource.emit(res);
90+
}
91+
this.close();
92+
}
93+
94+
setFormControlValue($event: any, formControlName: string) {
95+
this.form.controls[formControlName].setValue($event.target.value);
96+
this.form.controls[formControlName].markAsTouched();
97+
this.form.controls[formControlName].markAsDirty();
98+
}
99+
100+
getValueState(formControlName: string) {
101+
const control = this.form.controls[formControlName];
102+
return control.invalid && control.touched ? 'Negative' : 'None';
103+
}
104+
105+
onFieldBlur(formControlName: string) {
106+
this.form.controls[formControlName].markAsTouched();
107+
}
108+
109+
private createControls() {
110+
return {
111+
resource: new FormControl(null, Validators.required),
112+
};
113+
}
114+
}

projects/wc/src/app/components/generic-ui/list-view/list-view.component.html

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
}
5858
<ui5-table-cell
5959
class="actions-column delete-item"
60-
(click)="delete($event, item)"
60+
(click)="openDeleteResourceModal($event, item)"
6161
>
6262
<ui5-icon name="delete"></ui5-icon>
6363
</ui5-table-cell>
@@ -74,3 +74,9 @@
7474
[context]="context()"
7575
/>
7676
}
77+
78+
<delete-resource-modal
79+
#deleteModal
80+
(resource)="delete($event)"
81+
[context]="context()"
82+
/>

0 commit comments

Comments
 (0)