diff --git a/jslib b/jslib index 4165a782770..d4b3a16fd11 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 4165a78277048d7b37319e63bd7e6473cbba5156 +Subproject commit d4b3a16fd1196abd3134c23a9fa0b6c002790458 diff --git a/src/popup/app-routing.animations.ts b/src/popup/app-routing.animations.ts index 757b2fb0f77..849c2424630 100644 --- a/src/popup/app-routing.animations.ts +++ b/src/popup/app-routing.animations.ts @@ -102,17 +102,41 @@ export const routerTransition = trigger('routerTransition', [ transition('2fa-options => 2fa', outSlideDown), transition('2fa => tabs', inSlideLeft), - transition('tabs => ciphers', inSlideLeft), - transition('ciphers => tabs', outSlideRight), + transition((fromState, toState) => { + if (fromState == null || toState === null || toState.indexOf('ciphers_') !== 0) { + return false; + } + return fromState.indexOf('ciphers_direction=f') === 0 || fromState === 'tabs'; + }, inSlideLeft), + transition((fromState, toState) => { + if (fromState == null || toState === null || fromState.indexOf('ciphers_') !== 0) { + return false; + } + return (fromState.indexOf('ciphers_') === 0 && fromState.indexOf('ciphers_direction=f') === -1) || + toState === 'tabs'; + }, outSlideRight), - transition('tabs => view-cipher, ciphers => view-cipher', inSlideUp), - transition('view-cipher => tabs, view-cipher => ciphers', outSlideDown), + transition((fromState, toState) => { + if (fromState == null || toState === null) { + return false; + } + return fromState.indexOf('ciphers_') === 0 && (toState === 'view-cipher' || toState === 'add-cipher'); + }, inSlideUp), + transition((fromState, toState) => { + if (fromState == null || toState === null) { + return false; + } + return (fromState === 'view-cipher' || fromState === 'add-cipher') && toState.indexOf('ciphers_') === 0; + }, outSlideDown), + + transition('tabs => view-cipher', inSlideUp), + transition('view-cipher => tabs', outSlideDown), transition('view-cipher => edit-cipher, view-cipher => cipher-password-history', inSlideUp), transition('edit-cipher => view-cipher, cipher-password-history => view-cipher, edit-cipher => tabs', outSlideDown), - transition('tabs => add-cipher, ciphers => add-cipher', inSlideUp), - transition('add-cipher => tabs, add-cipher => ciphers', outSlideDown), + transition('tabs => add-cipher', inSlideUp), + transition('add-cipher => tabs', outSlideDown), transition('generator => generator-history, tabs => generator-history', inSlideLeft), transition('generator-history => generator, generator-history => tabs', outSlideRight), diff --git a/src/popup/app-routing.module.ts b/src/popup/app-routing.module.ts index 4b0b2c7ea20..a8d670931a3 100644 --- a/src/popup/app-routing.module.ts +++ b/src/popup/app-routing.module.ts @@ -1,5 +1,7 @@ import { NgModule } from '@angular/core'; import { + ActivatedRouteSnapshot, + RouteReuseStrategy, RouterModule, Routes, } from '@angular/router'; @@ -240,11 +242,34 @@ const routes: Routes = [ }, ]; +export class NoRouteReuseStrategy implements RouteReuseStrategy { + shouldDetach(route: ActivatedRouteSnapshot) { + return false; + } + + store(route: ActivatedRouteSnapshot, handle: {}) { /* Nothing */ } + + shouldAttach(route: ActivatedRouteSnapshot) { + return false; + } + + retrieve(route: ActivatedRouteSnapshot): any { + return null; + } + + shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) { + return false; + } +} + @NgModule({ imports: [RouterModule.forRoot(routes, { useHash: true, /*enableTracing: true,*/ })], exports: [RouterModule], + providers: [ + { provide: RouteReuseStrategy, useClass: NoRouteReuseStrategy }, + ], }) export class AppRoutingModule { } diff --git a/src/popup/app.component.ts b/src/popup/app.component.ts index 345031af082..bd0e0cead93 100644 --- a/src/popup/app.component.ts +++ b/src/popup/app.component.ts @@ -137,7 +137,15 @@ export class AppComponent implements OnInit { } getState(outlet: RouterOutlet) { - return BrowserApi.isEdge18 ? null : outlet.activatedRouteData.state; + if (BrowserApi.isEdge18) { + return null; + } else if (outlet.activatedRouteData.state === 'ciphers') { + return 'ciphers_direction=' + (outlet.activatedRoute.queryParams as any).value.direction + '_' + + (outlet.activatedRoute.queryParams as any).value.folderId + '_' + + (outlet.activatedRoute.queryParams as any).value.collectionId; + } else { + return outlet.activatedRouteData.state; + } } private async recordActivity() { diff --git a/src/popup/scss/base.scss b/src/popup/scss/base.scss index 0639685fe75..69e1a48e15d 100644 --- a/src/popup/scss/base.scss +++ b/src/popup/scss/base.scss @@ -374,6 +374,7 @@ content { align-items: center; height: 100%; flex-direction: column; + flex-grow: 1; } .no-items { diff --git a/src/popup/scss/box.scss b/src/popup/scss/box.scss index 7d9cc0dd776..e0d44a7e5af 100644 --- a/src/popup/scss/box.scss +++ b/src/popup/scss/box.scss @@ -475,3 +475,8 @@ } } } + +.stacked-boxes { + display: flex; + flex-direction: column; +} diff --git a/src/popup/vault/ciphers.component.html b/src/popup/vault/ciphers.component.html index 7a5cd7780f2..f4530563be3 100644 --- a/src/popup/vault/ciphers.component.html +++ b/src/popup/vault/ciphers.component.html @@ -16,7 +16,40 @@ - + +
+
+ {{'folders' | i18n}} +
+ +
+
+
+ {{'collections' | i18n}} +
+ +
@@ -28,8 +61,8 @@
+ infiniteScroll [infiniteScrollDistance]="1" [infiniteScrollContainer]="'content'" [fromRoot]="true" + [infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
{{groupingTitle}} {{isSearching() ? filteredCiphers.length : ciphers.length}} diff --git a/src/popup/vault/ciphers.component.ts b/src/popup/vault/ciphers.component.ts index 236f039c14d..35921094843 100644 --- a/src/popup/vault/ciphers.component.ts +++ b/src/popup/vault/ciphers.component.ts @@ -25,6 +25,10 @@ import { StateService } from 'jslib/abstractions/state.service'; import { CipherType } from 'jslib/enums/cipherType'; import { CipherView } from 'jslib/models/view/cipherView'; +import { CollectionView } from 'jslib/models/view/collectionView'; +import { FolderView } from 'jslib/models/view/folderView'; + +import { TreeNode } from 'jslib/models/domain/treeNode'; import { BroadcasterService } from 'jslib/angular/services/broadcaster.service'; @@ -45,6 +49,8 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On folderId: string = null; type: CipherType = null; pagedCiphers: CipherView[] = []; + nestedFolders: Array>; + nestedCollections: Array>; private didScroll = false; private selectedTimeout: number; @@ -88,9 +94,11 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On this.folderId = params.folderId === 'none' ? null : params.folderId; this.searchPlaceholder = this.i18nService.t('searchFolder'); if (this.folderId != null) { - const folder = await this.folderService.get(this.folderId); - if (folder != null) { - this.groupingTitle = (await folder.decrypt()).name; + const folderNode = await this.folderService.getNested(this.folderId); + if (folderNode != null && folderNode.node != null) { + this.groupingTitle = folderNode.node.name; + this.nestedFolders = folderNode.children != null && folderNode.children.length > 0 ? + folderNode.children : null; } } else { this.groupingTitle = this.i18nService.t('noneFolder'); @@ -99,9 +107,11 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On } else if (params.collectionId) { this.showAdd = false; this.searchPlaceholder = this.i18nService.t('searchCollection'); - const collection = await this.collectionService.get(params.collectionId); - if (collection != null) { - this.groupingTitle = (await collection.decrypt()).name; + const collectionNode = await this.collectionService.getNested(params.collectionId); + if (collectionNode != null && collectionNode.node != null) { + this.groupingTitle = collectionNode.node.name; + this.nestedCollections = collectionNode.children != null && collectionNode.children.length > 0 ? + collectionNode.children : null; } await super.load((c) => c.collectionIds != null && c.collectionIds.indexOf(params.collectionId) > -1); } else { @@ -115,6 +125,16 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On this.searchText = this.state.searchText; } window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state.scrollY), 0); + + // TODO: This is pushing a new page onto the browser navigation history. Figure out how to now do that + // so that we don't have to hit back button twice + const newUrl = this.router.createUrlTree([], { + queryParams: { direction: null }, + queryParamsHandling: 'merge', + preserveFragment: true, + replaceUrl: true, + }).toString(); + this.location.go(newUrl); }); this.broadcasterService.subscribe(ComponentId, (message: any) => { @@ -151,6 +171,16 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On }, 200); } + selectFolder(folder: FolderView) { + if (folder.id != null) { + this.router.navigate(['/ciphers'], { queryParams: { folderId: folder.id, direction: 'f' } }); + } + } + + selectCollection(collection: CollectionView) { + this.router.navigate(['/ciphers'], { queryParams: { collectionId: collection.id, direction: 'f' } }); + } + async launchCipher(cipher: CipherView) { if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) { return; @@ -200,6 +230,10 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On return !searching && this.ciphers.length > this.pageSize; } + routerCanReuse() { + return false; + } + async resetPaging() { this.pagedCiphers = []; this.loadMore(); diff --git a/src/popup/vault/groupings.component.html b/src/popup/vault/groupings.component.html index 01b91923553..3dac4713309 100644 --- a/src/popup/vault/groupings.component.html +++ b/src/popup/vault/groupings.component.html @@ -78,39 +78,39 @@
-
+ -
+
{{'collections' | i18n}} - {{collections.length}} + {{nestedCollections.length}}
diff --git a/src/popup/vault/groupings.component.ts b/src/popup/vault/groupings.component.ts index c49e54bb5b2..3709d941f38 100644 --- a/src/popup/vault/groupings.component.ts +++ b/src/popup/vault/groupings.component.ts @@ -82,7 +82,7 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit } get folderCount(): number { - return this.folders.length - (this.showNoFolderCiphers ? 0 : 1); + return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1); } async ngOnInit() { @@ -242,12 +242,12 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit async selectFolder(folder: FolderView) { super.selectFolder(folder); - this.router.navigate(['/ciphers'], { queryParams: { folderId: folder.id || 'none' } }); + this.router.navigate(['/ciphers'], { queryParams: { folderId: folder.id || 'none', direction: 'f' } }); } async selectCollection(collection: CollectionView) { super.selectCollection(collection); - this.router.navigate(['/ciphers'], { queryParams: { collectionId: collection.id } }); + this.router.navigate(['/ciphers'], { queryParams: { collectionId: collection.id, direction: 'f' } }); } async selectCipher(cipher: CipherView) {