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-options => 2fa', outSlideDown),
|
||||||
transition('2fa => tabs', inSlideLeft),
|
transition('2fa => tabs', inSlideLeft),
|
||||||
|
|
||||||
transition('tabs => ciphers', inSlideLeft),
|
transition((fromState, toState) => {
|
||||||
transition('ciphers => tabs', outSlideRight),
|
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((fromState, toState) => {
|
||||||
transition('view-cipher => tabs, view-cipher => ciphers', outSlideDown),
|
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('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('edit-cipher => view-cipher, cipher-password-history => view-cipher, edit-cipher => tabs', outSlideDown),
|
||||||
|
|
||||||
transition('tabs => add-cipher, ciphers => add-cipher', inSlideUp),
|
transition('tabs => add-cipher', inSlideUp),
|
||||||
transition('add-cipher => tabs, add-cipher => ciphers', outSlideDown),
|
transition('add-cipher => tabs', outSlideDown),
|
||||||
|
|
||||||
transition('generator => generator-history, tabs => generator-history', inSlideLeft),
|
transition('generator => generator-history, tabs => generator-history', inSlideLeft),
|
||||||
transition('generator-history => generator, generator-history => tabs', outSlideRight),
|
transition('generator-history => generator, generator-history => tabs', outSlideRight),
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
RouteReuseStrategy,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
Routes,
|
Routes,
|
||||||
} from '@angular/router';
|
} 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({
|
@NgModule({
|
||||||
imports: [RouterModule.forRoot(routes, {
|
imports: [RouterModule.forRoot(routes, {
|
||||||
useHash: true,
|
useHash: true,
|
||||||
/*enableTracing: true,*/
|
/*enableTracing: true,*/
|
||||||
})],
|
})],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
|
providers: [
|
||||||
|
{ provide: RouteReuseStrategy, useClass: NoRouteReuseStrategy },
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule { }
|
export class AppRoutingModule { }
|
||||||
|
@ -137,7 +137,15 @@ export class AppComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getState(outlet: RouterOutlet) {
|
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() {
|
private async recordActivity() {
|
||||||
|
@ -374,6 +374,7 @@ content {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-items {
|
.no-items {
|
||||||
|
@ -475,3 +475,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stacked-boxes {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
@ -16,7 +16,40 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</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">
|
<ng-container *ngIf="(!isPaging() ? ciphers : pagedCiphers) as filteredCiphers">
|
||||||
<div class="no-items" *ngIf="!filteredCiphers.length">
|
<div class="no-items" *ngIf="!filteredCiphers.length">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x" *ngIf="!loaded"></i>
|
<i class="fa fa-spinner fa-spin fa-3x" *ngIf="!loaded"></i>
|
||||||
@ -28,8 +61,8 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<div class="box list only-list" *ngIf="filteredCiphers.length > 0"
|
<div class="box list only-list" *ngIf="filteredCiphers.length > 0"
|
||||||
infiniteScroll [infiniteScrollDistance]="1" [infiniteScrollContainer]="'content'" [fromRoot]="true"
|
infiniteScroll [infiniteScrollDistance]="1" [infiniteScrollContainer]="'content'" [fromRoot]="true"
|
||||||
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
[infiniteScrollDisabled]="!isPaging()" (scrolled)="loadMore()">
|
||||||
<div class="box-header">
|
<div class="box-header">
|
||||||
{{groupingTitle}}
|
{{groupingTitle}}
|
||||||
<span class="flex-right">{{isSearching() ? filteredCiphers.length : ciphers.length}}</span>
|
<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 { CipherType } from 'jslib/enums/cipherType';
|
||||||
|
|
||||||
import { CipherView } from 'jslib/models/view/cipherView';
|
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';
|
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||||
|
|
||||||
@ -45,6 +49,8 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On
|
|||||||
folderId: string = null;
|
folderId: string = null;
|
||||||
type: CipherType = null;
|
type: CipherType = null;
|
||||||
pagedCiphers: CipherView[] = [];
|
pagedCiphers: CipherView[] = [];
|
||||||
|
nestedFolders: Array<TreeNode<FolderView>>;
|
||||||
|
nestedCollections: Array<TreeNode<CollectionView>>;
|
||||||
|
|
||||||
private didScroll = false;
|
private didScroll = false;
|
||||||
private selectedTimeout: number;
|
private selectedTimeout: number;
|
||||||
@ -88,9 +94,11 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On
|
|||||||
this.folderId = params.folderId === 'none' ? null : params.folderId;
|
this.folderId = params.folderId === 'none' ? null : params.folderId;
|
||||||
this.searchPlaceholder = this.i18nService.t('searchFolder');
|
this.searchPlaceholder = this.i18nService.t('searchFolder');
|
||||||
if (this.folderId != null) {
|
if (this.folderId != null) {
|
||||||
const folder = await this.folderService.get(this.folderId);
|
const folderNode = await this.folderService.getNested(this.folderId);
|
||||||
if (folder != null) {
|
if (folderNode != null && folderNode.node != null) {
|
||||||
this.groupingTitle = (await folder.decrypt()).name;
|
this.groupingTitle = folderNode.node.name;
|
||||||
|
this.nestedFolders = folderNode.children != null && folderNode.children.length > 0 ?
|
||||||
|
folderNode.children : null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.groupingTitle = this.i18nService.t('noneFolder');
|
this.groupingTitle = this.i18nService.t('noneFolder');
|
||||||
@ -99,9 +107,11 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On
|
|||||||
} else if (params.collectionId) {
|
} else if (params.collectionId) {
|
||||||
this.showAdd = false;
|
this.showAdd = false;
|
||||||
this.searchPlaceholder = this.i18nService.t('searchCollection');
|
this.searchPlaceholder = this.i18nService.t('searchCollection');
|
||||||
const collection = await this.collectionService.get(params.collectionId);
|
const collectionNode = await this.collectionService.getNested(params.collectionId);
|
||||||
if (collection != null) {
|
if (collectionNode != null && collectionNode.node != null) {
|
||||||
this.groupingTitle = (await collection.decrypt()).name;
|
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);
|
await super.load((c) => c.collectionIds != null && c.collectionIds.indexOf(params.collectionId) > -1);
|
||||||
} else {
|
} else {
|
||||||
@ -115,6 +125,16 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On
|
|||||||
this.searchText = this.state.searchText;
|
this.searchText = this.state.searchText;
|
||||||
}
|
}
|
||||||
window.setTimeout(() => this.popupUtils.setContentScrollY(window, this.state.scrollY), 0);
|
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) => {
|
this.broadcasterService.subscribe(ComponentId, (message: any) => {
|
||||||
@ -151,6 +171,16 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On
|
|||||||
}, 200);
|
}, 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) {
|
async launchCipher(cipher: CipherView) {
|
||||||
if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) {
|
if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) {
|
||||||
return;
|
return;
|
||||||
@ -200,6 +230,10 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On
|
|||||||
return !searching && this.ciphers.length > this.pageSize;
|
return !searching && this.ciphers.length > this.pageSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routerCanReuse() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async resetPaging() {
|
async resetPaging() {
|
||||||
this.pagedCiphers = [];
|
this.pagedCiphers = [];
|
||||||
this.loadMore();
|
this.loadMore();
|
||||||
|
@ -78,39 +78,39 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box list" *ngIf="folders.length">
|
<div class="box list" *ngIf="nestedFolders && nestedFolders.length">
|
||||||
<div class="box-header">
|
<div class="box-header">
|
||||||
{{'folders' | i18n}}
|
{{'folders' | i18n}}
|
||||||
<span class="flex-right">{{folderCount}}</span>
|
<span class="flex-right">{{folderCount}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content single-line">
|
<div class="box-content single-line">
|
||||||
<a *ngFor="let f of folders" href="#" class="box-content-row"
|
<a *ngFor="let f of nestedFolders" href="#" class="box-content-row"
|
||||||
appStopClick appBlurClick (click)="selectFolder(f)">
|
appStopClick appBlurClick (click)="selectFolder(f.node)">
|
||||||
<div class="row-main">
|
<div class="row-main">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<i class="fa fa-fw fa-lg"
|
<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>
|
</div>
|
||||||
<span class="text">{{f.name}}</span>
|
<span class="text">{{f.node.name}}</span>
|
||||||
</div>
|
</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>
|
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box list" *ngIf="collections.length">
|
<div class="box list" *ngIf="nestedCollections && nestedCollections.length">
|
||||||
<div class="box-header">
|
<div class="box-header">
|
||||||
{{'collections' | i18n}}
|
{{'collections' | i18n}}
|
||||||
<span class="flex-right">{{collections.length}}</span>
|
<span class="flex-right">{{nestedCollections.length}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-content single-line">
|
<div class="box-content single-line">
|
||||||
<a *ngFor="let c of collections" href="#" class="box-content-row"
|
<a *ngFor="let c of nestedCollections" href="#" class="box-content-row"
|
||||||
appStopClick appBlurClick (click)="selectCollection(c)">
|
appStopClick appBlurClick (click)="selectCollection(c.node)">
|
||||||
<div class="row-main">
|
<div class="row-main">
|
||||||
<div class="icon"><i class="fa fa-fw fa-lg fa-cube"></i></div>
|
<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>
|
</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>
|
<span><i class="fa fa-chevron-right fa-lg row-sub-icon"></i></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,7 +82,7 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
|
|||||||
}
|
}
|
||||||
|
|
||||||
get folderCount(): number {
|
get folderCount(): number {
|
||||||
return this.folders.length - (this.showNoFolderCiphers ? 0 : 1);
|
return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -242,12 +242,12 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit
|
|||||||
|
|
||||||
async selectFolder(folder: FolderView) {
|
async selectFolder(folder: FolderView) {
|
||||||
super.selectFolder(folder);
|
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) {
|
async selectCollection(collection: CollectionView) {
|
||||||
super.selectCollection(collection);
|
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) {
|
async selectCipher(cipher: CipherView) {
|
||||||
|
Loading…
Reference in New Issue
Block a user