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:
parent
b486fcc689
commit
a9d9130f01
@ -578,6 +578,15 @@
|
||||
"launchWebsite": {
|
||||
"message": "Launch website"
|
||||
},
|
||||
"launchWebsiteName": {
|
||||
"message": "Launch website $ITEMNAME$",
|
||||
"placeholders": {
|
||||
"itemname": {
|
||||
"content": "$1",
|
||||
"example": "Secret item"
|
||||
}
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"message": "Website"
|
||||
},
|
||||
|
@ -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 }}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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"
|
||||
|
@ -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) {
|
||||
const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
|
||||
if (!repromptPassed) {
|
||||
// We already have a view action in progress, don't start another
|
||||
if (this.viewCipherTimeout != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.router.navigate(["/view-cipher"], {
|
||||
queryParams: { cipherId: cipher.id, type: cipher.type },
|
||||
});
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user