mirror of
https://github.com/bitwarden/browser.git
synced 2024-12-24 16:49:26 +01:00
WIP: add support for nested folders and collection
This commit is contained in:
parent
9d1f8e43d9
commit
69e664a154
2
jslib
2
jslib
@ -1 +1 @@
|
||||
Subproject commit 4165a78277048d7b37319e63bd7e6473cbba5156
|
||||
Subproject commit d4b3a16fd1196abd3134c23a9fa0b6c002790458
|
@ -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),
|
||||
|
@ -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 { }
|
||||
|
@ -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() {
|
||||
|
@ -374,6 +374,7 @@ content {
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.no-items {
|
||||
|
@ -475,3 +475,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stacked-boxes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user