[end user vault refresh] Filters (#1487)

This commit is contained in:
Addison Beck 2022-05-03 07:45:08 -04:00 committed by GitHub
parent 8be88a731c
commit 309df55cf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 993 additions and 722 deletions

2
jslib

@ -1 +1 @@
Subproject commit f3a4fde513dc16779e85597bd00027b160671db7
Subproject commit 816822d3a4cc7696411240f58465a9e638454b7c

View File

@ -1,10 +1,8 @@
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { AuthGuardService } from "jslib-angular/services/auth-guard.service";
import { LockGuardService } from "jslib-angular/services/lock-guard.service";
import { LoginGuardService } from "../services/loginGuard.service";
import { AuthGuard } from "jslib-angular/guards/auth.guard";
import { LockGuard } from "jslib-angular/guards/lock.guard";
import { HintComponent } from "./accounts/hint.component";
import { LockComponent } from "./accounts/lock.component";
@ -15,6 +13,7 @@ import { SetPasswordComponent } from "./accounts/set-password.component";
import { SsoComponent } from "./accounts/sso.component";
import { TwoFactorComponent } from "./accounts/two-factor.component";
import { UpdateTempPasswordComponent } from "./accounts/update-temp-password.component";
import { LoginGuard } from "./guards/login.guard";
import { SendComponent } from "./send/send.component";
import { VaultComponent } from "./vault/vault.component";
@ -23,19 +22,19 @@ const routes: Routes = [
{
path: "lock",
component: LockComponent,
canActivate: [LockGuardService],
canActivate: [LockGuard],
},
{
path: "login",
component: LoginComponent,
canActivate: [LoginGuardService],
canActivate: [LoginGuard],
},
{ path: "2fa", component: TwoFactorComponent },
{ path: "register", component: RegisterComponent },
{
path: "vault",
component: VaultComponent,
canActivate: [AuthGuardService],
canActivate: [AuthGuard],
},
{ path: "hint", component: HintComponent },
{ path: "set-password", component: SetPasswordComponent },
@ -43,17 +42,17 @@ const routes: Routes = [
{
path: "send",
component: SendComponent,
canActivate: [AuthGuardService],
canActivate: [AuthGuard],
},
{
path: "update-temp-password",
component: UpdateTempPasswordComponent,
canActivate: [AuthGuardService],
canActivate: [AuthGuard],
},
{
path: "remove-password",
component: RemovePasswordComponent,
canActivate: [AuthGuardService],
canActivate: [AuthGuard],
data: { titleId: "removeMasterPassword" },
},
];

View File

@ -1,10 +1,6 @@
import "zone.js/dist/zone";
import { A11yModule } from "@angular/cdk/a11y";
import { DragDropModule } from "@angular/cdk/drag-drop";
import { OverlayModule } from "@angular/cdk/overlay";
import { ScrollingModule } from "@angular/cdk/scrolling";
import { DatePipe, registerLocaleData } from "@angular/common";
import { registerLocaleData } from "@angular/common";
import localeAf from "@angular/common/locales/af";
import localeAz from "@angular/common/locales/az";
import localeBe from "@angular/common/locales/be";
@ -59,11 +55,6 @@ import localeVi from "@angular/common/locales/vi";
import localeZhCn from "@angular/common/locales/zh-Hans";
import localeZhTw from "@angular/common/locales/zh-Hant";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { JslibModule } from "jslib-angular/jslib.module";
import { EnvironmentComponent } from "./accounts/environment.component";
import { HintComponent } from "./accounts/hint.component";
@ -88,10 +79,11 @@ import { AccountSwitcherComponent } from "./layout/account-switcher.component";
import { HeaderComponent } from "./layout/header.component";
import { NavComponent } from "./layout/nav.component";
import { SearchComponent } from "./layout/search/search.component";
import { SharedModule } from "./modules/shared.module";
import { VaultFilterModule } from "./modules/vault-filter/vault-filter.module";
import { AddEditComponent as SendAddEditComponent } from "./send/add-edit.component";
import { EffluxDatesComponent as SendEffluxDatesComponent } from "./send/efflux-dates.component";
import { SendComponent } from "./send/send.component";
import { ServicesModule } from "./services.module";
import { AddEditCustomFieldsComponent } from "./vault/add-edit-custom-fields.component";
import { AddEditComponent } from "./vault/add-edit.component";
import { AttachmentsComponent } from "./vault/attachments.component";
@ -100,7 +92,6 @@ import { CollectionsComponent } from "./vault/collections.component";
import { ExportComponent } from "./vault/export.component";
import { FolderAddEditComponent } from "./vault/folder-add-edit.component";
import { GeneratorComponent } from "./vault/generator.component";
import { GroupingsComponent } from "./vault/groupings.component";
import { PasswordGeneratorHistoryComponent } from "./vault/password-generator-history.component";
import { PasswordHistoryComponent } from "./vault/password-history.component";
import { ShareComponent } from "./vault/share.component";
@ -163,19 +154,7 @@ registerLocaleData(localeZhCn, "zh-CN");
registerLocaleData(localeZhTw, "zh-TW");
@NgModule({
imports: [
A11yModule,
AppRoutingModule,
BrowserAnimationsModule,
BrowserModule,
DragDropModule,
FormsModule,
JslibModule,
OverlayModule,
ReactiveFormsModule,
ScrollingModule,
ServicesModule,
],
imports: [SharedModule, AppRoutingModule, VaultFilterModule],
declarations: [
AccountSwitcherComponent,
AddEditComponent,
@ -187,7 +166,6 @@ registerLocaleData(localeZhTw, "zh-TW");
EnvironmentComponent,
ExportComponent,
FolderAddEditComponent,
GroupingsComponent,
HeaderComponent,
HintComponent,
LockComponent,
@ -218,7 +196,6 @@ registerLocaleData(localeZhTw, "zh-TW");
ViewComponent,
ViewCustomFieldsComponent,
],
providers: [DatePipe],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@ -8,7 +8,7 @@ import { StateService } from "jslib-common/abstractions/state.service";
const maxAllowedAccounts = 5;
@Injectable()
export class LoginGuardService implements CanActivate {
export class LoginGuard implements CanActivate {
protected homepage = "vault";
constructor(
private stateService: StateService,

View File

@ -34,14 +34,13 @@ import { ElectronRendererMessagingService } from "jslib-electron/services/electr
import { ElectronRendererSecureStorageService } from "jslib-electron/services/electronRendererSecureStorage.service";
import { ElectronRendererStorageService } from "jslib-electron/services/electronRendererStorage.service";
import { Account } from "../models/account";
import { I18nService } from "../services/i18n.service";
import { LoginGuardService } from "../services/loginGuard.service";
import { NativeMessagingService } from "../services/nativeMessaging.service";
import { PasswordRepromptService } from "../services/passwordReprompt.service";
import { StateService } from "../services/state.service";
import { SearchBarService } from "./layout/search/search-bar.service";
import { Account } from "../../models/account";
import { I18nService } from "../../services/i18n.service";
import { NativeMessagingService } from "../../services/nativeMessaging.service";
import { PasswordRepromptService } from "../../services/passwordReprompt.service";
import { StateService } from "../../services/state.service";
import { LoginGuard } from "../guards/login.guard";
import { SearchBarService } from "../layout/search/search-bar.service";
export function initFactory(
window: Window,
@ -167,8 +166,8 @@ export function initFactory(
NativeMessagingService,
SearchBarService,
{
provide: LoginGuardService,
useClass: LoginGuardService,
provide: LoginGuard,
useClass: LoginGuard,
deps: [StateServiceAbstraction, PlatformUtilsServiceAbstraction, I18nServiceAbstraction],
},
{

View File

@ -0,0 +1,43 @@
import { A11yModule } from "@angular/cdk/a11y";
import { DragDropModule } from "@angular/cdk/drag-drop";
import { OverlayModule } from "@angular/cdk/overlay";
import { ScrollingModule } from "@angular/cdk/scrolling";
import { DatePipe } from "@angular/common";
import { NgModule } from "@angular/core";
import { FormsModule, ReactiveFormsModule } from "@angular/forms";
import { BrowserModule } from "@angular/platform-browser";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { JslibModule } from "jslib-angular/jslib.module";
import { ServicesModule } from "./services.module";
@NgModule({
imports: [
A11yModule,
BrowserAnimationsModule,
BrowserModule,
DragDropModule,
FormsModule,
JslibModule,
OverlayModule,
ReactiveFormsModule,
ScrollingModule,
ServicesModule,
],
exports: [
A11yModule,
BrowserAnimationsModule,
BrowserModule,
DatePipe,
DragDropModule,
FormsModule,
JslibModule,
OverlayModule,
ReactiveFormsModule,
ScrollingModule,
ServicesModule,
],
providers: [DatePipe],
})
export class SharedModule {}

View File

@ -0,0 +1,72 @@
<ng-container *ngIf="show">
<div class="filter-heading">
<button
class="no-btn"
[attr.aria-expanded]="!isCollapsed(collectionsGrouping)"
aria-controls="collection-filters"
(click)="toggleCollapse(collectionsGrouping)"
appA11yTitle="{{ 'toggleCollapse' | i18n }}"
>
<i
class="bwi bwi-fw"
aria-hidden="true"
[ngClass]="{
'bwi-angle-right': isCollapsed(collectionsGrouping),
'bwi-angle-down': !isCollapsed(collectionsGrouping)
}"
></i>
</button>
<h2>&nbsp;{{ collectionsGrouping.name | i18n }}</h2>
</div>
<ul id="collection-filters" *ngIf="!isCollapsed(collectionsGrouping)" class="filter-options">
<ng-template #recursiveCollections let-collections>
<li
class="filter-option"
*ngFor="let c of collections"
[ngClass]="{ active: c.node.id === activeFilter.selectedCollectionId }"
>
<span class="filter-buttons">
<button
*ngIf="c.children.length"
class="toggle-button"
[attr.aria-expanded]="!isCollapsed(c.node)"
[attr.aria-controls]="c.node.name + '_children'"
(click)="toggleCollapse(c.node)"
appA11yTitle="{{ 'toggleCollapse' | i18n }}"
>
<i
class="bwi bwi-fw"
aria-hidden="true"
[ngClass]="{
'bwi-angle-right': isCollapsed(c.node),
'bwi-angle-down': !isCollapsed(c.node)
}"
></i>
</button>
<button class="filter-button" (click)="applyFilter(c.node)">
<i
*ngIf="c.children.length === 0"
class="bwi bwi-fw bwi-collection"
aria-hidden="true"
></i>
&nbsp;{{ c.node.name }}
</button>
</span>
<ul
[id]="c.node.name + '_children'"
class="nested-filter-options"
*ngIf="c.children.length && !isCollapsed(c.node)"
>
<ng-container
*ngTemplateOutlet="recursiveCollections; context: { $implicit: c.children }"
>
</ng-container>
</ul>
</li>
</ng-template>
<ng-container
*ngTemplateOutlet="recursiveCollections; context: { $implicit: nestedCollections }"
>
</ng-container>
</ul>
</ng-container>

View File

@ -0,0 +1,9 @@
import { Component } from "@angular/core";
import { CollectionFilterComponent as BaseCollectionFilterComponent } from "jslib-angular/modules/vault-filter/components/collection-filter.component";
@Component({
selector: "app-collection-filter",
templateUrl: "collection-filter.component.html",
})
export class CollectionFilterComponent extends BaseCollectionFilterComponent {}

View File

@ -0,0 +1,78 @@
<ng-container *ngIf="!hide">
<div class="filter-heading">
<button
class="toggle-button"
[attr.aria-expanded]="!isCollapsed(foldersGrouping)"
aria-controls="folder-filters"
(click)="toggleCollapse(foldersGrouping)"
appA11yTitle="{{ 'toggleCollapse' | i18n }}"
>
<i
class="bwi bwi-fw"
aria-hidden="true"
[ngClass]="{
'bwi-angle-right': isCollapsed(foldersGrouping),
'bwi-angle-down': !isCollapsed(foldersGrouping)
}"
></i>
</button>
<h2>&nbsp;{{ foldersGrouping.name | i18n }}</h2>
<button class="add-button" (click)="addFolder()" appA11yTitle="{{ 'addFolder' | i18n }}">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</button>
</div>
<ul id="folder-filters" class="filter-options" *ngIf="!isCollapsed(foldersGrouping)">
<ng-template #recursiveFolders let-folders>
<li
*ngFor="let f of folders"
[ngClass]="{
active: f.node.id === activeFilter.selectedFolderId && activeFilter.selectedFolder
}"
class="filter-option"
>
<span class="filter-buttons">
<button
class="toggle-button"
*ngIf="f.children.length"
(click)="toggleCollapse(f.node)"
appA11yTitle="{{ 'toggleCollapse' | i18n }}"
[attr.aria-expanded]="!isCollapsed(f.node)"
[attr.aria-controls]="f.node.name + '_children'"
>
<i
class="bwi bwi-fw"
aria-hidden="true"
[ngClass]="{
'bwi-angle-right': isCollapsed(f.node),
'bwi-angle-down': !isCollapsed(f.node)
}"
></i>
</button>
<button class="filter-button" (click)="applyFilter(f.node)">
<i *ngIf="f.children.length === 0" class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
&nbsp;{{ f.node.name }}
</button>
<button
class="edit-button"
*ngIf="f.node.id"
(click)="editFolder(f.node)"
appA11yTitle="{{ 'editFolder' | i18n }}"
>
<i class="bwi bwi-pencil bwi-fw" aria-hidden="true"></i>
</button>
</span>
<ul
[id]="f.node.name + '_children'"
class="nested-filter-options"
*ngIf="f.children.length && !isCollapsed(f.node)"
>
<ng-container *ngTemplateOutlet="recursiveFolders; context: { $implicit: f.children }">
</ng-container>
</ul>
</li>
</ng-template>
<ng-container
*ngTemplateOutlet="recursiveFolders; context: { $implicit: nestedFolders }"
></ng-container>
</ul>
</ng-container>

View File

@ -0,0 +1,9 @@
import { Component } from "@angular/core";
import { FolderFilterComponent as BaseFolderFilterComponent } from "jslib-angular/modules/vault-filter/components/folder-filter.component";
@Component({
selector: "app-folder-filter",
templateUrl: "folder-filter.component.html",
})
export class FolderFilterComponent extends BaseFolderFilterComponent {}

View File

@ -0,0 +1,86 @@
<ng-container *ngIf="show">
<ng-container [ngSwitch]="displayMode">
<ng-container *ngSwitchCase="'personalOwnershipPolicy'">
<div class="filter-heading" [ngClass]="{ active: !hasActiveFilter }">
<button
class="toggle-button"
appA11yTitle="{{ 'toggleCollapse' | i18n }}"
(click)="toggleCollapse()"
[attr.aria-expanded]="!isCollapsed"
aria-controls="organization-filters"
>
<i
class="bwi bwi-fw"
aria-hidden="true"
[ngClass]="{
'bwi-angle-right': isCollapsed,
'bwi-angle-down': !isCollapsed
}"
></i>
</button>
<button class="filter-button" (click)="clearFilter()">
<h2>&nbsp;{{ organizationGrouping.name | i18n }}</h2>
</button>
</div>
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options no-margin">
<li
class="filter-option"
*ngFor="let organization of organizations"
[ngClass]="{ active: organization.id === activeFilter.selectedOrganizationId }"
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyOrganizationFilter(organization)">
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
&nbsp;{{ organization.name }}
</button>
</span>
</li>
</ul>
</ng-container>
<ng-container *ngSwitchCase="'organizationMember'">
<div class="filter-heading" [ngClass]="{ active: !hasActiveFilter }">
<button
appA11yTitle="{{ 'toggleCollapse' | i18n }}"
(click)="toggleCollapse()"
[attr.aria-expanded]="!isCollapsed"
aria-controls="organization-filters"
>
<i
class="bwi bwi-fw"
aria-hidden="true"
[ngClass]="{
'bwi-angle-right': isCollapsed,
'bwi-angle-down': !isCollapsed
}"
></i>
</button>
<button class="filter-button" (click)="clearFilter()">
<h2>&nbsp;{{ organizationGrouping.name | i18n }}</h2>
</button>
</div>
<ul id="organization-filters" *ngIf="!isCollapsed" class="filter-options no-margin">
<li class="filter-option" [ngClass]="{ active: activeFilter.myVaultOnly }">
<span class="filter-buttons">
<button class="filter-button" (click)="applyMyVaultFilter()">
<i class="bwi bwi-fw bwi-user" aria-hidden="true"></i>
&nbsp;{{ "myVault" | i18n }}
</button>
</span>
</li>
<li
class="filter-option"
*ngFor="let organization of organizations"
[ngClass]="{ active: organization.id === activeFilter.selectedOrganizationId }"
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyOrganizationFilter(organization)">
<i class="bwi bwi-fw bwi-business" aria-hidden="true"></i>
&nbsp;{{ organization.name }}
</button>
</span>
</li>
</ul>
</ng-container>
</ng-container>
<hr />
</ng-container>

View File

@ -0,0 +1,19 @@
import { Component } from "@angular/core";
import { OrganizationFilterComponent as BaseOrganizationFilterComponent } from "jslib-angular/modules/vault-filter/components/organization-filter.component";
import { DisplayMode } from "jslib-angular/modules/vault-filter/models/display-mode";
@Component({
selector: "app-organization-filter",
templateUrl: "organization-filter.component.html",
})
export class OrganizationFilterComponent extends BaseOrganizationFilterComponent {
get show() {
const hiddenDisplayModes: DisplayMode[] = ["singleOrganizationAndPersonalOwnershipPolicies"];
return (
!this.hide &&
this.organizations.length > 0 &&
hiddenDisplayModes.indexOf(this.displayMode) === -1
);
}
}

View File

@ -0,0 +1,34 @@
<ng-container *ngIf="show">
<h2 class="sr-only">{{ "filters" | i18n }}</h2>
<ul class="filter-options">
<li class="filter-option" [ngClass]="{ active: activeFilter.status === 'all' }">
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter('all')">
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i>&nbsp;{{ "allItems" | i18n }}
</button>
</span>
</li>
<li
class="filter-option"
*ngIf="!hideFavorites"
[ngClass]="{ active: activeFilter.status === 'favorites' }"
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter('favorites')">
<i class="bwi bwi-fw bwi-star" aria-hidden="true"></i>&nbsp;{{ "favorites" | i18n }}
</button>
</span>
</li>
<li
class="filter-option"
*ngIf="!hideTrash"
[ngClass]="{ active: activeFilter.status === 'trash' }"
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter('trash')">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>&nbsp;{{ "trash" | i18n }}
</button>
</span>
</li>
</ul>
</ng-container>

View File

@ -0,0 +1,9 @@
import { Component } from "@angular/core";
import { StatusFilterComponent as BaseStatusFilterComponent } from "jslib-angular/modules/vault-filter/components/status-filter.component";
@Component({
selector: "app-status-filter",
templateUrl: "status-filter.component.html",
})
export class StatusFilterComponent extends BaseStatusFilterComponent {}

View File

@ -0,0 +1,60 @@
<div class="filter-heading">
<button
class="no-btn"
(click)="toggleCollapse()"
aria-expanded="!isCollapsed"
aria-controls="type-filters"
appA11yTitle="{{ 'toggleCollapse' | i18n }}"
>
<i
class="bwi bwi-fw"
aria-hidden="true"
[ngClass]="{
'bwi-angle-right': isCollapsed,
'bwi-angle-down': !isCollapsed
}"
></i>
</button>
<h2>&nbsp;{{ typesNode.name | i18n }}</h2>
</div>
<ul id="type-filters" *ngIf="!isCollapsed" class="filter-options">
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Login }"
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Login)">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>&nbsp;{{ "typeLogin" | i18n }}
</button>
</span>
</li>
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Card }">
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Card)">
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>&nbsp;{{ "typeCard" | i18n }}
</button>
</span>
</li>
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Identity }"
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.Identity)">
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i>&nbsp;{{ "typeIdentity" | i18n }}
</button>
</span>
</li>
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.SecureNote }"
>
<span class="filter-buttons">
<button class="filter-button" (click)="applyFilter(cipherTypeEnum.SecureNote)">
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>&nbsp;{{
"typeSecureNote" | i18n
}}
</button>
</span>
</li>
</ul>

View File

@ -0,0 +1,9 @@
import { Component } from "@angular/core";
import { TypeFilterComponent as BaseTypeFilterComponent } from "jslib-angular/modules/vault-filter/components/type-filter.component";
@Component({
selector: "app-type-filter",
templateUrl: "type-filter.component.html",
})
export class TypeFilterComponent extends BaseTypeFilterComponent {}

View File

@ -0,0 +1,50 @@
<div class="container loading-spinner" *ngIf="!isLoaded">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<ng-container *ngIf="isLoaded">
<app-organization-filter
class="filter"
[hide]="hideOrganizations"
[activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes"
[organizations]="organizations"
[activePersonalOwnershipPolicy]="activePersonalOwnershipPolicy"
[activeSingleOrganizationPolicy]="activeSingleOrganizationPolicy"
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
(onFilterChange)="applyFilter($event)"
></app-organization-filter>
<app-status-filter
class="filter"
[hideFavorites]="hideFavorites"
[hideTrash]="hideTrash"
[activeFilter]="activeFilter"
(onFilterChange)="applyFilter($event)"
></app-status-filter>
<app-type-filter
class="filter"
[activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes"
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
(onFilterChange)="applyFilter($event)"
></app-type-filter>
<app-folder-filter
class="filter"
[hide]="hideFolders"
[activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes"
[folderNodes]="folders"
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
(onFilterChange)="applyFilter($event)"
(onAddFolder)="addFolder()"
(onEditFolder)="editFolder($event)"
></app-folder-filter>
<app-collection-filter
class="filter"
[hide]="hideCollections"
[activeFilter]="activeFilter"
[collapsedFilterNodes]="collapsedFilterNodes"
[collectionNodes]="collections"
(onNodeCollapseStateChange)="toggleFilterNodeCollapseState($event)"
(onFilterChange)="applyFilter($event)"
></app-collection-filter>
</ng-container>

View File

@ -0,0 +1,9 @@
import { Component } from "@angular/core";
import { VaultFilterComponent as BaseVaultFilterComponent } from "jslib-angular/modules/vault-filter/vault-filter.component";
@Component({
selector: "app-vault-filter",
templateUrl: "vault-filter.component.html",
})
export class VaultFilterComponent extends BaseVaultFilterComponent {}

View File

@ -0,0 +1,27 @@
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { JslibModule } from "jslib-angular/jslib.module";
import { VaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service";
import { CollectionFilterComponent } from "./components/collection-filter.component";
import { FolderFilterComponent } from "./components/folder-filter.component";
import { OrganizationFilterComponent } from "./components/organization-filter.component";
import { StatusFilterComponent } from "./components/status-filter.component";
import { TypeFilterComponent } from "./components/type-filter.component";
import { VaultFilterComponent } from "./vault-filter.component";
@NgModule({
imports: [BrowserModule, JslibModule],
declarations: [
VaultFilterComponent,
CollectionFilterComponent,
FolderFilterComponent,
OrganizationFilterComponent,
StatusFilterComponent,
TypeFilterComponent,
],
exports: [VaultFilterComponent],
providers: [VaultFilterService],
})
export class VaultFilterModule {}

View File

@ -1,37 +1,59 @@
<div id="sends" class="vault">
<div class="groupings">
<div class="content">
<div class="inner-content">
<h2 class="sr-only">{{ "filters" | i18n }}</h2>
<ul>
<li [ngClass]="{ active: selectedAll }">
<a href="#" appStopClick appBlurClick (click)="selectAll()">
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i>&nbsp;{{ "allSends" | i18n }}
</a>
</li>
</ul>
<h2>{{ "types" | i18n }}</h2>
<ul>
<li [ngClass]="{ active: selectedType === sendType.Text }">
<a href="#" appStopClick appBlurClick (click)="selectType(sendType.Text)">
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>&nbsp;{{
"sendTypeText" | i18n
}}
</a>
</li>
<li [ngClass]="{ active: selectedType === sendType.File }">
<a href="#" appStopClick appBlurClick (click)="selectType(sendType.File)">
<i class="bwi bwi-fw bwi-file" aria-hidden="true"></i>&nbsp;{{
"sendTypeFile" | i18n
}}
</a>
<div class="left-nav">
<div class="vault-filters">
<h2 class="sr-only">{{ "filters" | i18n }}</h2>
<div class="filter">
<ul class="filter-options">
<li class="filter-option" [ngClass]="{ active: selectedAll }">
<span class="filter-buttons">
<button
class="filter-button"
(click)="selectAll()"
appA11yTitle="{{ 'applyFilter' | i18n }}"
>
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i>&nbsp;{{
"allSends" | i18n
}}
</button>
</span>
</li>
</ul>
</div>
<div class="footer">
<app-nav class="nav"></app-nav>
<div class="filter">
<div class="filter-heading">
<h2>{{ "types" | i18n }}</h2>
</div>
<ul class="filter-options">
<li class="filter-option" [ngClass]="{ active: selectedType === sendType.Text }">
<span class="filter-buttons">
<button
class="filter-button"
(click)="selectType(sendType.Text)"
appA11yTitle="{{ 'applyFilter' | i18n }}"
>
<i class="bwi bwi-fw bwi-file-text" aria-hidden="true"></i>&nbsp;{{
"sendTypeText" | i18n
}}
</button>
</span>
</li>
<li class="filter-option" [ngClass]="{ active: selectedType === sendType.File }">
<span class="filter-buttons">
<button
class="filter-button"
(click)="selectType(sendType.File)"
appA11yTitle="{{ 'applyFilter' | i18n }}"
>
<i class="bwi bwi-fw bwi-file" aria-hidden="true"></i>&nbsp;{{
"sendTypeFile" | i18n
}}
</button>
</span>
</li>
</ul>
</div>
</div>
<app-nav class="nav"></app-nav>
</div>
<div id="items" class="items">
<div class="content">

View File

@ -1,65 +1,67 @@
<div class="content">
<cdk-virtual-scroll-viewport
itemSize="42"
minBufferPx="400"
maxBufferPx="600"
*ngIf="ciphers.length"
>
<div class="list">
<a
*cdkVirtualFor="let c of ciphers; trackBy: trackByFn"
appStopClick
(click)="selectCipher(c)"
(contextmenu)="rightClickCipher(c)"
href="#"
title="{{ 'viewItem' | i18n }}"
[ngClass]="{ active: c.id === activeCipherId }"
class="flex-list-item virtual-scroll-item"
>
<app-vault-icon [cipher]="c"></app-vault-icon>
<div class="flex-cipher-list-item">
<span class="text">
{{ c.name }}
<ng-container *ngIf="c.organizationId">
<i
class="bwi bwi-collection text-muted"
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
<ng-container *ngIf="c.hasAttachments">
<i
class="bwi bwi-paperclip text-muted"
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachments" | i18n }}</span>
</ng-container>
</span>
<span *ngIf="c.subTitle" class="detail">{{ c.subTitle }}</span>
</div>
</a>
</div>
</cdk-virtual-scroll-viewport>
<div class="no-items" *ngIf="!ciphers.length">
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded" aria-hidden="true"></i>
<ng-container *ngIf="loaded">
<div class="container loading-spinner" *ngIf="!loaded">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<ng-container *ngIf="loaded">
<div class="content">
<cdk-virtual-scroll-viewport
itemSize="42"
minBufferPx="400"
maxBufferPx="600"
*ngIf="ciphers.length"
>
<div class="list">
<a
*cdkVirtualFor="let c of ciphers; trackBy: trackByFn"
appStopClick
(click)="selectCipher(c)"
(contextmenu)="rightClickCipher(c)"
href="#"
title="{{ 'viewItem' | i18n }}"
[ngClass]="{ active: c.id === activeCipherId }"
class="flex-list-item virtual-scroll-item"
>
<app-vault-icon [cipher]="c"></app-vault-icon>
<div class="flex-cipher-list-item">
<span class="text">
{{ c.name }}
<ng-container *ngIf="c.organizationId">
<i
class="bwi bwi-collection text-muted"
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
<ng-container *ngIf="c.hasAttachments">
<i
class="bwi bwi-paperclip text-muted"
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachments" | i18n }}</span>
</ng-container>
</span>
<span *ngIf="c.subTitle" class="detail">{{ c.subTitle }}</span>
</div>
</a>
</div>
</cdk-virtual-scroll-viewport>
<div class="no-items" *ngIf="!ciphers.length">
<img class="no-items-image" aria-hidden="true" />
<p>{{ "noItemsInList" | i18n }}</p>
<button (click)="addCipher()" class="btn block primary link">{{ "addItem" | i18n }}</button>
</ng-container>
</div>
</div>
</div>
<div class="footer">
<button
appBlurClick
(click)="addCipher()"
(contextmenu)="addCipherOptions()"
class="block primary"
appA11yTitle="{{ 'addItem' | i18n }}"
[disabled]="deleted"
>
<i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i>
</button>
</div>
<div class="footer">
<button
appBlurClick
(click)="addCipher()"
(contextmenu)="addCipherOptions()"
class="block primary"
appA11yTitle="{{ 'addItem' | i18n }}"
[disabled]="deleted"
>
<i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i>
</button>
</div>
</ng-container>

View File

@ -1,149 +0,0 @@
<div class="content">
<div class="inner-content">
<h2 class="sr-only">{{ "filters" | i18n }}</h2>
<ul>
<li [ngClass]="{ active: selectedAll }">
<a href="#" appStopClick appBlurClick (click)="selectAll()">
<i class="bwi bwi-fw bwi-filter" aria-hidden="true"></i>&nbsp;{{ "allItems" | i18n }}
</a>
</li>
<li [ngClass]="{ active: selectedFavorites }">
<a href="#" appStopClick appBlurClick (click)="selectFavorites()">
<i class="bwi bwi-fw bwi-star" aria-hidden="true"></i>&nbsp;{{ "favorites" | i18n }}
</a>
</li>
<li [ngClass]="{ active: selectedTrash }">
<a href="#" appStopClick appBlurClick (click)="selectTrash()">
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>&nbsp;{{ "trash" | i18n }}
</a>
</li>
</ul>
<h2>{{ "types" | i18n }}</h2>
<ul>
<li [ngClass]="{ active: selectedType === cipherType.Login }">
<a href="#" appStopClick appBlurClick (click)="selectType(cipherType.Login)">
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>&nbsp;{{ "typeLogin" | i18n }}
</a>
</li>
<li [ngClass]="{ active: selectedType === cipherType.Card }">
<a href="#" appStopClick appBlurClick (click)="selectType(cipherType.Card)">
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>&nbsp;{{ "typeCard" | i18n }}
</a>
</li>
<li [ngClass]="{ active: selectedType === cipherType.Identity }">
<a href="#" appStopClick appBlurClick (click)="selectType(cipherType.Identity)">
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i>&nbsp;{{ "typeIdentity" | i18n }}
</a>
</li>
<li [ngClass]="{ active: selectedType === cipherType.SecureNote }">
<a href="#" appStopClick appBlurClick (click)="selectType(cipherType.SecureNote)">
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>&nbsp;{{
"typeSecureNote" | i18n
}}
</a>
</li>
</ul>
<p *ngIf="!loaded" class="text-muted">{{ "loading" | i18n }}</p>
<ng-container *ngIf="loaded">
<div class="heading">
<h2>{{ "folders" | i18n }}</h2>
<button appBlurClick (click)="addFolder()" appA11yTitle="{{ 'addFolder' | i18n }}">
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
</button>
</div>
<ul>
<ng-template #recursiveFolders let-folders>
<li
*ngFor="let f of folders"
[ngClass]="{ active: selectedFolder && f.node.id === selectedFolderId }"
>
<a href="#" appStopClick appBlurClick (click)="selectFolder(f.node)">
<i
*ngIf="f.children.length"
class="bwi bwi-fw"
title="{{ 'toggleCollapse' | i18n }}"
aria-hidden="true"
[ngClass]="{
'bwi-angle-right': isCollapsed(f.node),
'bwi-angle-down': !isCollapsed(f.node)
}"
(click)="collapse(f.node)"
appStopProp
></i>
<i
*ngIf="f.children.length === 0"
class="bwi bwi-fw bwi-folder"
aria-hidden="true"
></i>
&nbsp;{{ f.node.name }}
<span
appStopProp
appStopClick
(click)="editFolder(f.node)"
role="button"
appA11yTitle="{{ 'editFolder' | i18n }}"
*ngIf="f.node.id"
>
<i class="bwi bwi-pencil bwi-fw" aria-hidden="true"></i>
</span>
</a>
<ul class="bwi-ul" *ngIf="f.children.length && !isCollapsed(f.node)">
<ng-container
*ngTemplateOutlet="recursiveFolders; context: { $implicit: f.children }"
>
</ng-container>
</ul>
</li>
</ng-template>
<ng-container
*ngTemplateOutlet="recursiveFolders; context: { $implicit: nestedFolders }"
></ng-container>
</ul>
<div *ngIf="collections && collections.length">
<h2>{{ "collections" | i18n }}</h2>
<ul>
<ng-template #recursiveCollections let-collections>
<li
*ngFor="let c of collections"
[ngClass]="{ active: c.node.id === selectedCollectionId }"
>
<a href="#" appStopClick appBlurClick (click)="selectCollection(c.node)">
<i
*ngIf="c.children.length"
class="bwi bwi-fw"
title="{{ 'toggleCollapse' | i18n }}"
aria-hidden="true"
[ngClass]="{
'bwi-angle-right': isCollapsed(c.node),
'bwi-angle-down': !isCollapsed(c.node)
}"
(click)="collapse(c.node)"
appStopProp
></i>
<i
*ngIf="c.children.length === 0"
class="bwi bwi-fw bwi-collection"
aria-hidden="true"
></i>
&nbsp;{{ c.node.name }}
</a>
<ul class="bwi-ul" *ngIf="c.children.length && !isCollapsed(c.node)">
<ng-container
*ngTemplateOutlet="recursiveCollections; context: { $implicit: c.children }"
>
</ng-container>
</ul>
</li>
</ng-template>
<ng-container
*ngTemplateOutlet="recursiveCollections; context: { $implicit: nestedCollections }"
>
</ng-container>
</ul>
</div>
</ng-container>
</div>
<div class="footer">
<app-nav class="nav"></app-nav>
</div>
</div>

View File

@ -1,20 +0,0 @@
import { Component } from "@angular/core";
import { GroupingsComponent as BaseGroupingsComponent } from "jslib-angular/components/groupings.component";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { StateService } from "jslib-common/abstractions/state.service";
@Component({
selector: "app-vault-groupings",
templateUrl: "groupings.component.html",
})
export class GroupingsComponent extends BaseGroupingsComponent {
constructor(
collectionService: CollectionService,
folderService: FolderService,
stateService: StateService
) {
super(collectionService, folderService, stateService);
}
}

View File

@ -52,19 +52,16 @@
</div>
</div>
</div>
<app-vault-groupings
id="groupings"
class="groupings"
(onAllClicked)="clearGroupingFilters()"
(onFavoritesClicked)="filterFavorites()"
(onCipherTypeClicked)="filterCipherType($event)"
(onFolderClicked)="filterFolder($event.id)"
(onAddFolder)="addFolder()"
(onEditFolder)="editFolder($event.id)"
(onCollectionClicked)="filterCollection($event.id)"
(onTrashClicked)="filterDeleted()"
>
</app-vault-groupings>
<div class="left-nav">
<app-vault-filter
class="vault-filters"
[activeFilter]="activeFilter"
(onFilterChange)="applyVaultFilter($event)"
(onAddFolder)="addFolder()"
(onEditFolder)="editFolder($event.id)"
></app-vault-filter>
<app-nav class="nav"></app-nav>
</div>
</div>
<ng-template #generator></ng-template>
<ng-template #attachments></ng-template>

View File

@ -11,6 +11,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { ModalRef } from "jslib-angular/components/modal/modal.ref";
import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model";
import { ModalService } from "jslib-angular/services/modal.service";
import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service";
import { EventService } from "jslib-common/abstractions/event.service";
@ -29,6 +30,7 @@ import { FolderView } from "jslib-common/models/view/folderView";
import { invokeMenu, RendererMenuItem } from "jslib-electron/utils";
import { SearchBarService } from "../layout/search/search-bar.service";
import { VaultFilterComponent } from "../modules/vault-filter/vault-filter.component";
import { AddEditComponent } from "./add-edit.component";
import { AttachmentsComponent } from "./attachments.component";
@ -36,7 +38,6 @@ import { CiphersComponent } from "./ciphers.component";
import { CollectionsComponent } from "./collections.component";
import { FolderAddEditComponent } from "./folder-add-edit.component";
import { GeneratorComponent } from "./generator.component";
import { GroupingsComponent } from "./groupings.component";
import { PasswordHistoryComponent } from "./password-history.component";
import { ShareComponent } from "./share.component";
import { ViewComponent } from "./view.component";
@ -51,9 +52,9 @@ export class VaultComponent implements OnInit, OnDestroy {
@ViewChild(ViewComponent) viewComponent: ViewComponent;
@ViewChild(AddEditComponent) addEditComponent: AddEditComponent;
@ViewChild(CiphersComponent, { static: true }) ciphersComponent: CiphersComponent;
@ViewChild(GroupingsComponent, { static: true }) groupingsComponent: GroupingsComponent;
@ViewChild("generator", { read: ViewContainerRef, static: true })
generatorModalRef: ViewContainerRef;
@ViewChild(VaultFilterComponent, { static: true }) vaultFilterComponent: VaultFilterComponent;
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
@ViewChild("passwordHistory", { read: ViewContainerRef, static: true })
@ -70,12 +71,15 @@ export class VaultComponent implements OnInit, OnDestroy {
type: CipherType = null;
folderId: string = null;
collectionId: string = null;
organizationId: string = null;
myVaultOnly = false;
addType: CipherType = null;
addOrganizationId: string = null;
addCollectionIds: string[] = null;
showingModal = false;
deleted = false;
userHasPremiumAccess = false;
activeFilter: VaultFilter = new VaultFilter();
private modal: ModalRef = null;
@ -125,6 +129,7 @@ export class VaultComponent implements OnInit, OnDestroy {
break;
case "syncCompleted":
await this.load();
await this.vaultFilterComponent.reloadCollectionsAndFolders(this.activeFilter);
break;
case "refreshCiphers":
this.ciphersComponent.refresh();
@ -211,49 +216,31 @@ export class VaultComponent implements OnInit, OnDestroy {
async load() {
this.route.queryParams.pipe(first()).subscribe(async (params) => {
await this.groupingsComponent.load();
if (params == null) {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.reload();
} else {
if (params.cipherId) {
const cipherView = new CipherView();
cipherView.id = params.cipherId;
if (params.action === "clone") {
await this.cloneCipher(cipherView);
} else if (params.action === "edit") {
await this.editCipher(cipherView);
} else {
await this.viewCipher(cipherView);
}
} else if (params.action === "add") {
this.addType = Number(params.addType);
this.addCipher(this.addType);
}
if (params.deleted) {
this.groupingsComponent.selectedTrash = true;
await this.filterDeleted();
} else if (params.favorites) {
this.groupingsComponent.selectedFavorites = true;
await this.filterFavorites();
} else if (params.type && params.action !== "add") {
const t = parseInt(params.type, null);
this.groupingsComponent.selectedType = t;
await this.filterCipherType(t);
} else if (params.folderId) {
this.groupingsComponent.selectedFolder = true;
this.groupingsComponent.selectedFolderId = params.folderId;
await this.filterFolder(params.folderId);
} else if (params.collectionId) {
this.groupingsComponent.selectedCollectionId = params.collectionId;
await this.filterCollection(params.collectionId);
if (params.cipherId) {
const cipherView = new CipherView();
cipherView.id = params.cipherId;
if (params.action === "clone") {
await this.cloneCipher(cipherView);
} else if (params.action === "edit") {
await this.editCipher(cipherView);
} else {
this.groupingsComponent.selectedAll = true;
await this.ciphersComponent.reload();
await this.viewCipher(cipherView);
}
} else if (params.action === "add") {
this.addType = Number(params.addType);
this.addCipher(this.addType);
}
this.activeFilter = new VaultFilter({
status: params.deleted ? "trash" : params.favorites ? "favorites" : "all",
cipherType:
params.action === "add" || params.type == null ? null : parseInt(params.type, null),
selectedFolderId: params.folderId,
selectedCollectionId: params.selectedCollectionId,
selectedOrganizationId: params.selectedOrganizationId,
myVaultOnly: params.myVaultOnly ?? false,
});
await this.ciphersComponent.reload(this.buildFilter());
});
}
@ -547,58 +534,75 @@ export class VaultComponent implements OnInit, OnDestroy {
this.go();
}
async clearGroupingFilters() {
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
await this.ciphersComponent.reload();
this.clearFilters();
this.go();
}
async filterFavorites() {
this.searchBarService.setPlaceholderText(this.i18nService.t("searchFavorites"));
await this.ciphersComponent.reload((c) => c.favorite);
this.clearFilters();
this.favorites = true;
this.go();
}
async filterDeleted() {
this.searchBarService.setPlaceholderText(this.i18nService.t("searchTrash"));
this.ciphersComponent.deleted = true;
await this.ciphersComponent.reload(null, true);
this.clearFilters();
this.deleted = true;
this.go();
}
async filterCipherType(type: CipherType) {
this.searchBarService.setPlaceholderText(this.i18nService.t("searchType"));
await this.ciphersComponent.reload((c) => c.type === type);
this.clearFilters();
this.type = type;
this.go();
}
async filterFolder(folderId: string) {
folderId = folderId === "none" ? null : folderId;
this.searchBarService.setPlaceholderText(this.i18nService.t("searchFolder"));
await this.ciphersComponent.reload((c) => c.folderId === folderId);
this.clearFilters();
this.folderId = folderId == null ? "none" : folderId;
this.go();
}
async filterCollection(collectionId: string) {
this.searchBarService.setPlaceholderText(this.i18nService.t("searchCollection"));
await this.ciphersComponent.reload(
(c) => c.collectionIds != null && c.collectionIds.indexOf(collectionId) > -1
async applyVaultFilter(vaultFilter: VaultFilter) {
this.searchBarService.setPlaceholderText(
this.i18nService.t(this.calculateSearchBarLocalizationString(vaultFilter))
);
this.clearFilters();
this.collectionId = collectionId;
this.updateCollectionProperties();
this.activeFilter = vaultFilter;
await this.ciphersComponent.reload(this.buildFilter());
this.go();
}
private calculateSearchBarLocalizationString(vaultFilter: VaultFilter): string {
if (vaultFilter.status === "favorites") {
return "searchFavorites";
}
if (vaultFilter.status === "trash") {
return "searchTrash";
}
if (vaultFilter.cipherType != null) {
return "searchType";
}
if (vaultFilter.selectedFolderId != null && vaultFilter.selectedFolderId != "none") {
return "searchFolder";
}
if (vaultFilter.selectedCollectionId != null) {
return "searchCollection";
}
if (vaultFilter.selectedOrganizationId != null) {
return "searchOrganization";
}
if (vaultFilter.myVaultOnly) {
return "searchMyVault";
}
return "searchVault";
}
private buildFilter(): (cipher: CipherView) => boolean {
return (cipher) => {
let cipherPassesFilter = true;
if (this.activeFilter.status === "favorites" && cipherPassesFilter) {
cipherPassesFilter = cipher.favorite;
}
if (this.activeFilter.status === "trash" && cipherPassesFilter) {
cipherPassesFilter = cipher.isDeleted;
}
if (this.activeFilter.cipherType != null && cipherPassesFilter) {
cipherPassesFilter = cipher.type === this.activeFilter.cipherType;
}
if (
this.activeFilter.selectedFolderId != null &&
this.activeFilter.selectedFolderId != "none" &&
cipherPassesFilter
) {
cipherPassesFilter = cipher.folderId === this.activeFilter.selectedFolderId;
}
if (this.activeFilter.selectedCollectionId != null && cipherPassesFilter) {
cipherPassesFilter =
cipher.collectionIds != null &&
cipher.collectionIds.indexOf(this.activeFilter.selectedCollectionId) > -1;
}
if (this.activeFilter.selectedOrganizationId != null && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === this.activeFilter.selectedOrganizationId;
}
if (this.activeFilter.myVaultOnly && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === null;
}
return cipherPassesFilter;
};
}
async openGenerator(comingFromAddEdit: boolean, passwordType = true) {
if (this.modal != null) {
this.modal.close();
@ -657,11 +661,11 @@ export class VaultComponent implements OnInit, OnDestroy {
childComponent.onSavedFolder.subscribe(async (folder: FolderView) => {
this.modal.close();
await this.groupingsComponent.loadFolders();
await this.vaultFilterComponent.reloadCollectionsAndFolders(this.activeFilter);
});
childComponent.onDeletedFolder.subscribe(async (folder: FolderView) => {
this.modal.close();
await this.groupingsComponent.loadFolders();
await this.vaultFilterComponent.reloadCollectionsAndFolders(this.activeFilter);
});
this.modal.onClosed.subscribe(() => {
@ -687,17 +691,6 @@ export class VaultComponent implements OnInit, OnDestroy {
return !confirmed;
}
private clearFilters() {
this.folderId = null;
this.collectionId = null;
this.favorites = false;
this.type = null;
this.addCollectionIds = null;
this.addType = null;
this.addOrganizationId = null;
this.deleted = false;
}
private go(queryParams: any = null) {
if (queryParams == null) {
queryParams = {
@ -708,6 +701,8 @@ export class VaultComponent implements OnInit, OnDestroy {
folderId: this.folderId,
collectionId: this.collectionId,
deleted: this.deleted ? true : null,
organizationId: this.organizationId,
myVaultOnly: this.myVaultOnly,
};
}
@ -753,10 +748,10 @@ export class VaultComponent implements OnInit, OnDestroy {
private updateCollectionProperties() {
if (this.collectionId != null) {
const collection = this.groupingsComponent.collections.filter(
const collection = this.vaultFilterComponent.collections?.fullList?.filter(
(c) => c.id === this.collectionId
);
if (collection.length > 0) {
if (collection != null && collection.length > 0) {
this.addOrganizationId = collection[0].organizationId;
this.addCollectionIds = [this.collectionId];
return;

View File

@ -1408,7 +1408,7 @@
"description": "Noun: a special folder to hold deleted items"
},
"searchTrash": {
"message": "Search trash"
"message": "Search Trash"
},
"permanentlyDeleteItem": {
"message": "Permanently Delete Item"
@ -1883,5 +1883,14 @@
},
"service": {
"message": "Service"
},
"allVaults": {
"message": "All Vaults"
},
"searchOrganization": {
"message": "Search Organization"
},
"searchMyVault": {
"message": "Search My Vault"
}
}

View File

@ -169,3 +169,12 @@
}
}
}
button.no-btn {
@extend a;
background: transparent;
border: none;
@include themify($themes) {
color: themed("textColor");
}
}

196
src/scss/left-nav.scss Normal file
View File

@ -0,0 +1,196 @@
.left-nav {
order: 1;
display: flex;
flex-direction: column;
width: 22%;
min-width: 175px;
max-width: 250px;
border-right: 1px solid #000000;
flex-grow: 1;
justify-content: space-between;
@include themify($themes) {
background-color: themed("backgroundColorAlt");
border-right-color: themed("borderColor");
}
}
.vault-filters {
user-select: none;
scrollbar-gutter: stable;
padding: 10px 15px;
overflow-x: hidden;
overflow-y: auto;
height: 100%;
.filter {
hr {
margin: 1em 0 1em 0;
@include themify($themes) {
border-color: themed("hrColor");
}
}
}
}
.filter-heading {
display: flex;
text-transform: uppercase;
font-weight: normal;
margin-bottom: 5px;
align-items: center;
padding-top: 5px;
padding-bottom: 5px;
h2 {
@include themify($themes) {
color: themed("headingColor");
}
font-size: $font-size-base;
}
button {
@extend .no-btn;
text-transform: uppercase;
@include themify($themes) {
color: themed("headingButtonColor");
}
&:hover,
&:focus {
@include themify($themes) {
color: themed("headingButtonHoverColor");
}
}
}
button.add-button {
margin-left: auto;
margin-right: 5px;
}
&.active {
.filter-button {
h2 {
@include themify($themes) {
color: themed("primaryColor");
}
}
}
}
.filter-button {
&:hover {
h2 {
@include themify($themes) {
color: themed("primaryColor");
}
}
}
}
}
.filter-options {
word-break: break-all;
padding: 0;
list-style: none;
width: 100%;
margin: 0 0 15px 0;
.nested-filter-options {
list-style: none;
margin-bottom: 0px;
padding-left: 0.85em;
}
}
.filter-option {
top: 8px;
width: 100%;
@include themify($themes) {
color: themed("textColor");
}
&.active {
> .filter-buttons {
.filter-button {
@include themify($themes) {
color: themed("primaryColor");
font-weight: bold;
}
}
.edit-button {
visibility: visible;
}
}
}
}
.filter-buttons {
padding: 5px 0;
display: flex;
align-items: center;
width: 100%;
&:hover,
&:focus {
.filter-button {
@include themify($themes) {
color: themed("primaryColor");
}
}
}
button {
@extend .no-btn;
}
.edit-button,
.toggle-button {
@include themify($themes) {
color: themed("headingButtonColor");
}
&:hover,
&:focus {
@include themify($themes) {
color: themed("headingButtonHoverColor");
}
}
}
.edit-button {
visibility: hidden;
margin-left: auto;
margin-right: 5px;
}
}
.nav {
height: 55px;
width: 100%;
display: flex;
.btn {
width: 100%;
font-size: $font-size-base * 0.8;
flex: 1;
border: 0;
border-radius: 0;
padding-bottom: 4px;
&:not(.active) {
@include themify($themes) {
background-color: themed("backgroundColorAlt");
}
}
i {
font-size: $font-size-base * 1.5;
display: block;
margin-bottom: 2px;
text-align: center;
}
}
}

10
src/scss/loading.scss Normal file
View File

@ -0,0 +1,10 @@
.container {
&.loading-spinner {
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
}
}

View File

@ -14,4 +14,6 @@
@import "plugins.scss";
@import "environment.scss";
@import "header.scss";
@import "left-nav.scss";
@import "loading.scss";
@import "../../jslib/angular/src/scss/icons.scss";

View File

@ -93,6 +93,7 @@ $themes: (
accountSwitcherBackgroundColor: $background-color,
accountSwitcherTextColor: #ffffff,
svgSuffix: "-light.svg",
hrColor: #eeeeee,
),
dark: (
textColor: #ffffff,
@ -146,6 +147,7 @@ $themes: (
accountSwitcherBackgroundColor: #2f2f2f,
accountSwitcherTextColor: #ffffff,
svgSuffix: "-dark.svg",
hrColor: #a3a3a3,
),
nord: (
textColor: $nord5,
@ -199,6 +201,7 @@ $themes: (
accountSwitcherBackgroundColor: $nord0,
accountSwitcherTextColor: $nord5,
svgSuffix: "-dark.svg",
hrColor: $nord4,
),
);

View File

@ -15,7 +15,6 @@ app-root {
height: 100%;
display: flex;
> .groupings,
> .items,
> .details,
> .logo {
@ -29,274 +28,6 @@ app-root {
> .items {
order: 2;
}
> .details {
order: 3;
}
> .logo {
order: 4;
}
> .groupings {
order: 1;
width: 22%;
min-width: 175px;
max-width: 250px;
border-right: 1px solid #000000;
@include themify($themes) {
background-color: themed("backgroundColorAlt");
border-right-color: themed("borderColor");
}
.content {
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: space-between;
.footer {
padding: 0;
}
.inner-content {
padding-bottom: 0;
padding-right: 5px;
user-select: none;
> ul,
> div > ul {
margin: 0 0 15px 0;
}
}
}
h2 {
text-transform: uppercase;
font-size: $font-size-base;
font-weight: normal;
margin-bottom: 5px;
@include themify($themes) {
color: themed("headingColor");
}
}
.heading {
display: flex;
button {
margin-left: auto;
background: none;
border: none;
@include themify($themes) {
color: themed("headingButtonColor");
}
&:hover,
&:focus {
cursor: pointer;
@include themify($themes) {
color: themed("headingButtonHoverColor");
}
}
}
}
ul:not(.bwi-ul) {
li {
margin: 0;
padding: 0;
list-style: none;
}
}
ul.bwi-ul {
li {
word-break: break-all;
.bwi-li {
top: 8px;
width: 1.1em;
}
}
}
// Nested indentions
ul.bwi-ul {
// Level 1
li {
> a {
padding-left: 12px;
}
.bwi-li {
left: -4px;
}
&.active > a .bwi-li {
left: 11px;
}
}
// Level 2
ul li {
> a {
padding-left: 23px;
}
.bwi-li {
left: 7px;
}
&.active > a .bwi-li {
left: 22px;
}
}
// Level 3
ul ul li {
> a {
padding-left: 34px;
}
.bwi-li {
left: 18px;
}
&.active > a .bwi-li {
left: 33px;
}
}
// Level 4
ul ul ul li {
> a {
padding-left: 45px;
}
.bwi-li {
left: 29px;
}
&.active > a .bwi-li {
left: 44px;
}
}
// Level 5
ul ul ul ul li {
> a {
padding-left: 56px;
}
.bwi-li {
left: 40px;
}
&.active > a .bwi-li {
left: 55px;
}
}
// Level 6
ul ul ul ul ul li {
> a {
padding-left: 67px;
}
.bwi-li {
left: 51px;
}
&.active > a .bwi-li {
left: 66px;
}
}
// Level 7
ul ul ul ul ul ul li {
> a {
padding-left: 78px;
}
.bwi-li {
left: 62px;
}
&.active > a .bwi-li {
left: 77px;
}
}
}
ul {
padding: 0;
margin: 0;
li {
a {
padding: 5px 0;
display: flex;
align-items: center;
@include themify($themes) {
color: themed("textColor");
}
span {
visibility: hidden;
margin-left: auto;
@include themify($themes) {
color: themed("headingButtonColor");
}
&:hover,
&:focus {
@include themify($themes) {
color: themed("headingButtonHoverColor");
}
}
}
&:hover,
&:focus {
span {
visibility: visible;
}
}
}
&.active {
margin-left: -15px;
margin-right: -5px;
padding-left: 15px;
padding-right: 5px;
@include themify($themes) {
background-color: themed("groupingsActiveColor");
}
ul {
@include themify($themes) {
background-color: themed("backgroundColorAlt");
}
margin-left: -15px;
margin-right: -5px;
padding-left: 15px;
padding-right: 5px;
}
}
}
}
}
> .items {
width: 28%;
min-width: 200px;
max-width: 350px;
@ -335,6 +66,7 @@ app-root {
> .details {
flex: 1;
min-width: 0;
order: 3;
@include themify($themes) {
background-color: themed("backgroundColorAlt2");
@ -367,6 +99,7 @@ app-root {
> .logo {
flex: 1;
min-width: 0;
order: 3;
.content {
overflow-y: hidden;
@ -426,31 +159,4 @@ app-root {
display: flex;
}
}
.nav {
height: 100%;
width: 100%;
display: flex;
.btn {
width: 100%;
font-size: $font-size-base * 0.8;
flex: 1;
border: 0;
border-radius: 0;
padding-bottom: 4px;
&:not(.active) {
@include themify($themes) {
background-color: themed("backgroundColorAlt");
}
}
i {
font-size: $font-size-base * 1.5;
display: block;
margin-bottom: 2px;
text-align: center;
}
}
}
}