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:
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
8
common/src/models/view/itemView.ts
Normal file
8
common/src/models/view/itemView.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user