Skip to content

Commit 38b5a73

Browse files
nabramovitznorman-abramovitz
authored andcommitted
User invite dialog: signal-native rewrite
Component had eight Observable<boolean> fields (connecting$, valid$, isBusy$, etc.) declared but never assigned — the | async pipes in the template were always undefined, so the loading bar / error banner / submit-disable were effectively static. Rewritten to actually drive UI state: signal()s for showSecret, isBusy, hasErrored; toSignal(form.statusChanges) for formStatus; computed() for formValid + canSubmit. submit() now toggles busy/error state during the configure call and surfaces failures via sticky red snackBar.error(). Template drops | async pipes for signal calls; show-secret toggle moves to a method; Cancel button gets type="button" so it doesn't accidentally submit; submit button binds [disabled]="!canSubmit()". UserInviteService public surface unchanged — left alone since it was already clean (HttpClient, no Store/ngrx coupling) and has multiple downstream consumers.
1 parent 8f8a2e2 commit 38b5a73

2 files changed

Lines changed: 75 additions & 58 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<div class="connection-dialog__loading-wrapper">
22
<app-progress-bar class="connection-dialog__loading"
3-
[color]="(connectingError$ | async) && (isBusy$ | async) === false ? 'warn' : 'primary'"
4-
[mode]="(isBusy$ | async) ? 'indeterminate' : 'solid'">
3+
[color]="hasErrored() && !isBusy() ? 'warn' : 'primary'"
4+
[mode]="isBusy() ? 'indeterminate' : 'solid'">
55
</app-progress-bar>
66
</div>
77
<div class="connection-dialog">
@@ -18,18 +18,18 @@ <h2 class="dialog-title">
1818
</div>
1919
<div class="form-field">
2020
<input class="form-input" placeholder="Client Secret" formControlName="clientSecret"
21-
[type]="!showSecret ? 'password' : 'text'">
22-
<button class="btn-icon" (click)="showSecret = !showSecret" [attr.aria-label]="'Hide Secret'"
23-
[attr.aria-pressed]="!showSecret" type='button'>
24-
<i class="material-icons">{{!showSecret ? 'visibility_off' : 'visibility'}}</i>
21+
[type]="!showSecret() ? 'password' : 'text'">
22+
<button class="btn-icon" (click)="toggleShowSecret()" [attr.aria-label]="'Hide Secret'"
23+
[attr.aria-pressed]="!showSecret()" type='button'>
24+
<i class="material-icons">{{!showSecret() ? 'visibility_off' : 'visibility'}}</i>
2525
</button>
2626
</div>
2727
</div>
28-
<app-dialog-error message="Could not connect, please try again." [show]="connectingError$ | async">
28+
<app-dialog-error message="Could not connect, please try again." [show]="hasErrored()">
2929
</app-dialog-error>
3030
<div class="dialog-actions connection-dialog__actions">
31-
<button (click)="dialogRef.close()" class="btn btn-warn">Cancel</button>
32-
<button type="submit" [disabled]="!endpointForm.valid" class="btn btn-primary">Configure</button>
31+
<button (click)="dialogRef.close()" type="button" class="btn btn-warn">Cancel</button>
32+
<button type="submit" [disabled]="!canSubmit()" class="btn btn-primary">Configure</button>
3333
</div>
3434
</form>
35-
</div>
35+
</div>

src/frontend/packages/cloud-foundry/src/features/cf/user-invites/configuration-dialog/user-invite-configuration-dialog.component.ts

Lines changed: 65 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,33 @@
1-
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
computed,
5+
inject,
6+
signal,
7+
} from '@angular/core';
28
import { CommonModule } from '@angular/common';
3-
import { FormBuilder, FormGroup, FormControl, Validators, ReactiveFormsModule } from '@angular/forms';
9+
import { toSignal } from '@angular/core/rxjs-interop';
10+
import {
11+
FormBuilder,
12+
FormControl,
13+
FormGroup,
14+
ReactiveFormsModule,
15+
Validators,
16+
} from '@angular/forms';
17+
import { take } from 'rxjs/operators';
418

5-
interface UserInviteConfigForm {
6-
clientID: FormControl<string>;
7-
clientSecret: FormControl<string>;
8-
}
919
import { AppProgressBarComponent } from '@stratosui/core';
1020
import { TailwindSnackBarService } from '@stratosui/core';
1121
import { TailwindDialogRef } from '@stratosui/core';
1222
import { DialogErrorComponent } from '@stratosui/core';
1323
import { MAT_DIALOG_DATA } from '@stratosui/core';
14-
import { Observable, Subscription } from 'rxjs';
15-
import { take, } from 'rxjs/operators';
1624

17-
import { ActionState } from '../../../../../../store/src/reducers/api-request-reducer/types';
1825
import { UserInviteConfigureService } from '../user-invite.service';
1926

27+
interface UserInviteConfigForm {
28+
clientID: FormControl<string>;
29+
clientSecret: FormControl<string>;
30+
}
2031

2132
@Component({
2233
selector: 'app-user-invite-configuration-dialog',
@@ -28,59 +39,65 @@ import { UserInviteConfigureService } from '../user-invite.service';
2839
CommonModule,
2940
ReactiveFormsModule,
3041
AppProgressBarComponent,
31-
DialogErrorComponent
32-
]
42+
DialogErrorComponent,
43+
],
3344
})
3445
export class UserInviteConfigurationDialogComponent {
35-
fb = inject(FormBuilder);
36-
dialogRef = inject<TailwindDialogRef<UserInviteConfigurationDialogComponent>>('TailwindDialogRef' as any);
37-
snackBar = inject(TailwindSnackBarService);
38-
userInviteConfigureService = inject(UserInviteConfigureService);
39-
data = inject<{
40-
guid: string;
41-
}>(MAT_DIALOG_DATA);
46+
private fb = inject(FormBuilder);
47+
dialogRef = inject<TailwindDialogRef<UserInviteConfigurationDialogComponent>>(
48+
'TailwindDialogRef' as any
49+
);
50+
private snackBar = inject(TailwindSnackBarService);
51+
private userInviteConfigureService = inject(UserInviteConfigureService);
52+
private data = inject<{ guid: string }>(MAT_DIALOG_DATA);
4253

43-
connecting$!: Observable<boolean>;
44-
connectingError$!: Observable<boolean>;
45-
fetchingInfo$!: Observable<boolean>;
46-
endpointConnected$!: Observable<boolean>;
47-
valid$!: Observable<boolean>;
48-
canSubmit$!: Observable<boolean>;
54+
// Local UI state — signal-native
55+
readonly showSecret = signal(false);
56+
readonly isBusy = signal(false);
57+
readonly hasErrored = signal(false);
4958

50-
51-
private update$: Observable<ActionState>;
52-
53-
isBusy$!: Observable<boolean>;
54-
55-
connectingSub!: Subscription;
56-
fetchSub!: Subscription;
5759
public endpointForm: FormGroup<UserInviteConfigForm>;
58-
59-
// We need a delay to ensure the BE has finished registering the endpoint.
60-
// If we don't do this and if we're quick enough, we can navigate to the application page
61-
// and end up with an empty list where we should have results.
62-
public connectDelay = 1000;
63-
64-
guid!: string;
65-
public showSecret = false;
60+
// Status of the reactive form lifted to a signal so the template can react
61+
// declaratively (mirrors the toSignal-at-the-boundary pattern used by other
62+
// signal-native dialogs).
63+
readonly formStatus;
64+
readonly formValid;
65+
readonly canSubmit;
6666

6767
constructor() {
6868
this.endpointForm = this.fb.group<UserInviteConfigForm>({
6969
clientID: this.fb.nonNullable.control('', Validators.required),
7070
clientSecret: this.fb.nonNullable.control('', Validators.required),
7171
});
72+
this.formStatus = toSignal(this.endpointForm.statusChanges, {
73+
initialValue: this.endpointForm.status,
74+
});
75+
this.formValid = computed(() => this.formStatus() === 'VALID');
76+
this.canSubmit = computed(() => this.formValid() && !this.isBusy());
77+
}
78+
79+
toggleShowSecret(): void {
80+
this.showSecret.update(v => !v);
7281
}
7382

74-
submit() {
75-
this.userInviteConfigureService.configure(
76-
this.data.guid,
77-
this.endpointForm.value.clientID ?? '',
78-
this.endpointForm.value.clientSecret ?? '')
79-
.pipe(
80-
take(1)
81-
).subscribe((v: any) => {
83+
submit(): void {
84+
if (!this.canSubmit()) {
85+
return;
86+
}
87+
this.isBusy.set(true);
88+
this.hasErrored.set(false);
89+
this.userInviteConfigureService
90+
.configure(
91+
this.data.guid,
92+
this.endpointForm.value.clientID ?? '',
93+
this.endpointForm.value.clientSecret ?? ''
94+
)
95+
.pipe(take(1))
96+
.subscribe(v => {
97+
this.isBusy.set(false);
8298
if (v.error) {
83-
this.snackBar.error(v.errorMessage, 'Close');
99+
this.hasErrored.set(true);
100+
this.snackBar.error(v.errorMessage ?? 'Failed to configure User Invitation');
84101
} else {
85102
this.dialogRef.close();
86103
}

0 commit comments

Comments
 (0)