1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 07:13:32 +00:00

[PM-9190] Edit Login - Autofill Options (#10274)

* [PM-8524] Update appA11yTitle to keep attributes in sync after first render

* [PM-8524] Introduce UriOptionComponent

* [PM-9190] Introduce AutofillOptionsComponent

* [PM-9190] Add AutofillOptions to LoginDetailsSection

* [PM-9190] Add autofill options component unit tests

* [PM-9190] Add UriOptionComponent unit tests

* [PM-9190] Add missing translations

* [PM-9190] Add autofill on page load field

* [PM-9190] Ensure updatedCipherView is completely separate from originalCipherView

* [CL-348] Do not override items if there are no OptionComponents available

* [PM-9190] Mock AutoFillOptions component in Login Details tests

* [PM-9190] Cleanup storybook and missing web translations

* [PM-9190] Ensure storybook decryptCipher returns a separate object
This commit is contained in:
Shane Melton
2024-08-01 08:35:04 -07:00
committed by GitHub
parent ffc9022f54
commit 0d76835cd8
17 changed files with 912 additions and 7 deletions

View File

@@ -0,0 +1,189 @@
import { LiveAnnouncer } from "@angular/cdk/a11y";
import { AsyncPipe, NgForOf, NgIf } from "@angular/common";
import { Component, OnInit, QueryList, ViewChildren } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { Subject, switchMap, take } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import {
CardComponent,
FormFieldModule,
IconButtonModule,
LinkModule,
SectionComponent,
SectionHeaderComponent,
SelectModule,
TypographyModule,
} from "@bitwarden/components";
import { CipherFormContainer } from "../../cipher-form-container";
import { UriOptionComponent } from "./uri-option.component";
interface UriField {
uri: string;
matchDetection: UriMatchStrategySetting;
}
@Component({
selector: "vault-autofill-options",
templateUrl: "./autofill-options.component.html",
standalone: true,
imports: [
SectionComponent,
SectionHeaderComponent,
TypographyModule,
JslibModule,
CardComponent,
ReactiveFormsModule,
NgForOf,
FormFieldModule,
SelectModule,
IconButtonModule,
UriOptionComponent,
LinkModule,
NgIf,
AsyncPipe,
],
})
export class AutofillOptionsComponent implements OnInit {
/**
* List of rendered UriOptionComponents. Used for focusing newly added Uri inputs.
*/
@ViewChildren(UriOptionComponent)
protected uriOptions: QueryList<UriOptionComponent>;
autofillOptionsForm = this.formBuilder.group({
uris: this.formBuilder.array<UriField>([]),
autofillOnPageLoad: [null as boolean],
});
protected get uriControls() {
return this.autofillOptionsForm.controls.uris.controls;
}
protected defaultMatchDetection$ = this.domainSettingsService.defaultUriMatchStrategy$;
protected autofillOptions: { label: string; value: boolean | null }[] = [
{ label: this.i18nService.t("default"), value: null },
{ label: this.i18nService.t("yes"), value: true },
{ label: this.i18nService.t("no"), value: false },
];
/**
* Emits when a new URI input is added to the form and should be focused.
*/
private focusOnNewInput$ = new Subject<void>();
constructor(
private cipherFormContainer: CipherFormContainer,
private formBuilder: FormBuilder,
private i18nService: I18nService,
private liveAnnouncer: LiveAnnouncer,
private domainSettingsService: DomainSettingsService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
) {
this.cipherFormContainer.registerChildForm("autoFillOptions", this.autofillOptionsForm);
this.autofillOptionsForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
this.cipherFormContainer.patchCipher((cipher) => {
cipher.login.uris = value.uris.map((uri: UriField) =>
Object.assign(new LoginUriView(), {
uri: uri.uri,
match: uri.matchDetection,
} as LoginUriView),
);
cipher.login.autofillOnPageLoad = value.autofillOnPageLoad;
return cipher;
});
});
this.updateDefaultAutofillLabel();
this.focusOnNewInput$
.pipe(
takeUntilDestroyed(),
// Wait for the new URI input to be added to the DOM
switchMap(() => this.uriOptions.changes.pipe(take(1))),
// Announce the new URI input before focusing it
switchMap(() => this.liveAnnouncer.announce(this.i18nService.t("websiteAdded"), "polite")),
)
.subscribe(() => {
this.uriOptions?.last?.focusInput();
});
}
ngOnInit() {
if (this.cipherFormContainer.originalCipherView?.login) {
this.initFromExistingCipher(this.cipherFormContainer.originalCipherView.login);
} else {
this.initNewCipher();
}
if (this.cipherFormContainer.config.mode === "partial-edit") {
this.autofillOptionsForm.disable();
}
}
private initFromExistingCipher(existingLogin: LoginView) {
existingLogin.uris?.forEach((uri) => {
this.addUri({
uri: uri.uri,
matchDetection: uri.match,
});
});
this.autofillOptionsForm.patchValue({
autofillOnPageLoad: existingLogin.autofillOnPageLoad,
});
}
private initNewCipher() {
this.addUri({
uri: this.cipherFormContainer.config.initialValues?.loginUri ?? null,
matchDetection: null,
});
this.autofillOptionsForm.patchValue({
autofillOnPageLoad: null,
});
}
private updateDefaultAutofillLabel() {
this.autofillSettingsService.autofillOnPageLoadDefault$
.pipe(takeUntilDestroyed())
.subscribe((value: boolean) => {
const defaultOption = this.autofillOptions.find((o) => o.value === value);
if (!defaultOption) {
return;
}
this.autofillOptions[0].label = this.i18nService.t("defaultLabel", defaultOption.label);
// Trigger change detection to update the label in the template
this.autofillOptions = [...this.autofillOptions];
});
}
/**
* Adds a new URI input to the form.
* @param uriFieldValue The initial value for the new URI input.
* @param focusNewInput If true, the new URI input will be focused after being added.
*/
addUri(uriFieldValue: UriField = { uri: null, matchDetection: null }, focusNewInput = false) {
this.autofillOptionsForm.controls.uris.push(this.formBuilder.control(uriFieldValue));
if (focusNewInput) {
this.focusOnNewInput$.next();
}
}
removeUri(i: number) {
this.autofillOptionsForm.controls.uris.removeAt(i);
}
}