[PM-9926] Linked Fields order (#10174)

* refactor linkedFieldOption to take in an attributes object

* add sort positions for existing linked fields

* sort linked fields on new custom fields
This commit is contained in:
Nick Krantz 2024-07-23 12:26:02 -05:00 committed by GitHub
parent 88f585e09d
commit decc7a3031
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 58 additions and 40 deletions

View File

@ -1,11 +1,30 @@
import { LinkedIdType } from "./enums"; import { LinkedIdType } from "./enums";
import { ItemView } from "./models/view/item.view"; 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 { export class LinkedMetadata {
private readonly _i18nKey: string;
readonly sortPosition: number;
constructor( constructor(
readonly propertyKey: string, readonly propertyKey: string,
private readonly _i18nKey?: string, attributes: LinkedMetadataAttributes,
) {} ) {
this._i18nKey = attributes?.i18nKey;
this.sortPosition = attributes.sortPosition;
}
get i18nKey() { get i18nKey() {
return this._i18nKey ?? this.propertyKey; 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 * 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. * 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 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 * @param options - {@link LinkedMetadataAttributes}
* of the class property will be used as the i18n key.
*/ */
export function linkedFieldOption(id: LinkedIdType, i18nKey?: string) { export function linkedFieldOption(id: LinkedIdType, attributes: LinkedMetadataAttributes) {
return (prototype: ItemView, propertyKey: string) => { return (prototype: ItemView, propertyKey: string) => {
if (prototype.linkedFieldOptions == null) { if (prototype.linkedFieldOptions == null) {
prototype.linkedFieldOptions = new Map<LinkedIdType, LinkedMetadata>(); prototype.linkedFieldOptions = new Map<LinkedIdType, LinkedMetadata>();
} }
prototype.linkedFieldOptions.set(id, new LinkedMetadata(propertyKey, i18nKey)); prototype.linkedFieldOptions.set(id, new LinkedMetadata(propertyKey, attributes));
}; };
} }

View File

@ -6,13 +6,13 @@ import { linkedFieldOption } from "../../linked-field-option.decorator";
import { ItemView } from "./item.view"; import { ItemView } from "./item.view";
export class CardView extends ItemView { export class CardView extends ItemView {
@linkedFieldOption(LinkedId.CardholderName) @linkedFieldOption(LinkedId.CardholderName, { sortPosition: 0 })
cardholderName: string = null; cardholderName: string = null;
@linkedFieldOption(LinkedId.ExpMonth, "expirationMonth") @linkedFieldOption(LinkedId.ExpMonth, { sortPosition: 3, i18nKey: "expirationMonth" })
expMonth: string = null; expMonth: string = null;
@linkedFieldOption(LinkedId.ExpYear, "expirationYear") @linkedFieldOption(LinkedId.ExpYear, { sortPosition: 4, i18nKey: "expirationYear" })
expYear: string = null; expYear: string = null;
@linkedFieldOption(LinkedId.Code, "securityCode") @linkedFieldOption(LinkedId.Code, { sortPosition: 5, i18nKey: "securityCode" })
code: string = null; code: string = null;
private _brand: string = null; private _brand: string = null;
@ -27,7 +27,7 @@ export class CardView extends ItemView {
return this.number != null ? "•".repeat(this.number.length) : null; return this.number != null ? "•".repeat(this.number.length) : null;
} }
@linkedFieldOption(LinkedId.Brand) @linkedFieldOption(LinkedId.Brand, { sortPosition: 2 })
get brand(): string { get brand(): string {
return this._brand; return this._brand;
} }
@ -36,7 +36,7 @@ export class CardView extends ItemView {
this._subTitle = null; this._subTitle = null;
} }
@linkedFieldOption(LinkedId.Number) @linkedFieldOption(LinkedId.Number, { sortPosition: 1 })
get number(): string { get number(): string {
return this._number; return this._number;
} }

View File

@ -7,37 +7,37 @@ import { linkedFieldOption } from "../../linked-field-option.decorator";
import { ItemView } from "./item.view"; import { ItemView } from "./item.view";
export class IdentityView extends ItemView { export class IdentityView extends ItemView {
@linkedFieldOption(LinkedId.Title) @linkedFieldOption(LinkedId.Title, { sortPosition: 0 })
title: string = null; title: string = null;
@linkedFieldOption(LinkedId.MiddleName) @linkedFieldOption(LinkedId.MiddleName, { sortPosition: 2 })
middleName: string = null; middleName: string = null;
@linkedFieldOption(LinkedId.Address1) @linkedFieldOption(LinkedId.Address1, { sortPosition: 12 })
address1: string = null; address1: string = null;
@linkedFieldOption(LinkedId.Address2) @linkedFieldOption(LinkedId.Address2, { sortPosition: 13 })
address2: string = null; address2: string = null;
@linkedFieldOption(LinkedId.Address3) @linkedFieldOption(LinkedId.Address3, { sortPosition: 14 })
address3: string = null; address3: string = null;
@linkedFieldOption(LinkedId.City, "cityTown") @linkedFieldOption(LinkedId.City, { sortPosition: 15, i18nKey: "cityTown" })
city: string = null; city: string = null;
@linkedFieldOption(LinkedId.State, "stateProvince") @linkedFieldOption(LinkedId.State, { sortPosition: 16, i18nKey: "stateProvince" })
state: string = null; state: string = null;
@linkedFieldOption(LinkedId.PostalCode, "zipPostalCode") @linkedFieldOption(LinkedId.PostalCode, { sortPosition: 17, i18nKey: "zipPostalCode" })
postalCode: string = null; postalCode: string = null;
@linkedFieldOption(LinkedId.Country) @linkedFieldOption(LinkedId.Country, { sortPosition: 18 })
country: string = null; country: string = null;
@linkedFieldOption(LinkedId.Company) @linkedFieldOption(LinkedId.Company, { sortPosition: 6 })
company: string = null; company: string = null;
@linkedFieldOption(LinkedId.Email) @linkedFieldOption(LinkedId.Email, { sortPosition: 10 })
email: string = null; email: string = null;
@linkedFieldOption(LinkedId.Phone) @linkedFieldOption(LinkedId.Phone, { sortPosition: 11 })
phone: string = null; phone: string = null;
@linkedFieldOption(LinkedId.Ssn) @linkedFieldOption(LinkedId.Ssn, { sortPosition: 7 })
ssn: string = null; ssn: string = null;
@linkedFieldOption(LinkedId.Username) @linkedFieldOption(LinkedId.Username, { sortPosition: 5 })
username: string = null; username: string = null;
@linkedFieldOption(LinkedId.PassportNumber) @linkedFieldOption(LinkedId.PassportNumber, { sortPosition: 8 })
passportNumber: string = null; passportNumber: string = null;
@linkedFieldOption(LinkedId.LicenseNumber) @linkedFieldOption(LinkedId.LicenseNumber, { sortPosition: 9 })
licenseNumber: string = null; licenseNumber: string = null;
private _firstName: string = null; private _firstName: string = null;
@ -48,7 +48,7 @@ export class IdentityView extends ItemView {
super(); super();
} }
@linkedFieldOption(LinkedId.FirstName) @linkedFieldOption(LinkedId.FirstName, { sortPosition: 1 })
get firstName(): string { get firstName(): string {
return this._firstName; return this._firstName;
} }
@ -57,7 +57,7 @@ export class IdentityView extends ItemView {
this._subTitle = null; this._subTitle = null;
} }
@linkedFieldOption(LinkedId.LastName) @linkedFieldOption(LinkedId.LastName, { sortPosition: 4 })
get lastName(): string { get lastName(): string {
return this._lastName; return this._lastName;
} }
@ -83,7 +83,7 @@ export class IdentityView extends ItemView {
return this._subTitle; return this._subTitle;
} }
@linkedFieldOption(LinkedId.FullName) @linkedFieldOption(LinkedId.FullName, { sortPosition: 3 })
get fullName(): string { get fullName(): string {
if ( if (
this.title != null || this.title != null ||

View File

@ -10,9 +10,9 @@ import { ItemView } from "./item.view";
import { LoginUriView } from "./login-uri.view"; import { LoginUriView } from "./login-uri.view";
export class LoginView extends ItemView { export class LoginView extends ItemView {
@linkedFieldOption(LinkedId.Username) @linkedFieldOption(LinkedId.Username, { sortPosition: 0 })
username: string = null; username: string = null;
@linkedFieldOption(LinkedId.Password) @linkedFieldOption(LinkedId.Password, { sortPosition: 1 })
password: string = null; password: string = null;
passwordRevisionDate?: Date = null; passwordRevisionDate?: Date = null;

View File

@ -20,7 +20,6 @@ import { Subject, zip } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; 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 { CipherType, FieldType, LinkedIdType } from "@bitwarden/common/vault/enums";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
@ -135,13 +134,14 @@ export class CustomFieldsComponent implements OnInit, AfterViewInit {
ngOnInit() { ngOnInit() {
const linkedFieldsOptionsForCipher = this.getLinkedFieldsOptionsForCipher(); 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 // Populate options for linked custom fields
this.linkedFieldOptions = Array.from(linkedFieldsOptionsForCipher?.entries() ?? []) this.linkedFieldOptions = optionsArray.map(([id, linkedFieldOption]) => ({
.map(([id, linkedFieldOption]) => ({ name: this.i18nService.t(linkedFieldOption.i18nKey),
name: this.i18nService.t(linkedFieldOption.i18nKey), value: id,
value: id, }));
}))
.sort(Utils.getSortFunction(this.i18nService, "name"));
// Populate the form with the existing fields // Populate the form with the existing fields
this.cipherFormContainer.originalCipherView?.fields?.forEach((field) => { this.cipherFormContainer.originalCipherView?.fields?.forEach((field) => {