1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-27 04:03:00 +02:00

cipher listing with action button and pop comps

This commit is contained in:
Kyle Spearrin 2018-04-05 15:35:56 -04:00
parent fa4589c15f
commit 3a9a7d3e64
16 changed files with 409 additions and 16 deletions

2
jslib

@ -1 +1 @@
Subproject commit a0ca51dda4ebbf710284a0f4e59d47ac8f31c8e7 Subproject commit 22f0f97cda0286859ceb889b9c80b9b5bb88affa

View File

@ -283,8 +283,8 @@
"viewItem": { "viewItem": {
"message": "View Item" "message": "View Item"
}, },
"launchWebsite": { "launch": {
"message": "Launch Website" "message": "Launch"
}, },
"website": { "website": {
"message": "Website" "message": "Website"
@ -383,8 +383,14 @@
"message": "Verification code is required." "message": "Verification code is required."
}, },
"valueCopied": { "valueCopied": {
"message": " copied", "message": "$VALUE$ copied",
"description": "' copied'. This is part of a sentence so be sure to leave the space prefix. For example: 'Password copied'" "description": "Value has been copied to the clipboard.",
"placeholders": {
"value": {
"content": "$1",
"example": "Password"
}
}
}, },
"autofillError": { "autofillError": {
"message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead."

View File

@ -15,6 +15,7 @@ import { RegisterComponent } from './accounts/register.component';
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
import { TwoFactorComponent } from './accounts/two-factor.component'; import { TwoFactorComponent } from './accounts/two-factor.component';
import { TabsComponent } from './tabs.component'; import { TabsComponent } from './tabs.component';
import { CiphersComponent } from './vault/ciphers.component';
import { CurrentTabComponent } from './vault/current-tab.component'; import { CurrentTabComponent } from './vault/current-tab.component';
import { GroupingsComponent } from './vault/groupings.component'; import { GroupingsComponent } from './vault/groupings.component';
@ -29,6 +30,7 @@ const routes: Routes = [
{ path: 'register', component: RegisterComponent }, { path: 'register', component: RegisterComponent },
{ path: 'hint', component: HintComponent }, { path: 'hint', component: HintComponent },
{ path: 'environment', component: EnvironmentComponent }, { path: 'environment', component: EnvironmentComponent },
{ path: 'ciphers', component: CiphersComponent },
{ {
path: 'tabs', component: TabsComponent, path: 'tabs', component: TabsComponent,
children: [ children: [

View File

@ -24,6 +24,7 @@ import { RegisterComponent } from './accounts/register.component';
import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component';
import { TwoFactorComponent } from './accounts/two-factor.component'; import { TwoFactorComponent } from './accounts/two-factor.component';
import { TabsComponent } from './tabs.component'; import { TabsComponent } from './tabs.component';
import { CiphersComponent } from './vault/ciphers.component';
import { CurrentTabComponent } from './vault/current-tab.component'; import { CurrentTabComponent } from './vault/current-tab.component';
import { GroupingsComponent } from './vault/groupings.component'; import { GroupingsComponent } from './vault/groupings.component';
@ -36,6 +37,12 @@ import { StopClickDirective } from 'jslib/angular/directives/stop-click.directiv
import { StopPropDirective } from 'jslib/angular/directives/stop-prop.directive'; import { StopPropDirective } from 'jslib/angular/directives/stop-prop.directive';
import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe'; import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe';
import { ActionButtonsComponent } from './components/action-buttons.component';
import { PopOutComponent } from './components/pop-out.component';
import { IconComponent } from 'jslib/angular/components/icon.component';
@NgModule({ @NgModule({
imports: [ imports: [
@ -52,11 +59,13 @@ import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
ToasterModule, ToasterModule,
], ],
declarations: [ declarations: [
ActionButtonsComponent,
ApiActionDirective, ApiActionDirective,
AppComponent, AppComponent,
AutofocusDirective, AutofocusDirective,
BlurClickDirective, BlurClickDirective,
BoxRowDirective, BoxRowDirective,
CiphersComponent,
CurrentTabComponent, CurrentTabComponent,
EnvironmentComponent, EnvironmentComponent,
FallbackSrcDirective, FallbackSrcDirective,
@ -64,9 +73,12 @@ import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe';
HomeComponent, HomeComponent,
HintComponent, HintComponent,
I18nPipe, I18nPipe,
IconComponent,
LockComponent, LockComponent,
LoginComponent, LoginComponent,
PopOutComponent,
RegisterComponent, RegisterComponent,
SearchCiphersPipe,
StopClickDirective, StopClickDirective,
StopPropDirective, StopPropDirective,
TabsComponent, TabsComponent,

View File

@ -0,0 +1,37 @@
<span class="row-btn" (click)="view()" appStopClick appBlurClick title="{{'view' | i18n}}" *ngIf="showView">
<i class="fa fa-lg fa-eye"></i>
</span>
<ng-container *ngIf="cipher.type === cipherType.Login">
<span class="row-btn" appStopClick appBlurClick title="{{'launch' | i18n}}" (click)="launch()"
*ngIf="!showView" [ngClass]="{disabled: !cipher.login.canLaunch}">
<i class="fa fa-lg fa-share-square-o"></i>
</span>
<span class="row-btn" appStopClick appBlurClick title="{{'copyUsername' | i18n}}"
(click)="copy(cipher.login.username, 'username', 'Username')"
[ngClass]="{disabled: !cipher.login.username}">
<i class="fa fa-lg fa-user"></i>
</span>
<span class="row-btn" appStopClick appBlurClick title="{{'copyPassword' | i18n}}"
(click)="copy(cipher.login.password, 'password', 'Password')"
[ngClass]="{disabled: !cipher.login.password}">
<i class="fa fa-lg fa-key"></i>
</span>
</ng-container>
<ng-container *ngIf="cipher.type === cipherType.Card">
<span class="row-btn" appStopClick appBlurClick title="{{'copyNumber' | i18n}}"
(click)="copy(cipher.card.number, 'number', 'Card Number')"
[ngClass]="{disabled: !cipher.card.number}">
<i class="fa fa-lg fa-hashtag"></i>
</span>
<span class="row-btn" appStopClick appBlurClick title="{{'copySecurityCode' | i18n}}"
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')"
[ngClass]="{disabled: !cipher.card.code}">
<i class="fa fa-lg fa-key"></i>
</span>
</ng-container>
<ng-container *ngIf="cipher.type === cipherType.SecureNote">
<span class="row-btn" appStopClick appBlurClick title="{{'copyNote' | i18n}}"
(click)="copy(cipher.notes, 'note', 'Note')" [ngClass]="{disabled: !cipher.notes}">
<i class="fa fa-lg fa-clipboard"></i>
</span>
</ng-container>

View File

@ -0,0 +1,64 @@
import * as template from './action-buttons.component.html';
import {
Component,
EventEmitter,
Input,
Output,
} from '@angular/core';
import { ToasterService } from 'angular2-toaster';
import { Angulartics2 } from 'angulartics2';
import { BrowserApi } from '../../browser/browserApi';
import { CipherType } from 'jslib/enums/cipherType';
import { CipherView } from 'jslib/models/view/cipherView';
import { I18nService } from 'jslib/abstractions/i18n.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PopupUtilsService } from '../services/popup-utils.service';
@Component({
selector: 'app-action-buttons',
template: template,
})
export class ActionButtonsComponent {
@Output() onView = new EventEmitter<CipherView>();
@Input() cipher: CipherView;
@Input() showView: boolean = false;
cipherType = CipherType;
constructor(private analytics: Angulartics2, private toasterService: ToasterService,
private i18nService: I18nService, private platformUtilsService: PlatformUtilsService,
private popupUtilsService: PopupUtilsService) { }
launch() {
if (this.cipher.type !== CipherType.Login || !this.cipher.login.canLaunch) {
return;
}
this.analytics.eventTrack.next({ action: 'Launched URI From Listing' });
BrowserApi.createNewTab(this.cipher.login.uri);
if (this.popupUtilsService.inPopup(window)) {
BrowserApi.closePopup(window);
}
}
copy(value: string, typeI18nKey: string, aType: string) {
if (value == null) {
return;
}
this.analytics.eventTrack.next({ action: 'Copied ' + aType });
this.platformUtilsService.copyToClipboard(value);
this.toasterService.popAsync('info', null,
this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey)));
}
view() {
this.onView.emit(this.cipher);
}
}

View File

@ -0,0 +1,3 @@
<button (click)="expand()" title="{{'popOutNewWindow' | i18n}}">
<i class="fa fa-external-link fa-rotate-270 fa-lg"></i>
</button>

View File

@ -0,0 +1,64 @@
import * as template from './pop-out.component.html';
import { Component } from '@angular/core';
import { Angulartics2 } from 'angulartics2';
import { BrowserApi } from '../../browser/browserApi';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { PopupUtilsService } from '../services/popup-utils.service';
@Component({
selector: 'app-pop-out',
template: template,
})
export class PopOutComponent {
constructor(private analytics: Angulartics2, private platformUtilsService: PlatformUtilsService,
private popupUtilsService: PopupUtilsService) { }
expand() {
this.analytics.eventTrack.next({ action: 'Pop Out Window' });
let href = window.location.href;
if (this.platformUtilsService.isEdge()) {
const popupIndex = href.indexOf('/popup/');
if (popupIndex > -1) {
href = href.substring(popupIndex);
}
}
if ((typeof chrome !== 'undefined') && chrome.windows && chrome.windows.create) {
if (href.indexOf('?uilocation=') > -1) {
href = href.replace('uilocation=popup', 'uilocation=popout')
.replace('uilocation=tab', 'uilocation=popout')
.replace('uilocation=sidebar', 'uilocation=popout');
} else {
const hrefParts = href.split('#');
href = hrefParts[0] + '?uilocation=popout' + (hrefParts.length > 0 ? '#' + hrefParts[1] : '');
}
const bodyRect = document.querySelector('body').getBoundingClientRect();
chrome.windows.create({
url: href,
type: 'popup',
width: bodyRect.width + 60,
height: bodyRect.height,
});
if (this.popupUtilsService.inPopup(window)) {
BrowserApi.closePopup(window);
}
} else if ((typeof chrome !== 'undefined') && chrome.tabs && chrome.tabs.create) {
href = href.replace('uilocation=popup', 'uilocation=tab')
.replace('uilocation=popout', 'uilocation=tab')
.replace('uilocation=sidebar', 'uilocation=tab');
chrome.tabs.create({
url: href,
});
} else if ((typeof safari !== 'undefined')) {
// Safari can't open popup in full page tab :(
}
}
}

View File

@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
@Injectable()
export class PopupUtilsService {
inSidebar(win: Window): boolean {
return win.location.search !== '' && win.location.search.indexOf('uilocation=sidebar') > -1;
}
inTab(win: Window): boolean {
return win.location.search !== '' && win.location.search.indexOf('uilocation=tab') > -1;
}
inPopout(win: Window): boolean {
return win.location.search !== '' && win.location.search.indexOf('uilocation=popout') > -1;
}
inPopup(win: Window): boolean {
return win.location.search === '' || win.location.search.indexOf('uilocation=') === -1 ||
win.location.search.indexOf('uilocation=popup') > -1;
}
}

View File

@ -25,6 +25,7 @@ import { MessagingService } from 'jslib/abstractions/messaging.service';
import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service'; import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service';
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
import { SettingsService } from 'jslib/abstractions/settings.service'; import { SettingsService } from 'jslib/abstractions/settings.service';
import { StateService as StateServiceAbstraction } from 'jslib/abstractions/state.service';
import { StorageService } from 'jslib/abstractions/storage.service'; import { StorageService } from 'jslib/abstractions/storage.service';
import { SyncService } from 'jslib/abstractions/sync.service'; import { SyncService } from 'jslib/abstractions/sync.service';
import { TokenService } from 'jslib/abstractions/token.service'; import { TokenService } from 'jslib/abstractions/token.service';
@ -33,11 +34,13 @@ import { UserService } from 'jslib/abstractions/user.service';
import { UtilsService } from 'jslib/abstractions/utils.service'; import { UtilsService } from 'jslib/abstractions/utils.service';
import { AutofillService } from '../../services/abstractions/autofill.service'; import { AutofillService } from '../../services/abstractions/autofill.service';
import BrowserMessagingService from '../../services/browserMessaging.service'; import BrowserMessagingService from '../../services/browserMessaging.service';
import { AuthService } from 'jslib/services/auth.service'; import { AuthService } from 'jslib/services/auth.service';
import { ConstantsService } from 'jslib/services/constants.service'; import { ConstantsService } from 'jslib/services/constants.service';
import { StateService } from 'jslib/services/state.service';
import { PopupUtilsService } from './popup-utils.service';
function getBgService<T>(service: string) { function getBgService<T>(service: string) {
return (): T => { return (): T => {
@ -46,6 +49,7 @@ function getBgService<T>(service: string) {
}; };
} }
const stateService = new StateService();
const messagingService = new BrowserMessagingService(getBgService<PlatformUtilsService>('platformUtilsService')()); const messagingService = new BrowserMessagingService(getBgService<PlatformUtilsService>('platformUtilsService')());
const authService = new AuthService(getBgService<CryptoService>('cryptoService')(), const authService = new AuthService(getBgService<CryptoService>('cryptoService')(),
getBgService<ApiService>('apiService')(), getBgService<UserService>('userService')(), getBgService<ApiService>('apiService')(), getBgService<UserService>('userService')(),
@ -53,11 +57,16 @@ const authService = new AuthService(getBgService<CryptoService>('cryptoService')
getBgService<I18nService>('i18n2Service')(), getBgService<PlatformUtilsService>('platformUtilsService')(), getBgService<I18nService>('i18n2Service')(), getBgService<PlatformUtilsService>('platformUtilsService')(),
getBgService<ConstantsService>('constantsService')(), messagingService); getBgService<ConstantsService>('constantsService')(), messagingService);
function initFactory(): Function { function initFactory(i18nService: I18nService, storageService: StorageService): Function {
return async () => { return async () => {
if (getBgService<I18nService>('i18n2Service')() != null) { const htmlEl = window.document.documentElement;
if (i18nService != null) {
authService.init(); authService.init();
htmlEl.classList.add('locale_' + i18nService.translationLocale);
} }
stateService.save(ConstantsService.disableFaviconKey,
await storageService.get<boolean>(ConstantsService.disableFaviconKey));
}; };
} }
@ -69,8 +78,10 @@ function initFactory(): Function {
providers: [ providers: [
ValidationService, ValidationService,
AuthGuardService, AuthGuardService,
PopupUtilsService,
{ provide: MessagingService, useValue: messagingService }, { provide: MessagingService, useValue: messagingService },
{ provide: AuthServiceAbstraction, useValue: authService }, { provide: AuthServiceAbstraction, useValue: authService },
{ provide: StateServiceAbstraction, useValue: stateService },
{ provide: AuditService, useFactory: getBgService<AuditService>('auditService'), deps: [] }, { provide: AuditService, useFactory: getBgService<AuditService>('auditService'), deps: [] },
{ provide: CipherService, useFactory: getBgService<CipherService>('cipherService'), deps: [] }, { provide: CipherService, useFactory: getBgService<CipherService>('cipherService'), deps: [] },
{ provide: FolderService, useFactory: getBgService<FolderService>('folderService'), deps: [] }, { provide: FolderService, useFactory: getBgService<FolderService>('folderService'), deps: [] },
@ -102,7 +113,7 @@ function initFactory(): Function {
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: initFactory, useFactory: initFactory,
deps: [], deps: [I18nService, StorageService],
multi: true, multi: true,
}, },
], ],

View File

@ -0,0 +1,50 @@
<header>
<div class="left">
<a routerLink="/tabs/vault">
<i class="fa fa-chevron-left"></i>
<span>{{'back' | i18n}}</span>
</a>
</div>
<div class="center">
{{searchPlaceholder || ('searchVault' | i18n)}}
</div>
<div class="right">
<button appBlurClick (click)="addCipher()" title="{{'addItem' | i18n}}">
<i class="fa fa-plus fa-lg"></i>
</button>
</div>
</header>
<content>
<ng-container *ngIf="(ciphers | searchCiphers: searchText) as searchedCiphers">
<div class="box list" *ngIf="searchedCiphers.length > 0">
<div class="box-header">
Some name here
</div>
<div class="box-content">
<a *ngFor="let c of searchedCiphers" appStopClick (click)="selectCipher(c)"
href="#" title="{{'viewItem' | i18n}}" class="box-content-row box-content-row-flex">
<div class="row-main">
<app-vault-icon [cipher]="c"></app-vault-icon>
<span class="text">
{{c.name}}
<i class="fa fa-share-alt text-muted" *ngIf="c.organizationId"
title="{{'shared' | i18n}}"></i>
<i class="fa fa-paperclip text-muted" *ngIf="c.hasAttachments"
title="{{'attachments' | i18n}}"></i>
</span>
<span class="detail">{{c.subTitle}}</span>
</div>
<app-action-buttons [cipher]="c" class="action-buttons"></app-action-buttons>
</a>
</div>
</div>
<div class="no-items" *ngIf="!searchedCiphers.length">
<i class="fa fa-spinner fa-spin fa-3x" *ngIf="!loaded"></i>
<ng-container *ngIf="loaded">
<i class="fa fa-frown-o fa-4x"></i>
<p>{{'noItemsInList' | i18n}}</p>
<button (click)="addCipher()" class="btn block primary link">{{'addItem' | i18n}}</button>
</ng-container>
</div>
</ng-container>
</content>

View File

@ -0,0 +1,45 @@
import * as template from './ciphers.component.html';
import {
Component,
OnInit,
} from '@angular/core';
import {
ActivatedRoute,
Router,
} from '@angular/router';
import { CipherService } from 'jslib/abstractions/cipher.service';
import { CipherView } from 'jslib/models/view/cipherView';
import { CiphersComponent as BaseCiphersComponent } from 'jslib/angular/components/ciphers.component';
@Component({
selector: 'app-vault-ciphers',
template: template,
})
export class CiphersComponent extends BaseCiphersComponent implements OnInit {
constructor(cipherService: CipherService, private route: ActivatedRoute) {
super(cipherService);
}
async ngOnInit() {
this.route.queryParams.subscribe(async (params) => {
if (params.type) {
const t = parseInt(params.type, null);
await super.load((c) => c.type === t);
} else if (params.folderId) {
await super.load((c) => c.folderId === params.folderId);
} else if (params.collectionId) {
await super.load((c) => c.collectionIds.indexOf(params.collectionId) > -1);
} else {
await super.load();
}
});
}
selectCipher(cipher: CipherView) {
super.selectCipher(cipher);
}
}

View File

@ -1,6 +1,6 @@
<header> <header>
<div class="left"> <div class="left">
<app-pop-out></app-pop-out>
</div> </div>
<div class="center"> <div class="center">
<span class="title">{{'myVault' | i18n}}</span> <span class="title">{{'myVault' | i18n}}</span>

View File

@ -4,10 +4,13 @@ import {
Component, Component,
OnInit, OnInit,
} from '@angular/core'; } from '@angular/core';
import { Router } from '@angular/router';
import { CipherType } from 'jslib/enums/cipherType'; import { CipherType } from 'jslib/enums/cipherType';
import { CollectionView } from 'jslib/models/view/collectionView';
import { CipherView } from 'jslib/models/view/cipherView'; import { CipherView } from 'jslib/models/view/cipherView';
import { FolderView } from 'jslib/models/view/folderView';
import { CollectionService } from 'jslib/abstractions/collection.service'; import { CollectionService } from 'jslib/abstractions/collection.service';
import { CipherService } from 'jslib/abstractions/cipher.service'; import { CipherService } from 'jslib/abstractions/cipher.service';
@ -27,15 +30,15 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
typeCounts = new Map<CipherType, number>(); typeCounts = new Map<CipherType, number>();
constructor(collectionService: CollectionService, folderService: FolderService, constructor(collectionService: CollectionService, folderService: FolderService,
private cipherService: CipherService) { private cipherService: CipherService, private router: Router) {
super(collectionService, folderService); super(collectionService, folderService);
} }
async ngOnInit() { async ngOnInit() {
this.load(); super.load();
this.loaded = false; super.loaded = false;
await this.loadCiphers(); await this.loadCiphers();
this.loaded = true; super.loaded = true;
} }
async loadCiphers() { async loadCiphers() {
@ -71,4 +74,19 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
} }
}); });
} }
selectType(type: CipherType) {
super.selectType(type);
this.router.navigate(['/ciphers', { queryParams: { type: type } }]);
}
selectFolder(folder: FolderView) {
super.selectFolder(folder);
this.router.navigate(['/ciphers', { queryParams: { folderId: folder.id } }]);
}
selectCollection(collection: CollectionView) {
super.selectCollection(collection);
this.router.navigate(['/ciphers', { queryParams: { collectionId: collection.id } }]);
}
} }

View File

@ -113,15 +113,16 @@ header {
text-align: center; text-align: center;
} }
button, a { app-pop-out > button, div > button, div > a {
background: $brand-primary; background: $brand-primary;
border: none; border: none;
color: white; color: white;
padding: 0 10px; padding: 0 10px;
text-decoration: none; text-decoration: none;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center;
&:hover, &:focus { &:hover, &:focus {
background-color: rgba(255, 255, 255, 0.1); background-color: rgba(255, 255, 255, 0.1);
@ -131,6 +132,14 @@ header {
&:focus { &:focus {
text-decoration: underline; text-decoration: underline;
} }
i + span {
margin-left: 5px;
}
}
app-pop-out {
display: flex;
} }
.title { .title {

View File

@ -255,4 +255,55 @@
font-size: $font-size-small; font-size: $font-size-small;
color: $text-muted; color: $text-muted;
} }
&.list {
margin-bottom: 0;
.box-content {
.box-content-row {
padding: 3px 10px;
color: $text-color;
text-decoration: none;
&:hover, &:focus, &.active {
background-color: $list-item-hover;
}
&:focus {
border-left: 5px solid $text-muted;
padding-left: 5px;
}
.text, .detail {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: block;
color: $text-color;
}
.detail {
font-size: $font-size-small;
color: $gray-light;
}
.icon {
display: flex;
justify-content: center;
align-items: center;
float: left;
height: 36px;
width: 34px;
margin-left: -5px;
color: $text-muted;
img {
border-radius: $border-radius;
max-height: 20px;
max-width: 20px;
}
}
}
}
}
} }