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": {
"message": "Launch website"
},
"launchWebsiteName": {
"message": "Launch website $ITEMNAME$",
"placeholders": {
"itemname": {
"content": "$1",
"example": "Secret item"
}
}
},
"website": {
"message": "Website"
},

View File

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

View File

@ -19,8 +19,6 @@ import {
} from "@bitwarden/components";
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 { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
@ -91,30 +89,6 @@ export class ItemMoreOptionsComponent implements OnInit {
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.
*/

View File

@ -28,6 +28,7 @@
bit-item-content
type="button"
(click)="onViewCipher(cipher)"
(dblclick)="launchCipher(cipher)"
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
class="{{ ItemHeightClass }}"
>
@ -60,6 +61,16 @@
{{ "fill" | i18n }}
</button>
</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-more-options
[cipher]="cipher"

View File

@ -5,6 +5,8 @@ import { Router, RouterLink } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
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 {
BadgeModule,
BitItemHeight,
@ -18,6 +20,8 @@ import {
} from "@bitwarden/components";
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 { PopupCipherView } from "../../../views/popup-cipher.view";
import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component";
@ -48,6 +52,12 @@ export class VaultListItemsContainerComponent {
protected ItemHeightClass = BitItemHeightClass;
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.
*/
@ -108,21 +118,60 @@ export class VaultListItemsContainerComponent {
private i18nService: I18nService,
private vaultPopupAutofillService: VaultPopupAutofillService,
private passwordRepromptService: PasswordRepromptService,
private cipherService: CipherService,
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) {
await this.vaultPopupAutofillService.doAutofill(cipher);
}
async onViewCipher(cipher: PopupCipherView) {
// We already have a view action in progress, don't start another
if (this.viewCipherTimeout != null) {
return;
}
// Wrap in a timeout to allow for double click to launch
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 { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { DeepJsonify } from "../../../types/deep-jsonify";
import { LinkedIdType } from "../../enums";
import { CipherType, LinkedIdType } from "../../enums";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type";
import { LocalData } from "../data/local.data";
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) {
const linkedFieldOption = this.linkedFieldOptions?.get(id);
if (linkedFieldOption == null) {