[PM-7103] Identity View (#10240)
* make headings of v2 view component lowercase * initial add of view identity sections * adding identification fields * add contact information section * add copy ability to all identity fields * add visibility toggle for passport and ssn * add testids for all identity fields * add test-ids to visibility toggles * refactor visibility methods to be called in `ngOnInit` rather than on each render * replace `disabled` with `readonly`
This commit is contained in:
parent
81212deaad
commit
84ee01caee
|
@ -116,6 +116,21 @@
|
|||
"copySecurityCode": {
|
||||
"message": "Copy security code"
|
||||
},
|
||||
"copyName": {
|
||||
"message": "Copy name"
|
||||
},
|
||||
"copyCompany": {
|
||||
"message": "Copy company"
|
||||
},
|
||||
"copySSN": {
|
||||
"message": "Copy Social Security number"
|
||||
},
|
||||
"copyPassportNumber": {
|
||||
"message": "Copy passport number"
|
||||
},
|
||||
"copyLicenseNumber": {
|
||||
"message": "Copy license number"
|
||||
},
|
||||
"autoFill": {
|
||||
"message": "Autofill"
|
||||
},
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
>
|
||||
</app-item-details-v2>
|
||||
|
||||
<!-- IDENTITY SECTIONS -->
|
||||
<app-view-identity-sections *ngIf="cipher.identity" [cipher]="cipher">
|
||||
</app-view-identity-sections>
|
||||
|
||||
<!-- ADDITIONAL OPTIONS -->
|
||||
<ng-container *ngIf="cipher.notes">
|
||||
<app-additional-options [notes]="cipher.notes"> </app-additional-options>
|
||||
|
|
|
@ -22,6 +22,7 @@ import { AttachmentsV2ViewComponent } from "./attachments/attachments-v2-view.co
|
|||
import { CustomFieldV2Component } from "./custom-fields/custom-fields-v2.component";
|
||||
import { ItemDetailsV2Component } from "./item-details/item-details-v2.component";
|
||||
import { ItemHistoryV2Component } from "./item-history/item-history-v2.component";
|
||||
import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-identity-sections.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-cipher-view",
|
||||
|
@ -39,6 +40,7 @@ import { ItemHistoryV2Component } from "./item-history/item-history-v2.component
|
|||
AttachmentsV2ViewComponent,
|
||||
ItemHistoryV2Component,
|
||||
CustomFieldV2Component,
|
||||
ViewIdentitySectionsComponent,
|
||||
],
|
||||
})
|
||||
export class CipherViewComponent implements OnInit {
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
<bit-section *ngIf="showPersonalDetails">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "personalDetails" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
|
||||
<bit-card>
|
||||
<bit-form-field *ngIf="cipher.identity.fullName">
|
||||
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||
<input bitInput [value]="cipher.identity.fullName" readonly data-testid="name" />
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'copyName' | i18n"
|
||||
[appCopyClick]="cipher.identity.fullName"
|
||||
showToast
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="cipher.identity.username">
|
||||
<bit-label>{{ "username" | i18n }}</bit-label>
|
||||
<input bitInput [value]="cipher.identity.username" readonly data-testid="username" />
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'copyUsername' | i18n"
|
||||
[appCopyClick]="cipher.identity.username"
|
||||
showToast
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="cipher.identity.company">
|
||||
<bit-label>{{ "company" | i18n }}</bit-label>
|
||||
<input bitInput [value]="cipher.identity.company" readonly data-testid="company" />
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'copyCompany' | i18n"
|
||||
[appCopyClick]="cipher.identity.company"
|
||||
showToast
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
|
||||
<bit-section *ngIf="showIdentificationDetails">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "identification" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
|
||||
<bit-card>
|
||||
<bit-form-field *ngIf="cipher.identity.ssn">
|
||||
<bit-label>{{ "ssn" | i18n }}</bit-label>
|
||||
<input bitInput type="password" [value]="cipher.identity.ssn" readonly data-testid="ssn" />
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
data-testid="ssn-toggle"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'copySSN' | i18n"
|
||||
[appCopyClick]="cipher.identity.ssn"
|
||||
showToast
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="cipher.identity.passportNumber">
|
||||
<bit-label>{{ "passportNumber" | i18n }}</bit-label>
|
||||
<input
|
||||
bitInput
|
||||
type="password"
|
||||
[value]="cipher.identity.passportNumber"
|
||||
readonly
|
||||
data-testid="passport"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton
|
||||
bitSuffix
|
||||
bitPasswordInputToggle
|
||||
data-testid="passport-toggle"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'copyPassportNumber' | i18n"
|
||||
[appCopyClick]="cipher.identity.passportNumber"
|
||||
showToast
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="cipher.identity.licenseNumber">
|
||||
<bit-label>{{ "licenseNumber" | i18n }}</bit-label>
|
||||
<input bitInput [value]="cipher.identity.licenseNumber" readonly data-testid="license" />
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'copyLicenseNumber' | i18n"
|
||||
[appCopyClick]="cipher.identity.licenseNumber"
|
||||
showToast
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
|
||||
<bit-section *ngIf="showContactDetails">
|
||||
<bit-section-header>
|
||||
<h2 bitTypography="h6">{{ "contactInfo" | i18n }}</h2>
|
||||
</bit-section-header>
|
||||
|
||||
<bit-card>
|
||||
<bit-form-field *ngIf="cipher.identity.email">
|
||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||
<input bitInput [value]="cipher.identity.email" readonly data-testid="email" />
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'copyEmail' | i18n"
|
||||
[appCopyClick]="cipher.identity.email"
|
||||
showToast
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="cipher.identity.phone">
|
||||
<bit-label>{{ "phone" | i18n }}</bit-label>
|
||||
<input bitInput [value]="cipher.identity.phone" readonly data-testid="phone" />
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'copyPhone' | i18n"
|
||||
[appCopyClick]="cipher.identity.phone"
|
||||
showToast
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<bit-form-field *ngIf="addressFields">
|
||||
<bit-label>{{ "address" | i18n }}</bit-label>
|
||||
<textarea
|
||||
bitInput
|
||||
class="tw-resize-none"
|
||||
[value]="addressFields"
|
||||
[rows]="addressRows"
|
||||
readonly
|
||||
data-testid="address"
|
||||
></textarea>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-clone"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'copyAddress' | i18n"
|
||||
[appCopyClick]="addressFields"
|
||||
showToast
|
||||
></button>
|
||||
</bit-form-field>
|
||||
</bit-card>
|
||||
</bit-section>
|
|
@ -0,0 +1,200 @@
|
|||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view";
|
||||
import { SectionHeaderComponent } from "@bitwarden/components";
|
||||
|
||||
import { BitInputDirective } from "../../../../components/src/input/input.directive";
|
||||
|
||||
import { ViewIdentitySectionsComponent } from "./view-identity-sections.component";
|
||||
|
||||
describe("ViewIdentitySectionsComponent", () => {
|
||||
let component: ViewIdentitySectionsComponent;
|
||||
let fixture: ComponentFixture<ViewIdentitySectionsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ViewIdentitySectionsComponent],
|
||||
providers: [
|
||||
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ViewIdentitySectionsComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.cipher = { identity: new IdentityView() } as CipherView;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
describe("personal details", () => {
|
||||
it("dynamically shows the section", () => {
|
||||
let personalDetailSection = fixture.debugElement.query(By.directive(SectionHeaderComponent));
|
||||
|
||||
expect(personalDetailSection).toBeNull();
|
||||
|
||||
component.cipher = {
|
||||
identity: {
|
||||
fullName: "Mr Ron Burgundy",
|
||||
},
|
||||
} as CipherView;
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
personalDetailSection = fixture.debugElement.query(By.directive(SectionHeaderComponent));
|
||||
|
||||
expect(personalDetailSection).not.toBeNull();
|
||||
expect(personalDetailSection.nativeElement.textContent).toBe("personalDetails");
|
||||
});
|
||||
|
||||
it("populates personal detail fields", () => {
|
||||
component.cipher = {
|
||||
identity: {
|
||||
fullName: "Mr Ron Burgundy",
|
||||
company: "Channel 4 News",
|
||||
username: "ron.burgundy",
|
||||
},
|
||||
} as CipherView;
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const fields = fixture.debugElement.queryAll(By.directive(BitInputDirective));
|
||||
|
||||
expect(fields[0].nativeElement.value).toBe("Mr Ron Burgundy");
|
||||
expect(fields[1].nativeElement.value).toBe("ron.burgundy");
|
||||
expect(fields[2].nativeElement.value).toBe("Channel 4 News");
|
||||
});
|
||||
});
|
||||
|
||||
describe("identification details", () => {
|
||||
it("dynamically shows the section", () => {
|
||||
let identificationDetailSection = fixture.debugElement.query(
|
||||
By.directive(SectionHeaderComponent),
|
||||
);
|
||||
|
||||
expect(identificationDetailSection).toBeNull();
|
||||
|
||||
component.cipher = {
|
||||
identity: {
|
||||
ssn: "123-45-6789",
|
||||
},
|
||||
} as CipherView;
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
identificationDetailSection = fixture.debugElement.query(
|
||||
By.directive(SectionHeaderComponent),
|
||||
);
|
||||
|
||||
expect(identificationDetailSection).not.toBeNull();
|
||||
expect(identificationDetailSection.nativeElement.textContent).toBe("identification");
|
||||
});
|
||||
|
||||
it("populates identification detail fields", () => {
|
||||
component.cipher = {
|
||||
identity: {
|
||||
ssn: "123-45-6789",
|
||||
passportNumber: "998-765-4321",
|
||||
licenseNumber: "404-HTTP",
|
||||
},
|
||||
} as CipherView;
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const fields = fixture.debugElement.queryAll(By.directive(BitInputDirective));
|
||||
|
||||
expect(fields[0].nativeElement.value).toBe("123-45-6789");
|
||||
expect(fields[1].nativeElement.value).toBe("998-765-4321");
|
||||
expect(fields[2].nativeElement.value).toBe("404-HTTP");
|
||||
});
|
||||
});
|
||||
|
||||
describe("contact details", () => {
|
||||
it("dynamically shows the section", () => {
|
||||
let contactDetailSection = fixture.debugElement.query(By.directive(SectionHeaderComponent));
|
||||
|
||||
expect(contactDetailSection).toBeNull();
|
||||
|
||||
component.cipher = {
|
||||
identity: {
|
||||
email: "jack@gnn.com",
|
||||
},
|
||||
} as CipherView;
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
contactDetailSection = fixture.debugElement.query(By.directive(SectionHeaderComponent));
|
||||
|
||||
expect(contactDetailSection).not.toBeNull();
|
||||
expect(contactDetailSection.nativeElement.textContent).toBe("contactInfo");
|
||||
});
|
||||
|
||||
it("populates contact detail fields", () => {
|
||||
component.cipher = {
|
||||
identity: {
|
||||
email: "jack@gnn.com",
|
||||
phone: "608-867-5309",
|
||||
address1: "2920 Zoo Dr",
|
||||
address2: "Exhibit 200",
|
||||
address3: "Tree 7",
|
||||
fullAddressPart2: "San Diego, CA 92101",
|
||||
country: "USA",
|
||||
},
|
||||
} as CipherView;
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const fields = fixture.debugElement.queryAll(By.directive(BitInputDirective));
|
||||
|
||||
expect(fields[0].nativeElement.value).toBe("jack@gnn.com");
|
||||
expect(fields[1].nativeElement.value).toBe("608-867-5309");
|
||||
expect(fields[2].nativeElement.value).toBe(
|
||||
"2920 Zoo Dr\nExhibit 200\nTree 7\nSan Diego, CA 92101\nUSA",
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the number of "rows" that should be assigned to the address textarea', () => {
|
||||
component.cipher = {
|
||||
identity: {
|
||||
address1: "2920 Zoo Dr",
|
||||
address2: "Exhibit 200",
|
||||
address3: "Tree 7",
|
||||
fullAddressPart2: "San Diego, CA 92101",
|
||||
country: "USA",
|
||||
},
|
||||
} as CipherView;
|
||||
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
let textarea = fixture.debugElement.query(By.css("textarea"));
|
||||
|
||||
expect(textarea.nativeElement.rows).toBe(5);
|
||||
|
||||
component.cipher = {
|
||||
identity: {
|
||||
address1: "2920 Zoo Dr",
|
||||
country: "USA",
|
||||
},
|
||||
} as CipherView;
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
textarea = fixture.debugElement.query(By.css("textarea"));
|
||||
|
||||
expect(textarea.nativeElement.rows).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
import { NgIf } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
CardComponent,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-view-identity-sections",
|
||||
templateUrl: "./view-identity-sections.component.html",
|
||||
imports: [
|
||||
NgIf,
|
||||
JslibModule,
|
||||
CardComponent,
|
||||
SectionComponent,
|
||||
SectionHeaderComponent,
|
||||
TypographyModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
],
|
||||
})
|
||||
export class ViewIdentitySectionsComponent implements OnInit {
|
||||
@Input() cipher: CipherView;
|
||||
|
||||
showPersonalDetails: boolean;
|
||||
showIdentificationDetails: boolean;
|
||||
showContactDetails: boolean;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.showPersonalDetails = this.hasPersonalDetails();
|
||||
this.showIdentificationDetails = this.hasIdentificationDetails();
|
||||
this.showContactDetails = this.hasContactDetails();
|
||||
}
|
||||
|
||||
/** Returns all populated address fields */
|
||||
get addressFields(): string {
|
||||
const { address1, address2, address3, fullAddressPart2, country } = this.cipher.identity;
|
||||
return [address1, address2, address3, fullAddressPart2, country].filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
/** Returns the number of "rows" that should be assigned to the address textarea */
|
||||
get addressRows(): number {
|
||||
return this.addressFields.split("\n").length;
|
||||
}
|
||||
|
||||
/** Returns true when any of the "personal detail" attributes are populated */
|
||||
private hasPersonalDetails(): boolean {
|
||||
const { username, company, fullName } = this.cipher.identity;
|
||||
return Boolean(fullName || username || company);
|
||||
}
|
||||
|
||||
/** Returns true when any of the "identification detail" attributes are populated */
|
||||
private hasIdentificationDetails(): boolean {
|
||||
const { ssn, passportNumber, licenseNumber } = this.cipher.identity;
|
||||
return Boolean(ssn || passportNumber || licenseNumber);
|
||||
}
|
||||
|
||||
/** Returns true when any of the "contact detail" attributes are populated */
|
||||
private hasContactDetails(): boolean {
|
||||
const { email, phone } = this.cipher.identity;
|
||||
|
||||
return Boolean(email || phone || this.addressFields);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue