WIP: add support for nested folders and collection

This commit is contained in:
Kyle Spearrin 2018-10-26 12:37:55 -04:00
parent 9d1f8e43d9
commit 69e664a154
10 changed files with 162 additions and 32 deletions

2
jslib

@ -1 +1 @@
Subproject commit 4165a78277048d7b37319e63bd7e6473cbba5156
Subproject commit d4b3a16fd1196abd3134c23a9fa0b6c002790458

View File

@ -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),

View File

@ -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 { }

View File

@ -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() {

View File

@ -374,6 +374,7 @@ content {
align-items: center;
height: 100%;
flex-direction: column;
flex-grow: 1;
}
.no-items {

View File

@ -475,3 +475,8 @@
}
}
}
.stacked-boxes {
display: flex;
flex-direction: column;
}

View File

@ -16,7 +16,40 @@
</button>
</div>
</header>
<content>
<content [ngClass]="{'stacked-boxes': nestedFolders && nestedFolders.length || nestedCollections && nestedCollections.length}">
<div class="box list" *ngIf="nestedFolders && nestedFolders.length">
<div class="box-header">
{{'folders' | i18n}}
</div>
<div class="box-content single-line">
<a *ngFor="let f of nestedFolders" href="#" class="box-content-row"
appStopClick appBlurClick (click)="selectFolder(f.node)">
<div class="row-main">
<div class="icon">
<i class="fa fa-fw fa-lg"
[ngClass]="{'fa-folder-open': f.node.id, 'fa-folder-open-o': !f.node.id}"></i>
</div>
<span class="text">{{f.node.name}}</span>
</div>
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
</a>
</div>
</div>
<div class="box list" *ngIf="nestedCollections && nestedCollections.length">
<div class="box-header">
{{'collections' | i18n}}
</div>
<div class="box-content single-line">
<a *ngFor="let c of nestedCollections" href="#" class="box-content-row"
appStopClick appBlurClick (click)="selectCollection(c.node)">
<div class="row-main">
<div class="icon"><i class="fa fa-fw fa-lg fa-cube"></i></div>
<span class="text">{{c.node.name}}</span>
</div>
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
</a>
</div>
</div>
<ng-container *ngIf="(!isPaging() ? ciphers : pagedCiphers) as filteredCiphers">
<div class="no-items" *ngIf="!filteredCiphers.length">
<i class="fa fa-spinner fa-spin fa-3x" *ngIf="!loaded"></i>
@ -28,8 +61,8 @@
</ng-container>
</div>
<div class="box list only-list" *ngIf="filteredCiphers.length > 0"
infiniteScroll [infiniteScrollDistance]="1" [infiniteScrollContainer]="'content'" [fromRoot]="true"
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
infiniteScroll [infiniteScrollDistance]="1" [infiniteScrollContainer]="'content'" [fromRoot]="true"
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
<div class="box-header">
{{groupingTitle}}
<span class="flex-right">{{isSearching() ? filteredCiphers.length : ciphers.length}}</span>

View File

@ -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<TreeNode<FolderView>>;
nestedCollections: Array<TreeNode<CollectionView>>;
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();

View File

@ -78,39 +78,39 @@
</a>
</div>
</div>
<div class="box list" *ngIf="folders.length">
<div class="box list" *ngIf="nestedFolders && nestedFolders.length">
<div class="box-header">
{{'folders' | i18n}}
<span class="flex-right">{{folderCount}}</span>
</div>
<div class="box-content single-line">
<a *ngFor="let f of folders" href="#" class="box-content-row"
appStopClick appBlurClick (click)="selectFolder(f)">
<a *ngFor="let f of nestedFolders" href="#" class="box-content-row"
appStopClick appBlurClick (click)="selectFolder(f.node)">
<div class="row-main">
<div class="icon">
<i class="fa fa-fw fa-lg"
[ngClass]="{'fa-folder-open': f.id, 'fa-folder-open-o': !f.id}"></i>
[ngClass]="{'fa-folder-open': f.node.id, 'fa-folder-open-o': !f.node.id}"></i>
</div>
<span class="text">{{f.name}}</span>
<span class="text">{{f.node.name}}</span>
</div>
<span class="row-sub-label">{{folderCounts.get(f.id) || 0}}</span>
<span class="row-sub-label">{{folderCounts.get(f.node.id) || 0}}</span>
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
</a>
</div>
</div>
<div class="box list" *ngIf="collections.length">
<div class="box list" *ngIf="nestedCollections && nestedCollections.length">
<div class="box-header">
{{'collections' | i18n}}
<span class="flex-right">{{collections.length}}</span>
<span class="flex-right">{{nestedCollections.length}}</span>
</div>
<div class="box-content single-line">
<a *ngFor="let c of collections" href="#" class="box-content-row"
appStopClick appBlurClick (click)="selectCollection(c)">
<a *ngFor="let c of nestedCollections" href="#" class="box-content-row"
appStopClick appBlurClick (click)="selectCollection(c.node)">
<div class="row-main">
<div class="icon"><i class="fa fa-fw fa-lg fa-cube"></i></div>
<span class="text">{{c.name}}</span>
<span class="text">{{c.node.name}}</span>
</div>
<span class="row-sub-label">{{collectionCounts.get(c.id) || 0}}</span>
<span class="row-sub-label">{{collectionCounts.get(c.node.id) || 0}}</span>
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
</a>
</div>

View File

@ -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) {