1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-12-21 16:18:28 +01:00

[PM-13907] [PM-13849] Browser Refresh - Improve launch login UX (#11680)

* [PM-13907] Move canLaunch logic to CipherView

* [PM-13907] Add external link icon to vault list items

* [PM-13907] Remove launch option from more options dropdown

* [PM-13849] Add double click to launch support
This commit is contained in:
Shane Melton 2024-10-24 10:51:38 -07:00 committed by GitHub
parent b486fcc689
commit a9d9130f01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 82 additions and 36 deletions

View File

@ -578,6 +578,15 @@
"launchWebsite": { "launchWebsite": {
"message": "Launch website" "message": "Launch website"
}, },
"launchWebsiteName": {
"message": "Launch website $ITEMNAME$",
"placeholders": {
"itemname": {
"content": "$1",
"example": "Secret item"
}
}
},
"website": { "website": {
"message": "Website" "message": "Website"
}, },

View File

@ -17,9 +17,6 @@
{{ "fillAndSave" | i18n }} {{ "fillAndSave" | i18n }}
</button> </button>
</ng-container> </ng-container>
<button type="button" bitMenuItem *ngIf="this.canLaunch" (click)="launchCipher()">
{{ "launchWebsite" | i18n }}
</button>
</ng-container> </ng-container>
<button type="button" bitMenuItem (click)="toggleFavorite()"> <button type="button" bitMenuItem (click)="toggleFavorite()">
{{ favoriteText | i18n }} {{ favoriteText | i18n }}

View File

@ -19,8 +19,6 @@ import {
} from "@bitwarden/components"; } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault"; import { PasswordRepromptService } from "@bitwarden/vault";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
@ -91,30 +89,6 @@ export class ItemMoreOptionsComponent implements OnInit {
await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher, false); await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher, false);
} }
/**
* Determines if the login cipher can be launched in a new browser tab.
*/
get canLaunch() {
return this.cipher.type === CipherType.Login && this.cipher.login.canLaunch;
}
/**
* Launches the login cipher in a new browser tab.
*/
async launchCipher() {
if (!this.canLaunch) {
return;
}
await this.cipherService.updateLastLaunchedDate(this.cipher.id);
await BrowserApi.createNewTab(this.cipher.login.launchUri);
if (BrowserPopupUtils.inPopup(window)) {
BrowserApi.closePopup(window);
}
}
/** /**
* Toggles the favorite status of the cipher and updates it on the server. * Toggles the favorite status of the cipher and updates it on the server.
*/ */

View File

@ -28,6 +28,7 @@
bit-item-content bit-item-content
type="button" type="button"
(click)="onViewCipher(cipher)" (click)="onViewCipher(cipher)"
(dblclick)="launchCipher(cipher)"
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name" [appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
class="{{ ItemHeightClass }}" class="{{ ItemHeightClass }}"
> >
@ -60,6 +61,16 @@
{{ "fill" | i18n }} {{ "fill" | i18n }}
</button> </button>
</bit-item-action> </bit-item-action>
<bit-item-action *ngIf="!showAutofillButton && cipher.canLaunch">
<button
type="button"
bitIconButton="bwi-external-link"
size="small"
(click)="launchCipher(cipher)"
[attr.aria-label]="'launchWebsiteName' | i18n: cipher.name"
[title]="'launchWebsiteName' | i18n: cipher.name"
></button>
</bit-item-action>
<app-item-copy-actions [cipher]="cipher"></app-item-copy-actions> <app-item-copy-actions [cipher]="cipher"></app-item-copy-actions>
<app-item-more-options <app-item-more-options
[cipher]="cipher" [cipher]="cipher"

View File

@ -5,6 +5,8 @@ import { Router, RouterLink } from "@angular/router";
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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { import {
BadgeModule, BadgeModule,
BitItemHeight, BitItemHeight,
@ -18,6 +20,8 @@ import {
} from "@bitwarden/components"; } from "@bitwarden/components";
import { OrgIconDirective, PasswordRepromptService } from "@bitwarden/vault"; import { OrgIconDirective, PasswordRepromptService } from "@bitwarden/vault";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
import { PopupCipherView } from "../../../views/popup-cipher.view"; import { PopupCipherView } from "../../../views/popup-cipher.view";
import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component"; import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component";
@ -48,6 +52,12 @@ export class VaultListItemsContainerComponent {
protected ItemHeightClass = BitItemHeightClass; protected ItemHeightClass = BitItemHeightClass;
protected ItemHeight = BitItemHeight; protected ItemHeight = BitItemHeight;
/**
* Timeout used to add a small delay when selecting a cipher to allow for double click to launch
* @private
*/
private viewCipherTimeout: number | null;
/** /**
* The list of ciphers to display. * The list of ciphers to display.
*/ */
@ -108,21 +118,60 @@ export class VaultListItemsContainerComponent {
private i18nService: I18nService, private i18nService: I18nService,
private vaultPopupAutofillService: VaultPopupAutofillService, private vaultPopupAutofillService: VaultPopupAutofillService,
private passwordRepromptService: PasswordRepromptService, private passwordRepromptService: PasswordRepromptService,
private cipherService: CipherService,
private router: Router, private router: Router,
) {} ) {}
/**
* Launches the login cipher in a new browser tab.
*/
async launchCipher(cipher: CipherView) {
if (!cipher.canLaunch) {
return;
}
// If there is a view action pending, clear it
if (this.viewCipherTimeout != null) {
window.clearTimeout(this.viewCipherTimeout);
this.viewCipherTimeout = null;
}
await this.cipherService.updateLastLaunchedDate(cipher.id);
await BrowserApi.createNewTab(cipher.login.launchUri);
if (BrowserPopupUtils.inPopup(window)) {
BrowserApi.closePopup(window);
}
}
async doAutofill(cipher: PopupCipherView) { async doAutofill(cipher: PopupCipherView) {
await this.vaultPopupAutofillService.doAutofill(cipher); await this.vaultPopupAutofillService.doAutofill(cipher);
} }
async onViewCipher(cipher: PopupCipherView) { async onViewCipher(cipher: PopupCipherView) {
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher); // We already have a view action in progress, don't start another
if (!repromptPassed) { if (this.viewCipherTimeout != null) {
return; return;
} }
await this.router.navigate(["/view-cipher"], { // Wrap in a timeout to allow for double click to launch
queryParams: { cipherId: cipher.id, type: cipher.type }, this.viewCipherTimeout = window.setTimeout(
}); async () => {
try {
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
if (!repromptPassed) {
return;
}
await this.router.navigate(["/view-cipher"], {
queryParams: { cipherId: cipher.id, type: cipher.type },
});
} finally {
// Ensure the timeout is always cleared
this.viewCipherTimeout = null;
}
},
cipher.canLaunch ? 200 : 0,
);
} }
} }

View File

@ -2,9 +2,8 @@ import { View } from "../../../models/view/view";
import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface"; import { InitializerMetadata } from "../../../platform/interfaces/initializer-metadata.interface";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { DeepJsonify } from "../../../types/deep-jsonify"; import { DeepJsonify } from "../../../types/deep-jsonify";
import { LinkedIdType } from "../../enums"; import { CipherType, LinkedIdType } from "../../enums";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { LocalData } from "../data/local.data"; import { LocalData } from "../data/local.data";
import { Cipher } from "../domain/cipher"; import { Cipher } from "../domain/cipher";
@ -132,6 +131,13 @@ export class CipherView implements View, InitializerMetadata {
); );
} }
/**
* Determines if the cipher can be launched in a new browser tab.
*/
get canLaunch(): boolean {
return this.type === CipherType.Login && this.login.canLaunch;
}
linkedFieldValue(id: LinkedIdType) { linkedFieldValue(id: LinkedIdType) {
const linkedFieldOption = this.linkedFieldOptions?.get(id); const linkedFieldOption = this.linkedFieldOptions?.get(id);
if (linkedFieldOption == null) { if (linkedFieldOption == null) {