[Linked fields] Add Linked Field as a custom field type (#1963)

* Proof of concept for Linked custom field type

* Linked Fields for all cipher types, use dropdown

* Fix linked icon alignment

* Tweak linked icon alignment and style

* Move add-edit custom fields to own component

* Disable copy for linked field

* Use Field.LinkedId to store linked field info
This commit is contained in:
Thomas Rittson 2021-11-04 07:40:42 +10:00 committed by GitHub
parent f20a1e7424
commit 2113c709a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 50 additions and 10 deletions

2
jslib

@ -1 +1 @@
Subproject commit 24fe836032354d4ec39435776e54dd0995e1b389 Subproject commit 2db9e1ce0d7a702f07f20ecb916dd8191ff617e1

View File

@ -983,6 +983,14 @@
"cfTypeBoolean": { "cfTypeBoolean": {
"message": "Boolean" "message": "Boolean"
}, },
"cfTypeLinked": {
"message": "Linked",
"description": "This describes a field that is 'linked' (tied) to another field."
},
"linkedValue": {
"message": "Linked value",
"description": "This describes a value that is 'linked' (tied) to another value."
},
"popup2faCloseMessage": { "popup2faCloseMessage": {
"message": "Clicking outside the popup window to check your email for your verification code will cause this popup to close. Do you want to open this popup in a new window so that it does not close?" "message": "Clicking outside the popup window to check your email for your verification code will cause this popup to close. Do you want to open this popup in a new window so that it does not close?"
}, },
@ -1085,6 +1093,9 @@
"lastName": { "lastName": {
"message": "Last Name" "message": "Last Name"
}, },
"fullName": {
"message": "Full Name"
},
"identityName": { "identityName": {
"message": "Identity Name" "message": "Identity Name"
}, },

View File

@ -271,7 +271,7 @@ export default class MainBackground {
const message = Object.assign({}, { command: subscriber }, arg); const message = Object.assign({}, { command: subscriber }, arg);
that.runtimeBackground.processMessage(message, that, null); that.runtimeBackground.processMessage(message, that, null);
} }
}(), this.vaultTimeoutService, this.logService); }(), this.vaultTimeoutService, this.logService, this.cryptoFunctionService);
} }
async bootstrap() { async bootstrap() {

View File

@ -531,6 +531,10 @@
color: themed('mutedColor'); color: themed('mutedColor');
} }
&.icon-small {
min-width: 25px;
}
img { img {
border-radius: $border-radius; border-radius: $border-radius;
max-height: 20px; max-height: 20px;

View File

@ -16,15 +16,20 @@
<div class="row-main"> <div class="row-main">
<input id="fieldName{{i}}" type="text" name="Field.Name{{i}}" [(ngModel)]="f.name" class="row-label" <input id="fieldName{{i}}" type="text" name="Field.Name{{i}}" [(ngModel)]="f.name" class="row-label"
placeholder="{{'name' | i18n}}" appInputVerbatim> placeholder="{{'name' | i18n}}" appInputVerbatim>
<!--Custom Field: Text--> <!-- Text -->
<input id="fieldValue{{i}}" type="text" name="Field.Value{{i}}" [(ngModel)]="f.value" <input id="fieldValue{{i}}" type="text" name="Field.Value{{i}}" [(ngModel)]="f.value"
*ngIf="f.type === fieldType.Text" placeholder="{{'value' | i18n}}" appInputVerbatim> *ngIf="f.type === fieldType.Text" placeholder="{{'value' | i18n}}" appInputVerbatim>
<!--Custom Field: Hidden--> <!-- Hidden -->
<input id="fieldValue{{i}}" type="{{f.showValue ? 'text' : 'password'}}" name="Field.Value{{i}}" <input id="fieldValue{{i}}" type="{{f.showValue ? 'text' : 'password'}}" name="Field.Value{{i}}"
[(ngModel)]="f.value" class="monospaced" appInputVerbatim *ngIf="f.type === fieldType.Hidden" [(ngModel)]="f.value" class="monospaced" appInputVerbatim *ngIf="f.type === fieldType.Hidden"
placeholder="{{'value' | i18n}}" [disabled]="!cipher.viewPassword && !f.newField"> placeholder="{{'value' | i18n}}" [disabled]="!cipher.viewPassword && !f.newField">
<!-- Linked -->
<select id="fieldValue{{i}}" name="Field.Value{{i}}" [(ngModel)]="f.linkedId"
*ngIf="f.type === fieldType.Linked && cipher.linkedFieldOptions != null">
<option *ngFor="let o of linkedFieldOptions" [ngValue]="o.value">{{o.name}}</option>
</select>
</div> </div>
<!--Custom Field: Boolean--> <!-- Boolean -->
<input id="fieldValue{{i}}" name="Field.Value{{i}}" type="checkbox" [(ngModel)]="f.value" <input id="fieldValue{{i}}" name="Field.Value{{i}}" type="checkbox" [(ngModel)]="f.value"
*ngIf="f.type === fieldType.Boolean" appTrueFalseValue trueValue="true" falseValue="false"> *ngIf="f.type === fieldType.Boolean" appTrueFalseValue trueValue="true" falseValue="false">
<div class="action-buttons" *ngIf="f.type === fieldType.Hidden && (cipher.viewPassword || f.newField)"> <div class="action-buttons" *ngIf="f.type === fieldType.Hidden && (cipher.viewPassword || f.newField)">
@ -47,6 +52,9 @@
<label for="addFieldType" class="sr-only">{{'type' | i18n}}</label> <label for="addFieldType" class="sr-only">{{'type' | i18n}}</label>
<select id="addFieldType" name="AddFieldType" [(ngModel)]="addFieldType" class="field-type"> <select id="addFieldType" name="AddFieldType" [(ngModel)]="addFieldType" class="field-type">
<option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{o.name}}</option> <option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{o.name}}</option>
<option *ngIf="cipher.linkedFieldOptions != null" [ngValue]="addFieldLinkedTypeOption.value">
{{addFieldLinkedTypeOption.name}}
</option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -324,7 +324,8 @@
</div> </div>
</div> </div>
</div> </div>
<app-vault-add-edit-custom-fields [cipher]="cipher" [editMode]="editMode"></app-vault-add-edit-custom-fields> <app-vault-add-edit-custom-fields [cipher]="cipher" [thisCipherType]="cipher.type" [editMode]="editMode">
</app-vault-add-edit-custom-fields>
<div class="box" *ngIf="allowOwnershipOptions()"> <div class="box" *ngIf="allowOwnershipOptions()">
<div class="box-header"> <div class="box-header">
{{'ownership' | i18n}} {{'ownership' | i18n}}

View File

@ -18,6 +18,13 @@
<i class="fa fa-square-o" *ngIf="field.value !== 'true'" aria-hidden="true"></i> <i class="fa fa-square-o" *ngIf="field.value !== 'true'" aria-hidden="true"></i>
<span class="sr-only">{{field.value}}</span> <span class="sr-only">{{field.value}}</span>
</div> </div>
<div *ngIf="field.type === fieldType.Linked" class="box-content-row-flex">
<div class="icon icon-small">
<i class="fa fa-link" aria-hidden="true" appA11yTitle="{{'linkedValue' | i18n}}"></i>
<span class="sr-only">{{'linkedValue' | i18n}}</span>
</div>
<span>{{cipher.linkedFieldI18nKey(field.linkedId) | i18n}}</span>
</div>
</div> </div>
<div class="action-buttons"> <div class="action-buttons">
<button type="button" class="row-btn" appStopClick appA11yTitle="{{'toggleVisibility' | i18n}}" <button type="button" class="row-btn" appStopClick appA11yTitle="{{'toggleVisibility' | i18n}}"
@ -27,7 +34,8 @@
[ngClass]="{'fa-eye': !field.showValue, 'fa-eye-slash': field.showValue}"></i> [ngClass]="{'fa-eye': !field.showValue, 'fa-eye-slash': field.showValue}"></i>
</button> </button>
<button type="button" class="row-btn" appStopClick appA11yTitle="{{'copyValue' | i18n}}" <button type="button" class="row-btn" appStopClick appA11yTitle="{{'copyValue' | i18n}}"
*ngIf="field.value && field.type !== fieldType.Boolean && !(field.type === fieldType.Hidden && !cipher.viewPassword)" *ngIf="field.value && field.type !== fieldType.Boolean && field.type !== fieldType.Linked &&
!(field.type === fieldType.Hidden && !cipher.viewPassword)"
(click)="copy(field.value, 'value', field.type === fieldType.Hidden ? 'H_Field' : 'Field')"> (click)="copy(field.value, 'value', field.type === fieldType.Hidden ? 'H_Field' : 'Field')">
<i class="fa fa-lg fa-clone" aria-hidden="true"></i> <i class="fa fa-lg fa-clone" aria-hidden="true"></i>
</button> </button>

View File

@ -12,6 +12,7 @@ import { EventType } from 'jslib-common/enums/eventType';
import { FieldType } from 'jslib-common/enums/fieldType'; import { FieldType } from 'jslib-common/enums/fieldType';
import { CipherView } from 'jslib-common/models/view/cipherView'; import { CipherView } from 'jslib-common/models/view/cipherView';
import { FieldView } from 'jslib-common/models/view/fieldView';
import AutofillField from '../models/autofillField'; import AutofillField from '../models/autofillField';
import AutofillPageDetails from '../models/autofillPageDetails'; import AutofillPageDetails from '../models/autofillPageDetails';
@ -318,9 +319,16 @@ export default class AutofillService implements AutofillServiceInterface {
const matchingIndex = this.findMatchingFieldIndex(field, fieldNames); const matchingIndex = this.findMatchingFieldIndex(field, fieldNames);
if (matchingIndex > -1) { if (matchingIndex > -1) {
let val = fields[matchingIndex].value; const matchingField: FieldView = fields[matchingIndex];
if (val == null && fields[matchingIndex].type === FieldType.Boolean) { let val;
val = 'false'; if (matchingField.type === FieldType.Linked) {
// Assumption: Linked Field is not being used to autofill a boolean value
val = options.cipher.linkedFieldValue(matchingField.linkedId);
} else {
val = matchingField.value;
if (val == null && matchingField.type === FieldType.Boolean) {
val = 'false';
}
} }
filledFields[field.opid] = field; filledFields[field.opid] = field;