diff --git a/apps/browser/jslib b/apps/browser/jslib index 9d411fd37d..141ade3c38 160000 --- a/apps/browser/jslib +++ b/apps/browser/jslib @@ -1 +1 @@ -Subproject commit 9d411fd37d61e5829d26062edab940f2b4578334 +Subproject commit 141ade3c381cb9ec8e89f15061b92330cb32d403 diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index c0bb1f495c..d858eae2ef 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -55,6 +55,9 @@ "myVault": { "message": "My Vault" }, + "allVaults": { + "message": "All Vaults" + }, "tools": { "message": "Tools" }, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 8bcc195db0..f6c38c2af0 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -85,6 +85,7 @@ import BrowserPlatformUtilsService from "../services/browserPlatformUtils.servic import BrowserStorageService from "../services/browserStorage.service"; import I18nService from "../services/i18n.service"; import { StateService } from "../services/state.service"; +import { VaultFilterService } from "../services/vaultFilter.service"; import VaultTimeoutService from "../services/vaultTimeout.service"; import CommandsBackground from "./commands.background"; @@ -138,6 +139,7 @@ export default class MainBackground { keyConnectorService: KeyConnectorServiceAbstraction; userVerificationService: UserVerificationServiceAbstraction; twoFactorService: TwoFactorServiceAbstraction; + vaultFilterService: VaultFilterService; usernameGenerationService: UsernameGenerationServiceAbstraction; onUpdatedRan: boolean; @@ -267,6 +269,14 @@ export default class MainBackground { this.organizationService, this.cryptoFunctionService ); + this.vaultFilterService = new VaultFilterService( + this.stateService, + this.organizationService, + this.folderService, + this.cipherService, + this.collectionService, + this.policyService + ); this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); @@ -589,6 +599,7 @@ export default class MainBackground { this.passwordGenerationService.clear(userId), this.vaultTimeoutService.clear(userId), this.keyConnectorService.clear(), + this.vaultFilterService.clear(), ]); await this.stateService.clean({ userId: userId }); diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index bbf37da29b..b9d741615a 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -1,9 +1,9 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; -import { AuthGuardService } from "jslib-angular/services/auth-guard.service"; -import { LockGuardService } from "jslib-angular/services/lock-guard.service"; -import { UnauthGuardService } from "jslib-angular/services/unauth-guard.service"; +import { AuthGuard } from "jslib-angular/guards/auth.guard"; +import { LockGuard } from "jslib-angular/guards/lock.guard"; +import { UnauthGuard } from "jslib-angular/guards/unauth.guard"; import { EnvironmentComponent } from "./accounts/environment.component"; import { HintComponent } from "./accounts/hint.component"; @@ -37,9 +37,9 @@ import { AttachmentsComponent } from "./vault/attachments.component"; import { CiphersComponent } from "./vault/ciphers.component"; import { CollectionsComponent } from "./vault/collections.component"; import { CurrentTabComponent } from "./vault/current-tab.component"; -import { GroupingsComponent } from "./vault/groupings.component"; import { PasswordHistoryComponent } from "./vault/password-history.component"; import { ShareComponent } from "./vault/share.component"; +import { VaultFilterComponent } from "./vault/vault-filter.component"; import { ViewComponent } from "./vault/view.component"; const routes: Routes = [ @@ -56,37 +56,37 @@ const routes: Routes = [ { path: "home", component: HomeComponent, - canActivate: [UnauthGuardService], + canActivate: [UnauthGuard], data: { state: "home" }, }, { path: "login", component: LoginComponent, - canActivate: [UnauthGuardService], + canActivate: [UnauthGuard], data: { state: "login" }, }, { path: "lock", component: LockComponent, - canActivate: [LockGuardService], + canActivate: [LockGuard], data: { state: "lock" }, }, { path: "2fa", component: TwoFactorComponent, - canActivate: [UnauthGuardService], + canActivate: [UnauthGuard], data: { state: "2fa" }, }, { path: "2fa-options", component: TwoFactorOptionsComponent, - canActivate: [UnauthGuardService], + canActivate: [UnauthGuard], data: { state: "2fa-options" }, }, { path: "sso", component: SsoComponent, - canActivate: [UnauthGuardService], + canActivate: [UnauthGuard], data: { state: "sso" }, }, { @@ -97,165 +97,165 @@ const routes: Routes = [ { path: "remove-password", component: RemovePasswordComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "remove-password" }, }, { path: "register", component: RegisterComponent, - canActivate: [UnauthGuardService], + canActivate: [UnauthGuard], data: { state: "register" }, }, { path: "hint", component: HintComponent, - canActivate: [UnauthGuardService], + canActivate: [UnauthGuard], data: { state: "hint" }, }, { path: "environment", component: EnvironmentComponent, - canActivate: [UnauthGuardService], + canActivate: [UnauthGuard], data: { state: "environment" }, }, { path: "ciphers", component: CiphersComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "ciphers" }, }, { path: "view-cipher", component: ViewComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "view-cipher" }, }, { path: "cipher-password-history", component: PasswordHistoryComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "cipher-password-history" }, }, { path: "add-cipher", component: AddEditComponent, - canActivate: [AuthGuardService, DebounceNavigationService], + canActivate: [AuthGuard, DebounceNavigationService], data: { state: "add-cipher" }, runGuardsAndResolvers: "always", }, { path: "edit-cipher", component: AddEditComponent, - canActivate: [AuthGuardService, DebounceNavigationService], + canActivate: [AuthGuard, DebounceNavigationService], data: { state: "edit-cipher" }, runGuardsAndResolvers: "always", }, { path: "share-cipher", component: ShareComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "share-cipher" }, }, { path: "collections", component: CollectionsComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "collections" }, }, { path: "attachments", component: AttachmentsComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "attachments" }, }, { path: "generator", component: GeneratorComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "generator" }, }, { path: "generator-history", component: PasswordGeneratorHistoryComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "generator-history" }, }, { path: "export", component: ExportComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "export" }, }, { path: "folders", component: FoldersComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "folders" }, }, { path: "add-folder", component: FolderAddEditComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "add-folder" }, }, { path: "edit-folder", component: FolderAddEditComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "edit-folder" }, }, { path: "sync", component: SyncComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "sync" }, }, { path: "excluded-domains", component: ExcludedDomainsComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "excluded-domains" }, }, { path: "premium", component: PremiumComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "premium" }, }, { path: "options", component: OptionsComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "options" }, }, { path: "clone-cipher", component: AddEditComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "clone-cipher" }, }, { path: "send-type", component: SendTypeComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "send-type" }, }, { path: "add-send", component: SendAddEditComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "add-send" }, }, { path: "edit-send", component: SendAddEditComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "edit-send" }, }, { path: "update-temp-password", component: UpdateTempPasswordComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "update-temp-password" }, }, { @@ -271,32 +271,32 @@ const routes: Routes = [ { path: "current", component: CurrentTabComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "tabs_current" }, runGuardsAndResolvers: "always", }, { path: "vault", - component: GroupingsComponent, - canActivate: [AuthGuardService], + component: VaultFilterComponent, + canActivate: [AuthGuard], data: { state: "tabs_vault" }, }, { path: "generator", component: GeneratorComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "tabs_generator" }, }, { path: "settings", component: SettingsComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "tabs_settings" }, }, { path: "send", component: SendGroupingsComponent, - canActivate: [AuthGuardService], + canActivate: [AuthGuard], data: { state: "tabs_send" }, }, ], diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index ce759eee54..8fc728ffcf 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -1,5 +1,6 @@ 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 { CurrencyPipe, DatePipe, registerLocaleData } from "@angular/common"; import localeAz from "@angular/common/locales/az"; @@ -106,9 +107,10 @@ import { AttachmentsComponent } from "./vault/attachments.component"; import { CiphersComponent } from "./vault/ciphers.component"; import { CollectionsComponent } from "./vault/collections.component"; import { CurrentTabComponent } from "./vault/current-tab.component"; -import { GroupingsComponent } from "./vault/groupings.component"; import { PasswordHistoryComponent } from "./vault/password-history.component"; import { ShareComponent } from "./vault/share.component"; +import { VaultFilterComponent } from "./vault/vault-filter.component"; +import { VaultSelectComponent } from "./vault/vault-select.component"; import { ViewCustomFieldsComponent } from "./vault/view-custom-fields.component"; import { ViewComponent } from "./vault/view.component"; @@ -179,6 +181,7 @@ registerLocaleData(localeZhTw, "zh-TW"); DragDropModule, FormsModule, JslibModule, + OverlayModule, ReactiveFormsModule, ScrollingModule, ServicesModule, @@ -198,7 +201,7 @@ registerLocaleData(localeZhTw, "zh-TW"); ExportComponent, FolderAddEditComponent, FoldersComponent, - GroupingsComponent, + VaultFilterComponent, HintComponent, HomeComponent, LockComponent, @@ -232,6 +235,7 @@ registerLocaleData(localeZhTw, "zh-TW"); ViewComponent, ViewCustomFieldsComponent, RemovePasswordComponent, + VaultSelectComponent, ], entryComponents: [], providers: [CurrencyPipe, DatePipe], diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 2115a6c115..8534ad2320 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -465,3 +465,59 @@ main { .cdk-virtual-scroll-content-wrapper { width: 100%; } + +.org-filter-content { + padding-bottom: 5px; + padding-left: 7px; + .org-filter { + @include themify($themes) { + background-color: themed("backgroundColor"); + } + border: 1px solid; + padding: 7px; + border-radius: $border-radius; + } +} +.vault-select { + @include themify($themes) { + background-color: themed("boxBackgroundColor"); + } + margin-right: 5px; + margin-top: 1px; + width: 160px; + @include themify($themes) { + border: 1px solid themed("borderColor"); + } + border-radius: $border-radius; + button { + border: none; + background: transparent; + width: 100%; + padding: 5px 10px; + text-align: start; + @include themify($themes) { + color: themed("textColor"); + } + + a { + @include themify($themes) { + color: themed("textColor"); + } + } + + &:hover { + @include themify($themes) { + background-color: themed("boxBackgroundHoverColor"); + } + } + } + .border { + @include themify($themes) { + background: themed("borderColor"); + } + left: 10px; + width: calc(100% - 20px); + height: 1px; + position: relative; + } +} diff --git a/apps/browser/src/popup/scss/popup.scss b/apps/browser/src/popup/scss/popup.scss index 925ff97740..14a75a62a5 100644 --- a/apps/browser/src/popup/scss/popup.scss +++ b/apps/browser/src/popup/scss/popup.scss @@ -11,3 +11,4 @@ @import "plugins.scss"; @import "environment.scss"; @import "pages.scss"; +@import "~@angular/cdk/overlay-prebuilt.css"; diff --git a/apps/browser/src/popup/services/lock-guard.service.ts b/apps/browser/src/popup/services/lock-guard.service.ts index 0862c313c7..ad62951568 100644 --- a/apps/browser/src/popup/services/lock-guard.service.ts +++ b/apps/browser/src/popup/services/lock-guard.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; -import { LockGuardService as BaseLockGuardService } from "jslib-angular/services/lock-guard.service"; +import { LockGuard as BaseLockGuardService } from "jslib-angular/guards/lock.guard"; @Injectable() export class LockGuardService extends BaseLockGuardService { diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 62591efd7c..bb38a01e12 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,8 +1,8 @@ import { APP_INITIALIZER, LOCALE_ID, NgModule } from "@angular/core"; +import { LockGuard as BaseLockGuardService } from "jslib-angular/guards/lock.guard"; +import { UnauthGuard as BaseUnauthGuardService } from "jslib-angular/guards/unauth.guard"; import { JslibServicesModule, SECURE_STORAGE } from "jslib-angular/services/jslib-services.module"; -import { LockGuardService as BaseLockGuardService } from "jslib-angular/services/lock-guard.service"; -import { UnauthGuardService as BaseUnauthGuardService } from "jslib-angular/services/unauth-guard.service"; import { ApiService } from "jslib-common/abstractions/api.service"; import { AppIdService } from "jslib-common/abstractions/appId.service"; import { AuditService } from "jslib-common/abstractions/audit.service"; @@ -49,6 +49,7 @@ import { AutofillService } from "../../services/abstractions/autofill.service"; import { StateService as StateServiceAbstraction } from "../../services/abstractions/state.service"; import BrowserMessagingService from "../../services/browserMessaging.service"; import BrowserMessagingPrivateModePopupService from "../../services/browserMessagingPrivateModePopup.service"; +import { VaultFilterService } from "../../services/vaultFilter.service"; import { DebounceNavigationService } from "./debounceNavigationService"; import { InitService } from "./init.service"; @@ -224,6 +225,20 @@ function getBgService(service: keyof MainBackground) { useFactory: getBgService("organizationService"), deps: [], }, + { + provide: VaultFilterService, + useFactory: () => { + return new VaultFilterService( + getBgService("stateService")(), + getBgService("organizationService")(), + getBgService("folderService")(), + getBgService("cipherService")(), + getBgService("collectionService")(), + getBgService("policyService")() + ); + }, + deps: [], + }, { provide: ProviderService, useFactory: getBgService("providerService"), diff --git a/apps/browser/src/popup/services/unauth-guard.service.ts b/apps/browser/src/popup/services/unauth-guard.service.ts index a85ab6c88c..71a497e03c 100644 --- a/apps/browser/src/popup/services/unauth-guard.service.ts +++ b/apps/browser/src/popup/services/unauth-guard.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@angular/core"; -import { UnauthGuardService as BaseUnauthGuardService } from "jslib-angular/services/unauth-guard.service"; +import { UnauthGuard as BaseUnauthGuardService } from "jslib-angular/guards/unauth.guard"; @Injectable() export class UnauthGuardService extends BaseUnauthGuardService { diff --git a/apps/browser/src/popup/vault/ciphers.component.html b/apps/browser/src/popup/vault/ciphers.component.html index 729a368c54..2de18ebac0 100644 --- a/apps/browser/src/popup/vault/ciphers.component.html +++ b/apps/browser/src/popup/vault/ciphers.component.html @@ -26,6 +26,10 @@
+

{{ "folders" | i18n }} @@ -89,7 +93,10 @@ maxBufferPx="600" *ngIf="ciphers.length" #virtualScrollViewport - > + >

{{ groupingTitle }} diff --git a/apps/browser/src/popup/vault/ciphers.component.ts b/apps/browser/src/popup/vault/ciphers.component.ts index 145ba4d8d0..8e7b734d14 100644 --- a/apps/browser/src/popup/vault/ciphers.component.ts +++ b/apps/browser/src/popup/vault/ciphers.component.ts @@ -4,11 +4,13 @@ import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; import { CiphersComponent as BaseCiphersComponent } from "jslib-angular/components/ciphers.component"; +import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model"; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; import { CipherService } from "jslib-common/abstractions/cipher.service"; import { CollectionService } from "jslib-common/abstractions/collection.service"; import { FolderService } from "jslib-common/abstractions/folder.service"; import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { OrganizationService } from "jslib-common/abstractions/organization.service"; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { SearchService } from "jslib-common/abstractions/search.service"; import { CipherType } from "jslib-common/enums/cipherType"; @@ -21,6 +23,7 @@ import { BrowserComponentState } from "src/models/browserComponentState"; import { BrowserApi } from "../../browser/browserApi"; import { StateService } from "../../services/abstractions/state.service"; +import { VaultFilterService } from "../../services/vaultFilter.service"; import { PopupUtilsService } from "../services/popup-utils.service"; const ComponentId = "CiphersComponent"; @@ -38,6 +41,11 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On nestedFolders: TreeNode[]; nestedCollections: TreeNode[]; searchTypeSearch = false; + showOrganizations = false; + vaultFilter: VaultFilter; + deleted = true; + noneFolder = false; + showVaultFilter = false; private selectedTimeout: number; private preventSelected = false; @@ -46,6 +54,7 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On constructor( searchService: SearchService, + private organizationService: OrganizationService, private route: ActivatedRoute, private router: Router, private location: Location, @@ -58,7 +67,8 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On private folderService: FolderService, private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService, - private cipherService: CipherService + private cipherService: CipherService, + private vaultFilterService: VaultFilterService ) { super(searchService); this.applySavedState = @@ -68,6 +78,8 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On async ngOnInit() { this.searchTypeSearch = !this.platformUtilsService.isSafari(); + this.showOrganizations = await this.organizationService.hasOrganizations(); + this.vaultFilter = this.vaultFilterService.getVaultFilter(); this.route.queryParams.pipe(first()).subscribe(async (params) => { if (this.applySavedState) { this.state = await this.stateService.getBrowserCipherComponentState(); @@ -77,10 +89,12 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On } if (params.deleted) { + this.showVaultFilter = true; this.groupingTitle = this.i18nService.t("trash"); this.searchPlaceholder = this.i18nService.t("searchTrash"); - await this.load(null, true); + await this.load(this.buildFilter(), true); } else if (params.type) { + this.showVaultFilter = true; this.searchPlaceholder = this.i18nService.t("searchType"); this.type = parseInt(params.type, null); switch (this.type) { @@ -99,11 +113,13 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On default: break; } - await this.load((c) => c.type === this.type); + await this.load(this.buildFilter()); } else if (params.folderId) { + this.showVaultFilter = false; this.folderId = params.folderId === "none" ? null : params.folderId; this.searchPlaceholder = this.i18nService.t("searchFolder"); if (this.folderId != null) { + this.showOrganizations = false; const folderNode = await this.folderService.getNested(this.folderId); if (folderNode != null && folderNode.node != null) { this.groupingTitle = folderNode.node.name; @@ -113,10 +129,12 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On : null; } } else { + this.noneFolder = true; this.groupingTitle = this.i18nService.t("noneFolder"); } - await this.load((c) => c.folderId === this.folderId); + await this.load(this.buildFilter()); } else if (params.collectionId) { + this.showVaultFilter = false; this.collectionId = params.collectionId; this.searchPlaceholder = this.i18nService.t("searchCollection"); const collectionNode = await this.collectionService.getNested(this.collectionId); @@ -131,8 +149,9 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On (c) => c.collectionIds != null && c.collectionIds.indexOf(this.collectionId) > -1 ); } else { + this.showVaultFilter = true; this.groupingTitle = this.i18nService.t("allItems"); - await this.load(); + await this.load(this.buildFilter()); } if (this.applySavedState && this.state != null) { @@ -232,6 +251,40 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On ); } + async changeVaultSelection() { + this.vaultFilter = this.vaultFilterService.getVaultFilter(); + await this.load(this.buildFilter(), this.deleted); + } + + private buildFilter(): (cipher: CipherView) => boolean { + return (cipher) => { + let cipherPassesFilter = true; + if (this.deleted && cipherPassesFilter) { + cipherPassesFilter = cipher.isDeleted; + } + if (this.type != null && cipherPassesFilter) { + cipherPassesFilter = cipher.type === this.type; + } + if (this.folderId != null && this.folderId != "none" && cipherPassesFilter) { + cipherPassesFilter = cipher.folderId === this.folderId; + } + if (this.noneFolder) { + cipherPassesFilter = cipher.folderId == null; + } + if (this.collectionId != null && cipherPassesFilter) { + cipherPassesFilter = + cipher.collectionIds != null && cipher.collectionIds.indexOf(this.collectionId) > -1; + } + if (this.vaultFilter.selectedOrganizationId != null && cipherPassesFilter) { + cipherPassesFilter = cipher.organizationId === this.vaultFilter.selectedOrganizationId; + } + if (this.vaultFilter.myVaultOnly && cipherPassesFilter) { + cipherPassesFilter = cipher.organizationId === null; + } + return cipherPassesFilter; + }; + } + private async saveState() { this.state = { scrollY: this.popupUtils.getContentScrollY(window, this.scrollingContainer), diff --git a/apps/browser/src/popup/vault/current-tab.component.html b/apps/browser/src/popup/vault/current-tab.component.html index f0d2277b22..0556e0d564 100644 --- a/apps/browser/src/popup/vault/current-tab.component.html +++ b/apps/browser/src/popup/vault/current-tab.component.html @@ -35,6 +35,7 @@

+

{{ "typeLogins" | i18n }} diff --git a/apps/browser/src/popup/vault/current-tab.component.ts b/apps/browser/src/popup/vault/current-tab.component.ts index d861047603..999e6c36b6 100644 --- a/apps/browser/src/popup/vault/current-tab.component.ts +++ b/apps/browser/src/popup/vault/current-tab.component.ts @@ -4,6 +4,7 @@ import { Router } from "@angular/router"; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; import { CipherService } from "jslib-common/abstractions/cipher.service"; import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { OrganizationService } from "jslib-common/abstractions/organization.service"; import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service"; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { SearchService } from "jslib-common/abstractions/search.service"; @@ -16,6 +17,7 @@ import { CipherView } from "jslib-common/models/view/cipherView"; import { BrowserApi } from "../../browser/browserApi"; import { AutofillService } from "../../services/abstractions/autofill.service"; +import { VaultFilterService } from "../../services/vaultFilter.service"; import { PopupUtilsService } from "../services/popup-utils.service"; const BroadcasterSubscriptionId = "CurrentTabComponent"; @@ -35,6 +37,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy { inSidebar = false; searchTypeSearch = false; loaded = false; + showOrganizations = false; private totpCode: string; private totpTimeout: number; @@ -54,7 +57,9 @@ export class CurrentTabComponent implements OnInit, OnDestroy { private syncService: SyncService, private searchService: SearchService, private stateService: StateService, - private passwordRepromptService: PasswordRepromptService + private passwordRepromptService: PasswordRepromptService, + private organizationService: OrganizationService, + private vaultFilterService: VaultFilterService ) {} async ngOnInit() { @@ -184,6 +189,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy { } private async load() { + this.loaded = false; const tab = await BrowserApi.getTabFromCurrentWindow(); if (tab != null) { this.url = tab.url; @@ -204,6 +210,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy { const otherTypes: CipherType[] = []; const dontShowCards = await this.stateService.getDontShowCardsCurrentTab(); const dontShowIdentities = await this.stateService.getDontShowIdentitiesCurrentTab(); + this.showOrganizations = await this.organizationService.hasOrganizations(); if (!dontShowCards) { otherTypes.push(CipherType.Card); } @@ -221,18 +228,20 @@ export class CurrentTabComponent implements OnInit, OnDestroy { this.identityCiphers = []; ciphers.forEach((c) => { - switch (c.type) { - case CipherType.Login: - this.loginCiphers.push(c); - break; - case CipherType.Card: - this.cardCiphers.push(c); - break; - case CipherType.Identity: - this.identityCiphers.push(c); - break; - default: - break; + if (!this.vaultFilterService.filterCipherForSelectedVault(c)) { + switch (c.type) { + case CipherType.Login: + this.loginCiphers.push(c); + break; + case CipherType.Card: + this.cardCiphers.push(c); + break; + case CipherType.Identity: + this.identityCiphers.push(c); + break; + default: + break; + } } }); diff --git a/apps/browser/src/popup/vault/groupings.component.html b/apps/browser/src/popup/vault/vault-filter.component.html similarity index 96% rename from apps/browser/src/popup/vault/groupings.component.html rename to apps/browser/src/popup/vault/vault-filter.component.html index bd352e4a60..0d4194f9f2 100644 --- a/apps/browser/src/popup/vault/groupings.component.html +++ b/apps/browser/src/popup/vault/vault-filter.component.html @@ -33,6 +33,7 @@

+

@@ -114,7 +115,7 @@

-
+

{{ "folders" | i18n }} {{ folderCount }} @@ -139,7 +140,7 @@

-
+

{{ "collections" | i18n }} {{ nestedCollections.length }} diff --git a/apps/browser/src/popup/vault/groupings.component.ts b/apps/browser/src/popup/vault/vault-filter.component.ts similarity index 73% rename from apps/browser/src/popup/vault/groupings.component.ts rename to apps/browser/src/popup/vault/vault-filter.component.ts index 765dc957c5..8bae6c7328 100644 --- a/apps/browser/src/popup/vault/groupings.component.ts +++ b/apps/browser/src/popup/vault/vault-filter.component.ts @@ -3,15 +3,14 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angula import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; -import { GroupingsComponent as BaseGroupingsComponent } from "jslib-angular/components/groupings.component"; +import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model"; import { BroadcasterService } from "jslib-common/abstractions/broadcaster.service"; import { CipherService } from "jslib-common/abstractions/cipher.service"; -import { CollectionService } from "jslib-common/abstractions/collection.service"; -import { FolderService } from "jslib-common/abstractions/folder.service"; import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service"; import { SearchService } from "jslib-common/abstractions/search.service"; import { SyncService } from "jslib-common/abstractions/sync.service"; import { CipherType } from "jslib-common/enums/cipherType"; +import { TreeNode } from "jslib-common/models/domain/treeNode"; import { CipherView } from "jslib-common/models/view/cipherView"; import { CollectionView } from "jslib-common/models/view/collectionView"; import { FolderView } from "jslib-common/models/view/folderView"; @@ -20,15 +19,16 @@ import { BrowserGroupingsComponentState } from "src/models/browserGroupingsCompo import { BrowserApi } from "../../browser/browserApi"; import { StateService } from "../../services/abstractions/state.service"; +import { VaultFilterService } from "../../services/vaultFilter.service"; import { PopupUtilsService } from "../services/popup-utils.service"; -const ComponentId = "GroupingsComponent"; +const ComponentId = "VaultComponent"; @Component({ - selector: "app-vault-groupings", - templateUrl: "groupings.component.html", + selector: "app-vault-filter", + templateUrl: "vault-filter.component.html", }) -export class GroupingsComponent extends BaseGroupingsComponent implements OnInit, OnDestroy { +export class VaultFilterComponent implements OnInit, OnDestroy { get showNoFolderCiphers(): boolean { return ( this.noFolderCiphers != null && @@ -40,6 +40,12 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit get folderCount(): number { return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1); } + folders: FolderView[]; + nestedFolders: TreeNode[]; + collections: CollectionView[]; + nestedCollections: TreeNode[]; + loaded = false; + cipherType = CipherType; ciphers: CipherView[]; favoriteCiphers: CipherView[]; noFolderCiphers: CipherView[]; @@ -52,6 +58,9 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit searchPending = false; searchTypeSearch = false; deletedCount = 0; + vaultFilter: VaultFilter; + selectedOrganization: string = null; + showCollections = true; private loadedTimeout: number; private selectedTimeout: number; @@ -63,8 +72,6 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit private allCiphers: CipherView[] = null; constructor( - collectionService: CollectionService, - folderService: FolderService, private cipherService: CipherService, private router: Router, private ngZone: NgZone, @@ -76,9 +83,9 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit private platformUtilsService: PlatformUtilsService, private searchService: SearchService, private location: Location, - private browserStateService: StateService + private browserStateService: StateService, + private vaultFilterService: VaultFilterService ) { - super(collectionService, folderService, browserStateService); this.noFolderListSize = 100; } @@ -143,14 +150,18 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit } async load() { - await super.load(false); + this.vaultFilter = this.vaultFilterService.getVaultFilter(); + + this.updateSelectedOrg(); + await this.loadCollectionsAndFolders(); await this.loadCiphers(); + if (this.showNoFolderCiphers && this.nestedFolders.length > 0) { // Remove "No Folder" from folder listing this.nestedFolders = this.nestedFolders.slice(0, this.nestedFolders.length - 1); } - super.loaded = true; + this.loaded = true; } async loadCiphers() { @@ -158,60 +169,20 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit if (!this.hasLoadedAllCiphers) { this.hasLoadedAllCiphers = !this.searchService.isSearchable(this.searchText); } - this.deletedCount = this.allCiphers.filter((c) => c.isDeleted).length; await this.search(null); - let favoriteCiphers: CipherView[] = null; - let noFolderCiphers: CipherView[] = null; - const folderCounts = new Map(); - const collectionCounts = new Map(); - const typeCounts = new Map(); + this.getCounts(); + } - this.ciphers.forEach((c) => { - if (c.isDeleted) { - return; - } - if (c.favorite) { - if (favoriteCiphers == null) { - favoriteCiphers = []; - } - favoriteCiphers.push(c); - } + async loadCollections() { + const allCollections = await this.vaultFilterService.buildCollections(); + this.collections = allCollections.fullList; + this.nestedCollections = allCollections.nestedList; + } - if (c.folderId == null) { - if (noFolderCiphers == null) { - noFolderCiphers = []; - } - noFolderCiphers.push(c); - } - - if (typeCounts.has(c.type)) { - typeCounts.set(c.type, typeCounts.get(c.type) + 1); - } else { - typeCounts.set(c.type, 1); - } - - if (folderCounts.has(c.folderId)) { - folderCounts.set(c.folderId, folderCounts.get(c.folderId) + 1); - } else { - folderCounts.set(c.folderId, 1); - } - - if (c.collectionIds != null) { - c.collectionIds.forEach((colId) => { - if (collectionCounts.has(colId)) { - collectionCounts.set(colId, collectionCounts.get(colId) + 1); - } else { - collectionCounts.set(colId, 1); - } - }); - } - }); - - this.favoriteCiphers = favoriteCiphers; - this.noFolderCiphers = noFolderCiphers; - this.typeCounts = typeCounts; - this.folderCounts = folderCounts; - this.collectionCounts = collectionCounts; + async loadFolders() { + const allFolders = await this.vaultFilterService.buildFolders(this.selectedOrganization); + this.folders = allFolders.fullList; + this.nestedFolders = await allFolders.nestedList; } async search(timeout: number = null) { @@ -227,6 +198,9 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit filterDeleted, this.allCiphers ); + this.ciphers = this.ciphers.filter( + (c) => !this.vaultFilterService.filterCipherForSelectedVault(c) + ); return; } this.searchPending = true; @@ -241,27 +215,26 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit this.allCiphers ); } + this.ciphers = this.ciphers.filter( + (c) => !this.vaultFilterService.filterCipherForSelectedVault(c) + ); this.searchPending = false; }, timeout); } async selectType(type: CipherType) { - super.selectType(type); this.router.navigate(["/ciphers"], { queryParams: { type: type } }); } async selectFolder(folder: FolderView) { - super.selectFolder(folder); this.router.navigate(["/ciphers"], { queryParams: { folderId: folder.id || "none" } }); } async selectCollection(collection: CollectionView) { - super.selectCollection(collection); this.router.navigate(["/ciphers"], { queryParams: { collectionId: collection.id } }); } async selectTrash() { - super.selectTrash(); this.router.navigate(["/ciphers"], { queryParams: { deleted: true } }); } @@ -294,6 +267,85 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit this.router.navigate(["/add-cipher"]); } + async vaultFilterChanged() { + if (this.showSearching) { + await this.search(); + } + this.updateSelectedOrg(); + await this.loadCollectionsAndFolders(); + this.getCounts(); + } + + updateSelectedOrg() { + this.vaultFilter = this.vaultFilterService.getVaultFilter(); + if (this.vaultFilter.selectedOrganizationId != null) { + this.selectedOrganization = this.vaultFilter.selectedOrganizationId; + } else { + this.selectedOrganization = null; + } + } + + getCounts() { + let favoriteCiphers: CipherView[] = null; + let noFolderCiphers: CipherView[] = null; + const folderCounts = new Map(); + const collectionCounts = new Map(); + const typeCounts = new Map(); + + this.deletedCount = this.allCiphers.filter( + (c) => c.isDeleted && !this.vaultFilterService.filterCipherForSelectedVault(c) + ).length; + + this.ciphers?.forEach((c) => { + if (!this.vaultFilterService.filterCipherForSelectedVault(c)) { + if (c.isDeleted) { + return; + } + if (c.favorite) { + if (favoriteCiphers == null) { + favoriteCiphers = []; + } + favoriteCiphers.push(c); + } + + if (c.folderId == null) { + if (noFolderCiphers == null) { + noFolderCiphers = []; + } + noFolderCiphers.push(c); + } + + if (typeCounts.has(c.type)) { + typeCounts.set(c.type, typeCounts.get(c.type) + 1); + } else { + typeCounts.set(c.type, 1); + } + + if (folderCounts.has(c.folderId)) { + folderCounts.set(c.folderId, folderCounts.get(c.folderId) + 1); + } else { + folderCounts.set(c.folderId, 1); + } + + if (c.collectionIds != null) { + c.collectionIds.forEach((colId) => { + if (collectionCounts.has(colId)) { + collectionCounts.set(colId, collectionCounts.get(colId) + 1); + } else { + collectionCounts.set(colId, 1); + } + }); + } + } + }); + + this.favoriteCiphers = favoriteCiphers; + this.noFolderCiphers = noFolderCiphers; + this.typeCounts = typeCounts; + this.folderCounts = folderCounts; + this.collectionCounts = collectionCounts; + } + showSearching() { return ( this.hasSearched || (!this.searchPending && this.searchService.isSearchable(this.searchText)) @@ -307,6 +359,12 @@ export class GroupingsComponent extends BaseGroupingsComponent implements OnInit } } + private async loadCollectionsAndFolders() { + this.showCollections = !this.vaultFilter.myVaultOnly; + await this.loadFolders(); + await this.loadCollections(); + } + private async saveState() { this.state = { scrollY: this.popupUtils.getContentScrollY(window), diff --git a/apps/browser/src/popup/vault/vault-select.component.html b/apps/browser/src/popup/vault/vault-select.component.html new file mode 100644 index 0000000000..1b389dcdc6 --- /dev/null +++ b/apps/browser/src/popup/vault/vault-select.component.html @@ -0,0 +1,74 @@ +
+ + + + +
diff --git a/apps/browser/src/popup/vault/vault-select.component.ts b/apps/browser/src/popup/vault/vault-select.component.ts new file mode 100644 index 0000000000..d323d2fe59 --- /dev/null +++ b/apps/browser/src/popup/vault/vault-select.component.ts @@ -0,0 +1,113 @@ +import { animate, state, style, transition, trigger } from "@angular/animations"; +import { ConnectedPosition } from "@angular/cdk/overlay"; +import { Component, EventEmitter, OnInit, Output } from "@angular/core"; + +import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model"; +import { I18nService } from "jslib-common/abstractions/i18n.service"; +import { Organization } from "jslib-common/models/domain/organization"; + +import { VaultFilterService } from "../../services/vaultFilter.service"; + +@Component({ + selector: "app-vault-select", + templateUrl: "vault-select.component.html", + animations: [ + trigger("transformPanel", [ + state( + "void", + style({ + opacity: 0, + }) + ), + transition( + "void => open", + animate( + "100ms linear", + style({ + opacity: 1, + }) + ) + ), + transition("* => void", animate("100ms linear", style({ opacity: 0 }))), + ]), + ], +}) +export class VaultSelectComponent implements OnInit { + @Output() onVaultSelectionChanged = new EventEmitter(); + + isOpen = false; + loaded = false; + showOrganizations = false; + organizations: Organization[]; + vaultFilter: VaultFilter = new VaultFilter(); + vaultFilterDisplay = ""; + enforcePersonalOwnwership = false; + overlayPostition: ConnectedPosition[] = [ + { + originX: "start", + originY: "bottom", + overlayX: "start", + overlayY: "top", + }, + ]; + + constructor(private vaultFilterService: VaultFilterService, private i18nService: I18nService) {} + + async ngOnInit() { + this.vaultFilter = this.vaultFilterService.getVaultFilter(); + this.organizations = await this.vaultFilterService.buildOrganizations(); + this.enforcePersonalOwnwership = + await this.vaultFilterService.checkForPersonalOwnershipPolicy(); + + if ( + (!this.enforcePersonalOwnwership && this.organizations.length > 0) || + (this.enforcePersonalOwnwership && this.organizations.length > 1) + ) { + this.showOrganizations = true; + + if (this.enforcePersonalOwnwership && !this.vaultFilter.myVaultOnly) { + this.vaultFilterService.setVaultFilter(this.organizations[0].id); + this.vaultFilter.selectedOrganizationId = this.organizations[0].id; + this.vaultFilterDisplay = this.organizations.find( + (o) => o.id === this.vaultFilter.selectedOrganizationId + ).name; + } else if (this.vaultFilter.myVaultOnly) { + this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.myVault); + } else if (this.vaultFilter.selectedOrganizationId != null) { + this.vaultFilterDisplay = this.organizations.find( + (o) => o.id === this.vaultFilter.selectedOrganizationId + ).name; + } else { + this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.allVaults); + } + } + this.loaded = true; + } + + toggle() { + this.isOpen = !this.isOpen; + } + + close() { + this.isOpen = false; + } + + selectOrganization(organization: Organization) { + this.vaultFilterDisplay = organization.name; + this.vaultFilterService.setVaultFilter(organization.id); + this.onVaultSelectionChanged.emit(); + this.close(); + } + selectAllVaults() { + this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.allVaults); + this.vaultFilterService.setVaultFilter(this.vaultFilterService.allVaults); + this.onVaultSelectionChanged.emit(); + this.close(); + } + selectMyVault() { + this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.myVault); + this.vaultFilterService.setVaultFilter(this.vaultFilterService.myVault); + this.onVaultSelectionChanged.emit(); + this.close(); + } +} diff --git a/apps/browser/src/services/vaultFilter.service.ts b/apps/browser/src/services/vaultFilter.service.ts new file mode 100644 index 0000000000..28ada46ae1 --- /dev/null +++ b/apps/browser/src/services/vaultFilter.service.ts @@ -0,0 +1,73 @@ +import { VaultFilter } from "jslib-angular/modules/vault-filter/models/vault-filter.model"; +import { VaultFilterService as BaseVaultFilterService } from "jslib-angular/modules/vault-filter/vault-filter.service"; +import { CipherService } from "jslib-common/abstractions/cipher.service"; +import { CollectionService } from "jslib-common/abstractions/collection.service"; +import { FolderService } from "jslib-common/abstractions/folder.service"; +import { OrganizationService } from "jslib-common/abstractions/organization.service"; +import { PolicyService } from "jslib-common/abstractions/policy.service"; +import { StateService } from "jslib-common/abstractions/state.service"; +import { CipherView } from "jslib-common/models/view/cipherView"; + +export class VaultFilterService extends BaseVaultFilterService { + vaultFilter: VaultFilter = new VaultFilter(); + + allVaults = "allVaults"; + myVault = "myVault"; + + constructor( + stateService: StateService, + organizationService: OrganizationService, + folderService: FolderService, + cipherService: CipherService, + collectionService: CollectionService, + policyService: PolicyService + ) { + super( + stateService, + organizationService, + folderService, + cipherService, + collectionService, + policyService + ); + this.vaultFilter.myVaultOnly = false; + this.vaultFilter.selectedOrganizationId = null; + } + + getVaultFilter() { + return this.vaultFilter; + } + + setVaultFilter(filter: string) { + if (filter === this.allVaults) { + this.vaultFilter.myVaultOnly = false; + this.vaultFilter.selectedOrganizationId = null; + } else if (filter === this.myVault) { + this.vaultFilter.myVaultOnly = true; + this.vaultFilter.selectedOrganizationId = null; + } else { + this.vaultFilter.myVaultOnly = false; + this.vaultFilter.selectedOrganizationId = filter; + } + } + + clear() { + this.setVaultFilter(this.allVaults); + } + + filterCipherForSelectedVault(cipher: CipherView) { + if (!this.vaultFilter.selectedOrganizationId && !this.vaultFilter.myVaultOnly) { + return false; + } + if (this.vaultFilter.selectedOrganizationId) { + if (cipher.organizationId === this.vaultFilter.selectedOrganizationId) { + return false; + } + } else if (this.vaultFilter.myVaultOnly) { + if (!cipher.organizationId) { + return false; + } + } + return true; + } +} diff --git a/apps/desktop/jslib b/apps/desktop/jslib index 80c834b52a..141ade3c38 160000 --- a/apps/desktop/jslib +++ b/apps/desktop/jslib @@ -1 +1 @@ -Subproject commit 80c834b52a8b00f88250a47a9bbd40c269fc2cba +Subproject commit 141ade3c381cb9ec8e89f15061b92330cb32d403 diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 7fc70050cf..38592bf509 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -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" }, }, ]; diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index 29e324cfe0..9e88eec960 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -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/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 {} diff --git a/apps/desktop/src/services/loginGuard.service.ts b/apps/desktop/src/app/guards/login.guard.ts similarity index 93% rename from apps/desktop/src/services/loginGuard.service.ts rename to apps/desktop/src/app/guards/login.guard.ts index 4f32bf844d..dcf8bbc3b9 100644 --- a/apps/desktop/src/services/loginGuard.service.ts +++ b/apps/desktop/src/app/guards/login.guard.ts @@ -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, diff --git a/apps/desktop/src/app/modules/shared.module.ts b/apps/desktop/src/app/modules/shared.module.ts new file mode 100644 index 0000000000..5108e9b281 --- /dev/null +++ b/apps/desktop/src/app/modules/shared.module.ts @@ -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/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 {} diff --git a/apps/desktop/src/app/modules/vault-filter/components/collection-filter.component.html b/apps/desktop/src/app/modules/vault-filter/components/collection-filter.component.html new file mode 100644 index 0000000000..aa0c1887fe --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/components/collection-filter.component.html @@ -0,0 +1,76 @@ + +
+ +

 {{ collectionsGrouping.name | i18n }}

+
+
    + +
  • + + + + +
      + + +
    +
  • +
    + + +
+
diff --git a/apps/desktop/src/app/modules/vault-filter/components/collection-filter.component.ts b/apps/desktop/src/app/modules/vault-filter/components/collection-filter.component.ts new file mode 100644 index 0000000000..e08c724a5a --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/components/collection-filter.component.ts @@ -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 {} diff --git a/apps/desktop/src/app/modules/vault-filter/components/folder-filter.component.html b/apps/desktop/src/app/modules/vault-filter/components/folder-filter.component.html new file mode 100644 index 0000000000..6c91f89d5f --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/components/folder-filter.component.html @@ -0,0 +1,84 @@ + +
+ +

 {{ foldersGrouping.name | i18n }}

+ +
+
    + +
  • + + + + + +
      + + +
    +
  • +
    + +
+
diff --git a/apps/desktop/src/app/modules/vault-filter/components/folder-filter.component.ts b/apps/desktop/src/app/modules/vault-filter/components/folder-filter.component.ts new file mode 100644 index 0000000000..6205239f77 --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/components/folder-filter.component.ts @@ -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 {} diff --git a/apps/desktop/src/app/modules/vault-filter/components/organization-filter.component.html b/apps/desktop/src/app/modules/vault-filter/components/organization-filter.component.html new file mode 100644 index 0000000000..cc62341758 --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/components/organization-filter.component.html @@ -0,0 +1,102 @@ + + + +
+ + +
+
    +
  • + + + +
  • +
+
+ +
+ + +
+
    +
  • + + + +
  • +
  • + + + +
  • +
+
+
+
+
diff --git a/apps/desktop/src/app/modules/vault-filter/components/organization-filter.component.ts b/apps/desktop/src/app/modules/vault-filter/components/organization-filter.component.ts new file mode 100644 index 0000000000..8703102cf6 --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/components/organization-filter.component.ts @@ -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 + ); + } +} diff --git a/apps/desktop/src/app/modules/vault-filter/components/status-filter.component.html b/apps/desktop/src/app/modules/vault-filter/components/status-filter.component.html new file mode 100644 index 0000000000..865f24a759 --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/components/status-filter.component.html @@ -0,0 +1,46 @@ + +

{{ "filters" | i18n }}

+
    +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
+
diff --git a/apps/desktop/src/app/modules/vault-filter/components/status-filter.component.ts b/apps/desktop/src/app/modules/vault-filter/components/status-filter.component.ts new file mode 100644 index 0000000000..c7c38aa6aa --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/components/status-filter.component.ts @@ -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 {} diff --git a/apps/desktop/src/app/modules/vault-filter/components/type-filter.component.html b/apps/desktop/src/app/modules/vault-filter/components/type-filter.component.html new file mode 100644 index 0000000000..19a06646e4 --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/components/type-filter.component.html @@ -0,0 +1,76 @@ +
+ +

 {{ typesNode.name | i18n }}

+
+
    +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
diff --git a/apps/desktop/src/app/modules/vault-filter/components/type-filter.component.ts b/apps/desktop/src/app/modules/vault-filter/components/type-filter.component.ts new file mode 100644 index 0000000000..794fc49787 --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/components/type-filter.component.ts @@ -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 {} diff --git a/apps/desktop/src/app/modules/vault-filter/vault-filter.component.html b/apps/desktop/src/app/modules/vault-filter/vault-filter.component.html new file mode 100644 index 0000000000..62158fd332 --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/vault-filter.component.html @@ -0,0 +1,50 @@ +
+ +
+ + + + + + + diff --git a/apps/desktop/src/app/modules/vault-filter/vault-filter.component.ts b/apps/desktop/src/app/modules/vault-filter/vault-filter.component.ts new file mode 100644 index 0000000000..b8a4a3605b --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/vault-filter.component.ts @@ -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 {} diff --git a/apps/desktop/src/app/modules/vault-filter/vault-filter.module.ts b/apps/desktop/src/app/modules/vault-filter/vault-filter.module.ts new file mode 100644 index 0000000000..9aad2bba52 --- /dev/null +++ b/apps/desktop/src/app/modules/vault-filter/vault-filter.module.ts @@ -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 {} diff --git a/apps/desktop/src/app/send/send.component.html b/apps/desktop/src/app/send/send.component.html index a019ca0d42..be37b30b59 100644 --- a/apps/desktop/src/app/send/send.component.html +++ b/apps/desktop/src/app/send/send.component.html @@ -1,52 +1,55 @@
-
-
-
-

{{ "filters" | i18n }}

-
    -
  • - -
  • -
-

{{ "types" | i18n }}

-
    -
  • - -
  • -
  • - +
    +
    +

    {{ "filters" | i18n }}

    +
    +
      +
    • + + +
    - +
    diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 2a5c5ff36b..e014b165c8 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -37,10 +37,10 @@ import { ElectronRendererStorageService } from "jslib-electron/services/electron 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 { LoginGuard } from "../guards/login.guard"; import { SearchBarService } from "../layout/search/search-bar.service"; import { InitService } from "./init.service"; @@ -54,7 +54,7 @@ const RELOAD_CALLBACK = new InjectionToken<() => any>("RELOAD_CALLBACK"); InitService, NativeMessagingService, SearchBarService, - LoginGuardService, + LoginGuard, { provide: APP_INITIALIZER, useFactory: (initService: InitService) => initService.init(), diff --git a/apps/desktop/src/app/vault/ciphers.component.html b/apps/desktop/src/app/vault/ciphers.component.html index fe153dda53..b2482e8f8d 100644 --- a/apps/desktop/src/app/vault/ciphers.component.html +++ b/apps/desktop/src/app/vault/ciphers.component.html @@ -1,65 +1,67 @@ -
    - -
    - -
    -
    -
    - - +
    + +
    + +
    + +
    + +
    +
    +

    {{ "noItemsInList" | i18n }}

    - +
    +
    -
    - + diff --git a/apps/desktop/src/app/vault/groupings.component.html b/apps/desktop/src/app/vault/groupings.component.html deleted file mode 100644 index 9d878295a1..0000000000 --- a/apps/desktop/src/app/vault/groupings.component.html +++ /dev/null @@ -1,190 +0,0 @@ -
    -
    -

    {{ "filters" | i18n }}

    -
      -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    -

    {{ "types" | i18n }}

    -
      -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    -

    {{ "loading" | i18n }}

    - -
    -

    {{ "folders" | i18n }}

    - -
    -
      - -
    • - -
        - - -
      -
    • -
      - -
    -
    -

    {{ "collections" | i18n }}

    -
      - -
    • - -
        - - -
      -
    • -
      - - -
    -
    -
    -
    - -
    diff --git a/apps/desktop/src/app/vault/groupings.component.ts b/apps/desktop/src/app/vault/groupings.component.ts deleted file mode 100644 index 12bc2859e1..0000000000 --- a/apps/desktop/src/app/vault/groupings.component.ts +++ /dev/null @@ -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); - } -} diff --git a/apps/desktop/src/app/vault/vault.component.html b/apps/desktop/src/app/vault/vault.component.html index 56e868f668..bf6773e468 100644 --- a/apps/desktop/src/app/vault/vault.component.html +++ b/apps/desktop/src/app/vault/vault.component.html @@ -52,19 +52,16 @@
    - - +
    + + +
    diff --git a/apps/desktop/src/app/vault/vault.component.ts b/apps/desktop/src/app/vault/vault.component.ts index a6f6f9b834..e3668fb3a3 100644 --- a/apps/desktop/src/app/vault/vault.component.ts +++ b/apps/desktop/src/app/vault/vault.component.ts @@ -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; diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index e204d4d59c..8e096e0996 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1420,7 +1420,7 @@ "description": "Noun: a special folder to hold deleted items" }, "searchTrash": { - "message": "Search trash" + "message": "Search Trash" }, "permanentlyDeleteItem": { "message": "Permanently Delete Item" @@ -1902,6 +1902,15 @@ "service": { "message": "Service" }, + "allVaults": { + "message": "All Vaults" + }, + "searchOrganization": { + "message": "Search Organization" + }, + "searchMyVault": { + "message": "Search My Vault" + }, "forwardedEmail": { "message": "Forwarded Email Alias" }, diff --git a/apps/desktop/src/scss/buttons.scss b/apps/desktop/src/scss/buttons.scss index 3d7ccfe2fa..f66a7812dd 100644 --- a/apps/desktop/src/scss/buttons.scss +++ b/apps/desktop/src/scss/buttons.scss @@ -168,3 +168,12 @@ } } } + +button.no-btn { + @extend a; + background: transparent; + border: none; + @include themify($themes) { + color: themed("textColor"); + } +} diff --git a/apps/desktop/src/scss/left-nav.scss b/apps/desktop/src/scss/left-nav.scss new file mode 100644 index 0000000000..bd993c3601 --- /dev/null +++ b/apps/desktop/src/scss/left-nav.scss @@ -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; + } + } +} diff --git a/apps/desktop/src/scss/loading.scss b/apps/desktop/src/scss/loading.scss new file mode 100644 index 0000000000..2562ca4998 --- /dev/null +++ b/apps/desktop/src/scss/loading.scss @@ -0,0 +1,10 @@ +.container { + &.loading-spinner { + display: flex; + height: 100%; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + } +} diff --git a/apps/desktop/src/scss/styles.scss b/apps/desktop/src/scss/styles.scss index a8705342e2..a2cc7681b9 100644 --- a/apps/desktop/src/scss/styles.scss +++ b/apps/desktop/src/scss/styles.scss @@ -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"; diff --git a/apps/desktop/src/scss/variables.scss b/apps/desktop/src/scss/variables.scss index 8e95667c7d..0f13d18c7a 100644 --- a/apps/desktop/src/scss/variables.scss +++ b/apps/desktop/src/scss/variables.scss @@ -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, ), ); diff --git a/apps/desktop/src/scss/vault.scss b/apps/desktop/src/scss/vault.scss index dad10af27a..5b82b76a20 100644 --- a/apps/desktop/src/scss/vault.scss +++ b/apps/desktop/src/scss/vault.scss @@ -15,7 +15,6 @@ app-root { height: 100%; display: flex; - > .groupings, > .items, > .details, > .logo { @@ -29,275 +28,6 @@ app-root { > .items { order: 2; - } - - > .details { - order: 3; - } - - > .logo { - order: 4; - } - - > .groupings { - order: 1; - width: 22%; - min-width: 175px; - max-width: 600px; - 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 { - > button { - padding-left: 12px; - } - - .bwi-li { - left: -4px; - } - - &.active > button .bwi-li { - left: 11px; - } - } - - // Level 2 - ul li { - > button { - padding-left: 23px; - } - - .bwi-li { - left: 7px; - } - - &.active > button .bwi-li { - left: 22px; - } - } - - // Level 3 - ul ul li { - > button { - padding-left: 34px; - } - - .bwi-li { - left: 18px; - } - - &.active > button .bwi-li { - left: 33px; - } - } - - // Level 4 - ul ul ul li { - > button { - padding-left: 45px; - } - - .bwi-li { - left: 29px; - } - - &.active > button .bwi-li { - left: 44px; - } - } - - // Level 5 - ul ul ul ul li { - > button { - padding-left: 56px; - } - - .bwi-li { - left: 40px; - } - - &.active > button .bwi-li { - left: 55px; - } - } - - // Level 6 - ul ul ul ul ul li { - > button { - padding-left: 67px; - } - - .bwi-li { - left: 51px; - } - - &.active > button .bwi-li { - left: 66px; - } - } - - // Level 7 - ul ul ul ul ul ul li { - > button { - padding-left: 78px; - } - - .bwi-li { - left: 62px; - } - - &.active > button .bwi-li { - left: 77px; - } - } - } - - ul { - padding: 0; - margin: 0; - - li { - button { - padding: 5px 0; - display: flex; - align-items: center; - width: 100%; - - @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; @@ -336,6 +66,7 @@ app-root { > .details { flex: 1; min-width: 0; + order: 3; @include themify($themes) { background-color: themed("backgroundColorAlt2"); @@ -378,6 +109,7 @@ app-root { > .logo { flex: 1; min-width: 0; + order: 3; .content { overflow-y: hidden; @@ -429,31 +161,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; - } - } - } }