mirror of
https://github.com/bitwarden/browser
synced 2025-12-19 17:53:39 +00:00
Move desktop into apps/desktop
This commit is contained in:
100
apps/desktop/src/app/layout/account-switcher.component.html
Normal file
100
apps/desktop/src/app/layout/account-switcher.component.html
Normal file
@@ -0,0 +1,100 @@
|
||||
<button
|
||||
class="account-switcher"
|
||||
(click)="toggle()"
|
||||
cdkOverlayOrigin
|
||||
#trigger="cdkOverlayOrigin"
|
||||
[hidden]="!showSwitcher"
|
||||
aria-haspopup="menu"
|
||||
aria-controls="cdk-overlay-container"
|
||||
[attr.aria-expanded]="isOpen"
|
||||
>
|
||||
<ng-container *ngIf="activeAccountEmail != null; else noActiveAccount">
|
||||
<app-avatar
|
||||
[data]="activeAccountEmail"
|
||||
size="25"
|
||||
[circle]="true"
|
||||
[fontSize]="14"
|
||||
[dynamic]="true"
|
||||
*ngIf="activeAccountEmail != null"
|
||||
aria-hidden="true"
|
||||
></app-avatar>
|
||||
<span>{{ activeAccountEmail }}</span>
|
||||
</ng-container>
|
||||
<ng-template #noActiveAccount>
|
||||
<span>{{ "switchAccount" | i18n }}</span>
|
||||
</ng-template>
|
||||
<i
|
||||
class="bwi"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-chevron-up': isOpen }"
|
||||
></i>
|
||||
</button>
|
||||
|
||||
<ng-template
|
||||
cdkConnectedOverlay
|
||||
[cdkConnectedOverlayOrigin]="trigger"
|
||||
[cdkConnectedOverlayHasBackdrop]="true"
|
||||
[cdkConnectedOverlayBackdropClass]="'cdk-overlay-transparent-backdrop'"
|
||||
(backdropClick)="close()"
|
||||
(detach)="close()"
|
||||
[cdkConnectedOverlayOpen]="showSwitcher && isOpen"
|
||||
[cdkConnectedOverlayPositions]="overlayPostition"
|
||||
cdkConnectedOverlayMinWidth="250px"
|
||||
>
|
||||
<div
|
||||
class="account-switcher-dropdown"
|
||||
[@transformPanel]="'open'"
|
||||
cdkTrapFocus
|
||||
cdkTrapFocusAutoCapture
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="accounts" *ngIf="numberOfAccounts > 0">
|
||||
<button
|
||||
*ngFor="let a of accounts | keyvalue"
|
||||
class="account"
|
||||
(click)="switch(a.key)"
|
||||
appA11yTitle="{{ 'loggedInAsOn' | i18n: a.value.profile.email:a.value.serverUrl }}"
|
||||
attr.aria-label="{{ 'switchAccount' | i18n }}"
|
||||
>
|
||||
<app-avatar
|
||||
[data]="a.value.profile.email"
|
||||
size="25"
|
||||
[circle]="true"
|
||||
[fontSize]="14"
|
||||
[dynamic]="true"
|
||||
*ngIf="a.value.profile.email != null"
|
||||
aria-hidden="true"
|
||||
></app-avatar>
|
||||
<div class="accountInfo">
|
||||
<span class="email" aria-hidden="true">{{ a.value.profile.email }}</span>
|
||||
<span class="server" aria-hidden="true" *ngIf="a.value.serverUrl != 'bitwarden.com'">{{
|
||||
a.value.serverUrl
|
||||
}}</span>
|
||||
<span class="status" aria-hidden="true">{{
|
||||
(a.value.profile.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
|
||||
| i18n
|
||||
}}</span>
|
||||
</div>
|
||||
<i
|
||||
class="bwi bwi-2x text-muted"
|
||||
[ngClass]="
|
||||
a.value.profile.authenticationStatus == authStatus.Unlocked ? 'bwi-unlock' : 'bwi-lock'
|
||||
"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
<ng-container *ngIf="activeAccountEmail != null">
|
||||
<div class="border" *ngIf="numberOfAccounts > 0"></div>
|
||||
<ng-container *ngIf="numberOfAccounts < 4">
|
||||
<button class="add" routerLink="/login" (click)="addAccount()">
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i> {{ "addAccount" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="numberOfAccounts == 4">
|
||||
<span class="accountLimitReached">{{ "accountSwitcherLimitReached" | i18n }} </span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</ng-template>
|
||||
135
apps/desktop/src/app/layout/account-switcher.component.ts
Normal file
135
apps/desktop/src/app/layout/account-switcher.component.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { animate, state, style, transition, trigger } from "@angular/animations";
|
||||
import { ConnectedPosition } from "@angular/cdk/overlay";
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
|
||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
import { Account } from "jslib-common/models/domain/account";
|
||||
|
||||
export class SwitcherAccount extends Account {
|
||||
get serverUrl() {
|
||||
return this.removeWebProtocolFromString(
|
||||
this.settings?.environmentUrls?.base ??
|
||||
this.settings?.environmentUrls.api ??
|
||||
"https://bitwarden.com"
|
||||
);
|
||||
}
|
||||
|
||||
private removeWebProtocolFromString(urlString: string) {
|
||||
const regex = /http(s)?(:)?(\/\/)?|(\/\/)?(www\.)?/g;
|
||||
return urlString.replace(regex, "");
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-account-switcher",
|
||||
templateUrl: "account-switcher.component.html",
|
||||
animations: [
|
||||
trigger("transformPanel", [
|
||||
state(
|
||||
"void",
|
||||
style({
|
||||
opacity: 0,
|
||||
})
|
||||
),
|
||||
transition(
|
||||
"void => open",
|
||||
animate(
|
||||
"100ms linear",
|
||||
style({
|
||||
opacity: 1,
|
||||
})
|
||||
)
|
||||
),
|
||||
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class AccountSwitcherComponent implements OnInit {
|
||||
isOpen = false;
|
||||
accounts: { [userId: string]: SwitcherAccount } = {};
|
||||
activeAccountEmail: string;
|
||||
serverUrl: string;
|
||||
authStatus = AuthenticationStatus;
|
||||
overlayPostition: ConnectedPosition[] = [
|
||||
{
|
||||
originX: "end",
|
||||
originY: "bottom",
|
||||
overlayX: "end",
|
||||
overlayY: "top",
|
||||
},
|
||||
];
|
||||
|
||||
get showSwitcher() {
|
||||
const userIsInAVault = !Utils.isNullOrWhitespace(this.activeAccountEmail);
|
||||
const userIsAddingAnAdditionalAccount = Object.keys(this.accounts).length > 0;
|
||||
return userIsInAVault || userIsAddingAnAdditionalAccount;
|
||||
}
|
||||
|
||||
get numberOfAccounts() {
|
||||
if (this.accounts == null) {
|
||||
this.isOpen = false;
|
||||
return 0;
|
||||
}
|
||||
return Object.keys(this.accounts).length;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private authService: AuthService,
|
||||
private messagingService: MessagingService
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this.stateService.accounts.subscribe(async (accounts: { [userId: string]: Account }) => {
|
||||
for (const userId in accounts) {
|
||||
accounts[userId].profile.authenticationStatus = await this.authService.getAuthStatus(
|
||||
userId
|
||||
);
|
||||
}
|
||||
|
||||
this.accounts = await this.createSwitcherAccounts(accounts);
|
||||
this.activeAccountEmail = await this.stateService.getEmail();
|
||||
});
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
}
|
||||
|
||||
async switch(userId: string) {
|
||||
this.close();
|
||||
|
||||
this.messagingService.send("switchAccount", { userId: userId });
|
||||
}
|
||||
|
||||
async addAccount() {
|
||||
this.close();
|
||||
await this.stateService.setActiveUser(null);
|
||||
}
|
||||
|
||||
private async createSwitcherAccounts(baseAccounts: {
|
||||
[userId: string]: Account;
|
||||
}): Promise<{ [userId: string]: SwitcherAccount }> {
|
||||
const switcherAccounts: { [userId: string]: SwitcherAccount } = {};
|
||||
for (const userId in baseAccounts) {
|
||||
if (userId == null || userId === (await this.stateService.getUserId())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// environmentUrls are stored on disk and must be retrieved seperatly from the in memory state offered from subscribing to accounts
|
||||
baseAccounts[userId].settings.environmentUrls = await this.stateService.getEnvironmentUrls({
|
||||
userId: userId,
|
||||
});
|
||||
switcherAccounts[userId] = new SwitcherAccount(baseAccounts[userId]);
|
||||
}
|
||||
return switcherAccounts;
|
||||
}
|
||||
}
|
||||
4
apps/desktop/src/app/layout/header.component.html
Normal file
4
apps/desktop/src/app/layout/header.component.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<div class="header">
|
||||
<app-search></app-search>
|
||||
<app-account-switcher></app-account-switcher>
|
||||
</div>
|
||||
7
apps/desktop/src/app/layout/header.component.ts
Normal file
7
apps/desktop/src/app/layout/header.component.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-header",
|
||||
templateUrl: "header.component.html",
|
||||
})
|
||||
export class HeaderComponent {}
|
||||
13
apps/desktop/src/app/layout/nav.component.html
Normal file
13
apps/desktop/src/app/layout/nav.component.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<ng-container *ngFor="let item of items">
|
||||
<button
|
||||
type="button"
|
||||
[routerLink]="item.link"
|
||||
class="btn primary"
|
||||
routerLinkActive="active"
|
||||
[title]="item.label"
|
||||
#rla="routerLinkActive"
|
||||
[attr.aria-pressed]="rla.isActive"
|
||||
>
|
||||
<i class="bwi" [ngClass]="item.icon"></i>{{ item.label }}
|
||||
</button>
|
||||
</ng-container>
|
||||
24
apps/desktop/src/app/layout/nav.component.ts
Normal file
24
apps/desktop/src/app/layout/nav.component.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-nav",
|
||||
templateUrl: "nav.component.html",
|
||||
})
|
||||
export class NavComponent {
|
||||
items: any[] = [
|
||||
{
|
||||
link: "/vault",
|
||||
icon: "bwi-lock-f",
|
||||
label: this.i18nService.translate("myVault"),
|
||||
},
|
||||
{
|
||||
link: "/send",
|
||||
icon: "bwi-send-f",
|
||||
label: "Send",
|
||||
},
|
||||
];
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
}
|
||||
38
apps/desktop/src/app/layout/search/search-bar.service.ts
Normal file
38
apps/desktop/src/app/layout/search/search-bar.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
export type SearchBarState = {
|
||||
enabled: boolean;
|
||||
placeholderText: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class SearchBarService {
|
||||
searchText = new BehaviorSubject<string>(null);
|
||||
|
||||
private _state = {
|
||||
enabled: false,
|
||||
placeholderText: "",
|
||||
};
|
||||
|
||||
// tslint:disable-next-line:member-ordering
|
||||
state = new BehaviorSubject<SearchBarState>(this._state);
|
||||
|
||||
setEnabled(enabled: boolean) {
|
||||
this._state.enabled = enabled;
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
setPlaceholderText(placeholderText: string) {
|
||||
this._state.placeholderText = placeholderText;
|
||||
this.updateState();
|
||||
}
|
||||
|
||||
setSearchText(value: string) {
|
||||
this.searchText.next(value);
|
||||
}
|
||||
|
||||
private updateState() {
|
||||
this.state.next(this._state);
|
||||
}
|
||||
}
|
||||
11
apps/desktop/src/app/layout/search/search.component.html
Normal file
11
apps/desktop/src/app/layout/search/search.component.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="search" *ngIf="state.enabled">
|
||||
<input
|
||||
type="search"
|
||||
[placeholder]="state.placeholderText"
|
||||
id="search"
|
||||
autocomplete="off"
|
||||
[formControl]="searchText"
|
||||
appAutofocus
|
||||
/>
|
||||
<i class="bwi bwi-search" aria-hidden="true"></i>
|
||||
</div>
|
||||
23
apps/desktop/src/app/layout/search/search.component.ts
Normal file
23
apps/desktop/src/app/layout/search/search.component.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { FormControl } from "@angular/forms";
|
||||
|
||||
import { SearchBarService, SearchBarState } from "./search-bar.service";
|
||||
|
||||
@Component({
|
||||
selector: "app-search",
|
||||
templateUrl: "search.component.html",
|
||||
})
|
||||
export class SearchComponent {
|
||||
state: SearchBarState;
|
||||
searchText: FormControl = new FormControl(null);
|
||||
|
||||
constructor(private searchBarService: SearchBarService) {
|
||||
this.searchBarService.state.subscribe((state) => {
|
||||
this.state = state;
|
||||
});
|
||||
|
||||
this.searchText.valueChanges.subscribe((value) => {
|
||||
this.searchBarService.setSearchText(value);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user