From 3a9a7d3e64c385c0763a96d37638108eb06dadd6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Apr 2018 15:35:56 -0400 Subject: [PATCH] cipher listing with action button and pop comps --- jslib | 2 +- src/_locales/en/messages.json | 14 ++-- src/popup2/app-routing.module.ts | 2 + src/popup2/app.module.ts | 12 ++++ .../components/action-buttons.component.html | 37 +++++++++++ .../components/action-buttons.component.ts | 64 +++++++++++++++++++ src/popup2/components/pop-out.component.html | 3 + src/popup2/components/pop-out.component.ts | 64 +++++++++++++++++++ src/popup2/services/popup-utils.service.ts | 21 ++++++ src/popup2/services/services.module.ts | 19 ++++-- src/popup2/vault/ciphers.component.html | 50 +++++++++++++++ src/popup2/vault/ciphers.component.ts | 45 +++++++++++++ src/popup2/vault/groupings.component.html | 2 +- src/popup2/vault/groupings.component.ts | 26 ++++++-- src/scss/base.scss | 13 +++- src/scss/box.scss | 51 +++++++++++++++ 16 files changed, 409 insertions(+), 16 deletions(-) create mode 100644 src/popup2/components/action-buttons.component.html create mode 100644 src/popup2/components/action-buttons.component.ts create mode 100644 src/popup2/components/pop-out.component.html create mode 100644 src/popup2/components/pop-out.component.ts create mode 100644 src/popup2/services/popup-utils.service.ts create mode 100644 src/popup2/vault/ciphers.component.html create mode 100644 src/popup2/vault/ciphers.component.ts diff --git a/jslib b/jslib index a0ca51dda4e..22f0f97cda0 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit a0ca51dda4ebbf710284a0f4e59d47ac8f31c8e7 +Subproject commit 22f0f97cda0286859ceb889b9c80b9b5bb88affa diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index e5c6acb5a5b..014136506eb 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -283,8 +283,8 @@ "viewItem": { "message": "View Item" }, - "launchWebsite": { - "message": "Launch Website" + "launch": { + "message": "Launch" }, "website": { "message": "Website" @@ -383,8 +383,14 @@ "message": "Verification code is required." }, "valueCopied": { - "message": " copied", - "description": "' copied'. This is part of a sentence so be sure to leave the space prefix. For example: 'Password copied'" + "message": "$VALUE$ copied", + "description": "Value has been copied to the clipboard.", + "placeholders": { + "value": { + "content": "$1", + "example": "Password" + } + } }, "autofillError": { "message": "Unable to auto-fill the selected item on this page. Copy and paste the information instead." diff --git a/src/popup2/app-routing.module.ts b/src/popup2/app-routing.module.ts index f22d66cb8f2..66dd4892f8e 100644 --- a/src/popup2/app-routing.module.ts +++ b/src/popup2/app-routing.module.ts @@ -15,6 +15,7 @@ import { RegisterComponent } from './accounts/register.component'; import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; import { TwoFactorComponent } from './accounts/two-factor.component'; import { TabsComponent } from './tabs.component'; +import { CiphersComponent } from './vault/ciphers.component'; import { CurrentTabComponent } from './vault/current-tab.component'; import { GroupingsComponent } from './vault/groupings.component'; @@ -29,6 +30,7 @@ const routes: Routes = [ { path: 'register', component: RegisterComponent }, { path: 'hint', component: HintComponent }, { path: 'environment', component: EnvironmentComponent }, + { path: 'ciphers', component: CiphersComponent }, { path: 'tabs', component: TabsComponent, children: [ diff --git a/src/popup2/app.module.ts b/src/popup2/app.module.ts index 4d25cd64c8c..885215d7e6f 100644 --- a/src/popup2/app.module.ts +++ b/src/popup2/app.module.ts @@ -24,6 +24,7 @@ import { RegisterComponent } from './accounts/register.component'; import { TwoFactorOptionsComponent } from './accounts/two-factor-options.component'; import { TwoFactorComponent } from './accounts/two-factor.component'; import { TabsComponent } from './tabs.component'; +import { CiphersComponent } from './vault/ciphers.component'; import { CurrentTabComponent } from './vault/current-tab.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 { 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({ imports: [ @@ -52,11 +59,13 @@ import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe'; ToasterModule, ], declarations: [ + ActionButtonsComponent, ApiActionDirective, AppComponent, AutofocusDirective, BlurClickDirective, BoxRowDirective, + CiphersComponent, CurrentTabComponent, EnvironmentComponent, FallbackSrcDirective, @@ -64,9 +73,12 @@ import { I18nPipe } from 'jslib/angular/pipes/i18n.pipe'; HomeComponent, HintComponent, I18nPipe, + IconComponent, LockComponent, LoginComponent, + PopOutComponent, RegisterComponent, + SearchCiphersPipe, StopClickDirective, StopPropDirective, TabsComponent, diff --git a/src/popup2/components/action-buttons.component.html b/src/popup2/components/action-buttons.component.html new file mode 100644 index 00000000000..411c1588a44 --- /dev/null +++ b/src/popup2/components/action-buttons.component.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/popup2/components/action-buttons.component.ts b/src/popup2/components/action-buttons.component.ts new file mode 100644 index 00000000000..69d0a144bd4 --- /dev/null +++ b/src/popup2/components/action-buttons.component.ts @@ -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(); + @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); + } +} diff --git a/src/popup2/components/pop-out.component.html b/src/popup2/components/pop-out.component.html new file mode 100644 index 00000000000..2507d233f81 --- /dev/null +++ b/src/popup2/components/pop-out.component.html @@ -0,0 +1,3 @@ + diff --git a/src/popup2/components/pop-out.component.ts b/src/popup2/components/pop-out.component.ts new file mode 100644 index 00000000000..78343efb10f --- /dev/null +++ b/src/popup2/components/pop-out.component.ts @@ -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 :( + } + } +} diff --git a/src/popup2/services/popup-utils.service.ts b/src/popup2/services/popup-utils.service.ts new file mode 100644 index 00000000000..a794f141463 --- /dev/null +++ b/src/popup2/services/popup-utils.service.ts @@ -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; + } +} diff --git a/src/popup2/services/services.module.ts b/src/popup2/services/services.module.ts index 56001d47dc3..1163c850445 100644 --- a/src/popup2/services/services.module.ts +++ b/src/popup2/services/services.module.ts @@ -25,6 +25,7 @@ import { MessagingService } from 'jslib/abstractions/messaging.service'; import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.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 { SyncService } from 'jslib/abstractions/sync.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 { AutofillService } from '../../services/abstractions/autofill.service'; - import BrowserMessagingService from '../../services/browserMessaging.service'; import { AuthService } from 'jslib/services/auth.service'; import { ConstantsService } from 'jslib/services/constants.service'; +import { StateService } from 'jslib/services/state.service'; + +import { PopupUtilsService } from './popup-utils.service'; function getBgService(service: string) { return (): T => { @@ -46,6 +49,7 @@ function getBgService(service: string) { }; } +const stateService = new StateService(); const messagingService = new BrowserMessagingService(getBgService('platformUtilsService')()); const authService = new AuthService(getBgService('cryptoService')(), getBgService('apiService')(), getBgService('userService')(), @@ -53,11 +57,16 @@ const authService = new AuthService(getBgService('cryptoService') getBgService('i18n2Service')(), getBgService('platformUtilsService')(), getBgService('constantsService')(), messagingService); -function initFactory(): Function { +function initFactory(i18nService: I18nService, storageService: StorageService): Function { return async () => { - if (getBgService('i18n2Service')() != null) { + const htmlEl = window.document.documentElement; + if (i18nService != null) { authService.init(); + htmlEl.classList.add('locale_' + i18nService.translationLocale); } + + stateService.save(ConstantsService.disableFaviconKey, + await storageService.get(ConstantsService.disableFaviconKey)); }; } @@ -69,8 +78,10 @@ function initFactory(): Function { providers: [ ValidationService, AuthGuardService, + PopupUtilsService, { provide: MessagingService, useValue: messagingService }, { provide: AuthServiceAbstraction, useValue: authService }, + { provide: StateServiceAbstraction, useValue: stateService }, { provide: AuditService, useFactory: getBgService('auditService'), deps: [] }, { provide: CipherService, useFactory: getBgService('cipherService'), deps: [] }, { provide: FolderService, useFactory: getBgService('folderService'), deps: [] }, @@ -102,7 +113,7 @@ function initFactory(): Function { { provide: APP_INITIALIZER, useFactory: initFactory, - deps: [], + deps: [I18nService, StorageService], multi: true, }, ], diff --git a/src/popup2/vault/ciphers.component.html b/src/popup2/vault/ciphers.component.html new file mode 100644 index 00000000000..e21608749a2 --- /dev/null +++ b/src/popup2/vault/ciphers.component.html @@ -0,0 +1,50 @@ +
+ +
+ {{searchPlaceholder || ('searchVault' | i18n)}} +
+
+ +
+
+ + + +
+ + + +

{{'noItemsInList' | i18n}}

+ +
+
+
+
diff --git a/src/popup2/vault/ciphers.component.ts b/src/popup2/vault/ciphers.component.ts new file mode 100644 index 00000000000..243986d887d --- /dev/null +++ b/src/popup2/vault/ciphers.component.ts @@ -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); + } +} diff --git a/src/popup2/vault/groupings.component.html b/src/popup2/vault/groupings.component.html index 6cebc13c8f7..e244f57ebf4 100644 --- a/src/popup2/vault/groupings.component.html +++ b/src/popup2/vault/groupings.component.html @@ -1,6 +1,6 @@
- +
{{'myVault' | i18n}} diff --git a/src/popup2/vault/groupings.component.ts b/src/popup2/vault/groupings.component.ts index 26d317c3fa1..a8fde9b7184 100644 --- a/src/popup2/vault/groupings.component.ts +++ b/src/popup2/vault/groupings.component.ts @@ -4,10 +4,13 @@ import { Component, OnInit, } from '@angular/core'; +import { Router } from '@angular/router'; import { CipherType } from 'jslib/enums/cipherType'; +import { CollectionView } from 'jslib/models/view/collectionView'; import { CipherView } from 'jslib/models/view/cipherView'; +import { FolderView } from 'jslib/models/view/folderView'; import { CollectionService } from 'jslib/abstractions/collection.service'; import { CipherService } from 'jslib/abstractions/cipher.service'; @@ -27,15 +30,15 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit typeCounts = new Map(); constructor(collectionService: CollectionService, folderService: FolderService, - private cipherService: CipherService) { + private cipherService: CipherService, private router: Router) { super(collectionService, folderService); } async ngOnInit() { - this.load(); - this.loaded = false; + super.load(); + super.loaded = false; await this.loadCiphers(); - this.loaded = true; + super.loaded = true; } 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 } }]); + } } diff --git a/src/scss/base.scss b/src/scss/base.scss index 402dbe0855f..595dec7377c 100644 --- a/src/scss/base.scss +++ b/src/scss/base.scss @@ -113,15 +113,16 @@ header { text-align: center; } - button, a { + app-pop-out > button, div > button, div > a { background: $brand-primary; border: none; color: white; padding: 0 10px; text-decoration: none; display: flex; - flex-direction: column; + flex-direction: row; justify-content: center; + align-items: center; &:hover, &:focus { background-color: rgba(255, 255, 255, 0.1); @@ -131,6 +132,14 @@ header { &:focus { text-decoration: underline; } + + i + span { + margin-left: 5px; + } + } + + app-pop-out { + display: flex; } .title { diff --git a/src/scss/box.scss b/src/scss/box.scss index 25d35fe43ff..49bf070bce7 100644 --- a/src/scss/box.scss +++ b/src/scss/box.scss @@ -255,4 +255,55 @@ font-size: $font-size-small; 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; + } + } + } + } + } }