diff --git a/libs/common/src/vault/linked-field-option.decorator.ts b/libs/common/src/vault/linked-field-option.decorator.ts index 9f296c92cd..91fe1ee28f 100644 --- a/libs/common/src/vault/linked-field-option.decorator.ts +++ b/libs/common/src/vault/linked-field-option.decorator.ts @@ -1,11 +1,30 @@ import { LinkedIdType } from "./enums"; import { ItemView } from "./models/view/item.view"; +type LinkedMetadataAttributes = { + /** + * The i18n key used to describe the decorated class property in the UI. + * If it is null, then the name of the class property will be used as the i18n key. + */ + i18nKey?: string; + + /** + * The position of the individual field to be applied when sorted. + */ + sortPosition: number; +}; + export class LinkedMetadata { + private readonly _i18nKey: string; + readonly sortPosition: number; + constructor( readonly propertyKey: string, - private readonly _i18nKey?: string, - ) {} + attributes: LinkedMetadataAttributes, + ) { + this._i18nKey = attributes?.i18nKey; + this.sortPosition = attributes.sortPosition; + } get i18nKey() { return this._i18nKey ?? this.propertyKey; @@ -16,15 +35,14 @@ export class LinkedMetadata { * A decorator used to set metadata used by Linked custom fields. Apply it to a class property or getter to make it * available as a Linked custom field option. * @param id - A unique value that is saved in the Field model. It is used to look up the decorated class property. - * @param i18nKey - The i18n key used to describe the decorated class property in the UI. If it is null, then the name - * of the class property will be used as the i18n key. + * @param options - {@link LinkedMetadataAttributes} */ -export function linkedFieldOption(id: LinkedIdType, i18nKey?: string) { +export function linkedFieldOption(id: LinkedIdType, attributes: LinkedMetadataAttributes) { return (prototype: ItemView, propertyKey: string) => { if (prototype.linkedFieldOptions == null) { prototype.linkedFieldOptions = new Map(); } - prototype.linkedFieldOptions.set(id, new LinkedMetadata(propertyKey, i18nKey)); + prototype.linkedFieldOptions.set(id, new LinkedMetadata(propertyKey, attributes)); }; } diff --git a/libs/common/src/vault/models/view/card.view.ts b/libs/common/src/vault/models/view/card.view.ts index c45d27968e..d83b2c6f0a 100644 --- a/libs/common/src/vault/models/view/card.view.ts +++ b/libs/common/src/vault/models/view/card.view.ts @@ -6,13 +6,13 @@ import { linkedFieldOption } from "../../linked-field-option.decorator"; import { ItemView } from "./item.view"; export class CardView extends ItemView { - @linkedFieldOption(LinkedId.CardholderName) + @linkedFieldOption(LinkedId.CardholderName, { sortPosition: 0 }) cardholderName: string = null; - @linkedFieldOption(LinkedId.ExpMonth, "expirationMonth") + @linkedFieldOption(LinkedId.ExpMonth, { sortPosition: 3, i18nKey: "expirationMonth" }) expMonth: string = null; - @linkedFieldOption(LinkedId.ExpYear, "expirationYear") + @linkedFieldOption(LinkedId.ExpYear, { sortPosition: 4, i18nKey: "expirationYear" }) expYear: string = null; - @linkedFieldOption(LinkedId.Code, "securityCode") + @linkedFieldOption(LinkedId.Code, { sortPosition: 5, i18nKey: "securityCode" }) code: string = null; private _brand: string = null; @@ -27,7 +27,7 @@ export class CardView extends ItemView { return this.number != null ? "•".repeat(this.number.length) : null; } - @linkedFieldOption(LinkedId.Brand) + @linkedFieldOption(LinkedId.Brand, { sortPosition: 2 }) get brand(): string { return this._brand; } @@ -36,7 +36,7 @@ export class CardView extends ItemView { this._subTitle = null; } - @linkedFieldOption(LinkedId.Number) + @linkedFieldOption(LinkedId.Number, { sortPosition: 1 }) get number(): string { return this._number; } diff --git a/libs/common/src/vault/models/view/identity.view.ts b/libs/common/src/vault/models/view/identity.view.ts index 02db81b929..8854b8664e 100644 --- a/libs/common/src/vault/models/view/identity.view.ts +++ b/libs/common/src/vault/models/view/identity.view.ts @@ -7,37 +7,37 @@ import { linkedFieldOption } from "../../linked-field-option.decorator"; import { ItemView } from "./item.view"; export class IdentityView extends ItemView { - @linkedFieldOption(LinkedId.Title) + @linkedFieldOption(LinkedId.Title, { sortPosition: 0 }) title: string = null; - @linkedFieldOption(LinkedId.MiddleName) + @linkedFieldOption(LinkedId.MiddleName, { sortPosition: 2 }) middleName: string = null; - @linkedFieldOption(LinkedId.Address1) + @linkedFieldOption(LinkedId.Address1, { sortPosition: 12 }) address1: string = null; - @linkedFieldOption(LinkedId.Address2) + @linkedFieldOption(LinkedId.Address2, { sortPosition: 13 }) address2: string = null; - @linkedFieldOption(LinkedId.Address3) + @linkedFieldOption(LinkedId.Address3, { sortPosition: 14 }) address3: string = null; - @linkedFieldOption(LinkedId.City, "cityTown") + @linkedFieldOption(LinkedId.City, { sortPosition: 15, i18nKey: "cityTown" }) city: string = null; - @linkedFieldOption(LinkedId.State, "stateProvince") + @linkedFieldOption(LinkedId.State, { sortPosition: 16, i18nKey: "stateProvince" }) state: string = null; - @linkedFieldOption(LinkedId.PostalCode, "zipPostalCode") + @linkedFieldOption(LinkedId.PostalCode, { sortPosition: 17, i18nKey: "zipPostalCode" }) postalCode: string = null; - @linkedFieldOption(LinkedId.Country) + @linkedFieldOption(LinkedId.Country, { sortPosition: 18 }) country: string = null; - @linkedFieldOption(LinkedId.Company) + @linkedFieldOption(LinkedId.Company, { sortPosition: 6 }) company: string = null; - @linkedFieldOption(LinkedId.Email) + @linkedFieldOption(LinkedId.Email, { sortPosition: 10 }) email: string = null; - @linkedFieldOption(LinkedId.Phone) + @linkedFieldOption(LinkedId.Phone, { sortPosition: 11 }) phone: string = null; - @linkedFieldOption(LinkedId.Ssn) + @linkedFieldOption(LinkedId.Ssn, { sortPosition: 7 }) ssn: string = null; - @linkedFieldOption(LinkedId.Username) + @linkedFieldOption(LinkedId.Username, { sortPosition: 5 }) username: string = null; - @linkedFieldOption(LinkedId.PassportNumber) + @linkedFieldOption(LinkedId.PassportNumber, { sortPosition: 8 }) passportNumber: string = null; - @linkedFieldOption(LinkedId.LicenseNumber) + @linkedFieldOption(LinkedId.LicenseNumber, { sortPosition: 9 }) licenseNumber: string = null; private _firstName: string = null; @@ -48,7 +48,7 @@ export class IdentityView extends ItemView { super(); } - @linkedFieldOption(LinkedId.FirstName) + @linkedFieldOption(LinkedId.FirstName, { sortPosition: 1 }) get firstName(): string { return this._firstName; } @@ -57,7 +57,7 @@ export class IdentityView extends ItemView { this._subTitle = null; } - @linkedFieldOption(LinkedId.LastName) + @linkedFieldOption(LinkedId.LastName, { sortPosition: 4 }) get lastName(): string { return this._lastName; } @@ -83,7 +83,7 @@ export class IdentityView extends ItemView { return this._subTitle; } - @linkedFieldOption(LinkedId.FullName) + @linkedFieldOption(LinkedId.FullName, { sortPosition: 3 }) get fullName(): string { if ( this.title != null || diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index 1236e047b6..2f525b2413 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -10,9 +10,9 @@ import { ItemView } from "./item.view"; import { LoginUriView } from "./login-uri.view"; export class LoginView extends ItemView { - @linkedFieldOption(LinkedId.Username) + @linkedFieldOption(LinkedId.Username, { sortPosition: 0 }) username: string = null; - @linkedFieldOption(LinkedId.Password) + @linkedFieldOption(LinkedId.Password, { sortPosition: 1 }) password: string = null; passwordRevisionDate?: Date = null; diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts index cb57089f41..44464fa324 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.ts @@ -20,7 +20,6 @@ import { Subject, zip } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType, FieldType, LinkedIdType } from "@bitwarden/common/vault/enums"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; @@ -135,13 +134,14 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit { ngOnInit() { const linkedFieldsOptionsForCipher = this.getLinkedFieldsOptionsForCipher(); + const optionsArray = Array.from(linkedFieldsOptionsForCipher?.entries() ?? []); + optionsArray.sort((a, b) => a[1].sortPosition - b[1].sortPosition); + // Populate options for linked custom fields - this.linkedFieldOptions = Array.from(linkedFieldsOptionsForCipher?.entries() ?? []) - .map(([id, linkedFieldOption]) => ({ - name: this.i18nService.t(linkedFieldOption.i18nKey), - value: id, - })) - .sort(Utils.getSortFunction(this.i18nService, "name")); + this.linkedFieldOptions = optionsArray.map(([id, linkedFieldOption]) => ({ + name: this.i18nService.t(linkedFieldOption.i18nKey), + value: id, + })); // Populate the form with the existing fields this.cipherFormContainer.originalCipherView?.fields?.forEach((field) => {