1
0
mirror of https://github.com/bitwarden/jslib synced 2025-12-15 15:53:51 +00:00

Add Linked Field as custom field type (#431)

* Basic proof of concept of Linked custom fields

* Linked Fields for all cipher types, use dropdown

* Move linkedFieldOptions to view models

* Move add-edit custom fields to own component

* Fix change handling if cipherType changes

* Use Field.LinkedId to store linked field info

* Refactor accessors in cipherView for type safety

* Use map for linkedFieldOptions

* Refactor: use decorators to record linkable info

* Add ItemView

* Use enums for linked field ids

* Add union type for linkedId enums, add jsdoc comment

* Use parameter properties for linkedFieldOption

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>

* Fix type casting

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
This commit is contained in:
Thomas Rittson
2021-11-03 08:03:37 +10:00
committed by GitHub
parent 1bd968a023
commit dbda39e10f
16 changed files with 206 additions and 17 deletions

View File

@@ -1,11 +1,13 @@
import { BaseResponse } from '../response/baseResponse';
import { FieldType } from '../../enums/fieldType';
import { LinkedIdType } from '../../enums/linkedIdType';
export class FieldApi extends BaseResponse {
name: string;
value: string;
type: FieldType;
linkedId: LinkedIdType;
constructor(data: any = null) {
super(data);
@@ -15,5 +17,6 @@ export class FieldApi extends BaseResponse {
this.type = this.getResponseProperty('Type');
this.name = this.getResponseProperty('Name');
this.value = this.getResponseProperty('Value');
this.linkedId = this.getResponseProperty('linkedId');
}
}

View File

@@ -1,4 +1,5 @@
import { FieldType } from '../../enums/fieldType';
import { LinkedIdType } from '../../enums/linkedIdType';
import { FieldApi } from '../api/fieldApi';
@@ -6,6 +7,7 @@ export class FieldData {
type: FieldType;
name: string;
value: string;
linkedId: LinkedIdType;
constructor(response?: FieldApi) {
if (response == null) {
@@ -14,5 +16,6 @@ export class FieldData {
this.type = response.type;
this.name = response.name;
this.value = response.value;
this.linkedId = response.linkedId;
}
}

View File

@@ -1,4 +1,5 @@
import { FieldType } from '../../enums/fieldType';
import { LinkedIdType } from '../../enums/linkedIdType';
import { FieldData } from '../data/fieldData';
@@ -12,6 +13,7 @@ export class Field extends Domain {
name: EncString;
value: EncString;
type: FieldType;
linkedId: LinkedIdType;
constructor(obj?: FieldData, alreadyEncrypted: boolean = false) {
super();
@@ -20,6 +22,7 @@ export class Field extends Domain {
}
this.type = obj.type;
this.linkedId = obj.linkedId;
this.buildDomainModel(this, obj, {
name: null,
value: null,
@@ -39,7 +42,8 @@ export class Field extends Domain {
name: null,
value: null,
type: null,
}, ['type']);
linkedId: null,
}, ['type', 'linkedId']);
return f;
}
}

View File

@@ -119,6 +119,7 @@ export class CipherRequest {
field.type = f.type;
field.name = f.name ? f.name.encryptedString : null;
field.value = f.value ? f.value.encryptedString : null;
field.linkedId = f.linkedId;
return field;
});
}

View File

@@ -1,11 +1,19 @@
import { View } from './view';
import { ItemView } from './itemView';
import { Card } from '../domain/card';
export class CardView implements View {
import { CardLinkedId as LinkedId } from '../../enums/linkedIdType';
import { linkedFieldOption } from '../../misc/linkedFieldOption.decorator';
export class CardView extends ItemView {
@linkedFieldOption(LinkedId.CardholderName)
cardholderName: string = null;
@linkedFieldOption(LinkedId.ExpMonth, 'expirationMonth')
expMonth: string = null;
@linkedFieldOption(LinkedId.ExpYear, 'expirationYear')
expYear: string = null;
@linkedFieldOption(LinkedId.Code, 'securityCode')
code: string = null;
// tslint:disable
@@ -15,7 +23,7 @@ export class CardView implements View {
// tslint:enable
constructor(c?: Card) {
// ctor
super();
}
get maskedCode(): string {
@@ -26,6 +34,7 @@ export class CardView implements View {
return this.number != null ? '•'.repeat(this.number.length) : null;
}
@linkedFieldOption(LinkedId.Brand)
get brand(): string {
return this._brand;
}
@@ -34,6 +43,7 @@ export class CardView implements View {
this._subTitle = null;
}
@linkedFieldOption(LinkedId.Number)
get number(): string {
return this._number;
}

View File

@@ -1,5 +1,6 @@
import { CipherRepromptType } from '../../enums/cipherRepromptType';
import { CipherType } from '../../enums/cipherType';
import { LinkedIdType } from '../../enums/linkedIdType';
import { Cipher } from '../domain/cipher';
@@ -7,6 +8,7 @@ import { AttachmentView } from './attachmentView';
import { CardView } from './cardView';
import { FieldView } from './fieldView';
import { IdentityView } from './identityView';
import { ItemView } from './itemView';
import { LoginView } from './loginView';
import { PasswordHistoryView } from './passwordHistoryView';
import { SecureNoteView } from './secureNoteView';
@@ -57,16 +59,16 @@ export class CipherView implements View {
this.reprompt = c.reprompt ?? CipherRepromptType.None;
}
get subTitle(): string {
private get item() {
switch (this.type) {
case CipherType.Login:
return this.login.subTitle;
return this.login;
case CipherType.SecureNote:
return this.secureNote.subTitle;
return this.secureNote;
case CipherType.Card:
return this.card.subTitle;
return this.card;
case CipherType.Identity:
return this.identity.subTitle;
return this.identity;
default:
break;
}
@@ -74,6 +76,10 @@ export class CipherView implements View {
return null;
}
get subTitle(): string {
return this.item.subTitle;
}
get hasPasswordHistory(): boolean {
return this.passwordHistory && this.passwordHistory.length > 0;
}
@@ -109,4 +115,22 @@ export class CipherView implements View {
get isDeleted(): boolean {
return this.deletedDate != null;
}
get linkedFieldOptions() {
return this.item.linkedFieldOptions;
}
linkedFieldValue(id: LinkedIdType) {
const linkedFieldOption = this.linkedFieldOptions?.get(id);
if (linkedFieldOption == null) {
return null;
}
const item = this.item;
return this.item[linkedFieldOption.propertyKey as keyof typeof item];
}
linkedFieldI18nKey(id: LinkedIdType): string {
return this.linkedFieldOptions.get(id)?.i18nKey;
}
}

View File

@@ -1,4 +1,5 @@
import { FieldType } from '../../enums/fieldType';
import { LinkedIdType } from '../../enums/linkedIdType';
import { View } from './view';
@@ -10,6 +11,7 @@ export class FieldView implements View {
type: FieldType = null;
newField: boolean = false; // Marks if the field is new and hasn't been saved
showValue: boolean = false;
linkedId: LinkedIdType = null;
constructor(f?: Field) {
if (!f) {
@@ -17,6 +19,7 @@ export class FieldView implements View {
}
this.type = f.type;
this.linkedId = f.linkedId;
}
get maskedValue(): string {

View File

@@ -1,25 +1,45 @@
import { View } from './view';
import { ItemView } from './itemView';
import { Identity } from '../domain/identity';
import { Utils } from '../../misc/utils';
export class IdentityView implements View {
import { IdentityLinkedId as LinkedId } from '../../enums/linkedIdType';
import { linkedFieldOption } from '../../misc/linkedFieldOption.decorator';
export class IdentityView extends ItemView {
@linkedFieldOption(LinkedId.Title)
title: string = null;
@linkedFieldOption(LinkedId.MiddleName)
middleName: string = null;
@linkedFieldOption(LinkedId.Address1)
address1: string = null;
@linkedFieldOption(LinkedId.Address2)
address2: string = null;
@linkedFieldOption(LinkedId.Address3)
address3: string = null;
@linkedFieldOption(LinkedId.City, 'cityTown')
city: string = null;
@linkedFieldOption(LinkedId.State, 'stateProvince')
state: string = null;
@linkedFieldOption(LinkedId.PostalCode, 'zipPostalCode')
postalCode: string = null;
@linkedFieldOption(LinkedId.Country)
country: string = null;
@linkedFieldOption(LinkedId.Company)
company: string = null;
@linkedFieldOption(LinkedId.Email)
email: string = null;
@linkedFieldOption(LinkedId.Phone)
phone: string = null;
@linkedFieldOption(LinkedId.Ssn)
ssn: string = null;
@linkedFieldOption(LinkedId.Username)
username: string = null;
@linkedFieldOption(LinkedId.PassportNumber)
passportNumber: string = null;
@linkedFieldOption(LinkedId.LicenseNumber)
licenseNumber: string = null;
// tslint:disable
@@ -29,9 +49,10 @@ export class IdentityView implements View {
// tslint:enable
constructor(i?: Identity) {
// ctor
super();
}
@linkedFieldOption(LinkedId.FirstName)
get firstName(): string {
return this._firstName;
}
@@ -40,6 +61,7 @@ export class IdentityView implements View {
this._subTitle = null;
}
@linkedFieldOption(LinkedId.LastName)
get lastName(): string {
return this._lastName;
}
@@ -65,6 +87,7 @@ export class IdentityView implements View {
return this._subTitle;
}
@linkedFieldOption(LinkedId.FullName)
get fullName(): string {
if (this.title != null || this.firstName != null || this.middleName != null || this.lastName != null) {
let name = '';

View File

@@ -0,0 +1,8 @@
import { View } from './view';
import { LinkedMetadata } from '../../misc/linkedFieldOption.decorator';
export abstract class ItemView implements View {
linkedFieldOptions: Map<number, LinkedMetadata>;
abstract get subTitle(): string;
}

View File

@@ -1,18 +1,27 @@
import { ItemView } from './itemView';
import { LoginUriView } from './loginUriView';
import { View } from './view';
import { Utils } from '../../misc/utils';
import { Login } from '../domain/login';
export class LoginView implements View {
import { LoginLinkedId as LinkedId } from '../../enums/linkedIdType';
import { linkedFieldOption } from '../../misc/linkedFieldOption.decorator';
export class LoginView extends ItemView {
@linkedFieldOption(LinkedId.Username)
username: string = null;
@linkedFieldOption(LinkedId.Password)
password: string = null;
passwordRevisionDate?: Date = null;
totp: string = null;
uris: LoginUriView[] = null;
autofillOnPageLoad: boolean = null;
constructor(l?: Login) {
super();
if (!l) {
return;
}

View File

@@ -1,13 +1,14 @@
import { SecureNoteType } from '../../enums/secureNoteType';
import { View } from './view';
import { ItemView } from './itemView';
import { SecureNote } from '../domain/secureNote';
export class SecureNoteView implements View {
export class SecureNoteView extends ItemView {
type: SecureNoteType = null;
constructor(n?: SecureNote) {
super();
if (!n) {
return;
}