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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user