1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-11-22 11:45:59 +01:00

[feat] End User Vault Refresh (#790)

* Move access logic to org model (#713)

* [feature] Allow for top level groupings to be collapsed (#712)

* [End User Vault Refresh] Refactor route permission checking (#727)

* Update admin access logic

* Centralize route permission handling

* Add permission check for disabled orgs

* [EndUserVaultRefresh] Add base routing guard (#732)

* Add a base class for Angular routing guards

* Update Guard naming convention

* Bump node-forge to 1.2.1 (#722)

* Remove Internet Explorer logic (#723)

* Username generator (#734)

* add support for username generation

* remove unused Router

* pr feedback

* Bump electron and related dependencies (#736)

* PS-91 make isMacAppStore return true/false (#735)

* return false if undefined from isMacAppStore

* PS-91 use strict equality instead of null coalescing

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>

Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>

* [bug] Fix Safari CSV importer for URL and Notes (#730)

* Fix import path for safari importer (#740)

* Force updates to be silent (#739)

* support for username gen website setting (#738)

* Fix jslibModule forms (#742)

* Add DatePipe provider to JslibModule (#741)

* Feature/move to jest (#744)

* Switch to jest

* Fix jslib-angular package name

* Make angular test project

* Split up tests by jslib project

* Remove obsolete node test script

* Use legacy deps with jest-preset-angular

* Move web tests to common

* Remove build from pipeline

This was only being used because we were not using ts runners.
We are now, so build is unnecessary

* Remove the VerifyMasterPasswordComponent from jslib module (#747)

* Add ellipsis pipe to jslib module (#746)

* add ellipsis pipe to jslib module

* Add ellipsis pipe to exports

* Add ColorPasswordCountPipe to JslibModule (#751)

* Generator cleanup (#753)

* type is null by default

* rename generator component

* remove showWebsiteOption

* shorthand if check

* EC-134 Fix api token refresh (#749)

* Fix apikey token refresh

* Refactor: use class for TokenRequestTwoFactor

* Remove keytar and biometric logic (#706)

* [bug] CL - fix default button display and callout header class (#756)

* [EC-142] Fix error during import of 1pux containing new email field format (#758)

* Add support for complex email field type

* Ensure complex email field type gets imported on identities

* [euvr] Separate Billing Payment/History APIs (#750)

* [euvr] Separate Billing Payment/History APIs

* Updated to new accounts billing API

* Removed getUserBilling as it will become obsolete once merged

* [end user vault refresh] Base Changes For Vault Filters (#737)

* [dependency] Update icons

* Avoid duplicate fullSync api calls (#716)

* Tweak component library slightly (#715)

* Check runtime name vs mangled name (#724)

* Add Chromatic (#719)

* Update SECURITY.md (#725)

* Update SECURITY.md

Add link to our HackerOne program for submitting potential security issues.

* Revise language on SECURITY.md

* Remove error Response type check (#731)

* Remove error Response type check

Minimization is impacting type checking in a non-consistent way.
The previous type check works locally,
but not from build artifacts 🤷. We only set `captchaRequired` on
our errors when we want a resubmit with captcha included, so we're safe
keying off that

* linter

* [JslibModule] Add JslibModule (#733)

* Add ellipsis pipe (#728)

* add ellipsis pipe

* run prettier

* Account for ellipsis length in returned string

* Fix complete words case

* Fix another complete words issue

* fix for if there are not spaces in long value

* extract length check to beginning of method

* condense if statements

* remove log

* [refactor] Add optional folders param to folderService.getAllNested()

This will be used later for use cases where the vault filters service needs to build a list of nested folders that have been filtered by organization

* [feature] Add organization filters

This is an MVP implementation of the changes needed for the vault refresh. This includes collapsable top level groupings, and organization based filters that dynamically adjust folders and collections.

* [refactor] Break down vault filter into several components

These changes rename and rewrite the GroupingsComponent into a VaultFiltersModule. The module follows typical angular patterns for structure and purpose, and contain components for each filter type. The mostly communicate via Input and Output, and depend on a VaultFilterService for sending and recieving data from other parts of the product.

* [bug] Add missing events for folder add/edit

* [refactor] Dont directly change activeFilter in VaultFilterComponent

* [refactor] Move DisplayMode to a dedicated file

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>

* [CL-16 Component Library] Menu Dropdown (#761)

* [bug] Add missing null check in vault filters (#769)

* [bug] Add @Injectable to VaultFilterService (#781)

* [fix] Ran prettier

* [fix] Fix merge issue

I used createUrlTree when merging guards because I knew that was the angular standard, didn't notice that redirect was a helper method from us

* Remove BaseGuard (#791)

Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com>
Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
Co-authored-by: Oscar Hinton <oscar@oscarhinton.com>
Co-authored-by: Kyle Spearrin <kspearrin@users.noreply.github.com>
Co-authored-by: Jake Fink <jfink@bitwarden.com>
Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com>
Co-authored-by: David Frankel <42774874+frankeld@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: Robyn MacCallum <robyntmaccallum@gmail.com>
Co-authored-by: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com>
Co-authored-by: Vincent Salucci <vincesalucci21@gmail.com>
This commit is contained in:
Addison Beck 2022-05-09 08:09:46 -04:00 committed by GitHub
parent 52321c51cc
commit 141ade3c38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 992 additions and 181 deletions

View File

@ -1,157 +0,0 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { CollectionService } from "jslib-common/abstractions/collection.service";
import { FolderService } from "jslib-common/abstractions/folder.service";
import { StateService } from "jslib-common/abstractions/state.service";
import { CipherType } from "jslib-common/enums/cipherType";
import { TreeNode } from "jslib-common/models/domain/treeNode";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
@Directive()
export class GroupingsComponent {
@Input() showFolders = true;
@Input() showCollections = true;
@Input() showFavorites = true;
@Input() showTrash = true;
@Output() onAllClicked = new EventEmitter();
@Output() onFavoritesClicked = new EventEmitter();
@Output() onTrashClicked = new EventEmitter();
@Output() onCipherTypeClicked = new EventEmitter<CipherType>();
@Output() onFolderClicked = new EventEmitter<FolderView>();
@Output() onAddFolder = new EventEmitter();
@Output() onEditFolder = new EventEmitter<FolderView>();
@Output() onCollectionClicked = new EventEmitter<CollectionView>();
folders: FolderView[];
nestedFolders: TreeNode<FolderView>[];
collections: CollectionView[];
nestedCollections: TreeNode<CollectionView>[];
loaded = false;
cipherType = CipherType;
selectedAll = false;
selectedFavorites = false;
selectedTrash = false;
selectedType: CipherType = null;
selectedFolder = false;
selectedFolderId: string = null;
selectedCollectionId: string = null;
private collapsedGroupings: Set<string>;
constructor(
protected collectionService: CollectionService,
protected folderService: FolderService,
protected stateService: StateService
) {}
async load(setLoaded = true) {
const collapsedGroupings = await this.stateService.getCollapsedGroupings();
if (collapsedGroupings == null) {
this.collapsedGroupings = new Set<string>();
} else {
this.collapsedGroupings = new Set(collapsedGroupings);
}
await this.loadFolders();
await this.loadCollections();
if (setLoaded) {
this.loaded = true;
}
}
async loadCollections(organizationId?: string) {
if (!this.showCollections) {
return;
}
const collections = await this.collectionService.getAllDecrypted();
if (organizationId != null) {
this.collections = collections.filter((c) => c.organizationId === organizationId);
} else {
this.collections = collections;
}
this.nestedCollections = await this.collectionService.getAllNested(this.collections);
}
async loadFolders() {
if (!this.showFolders) {
return;
}
this.folders = await this.folderService.getAllDecrypted();
this.nestedFolders = await this.folderService.getAllNested();
}
selectAll() {
this.clearSelections();
this.selectedAll = true;
this.onAllClicked.emit();
}
selectFavorites() {
this.clearSelections();
this.selectedFavorites = true;
this.onFavoritesClicked.emit();
}
selectTrash() {
this.clearSelections();
this.selectedTrash = true;
this.onTrashClicked.emit();
}
selectType(type: CipherType) {
this.clearSelections();
this.selectedType = type;
this.onCipherTypeClicked.emit(type);
}
selectFolder(folder: FolderView) {
this.clearSelections();
this.selectedFolder = true;
this.selectedFolderId = folder.id;
this.onFolderClicked.emit(folder);
}
addFolder() {
this.onAddFolder.emit();
}
editFolder(folder: FolderView) {
this.onEditFolder.emit(folder);
}
selectCollection(collection: CollectionView) {
this.clearSelections();
this.selectedCollectionId = collection.id;
this.onCollectionClicked.emit(collection);
}
clearSelections() {
this.selectedAll = false;
this.selectedFavorites = false;
this.selectedTrash = false;
this.selectedType = null;
this.selectedFolder = false;
this.selectedFolderId = null;
this.selectedCollectionId = null;
}
async collapse(grouping: FolderView | CollectionView, idPrefix = "") {
if (grouping.id == null) {
return;
}
const id = idPrefix + grouping.id;
if (this.isCollapsed(grouping, idPrefix)) {
this.collapsedGroupings.delete(id);
} else {
this.collapsedGroupings.add(id);
}
await this.stateService.setCollapsedGroupings(Array.from(this.collapsedGroupings));
}
isCollapsed(grouping: FolderView | CollectionView, idPrefix = "") {
return this.collapsedGroupings.has(idPrefix + grouping.id);
}
}

View File

@ -7,7 +7,7 @@ import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
@Injectable()
export class AuthGuardService implements CanActivate {
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router,

View File

@ -5,7 +5,7 @@ import { AuthService } from "jslib-common/abstractions/auth.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
@Injectable()
export class LockGuardService implements CanActivate {
export class LockGuard implements CanActivate {
protected homepage = "vault";
protected loginpage = "login";
constructor(private authService: AuthService, private router: Router) {}
@ -20,7 +20,6 @@ export class LockGuardService implements CanActivate {
const redirectUrl =
authStatus === AuthenticationStatus.LoggedOut ? [this.loginpage] : [this.homepage];
this.router.navigate(redirectUrl);
return false;
return this.router.createUrlTree([redirectUrl]);
}
}

View File

@ -5,7 +5,7 @@ import { AuthService } from "jslib-common/abstractions/auth.service";
import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
@Injectable()
export class UnauthGuardService implements CanActivate {
export class UnauthGuard implements CanActivate {
protected homepage = "vault";
constructor(private authService: AuthService, private router: Router) {}

View File

@ -0,0 +1,51 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
import { TopLevelTreeNode } from "../models/top-level-tree-node.model";
import { VaultFilter } from "../models/vault-filter.model";
@Directive()
export class CollectionFilterComponent {
@Input() hide = false;
@Input() collapsedFilterNodes: Set<string>;
@Input() collectionNodes: DynamicTreeNode<CollectionView>;
@Input() activeFilter: VaultFilter;
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
new EventEmitter<ITreeNodeObject>();
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
readonly collectionsGrouping: TopLevelTreeNode = {
id: "collections",
name: "collections",
};
get collections() {
return this.collectionNodes?.fullList;
}
get nestedCollections() {
return this.collectionNodes?.nestedList;
}
get show() {
return !this.hide && this.collections != null && this.collections.length > 0;
}
isCollapsed(node: ITreeNodeObject) {
return this.collapsedFilterNodes.has(node.id);
}
applyFilter(collection: CollectionView) {
this.activeFilter.resetFilter();
this.activeFilter.selectedCollectionId = collection.id;
this.onFilterChange.emit(this.activeFilter);
}
async toggleCollapse(node: ITreeNodeObject) {
this.onNodeCollapseStateChange.emit(node);
}
}

View File

@ -0,0 +1,58 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
import { FolderView } from "jslib-common/models/view/folderView";
import { DynamicTreeNode } from "../models/dynamic-tree-node.model";
import { TopLevelTreeNode } from "../models/top-level-tree-node.model";
import { VaultFilter } from "../models/vault-filter.model";
@Directive()
export class FolderFilterComponent {
@Input() hide = false;
@Input() collapsedFilterNodes: Set<string>;
@Input() folderNodes: DynamicTreeNode<FolderView>;
@Input() activeFilter: VaultFilter;
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
new EventEmitter<ITreeNodeObject>();
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
@Output() onAddFolder = new EventEmitter();
@Output() onEditFolder = new EventEmitter<FolderView>();
get folders() {
return this.folderNodes?.fullList;
}
get nestedFolders() {
return this.folderNodes?.nestedList;
}
readonly foldersGrouping: TopLevelTreeNode = {
id: "folders",
name: "folders",
};
applyFilter(folder: FolderView) {
this.activeFilter.resetFilter();
this.activeFilter.selectedFolder = true;
this.activeFilter.selectedFolderId = folder.id;
this.onFilterChange.emit(this.activeFilter);
}
addFolder() {
this.onAddFolder.emit();
}
editFolder(folder: FolderView) {
this.onEditFolder.emit(folder);
}
isCollapsed(node: ITreeNodeObject) {
return this.collapsedFilterNodes.has(node.id);
}
async toggleCollapse(node: ITreeNodeObject) {
this.onNodeCollapseStateChange.emit(node);
}
}

View File

@ -0,0 +1,78 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { Organization } from "jslib-common/models/domain/organization";
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
import { DisplayMode } from "../models/display-mode";
import { TopLevelTreeNode } from "../models/top-level-tree-node.model";
import { VaultFilter } from "../models/vault-filter.model";
@Directive()
export class OrganizationFilterComponent {
@Input() hide = false;
@Input() collapsedFilterNodes: Set<string>;
@Input() organizations: Organization[];
@Input() activeFilter: VaultFilter;
@Input() activePersonalOwnershipPolicy: boolean;
@Input() activeSingleOrganizationPolicy: boolean;
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
new EventEmitter<ITreeNodeObject>();
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
get displayMode(): DisplayMode {
let displayMode: DisplayMode = "organizationMember";
if (this.organizations == null || this.organizations.length < 1) {
displayMode = "noOrganizations";
} else if (this.activePersonalOwnershipPolicy && !this.activeSingleOrganizationPolicy) {
displayMode = "personalOwnershipPolicy";
} else if (!this.activePersonalOwnershipPolicy && this.activeSingleOrganizationPolicy) {
displayMode = "singleOrganizationPolicy";
} else if (this.activePersonalOwnershipPolicy && this.activeSingleOrganizationPolicy) {
displayMode = "singleOrganizationAndPersonalOwnershipPolicies";
}
return displayMode;
}
get hasActiveFilter() {
return this.activeFilter.myVaultOnly || this.activeFilter.selectedOrganizationId != null;
}
readonly organizationGrouping: TopLevelTreeNode = {
id: "vaults",
name: "allVaults",
};
async applyOrganizationFilter(organization: Organization) {
this.activeFilter.selectedOrganizationId = organization.id;
this.activeFilter.myVaultOnly = false;
this.activeFilter.refreshCollectionsAndFolders = true;
this.applyFilter(this.activeFilter);
}
async applyMyVaultFilter() {
this.activeFilter.selectedOrganizationId = null;
this.activeFilter.myVaultOnly = true;
this.activeFilter.refreshCollectionsAndFolders = true;
this.applyFilter(this.activeFilter);
}
clearFilter() {
this.activeFilter.myVaultOnly = false;
this.activeFilter.selectedOrganizationId = null;
this.applyFilter(new VaultFilter(this.activeFilter));
}
private applyFilter(filter: VaultFilter) {
this.onFilterChange.emit(filter);
}
async toggleCollapse() {
this.onNodeCollapseStateChange.emit(this.organizationGrouping);
}
get isCollapsed() {
return this.collapsedFilterNodes.has(this.organizationGrouping.id);
}
}

View File

@ -0,0 +1,22 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { CipherStatus } from "../models/cipher-status.model";
import { VaultFilter } from "../models/vault-filter.model";
@Directive()
export class StatusFilterComponent {
@Input() hideFavorites = false;
@Input() hideTrash = false;
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
@Input() activeFilter: VaultFilter;
get show() {
return !this.hideFavorites && !this.hideTrash;
}
applyFilter(cipherStatus: CipherStatus) {
this.activeFilter.resetFilter();
this.activeFilter.status = cipherStatus;
this.onFilterChange.emit(this.activeFilter);
}
}

View File

@ -0,0 +1,40 @@
import { Directive, EventEmitter, Input, Output } from "@angular/core";
import { CipherType } from "jslib-common/enums/cipherType";
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
import { TopLevelTreeNode } from "../models/top-level-tree-node.model";
import { VaultFilter } from "../models/vault-filter.model";
@Directive()
export class TypeFilterComponent {
@Input() hide = false;
@Input() collapsedFilterNodes: Set<string>;
@Input() selectedCipherType: CipherType = null;
@Input() activeFilter: VaultFilter;
@Output() onNodeCollapseStateChange: EventEmitter<ITreeNodeObject> =
new EventEmitter<ITreeNodeObject>();
@Output() onFilterChange: EventEmitter<VaultFilter> = new EventEmitter<VaultFilter>();
readonly typesNode: TopLevelTreeNode = {
id: "types",
name: "types",
};
cipherTypeEnum = CipherType; // used in the template
get isCollapsed() {
return this.collapsedFilterNodes.has(this.typesNode.id);
}
applyFilter(cipherType: CipherType) {
this.activeFilter.resetFilter();
this.activeFilter.cipherType = cipherType;
this.onFilterChange.emit(this.activeFilter);
}
async toggleCollapse() {
this.onNodeCollapseStateChange.emit(this.typesNode);
}
}

View File

@ -0,0 +1 @@
export type CipherStatus = "all" | "favorites" | "trash";

View File

@ -0,0 +1,6 @@
export type DisplayMode =
| "noOrganizations"
| "organizationMember"
| "singleOrganizationPolicy"
| "personalOwnershipPolicy"
| "singleOrganizationAndPersonalOwnershipPolicies";

View File

@ -0,0 +1,16 @@
import { TreeNode } from "jslib-common/models/domain/treeNode";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
export class DynamicTreeNode<T extends CollectionView | FolderView> {
fullList: T[];
nestedList: TreeNode<T>[];
hasId(id: string): boolean {
return this.fullList != null && this.fullList.filter((i: T) => i.id === id).length > 0;
}
constructor(init?: Partial<DynamicTreeNode<T>>) {
Object.assign(this, init);
}
}

View File

@ -0,0 +1,7 @@
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
export type TopLevelTreeNodeId = "vaults" | "types" | "collections" | "folders";
export class TopLevelTreeNode implements ITreeNodeObject {
id: TopLevelTreeNodeId;
name: string; // localizationString
}

View File

@ -0,0 +1,32 @@
import { CipherType } from "jslib-common/enums/cipherType";
import { CipherStatus } from "./cipher-status.model";
export class VaultFilter {
cipherType?: CipherType;
selectedCollectionId?: string;
status?: CipherStatus;
selectedFolder = false; // This is needed because of how the "No Folder" folder works. It has a null id.
selectedFolderId?: string;
selectedOrganizationId?: string;
myVaultOnly = false;
refreshCollectionsAndFolders = false;
constructor(init?: Partial<VaultFilter>) {
Object.assign(this, init);
}
resetFilter() {
this.cipherType = null;
this.status = null;
this.selectedCollectionId = null;
this.selectedFolder = false;
this.selectedFolderId = null;
}
resetOrganization() {
this.myVaultOnly = false;
this.selectedOrganizationId = null;
this.resetFilter();
}
}

View File

@ -0,0 +1,108 @@
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { Organization } from "jslib-common/models/domain/organization";
import { ITreeNodeObject } from "jslib-common/models/domain/treeNode";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
import { DynamicTreeNode } from "./models/dynamic-tree-node.model";
import { VaultFilter } from "./models/vault-filter.model";
import { VaultFilterService } from "./vault-filter.service";
@Directive()
export class VaultFilterComponent implements OnInit {
@Input() activeFilter: VaultFilter = new VaultFilter();
@Input() hideFolders = false;
@Input() hideCollections = false;
@Input() hideFavorites = false;
@Input() hideTrash = false;
@Input() hideOrganizations = false;
@Output() onFilterChange = new EventEmitter<VaultFilter>();
@Output() onAddFolder = new EventEmitter<never>();
@Output() onEditFolder = new EventEmitter<FolderView>();
isLoaded = false;
collapsedFilterNodes: Set<string>;
organizations: Organization[];
activePersonalOwnershipPolicy: boolean;
activeSingleOrganizationPolicy: boolean;
collections: DynamicTreeNode<CollectionView>;
folders: DynamicTreeNode<FolderView>;
constructor(protected vaultFilterService: VaultFilterService) {}
get displayCollections() {
return this.collections?.fullList != null && this.collections.fullList.length > 0;
}
async ngOnInit(): Promise<void> {
this.collapsedFilterNodes = await this.vaultFilterService.buildCollapsedFilterNodes();
this.organizations = await this.vaultFilterService.buildOrganizations();
if (this.organizations != null && this.organizations.length > 0) {
this.activePersonalOwnershipPolicy =
await this.vaultFilterService.checkForPersonalOwnershipPolicy();
this.activeSingleOrganizationPolicy =
await this.vaultFilterService.checkForSingleOrganizationPolicy();
}
this.folders = await this.vaultFilterService.buildFolders();
this.collections = await this.vaultFilterService.buildCollections();
this.isLoaded = true;
}
async toggleFilterNodeCollapseState(node: ITreeNodeObject) {
if (this.collapsedFilterNodes.has(node.id)) {
this.collapsedFilterNodes.delete(node.id);
} else {
this.collapsedFilterNodes.add(node.id);
}
await this.vaultFilterService.storeCollapsedFilterNodes(this.collapsedFilterNodes);
}
async applyFilter(filter: VaultFilter) {
if (filter.refreshCollectionsAndFolders) {
await this.reloadCollectionsAndFolders(filter);
filter = this.pruneInvalidatedFilterSelections(filter);
}
this.onFilterChange.emit(filter);
}
async reloadCollectionsAndFolders(filter: VaultFilter) {
this.folders = await this.vaultFilterService.buildFolders(filter.selectedOrganizationId);
this.collections = filter.myVaultOnly
? null
: await this.vaultFilterService.buildCollections(filter.selectedOrganizationId);
}
addFolder() {
this.onAddFolder.emit();
}
editFolder(folder: FolderView) {
this.onEditFolder.emit(folder);
}
protected pruneInvalidatedFilterSelections(filter: VaultFilter): VaultFilter {
filter = this.pruneInvalidFolderSelection(filter);
filter = this.pruneInvalidCollectionSelection(filter);
return filter;
}
protected pruneInvalidFolderSelection(filter: VaultFilter): VaultFilter {
if (filter.selectedFolder && !this.folders?.hasId(filter.selectedFolderId)) {
filter.selectedFolder = false;
filter.selectedFolderId = null;
}
return filter;
}
protected pruneInvalidCollectionSelection(filter: VaultFilter): VaultFilter {
if (
filter.selectedCollectionId != null &&
!this.collections?.hasId(filter.selectedCollectionId)
) {
filter.selectedCollectionId = null;
}
return filter;
}
}

View File

@ -0,0 +1,82 @@
import { Injectable } from "@angular/core";
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 { PolicyType } from "jslib-common/enums/policyType";
import { Organization } from "jslib-common/models/domain/organization";
import { CollectionView } from "jslib-common/models/view/collectionView";
import { FolderView } from "jslib-common/models/view/folderView";
import { DynamicTreeNode } from "./models/dynamic-tree-node.model";
@Injectable()
export class VaultFilterService {
constructor(
protected stateService: StateService,
protected organizationService: OrganizationService,
protected folderService: FolderService,
protected cipherService: CipherService,
protected collectionService: CollectionService,
protected policyService: PolicyService
) {}
async storeCollapsedFilterNodes(collapsedFilterNodes: Set<string>): Promise<void> {
await this.stateService.setCollapsedGroupings(Array.from(collapsedFilterNodes));
}
async buildCollapsedFilterNodes(): Promise<Set<string>> {
return new Set(await this.stateService.getCollapsedGroupings());
}
async buildOrganizations(): Promise<Organization[]> {
return await this.organizationService.getAll();
}
async buildFolders(organizationId?: string): Promise<DynamicTreeNode<FolderView>> {
const storedFolders = await this.folderService.getAllDecrypted();
let folders: FolderView[];
if (organizationId != null) {
const ciphers = await this.cipherService.getAllDecrypted();
const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId);
folders = storedFolders.filter(
(f) =>
orgCiphers.filter((oc) => oc.folderId == f.id).length > 0 ||
ciphers.filter((c) => c.folderId == f.id).length < 1
);
} else {
folders = storedFolders;
}
const nestedFolders = await this.folderService.getAllNested(folders);
return new DynamicTreeNode<FolderView>({
fullList: folders,
nestedList: nestedFolders,
});
}
async buildCollections(organizationId?: string): Promise<DynamicTreeNode<CollectionView>> {
const storedCollections = await this.collectionService.getAllDecrypted();
let collections: CollectionView[];
if (organizationId != null) {
collections = storedCollections.filter((c) => c.organizationId === organizationId);
} else {
collections = storedCollections;
}
const nestedCollections = await this.collectionService.getAllNested(collections);
return new DynamicTreeNode<CollectionView>({
fullList: collections,
nestedList: nestedCollections,
});
}
async checkForSingleOrganizationPolicy(): Promise<boolean> {
return await this.policyService.policyAppliesToUser(PolicyType.SingleOrg);
}
async checkForPersonalOwnershipPolicy(): Promise<boolean> {
return await this.policyService.policyAppliesToUser(PolicyType.PersonalOwnership);
}
}

View File

@ -159,11 +159,15 @@
<glyph unicode="&#xe982;" glyph-name="folder-closed-f" data-tags="bw-folder-closed-f" d="M589.227 683.038h340.031c25.254-0.32 49.35-10.594 67.084-28.583 17.733-17.987 27.687-42.248 27.659-67.503v-554.809c0.065-12.768-2.431-25.447-7.331-37.254-4.903-11.811-12.134-22.507-21.222-31.495-8.703-8.703-19.078-15.591-30.439-20.295s-23.558-7.107-35.878-7.107l-834.264 0.639c-25.285 0.191-49.485 10.369-67.279 28.39s-27.717 42.311-27.589 67.629v703.321c-0.128 25.285 9.762 49.547 27.525 67.567s41.927 28.233 67.212 28.457h364.868c25.285-0.192 49.485-10.37 67.279-28.39s27.717-42.317 27.589-67.63v-16.515c-0.191-9.411 3.361-18.532 9.858-25.382s15.46-10.818 24.901-11.042zM459.727 772.401l-364.868 0.037c-9.409-0.224-18.339-4.195-24.837-11.041s-10.019-15.972-9.795-25.382v-100.182c-0.051-0.737-0.079-1.479-0.079-2.23v-601.598c0-17.671 14.331-31.999 32-31.999s31.999 14.331 31.999 31.999v494.078c0 17.677 14.331 31.999 31.999 31.999h255.999c0.598 0 1.193-0.017 1.787-0.051h527.030c3.008-0.037 6.023 0.576 8.806 1.697s5.343 2.817 7.488 4.929c2.144 2.112 3.873 4.672 5.024 7.429s1.792 5.763 1.792 8.773v6.295c0.224 9.41-3.299 18.503-9.823 25.321s-15.465 10.755-24.873 10.979h-340.031c-25.285 0.289-49.485 10.498-67.244 28.486s-27.749 42.249-27.749 67.535v16.515c0.224 9.445-3.299 18.564-9.795 25.381s-15.426 10.789-24.837 11.042z" />
<glyph unicode="&#xe983;" glyph-name="providers" data-tags="bw-providers" d="M795.946 895.999c-68.137 0-123.373-55.236-123.373-123.373 0-26.887 8.6-51.764 23.2-72.032 5.421-7.527 5.928-17.864 0.072-25.057l-78.409-96.295c-5.865-7.204-16.129-8.871-24.347-4.54-33.264 17.525-71.159 27.444-111.37 27.444-65.26 0-124.418-26.127-167.584-68.491-6.851-6.723-17.598-7.916-25.311-2.203l-43.869 32.484c-7.393 5.474-9.491 15.502-6.223 24.099 5.178 13.62 8.013 28.396 8.013 43.833 0 68.137-55.236 123.373-123.373 123.373s-123.373-55.237-123.373-123.373c0-68.138 55.236-123.373 123.373-123.373 25.493 0 49.178 7.732 68.842 20.977 7.179 4.836 16.702 5.327 23.659 0.175l46.671-34.559c7.637-5.655 9.676-16.143 5.398-24.629-16.308-32.342-25.491-68.889-25.491-107.582 0-60.534 22.479-115.818 59.545-157.959 6.345-7.212 6.945-18.022 0.806-25.412l-52.717-63.466c-5.597-6.739-15.107-8.483-23.293-5.359-13.659 5.211-28.482 8.066-43.972 8.066-68.137 0-123.373-55.236-123.373-123.373s55.236-123.373 123.373-123.373c68.138 0 123.373 55.236 123.373 123.373 0 25.451-7.706 49.1-20.912 68.745-5.074 7.547-5.342 17.651 0.469 24.646l51.951 62.544c6.069 7.305 16.644 8.761 24.876 4.024 35.082-20.183 75.765-31.727 119.143-31.727 67.152 0 127.843 27.664 171.299 72.217 6.851 7.025 17.879 8.346 25.73 2.459l102.412-76.81c7.559-5.669 9.482-16.073 5.8-24.775-6.25-14.768-9.706-31.005-9.706-48.049 0-68.138 55.236-123.373 123.373-123.373s123.373 55.236 123.373 123.373c0 68.137-55.237 123.373-123.373 123.373-23.816 0-46.054-6.749-64.91-18.436-7.147-4.43-16.358-4.684-23.084 0.36l-109.29 81.968c-7.431 5.573-9.506 15.76-5.509 24.144 14.846 31.149 23.156 66.013 23.156 102.818 0 63.709-24.9 121.603-65.499 164.48-6.747 7.126-7.628 18.199-1.432 25.809l78.978 96.995c5.415 6.651 14.649 8.563 22.759 5.773 12.588-4.33 26.095-6.681 40.152-6.681 68.138 0 123.373 55.237 123.373 123.373s-55.236 123.373-123.373 123.373zM709.641 772.626c0 47.665 38.639 86.305 86.305 86.305s86.306-38.64 86.306-86.305c0-47.665-38.641-86.305-86.306-86.305s-86.305 38.639-86.305 86.305zM110.305 546.545c-41.677 6.139-73.666 42.052-73.666 85.435 0 47.696 38.665 86.361 86.361 86.361 13.198 0 25.705-2.961 36.891-8.254 29.41-13.755 49.785-43.609 49.785-78.22 0-47.665-38.641-86.306-86.306-86.306-4.441 0-8.804 0.336-13.065 0.983zM481.718 540.459c83.906 0 154.219-58.19 172.787-136.417 3.378-13.734 5.169-28.091 5.169-42.867 0-98.799-80.093-178.892-178.892-178.892s-178.892 80.093-178.892 178.892c0 78.88 51.053 145.837 121.919 169.629 18.151 6.258 37.633 9.655 57.908 9.655zM814.321 48.648c0 20.601 7.219 39.515 19.264 54.353 1.489 1.178 2.854 2.565 4.047 4.156 0.415 0.551 0.798 1.117 1.154 1.691 15.678 16.103 37.592 26.103 61.842 26.103 47.665 0 86.305-38.639 86.305-86.305s-38.639-86.306-86.305-86.306c-47.665 0-86.306 38.641-86.306 86.306zM96.513-4.628c0 47.665 38.64 86.305 86.305 86.305s86.306-38.639 86.306-86.305c0-47.665-38.641-86.305-86.306-86.305s-86.305 38.639-86.305 86.305z" />
<glyph unicode="&#xe984;" glyph-name="vault" data-tags="bw-vault" d="M418.067 645.284c-9.153 0-16.572-7.42-16.572-16.572v-57.561c0-5.573-4.163-10.223-9.627-11.318-29.41-5.9-55.303-21.534-74.176-43.397-3.666-4.249-9.823-5.568-14.686-2.762l-50.385 29.090c-7.925 4.576-18.061 1.861-22.637-6.066-4.576-7.925-1.861-18.061 6.065-22.637l50.881-29.377c4.78-2.76 6.741-8.612 5.036-13.863-4.189-12.901-6.453-26.672-6.453-40.97 0-15.287 2.587-29.97 7.349-43.635 1.849-5.305-0.085-11.304-4.95-14.112l-51.864-29.945c-7.925-4.576-10.641-14.712-6.065-22.637s14.712-10.642 22.637-6.065l52.418 30.263c4.776 2.759 10.821 1.533 14.514-2.565 18.697-20.758 43.859-35.576 72.314-41.287 5.463-1.096 9.627-5.746 9.627-11.318v-61.043c0-9.153 7.42-16.572 16.572-16.572s16.572 7.42 16.572 16.572v61.035c0 5.574 4.165 10.223 9.63 11.318 28.465 5.701 53.638 20.517 72.342 41.276 3.693 4.097 9.737 5.322 14.512 2.564l52.383-30.243c7.927-4.576 18.061-1.861 22.639 6.066 4.576 7.925 1.861 18.061-6.066 22.637l-51.82 29.918c-4.866 2.809-6.799 8.809-4.948 14.115 4.766 13.671 7.355 28.362 7.355 43.657 0 14.307-2.266 28.085-6.458 40.995-1.706 5.251 0.254 11.104 5.035 13.866l50.837 29.351c7.927 4.576 10.642 14.712 6.066 22.639-4.578 7.925-14.712 10.641-22.639 6.065l-50.351-29.070c-4.859-2.807-11.014-1.488-14.683 2.761-18.881 21.864-44.782 37.494-74.204 43.386-5.466 1.095-9.631 5.746-9.631 11.318v57.553c0 9.153-7.419 16.572-16.572 16.572zM517.524 429.848c0-54.914-44.517-99.432-99.432-99.432s-99.432 44.517-99.432 99.432 44.517 99.432 99.432 99.432c54.914 0 99.432-44.517 99.432-99.432zM119.798 683.951v-519.253c0-24.405 19.787-44.192 44.192-44.192h662.877c24.405 0 44.192 19.787 44.192 44.192v0.038c29.484 1.045 53.029 22.886 53.029 49.677v88.384c0 26.791-23.546 48.634-53.029 49.677v143.7c29.484 1.045 53.029 22.886 53.029 49.677v99.432c0 27.211-24.291 49.317-54.421 49.711-4.904 19.064-22.208 33.15-42.8 33.15h-662.877c-24.405 0-44.192-19.787-44.192-44.192zM163.99 694.999h662.877c6.102 0 11.048-4.946 11.048-11.048v-519.253c0-6.102-4.946-11.048-11.048-11.048h-662.877c-6.102 0-11.048 4.946-11.048 11.048v519.253c0 6.102 4.946 11.048 11.048 11.048zM871.058 316.177c0 1.643 1.435 2.936 3.022 2.515 7.622-2.029 13.182-8.377 13.182-15.893v-88.384c0-7.518-5.561-13.866-13.182-15.893-1.587-0.423-3.022 0.873-3.022 2.515v115.142zM874.080 529.958c-1.587-0.423-3.022 0.873-3.022 2.515v126.19c0 1.643 1.435 2.936 3.022 2.515 7.622-2.029 13.182-8.377 13.182-15.893v-99.432c0-7.518-5.561-13.866-13.182-15.893zM86.654 827.575h850.692c36.61 0 66.288-29.678 66.288-66.288v-673.924c0-31.251-21.624-57.45-50.726-64.45-5.591-1.346-10.038-6.036-10.038-11.785v-9.939c0-33.559-27.204-60.763-60.763-60.763h-66.288c-33.559 0-60.763 27.204-60.763 60.763v8.838c0 6.102-4.946 11.048-11.048 11.048h-464.014c-6.102 0-11.048-4.946-11.048-11.048v-8.838c0-33.559-27.204-60.763-60.763-60.763h-66.288c-33.559 0-60.763 27.204-60.763 60.763v9.939c0 5.749-4.448 10.439-10.039 11.785-29.102 7.001-50.726 33.2-50.726 64.45v673.924c0 36.61 29.678 66.288 66.288 66.288zM937.346 783.383h-850.692c-12.204 0-22.096-9.892-22.096-22.096v-673.924c0-12.204 9.892-22.096 22.096-22.096h850.692c12.204 0 22.096 9.892 22.096 22.096v673.924c0 12.204-9.892 22.096-22.096 22.096zM909.727 10.027c0 6.102-4.946 11.048-11.048 11.048h-99.432c-6.102 0-11.048-4.946-11.048-11.048v-8.838c0-15.255 12.366-27.619 27.619-27.619h66.288c15.255 0 27.619 12.366 27.619 27.619v8.838zM114.273 1.188c0-15.255 12.366-27.619 27.619-27.619h66.288c15.255 0 27.619 12.366 27.619 27.619v8.838c0 6.102-4.946 11.048-11.048 11.048h-99.432c-6.102 0-11.048-4.946-11.048-11.048v-8.838z" />
<glyph unicode="&#xe985;" glyph-name="browser" data-tags="bw-browser" d="M935.28 729.6h-846.557c-23.99-1.351-46.483-12.034-62.549-29.691s-24.403-40.874-23.226-64.565v-502.692c-1.177-23.722 7.158-46.91 23.226-64.566 16.065-17.658 38.529-28.339 62.549-29.691h846.304c24.053 1.289 46.578 11.939 62.711 29.598s24.5 40.905 23.323 64.658v502.692c1.177 23.722-7.158 46.909-23.226 64.565s-38.562 28.339-62.549 29.691zM935.28 666.763c7.987-0.47 15.43-4.054 20.774-9.93s8.114-13.603 7.731-21.489v-18.375c0.383-6.819-2.003-13.478-6.616-18.538-4.614-5.059-11.041-8.138-17.944-8.547h-854.512c-6.874 0.408-13.3 3.488-17.912 8.547-4.614 5.060-6.968 11.72-6.585 18.538v18.375c-0.383 7.887 2.386 15.614 7.731 21.489s12.791 9.455 20.774 9.93h846.557zM935.28 101.613h-846.557c-7.922 0.471-15.303 3.991-20.648 9.802s-8.146 13.415-7.857 21.238v366.938c-0.351 6.819 2.003 13.477 6.616 18.537s11.041 8.138 17.944 8.547h854.576c6.905-0.376 13.333-3.457 17.944-8.547s7-11.75 6.616-18.537v-366.938c0.383-7.919-2.417-15.646-7.762-21.52-5.345-5.876-12.854-9.425-20.871-9.898v0.376zM662.415 234.057l-47.904 130.649c-3.818 10.406 6.572 20.378 16.813 16.141l125.055-51.748c11.021-4.563 10.276-20.417-1.122-23.923l-42.823-13.175c-3.838-1.183-6.892-4.103-8.242-7.884l-17.845-49.959c-4.002-11.213-19.834-11.279-23.933-0.102z" />
<glyph unicode="&#xe985;" glyph-name="browser" data-tags="bw-browser" d="M935.28 697.6h-846.557c-23.99-1.351-46.483-12.034-62.549-29.691s-24.403-40.874-23.226-64.565v-502.692c-1.177-23.722 7.158-46.91 23.226-64.566 16.065-17.658 38.529-28.339 62.549-29.691h846.304c24.053 1.289 46.578 11.939 62.711 29.598s24.5 40.905 23.323 64.658v502.692c1.177 23.722-7.158 46.909-23.226 64.565s-38.562 28.339-62.549 29.691zM935.28 634.763c7.987-0.47 15.43-4.054 20.774-9.93s8.114-13.603 7.731-21.489v-18.375c0.383-6.819-2.003-13.478-6.616-18.538-4.614-5.059-11.041-8.138-17.944-8.547h-854.512c-6.874 0.408-13.3 3.488-17.912 8.547-4.614 5.060-6.968 11.72-6.585 18.538v18.375c-0.383 7.887 2.386 15.614 7.731 21.489s12.791 9.455 20.774 9.93h846.557zM935.28 69.613h-846.557c-7.922 0.471-15.303 3.991-20.648 9.802s-8.146 13.415-7.857 21.238v366.938c-0.351 6.819 2.003 13.477 6.616 18.537s11.041 8.138 17.944 8.547h854.576c6.905-0.376 13.333-3.457 17.944-8.547s7-11.75 6.616-18.537v-366.938c0.383-7.919-2.417-15.646-7.762-21.52-5.345-5.876-12.854-9.425-20.871-9.898v0.376zM662.415 234.057l-47.904 130.649c-3.818 10.406 6.572 20.378 16.813 16.141l125.055-51.748c11.021-4.563 10.276-20.417-1.122-23.923l-42.823-13.175c-3.838-1.183-6.892-4.103-8.242-7.884l-17.845-49.959c-4.002-11.213-19.834-11.279-23.933-0.102z" />
<glyph unicode="&#xe986;" glyph-name="mobile" data-tags="bw-mobile" d="M517.369-31.195c14.501 0 26.256 11.756 26.256 26.256s-11.755 26.256-26.256 26.256-26.256-11.755-26.256-26.256c0-14.5 11.755-26.256 26.256-26.256zM239.492 861.091c16.574 18.214 39.779 29.234 64.526 30.627h415.965c24.747-1.393 47.951-12.413 64.525-30.627s25.173-42.166 23.958-66.603v-175.116c0.151-3.031 0.152-6.081 0-9.14v-452.461c0.152-3.059 0.151-6.11 0-9.141v-175.116c1.216-24.438-7.385-48.389-23.958-66.603s-39.778-29.234-64.525-30.628h-415.965c-24.747 1.394-47.952 12.414-64.526 30.628s-25.173 42.165-23.958 66.603v175.13c-0.151 3.027-0.152 6.072 0 9.127v452.461c-0.152 3.059-0.151 6.109 0 9.14v175.116c-1.215 24.437 7.385 48.389 23.958 66.603zM284.75 816.656c-5.514-6.060-8.369-14.034-7.976-22.168v-32.082c-0.393-7.033 2.069-13.904 6.827-19.123 4.76-5.217 11.389-8.394 18.511-8.816h419.776c7.122 0.421 13.752 3.598 18.511 8.816s7.221 12.090 6.828 19.123v32.082c0.393 8.135-2.462 16.108-7.977 22.168-5.514 6.061-13.194 9.756-21.433 10.242h-411.634c-8.239-0.486-15.918-4.181-21.433-10.242zM301.242 112.908c0.917-0.137 1.843-0.234 2.776-0.289h415.965c0.932 0.055 1.856 0.154 2.774 0.29 6.372 1.386 12.193 4.696 16.591 9.53 4.853 5.335 7.672 12.153 8.010 19.28 0.045 0.958 0.045 1.922 0 2.888l-0.003 0.034v494.801c-0.343 7.114-3.161 13.919-8.007 19.246-4.398 4.835-10.216 8.145-16.59 9.532-0.916 0.135-1.843 0.232-2.775 0.288h-415.965c-0.933-0.055-1.857-0.154-2.774-0.29-6.373-1.386-12.194-4.695-16.591-9.53-4.846-5.327-7.665-12.132-8.008-19.246v-497.756c0.343-7.115 3.162-13.92 8.008-19.247 4.398-4.834 10.216-8.145 16.59-9.531zM739.25-48.655c5.515 6.060 8.369 14.034 7.977 22.168v45.21c0.393 7.034-2.069 13.905-6.828 19.123s-11.389 8.394-18.511 8.816h-419.776c-7.122-0.421-13.752-3.597-18.511-8.816-4.758-5.217-7.22-12.088-6.827-19.123v-45.21c-0.393-8.134 2.462-16.108 7.976-22.168s13.194-9.756 21.433-10.243h411.637c8.237 0.487 15.917 4.181 21.43 10.243z" />
<glyph unicode="&#xe987;" glyph-name="cli" data-tags="bw-cli" d="M564.293 401.426c8.527 6.203 20.465 4.315 26.668-4.209l76.359-104.993c4.994-6.865 4.854-16.203-0.343-22.912l-76.359-98.631c-6.453-8.337-18.443-9.864-26.78-3.408-8.335 6.453-9.862 18.443-3.407 26.78l67.589 87.302-67.932 93.405c-6.203 8.527-4.315 20.465 4.209 26.666zM709.15 207.82c-10.543 0-19.089-8.548-19.089-19.089s8.548-19.089 19.089-19.089h139.99c10.543 0 19.089 8.548 19.089 19.089s-8.548 19.089-19.089 19.089h-139.99zM935.28 729.6h-846.557c-23.99-1.351-46.483-12.034-62.549-29.691s-24.403-40.874-23.226-64.565v-502.692c-1.177-23.722 7.158-46.91 23.226-64.566 16.065-17.658 38.529-28.339 62.549-29.691h846.304c24.053 1.289 46.578 11.939 62.711 29.598s24.5 40.905 23.323 64.658v502.692c1.177 23.722-7.158 46.909-23.226 64.565s-38.562 28.339-62.549 29.691zM935.28 666.763c7.987-0.47 15.43-4.054 20.774-9.93s8.114-13.603 7.731-21.489v-18.375c0.383-6.819-2.003-13.478-6.616-18.538-4.614-5.059-11.041-8.138-17.944-8.547h-854.512c-6.874 0.408-13.3 3.488-17.912 8.547-4.614 5.060-6.968 11.72-6.585 18.538v18.375c-0.383 7.887 2.386 15.614 7.731 21.489s12.791 9.455 20.774 9.93h846.557zM935.28 101.613h-846.557c-7.922 0.471-15.303 3.991-20.648 9.802s-8.146 13.415-7.857 21.238v366.938c-0.351 6.819 2.003 13.477 6.616 18.537s11.041 8.138 17.944 8.547h854.576c6.905-0.376 13.333-3.457 17.944-8.547s7-11.75 6.616-18.537v-366.938c0.383-7.919-2.417-15.646-7.762-21.52-5.345-5.876-12.854-9.425-20.871-9.898v0.376z" />
<glyph unicode="&#xe988;" glyph-name="save-changes" data-tags="bw-save-changes" d="M37.926 839.11v-910.222c0-31.42 25.47-56.889 56.889-56.889h834.37c31.42 0 56.889 25.469 56.889 56.889v754.218c0 26.878-11.408 52.494-31.387 70.475l-131.199 118.079c-17.41 15.669-40.004 24.34-63.427 24.34h-665.245c-31.419 0-56.889-25.47-56.889-56.889zM884.983 688.755c3.995-3.595 6.277-8.719 6.277-14.095v-688.883c0-10.473-8.49-18.963-18.963-18.963h-37.926c-10.473 0-18.963 8.49-18.963 18.963v334.127c0 14.454-12.736 26.169-28.444 26.169h-549.926c-15.709 0-28.444-11.715-28.444-26.169v-334.127c0-10.473-8.49-18.963-18.963-18.963h-37.926c-10.473 0-18.963 8.49-18.963 18.963v796.444c0 10.473 8.49 18.963 18.963 18.963h37.926c10.473 0 18.963-8.49 18.963-18.963v-199.111c0-15.709 12.245-28.444 27.35-28.444h476.262c15.106 0 27.35 12.736 27.35 28.444v203.255c0 8.184 6.635 14.819 14.82 14.819 3.66 0 7.191-1.355 9.912-3.804l120.695-108.626zM282.256 611.555c-10.473 0-18.963 8.49-18.963 18.963v170.667h229.744c10.473 0 18.963-8.49 18.963-18.963v-75.852c0-20.946 16.979-37.926 37.926-37.926h37.926c20.946 0 37.926 16.979 37.926 37.926v75.852c0 10.473 8.49 18.963 18.963 18.963h21.151c10.473 0 18.963-8.49 18.963-18.963v-151.704c0-10.473-8.49-18.963-18.963-18.963h-383.636zM265.482-14.223v288.996c0 10.473 8.49 18.963 18.963 18.963h474.074v-307.959c0-10.473-8.49-18.963-18.963-18.963h-455.111c-10.473 0-18.963 8.49-18.963 18.963z" />
<glyph unicode="&#xe989;" glyph-name="numbered-list" data-tags="bw-numbered-list" d="M128.654 569.533v138.823c-29.309-19.833-49.039-29.749-59.194-29.749-4.847 0-9.173 1.679-12.981 5.037-3.693 3.458-5.539 7.424-5.539 11.898 0 5.186 1.846 8.999 5.539 11.438s10.213 5.593 19.557 9.459c13.963 5.798 25.097 11.898 33.405 18.306 8.424 6.406 15.867 13.577 22.328 21.507 6.463 7.935 10.675 12.814 12.636 14.646s5.654 2.748 11.079 2.748c6.115 0 11.019-2.085 14.711-6.256 3.693-4.169 5.539-9.916 5.539-17.239v-174.673c0-20.441-7.903-30.662-23.711-30.662-7.039 0-12.692 2.084-16.962 6.256-4.271 4.169-6.405 10.322-6.405 18.459zM107.712 309.403h90.35c9.001 0 15.867-1.629 20.599-4.883 4.729-3.253 7.097-7.676 7.097-13.273 0-4.983-1.903-9.204-5.711-12.662-3.692-3.458-9.348-5.186-16.962-5.186h-127.391c-8.655 0-15.405 2.084-20.251 6.255-4.846 4.273-7.27 9.255-7.27 14.95 0 3.663 1.557 8.491 4.673 14.492 3.116 6.103 6.519 10.883 10.214 14.342 15.346 14.035 29.195 26.035 41.542 36.002 12.347 10.069 21.175 16.68 26.482 19.833 9.462 5.897 17.307 11.797 23.539 17.695 6.346 6.002 11.134 12.103 14.367 18.306 3.347 6.305 5.019 12.458 5.019 18.459 0 6.511-1.789 12.307-5.366 17.391-3.461 5.187-8.252 9.204-14.367 12.053-6.002 2.847-12.579 4.273-19.731 4.273-15.116 0-27.003-5.847-35.655-17.545-1.155-1.525-3.116-5.696-5.885-12.509-2.654-6.815-5.711-12.053-9.173-15.712-3.348-3.661-8.308-5.491-14.887-5.491-5.77 0-10.557 1.679-14.366 5.033-3.81 3.357-5.713 7.935-5.713 13.73 0 7.017 1.789 14.341 5.366 21.967s8.886 14.542 15.925 20.747c7.156 6.205 16.156 11.188 27.003 14.952 10.962 3.864 23.77 5.796 38.426 5.796 17.656 0 32.714-2.439 45.176-7.322 8.076-3.253 15.174-7.729 21.289-13.423s10.847-12.307 14.194-19.832c3.461-7.425 5.192-15.152 5.192-23.187 0-12.611-3.578-24.105-10.731-34.476-7.038-10.272-14.25-18.358-21.636-24.255-7.384-5.798-19.789-14.952-37.213-27.459-17.307-12.509-29.195-22.223-35.655-29.138-2.769-2.747-5.598-6.051-8.482-9.914zM125.712 133.175c10.615 0 19.731 2.748 27.348 8.239 7.731 5.492 11.596 13.373 11.596 23.646 0 7.831-3.058 14.542-9.173 20.137-6.115 5.697-14.367 8.544-24.753 8.544-7.039 0-12.868-0.865-17.483-2.593-4.498-1.728-8.076-4.017-10.73-6.866-2.654-2.847-5.191-6.51-7.614-10.983-2.309-4.477-4.444-8.696-6.404-12.662-1.156-2.135-3.231-3.814-6.232-5.037-3-1.22-6.464-1.83-10.385-1.83-4.615 0-8.886 1.628-12.809 4.882-3.809 3.358-5.711 7.781-5.711 13.273 0 5.289 1.788 10.832 5.366 16.63 3.692 5.897 8.999 11.493 15.924 16.78 7.039 5.287 15.753 9.51 26.138 12.662 10.385 3.253 21.982 4.882 34.789 4.882 11.192 0 21.406-1.373 30.637-4.118 9.231-2.644 17.251-6.51 24.059-11.594 6.809-5.085 11.943-10.984 15.405-17.695s5.192-13.933 5.192-21.663c0-10.17-2.537-18.914-7.614-26.239-4.96-7.22-12.117-14.29-21.465-21.203 9.001-4.273 16.558-9.155 22.675-14.647 6.231-5.491 10.902-11.594 14.018-18.305 3.116-6.611 4.673-13.781 4.673-21.512 0-9.254-2.133-18.204-6.404-26.849-4.155-8.644-10.329-16.374-18.521-23.186-8.193-6.712-17.945-12.002-29.251-15.867-11.192-3.763-23.598-5.645-37.213-5.645-13.846 0-26.252 2.187-37.212 6.559s-20.020 9.815-27.176 16.325c-7.038 6.61-12.406 13.423-16.097 20.441-3.578 7.017-5.366 12.814-5.366 17.392 0 5.897 2.133 10.628 6.404 14.187 4.386 3.661 9.81 5.491 16.269 5.491 3.231 0 6.347-0.865 9.348-2.593 2.999-1.628 4.96-3.609 5.885-5.951 6-14.136 12.405-24.662 19.212-31.577 6.925-6.816 16.617-10.221 29.078-10.221 7.156 0 14.019 1.525 20.599 4.578 6.693 3.153 12.174 7.779 16.441 13.882 4.386 6.102 6.577 13.171 6.577 21.203 0 11.898-3.692 21.204-11.078 27.917-7.384 6.816-17.655 10.221-30.809 10.221-2.309 0-5.885-0.204-10.73-0.609s-7.963-0.609-9.348-0.609c-6.347 0-11.251 1.373-14.712 4.118-3.461 2.848-5.191 6.763-5.191 11.748 0 4.883 2.075 8.796 6.231 11.748 4.155 3.052 10.329 4.577 18.521 4.577h7.098zM356.64 680.597h607.317c15.6 0 28.248-12.647 28.248-28.248s-12.647-28.248-28.248-28.248h-607.317c-15.6 0-28.248 12.647-28.248 28.248s12.647 28.248 28.248 28.248zM356.64 412.248h607.317c15.6 0 28.248-12.647 28.248-28.248s-12.647-28.248-28.248-28.248h-607.317c-15.6 0-28.248 12.647-28.248 28.248s12.647 28.248 28.248 28.248zM356.64 129.776h607.317c15.6 0 28.248-12.647 28.248-28.248s-12.647-28.248-28.248-28.248h-607.317c-15.6 0-28.248 12.647-28.248 28.248s12.647 28.248 28.248 28.248z" />
<glyph unicode="&#xe98a;" glyph-name="bwi-billing" data-tags="bwi-billing" d="M64 800v-832c0-53.018 42.98-96 96-96h704c53.018 0 96 42.982 96 96v551.912c0 25.756-10.349 50.43-28.723 68.48l-285.088 280.088c-17.953 17.637-42.113 27.52-67.279 27.52h-418.91c-53.020 0-96-42.981-96-96zM886.426 542.739c6.125-6.017 9.574-14.242 9.574-22.827v-551.912c0-17.67-14.33-32-32-32h-704c-17.673 0-32 14.33-32 32v832c0 17.673 14.327 32 32 32h418.91c8.388 0 16.442-3.294 22.426-9.173l285.089-280.088zM608 864c-17.673 0-32-14.327-32-32v-256c0-35.346 28.654-64 64-64h256c17.67 0 32 14.327 32 32s-14.33 32-32 32h-256v256c0 17.673-14.327 32-32 32zM352 583.111c-53.020 0-96-38.205-96-85.333s42.98-85.334 96-85.334c88.365 0 160-63.675 160-142.222s-71.635-142.222-160-142.222c-77.479 0-142.095 48.954-156.841 113.958-3.493 15.4 11.168 28.264 28.841 28.264s31.478-13.084 37.26-27.931c13.014-33.408 48.724-57.402 90.74-57.402 53.020 0 96 38.202 96 85.332 0 47.128-42.98 85.334-96 85.334-88.365 0-160 63.675-160 142.222s71.635 142.222 160 142.222c77.479 0 142.095-48.952 156.841-113.96 3.493-15.399-11.168-28.262-28.841-28.262s-31.478 13.084-37.26 27.93c-13.014 33.412-48.724 57.404-90.74 57.404zM608 448c-17.673 0-32-14.327-32-32s14.327-32 32-32h128c17.67 0 32 14.327 32 32s-14.33 32-32 32h-128zM576 288c0 17.673 14.327 32 32 32h128c17.67 0 32-14.327 32-32s-14.33-32-32-32h-128c-17.673 0-32 14.327-32 32zM608 192c-17.673 0-32-14.33-32-32s14.327-32 32-32h128c17.67 0 32 14.33 32 32s-14.33 32-32 32h-128zM352 768c-17.673 0-32-14.327-32-32 0-37.333 0-74.667 0-112 0-17.673 14.327-32 32-32s32 14.327 32 32c0 37.333 0 74.667 0 112 0 17.673-14.327 32-32 32zM352 176c-17.673 0-32-14.33-32-32 0-37.331 0-74.669 0-112 0-17.67 14.327-32 32-32s32 14.33 32 32c0 37.331 0 74.669 0 112 0 17.67-14.327 32-32 32z" />
<glyph unicode="&#xe98b;" glyph-name="bwi-family" data-tags="bwi-family" d="M876.16 417.364c33.907 23.117 59.514 56.49 73.069 95.225 13.549 38.734 14.336 80.792 2.24 120.006s-36.435 73.52-69.453 97.889c-33.024 24.369-72.979 37.517-114.016 37.517s-80.992-13.148-114.016-37.517c-33.015-24.369-57.357-58.675-69.453-97.889s-11.31-81.272 2.243-120.006c13.553-38.735 39.16-72.108 73.066-95.225-14.515-7.995-28.049-17.66-40.32-28.8-11.604 17.965-27.524 32.735-46.306 42.963s-39.828 15.587-61.214 15.587c-21.386 0-42.432-5.359-61.214-15.587s-34.702-24.998-46.306-42.963c-12.433 10.938-25.942 20.587-40.32 28.8 33.906 23.117 59.513 56.49 73.066 95.225s14.339 80.792 2.243 120.006c-12.096 39.213-36.438 73.52-69.457 97.889s-72.976 37.517-114.013 37.517c-41.037 0-80.995-13.148-114.013-37.517s-57.361-58.675-69.457-97.889c-12.096-39.214-11.31-81.272 2.243-120.006s39.16-72.108 73.066-95.225c-41.25-25.997-76.013-61.078-101.636-102.561s-41.426-88.275-46.204-136.799c0-21.76 5.76-49.92 27.52-49.92h316.8c-12.806-27.712-21.020-57.325-24.32-87.68 0-17.28 4.48-40.32 20.48-40.32h334.080c20.48 0 31.36 14.72 28.16 40.32-3.482 30.17-11.238 59.693-23.040 87.68h305.92c27.52 0 41.6 17.92 37.76 49.92-4.704 48.467-20.41 95.223-45.92 136.703s-60.147 76.59-101.28 102.658zM640 576.084c0 25.316 7.507 50.063 21.574 71.113 14.061 21.050 34.054 37.455 57.44 47.144 23.392 9.688 49.126 12.223 73.958 7.284 24.826-4.939 47.635-17.13 65.536-35.031s30.093-40.708 35.034-65.538c4.934-24.829 2.4-50.566-7.283-73.955-9.69-23.389-26.099-43.38-47.149-57.444s-45.792-21.572-71.11-21.572c-33.946 0-66.502 13.485-90.509 37.491s-37.491 56.562-37.491 90.509zM512 384.084c14.449 0.495 28.639-3.917 40.261-12.516s19.988-20.881 23.739-34.844c0.664-5.527 0.664-11.113 0-16.64-0.136-13.043-4.255-25.734-11.804-36.371s-18.168-18.714-30.436-23.149c-6.873-2.958-14.277-4.484-21.76-4.484s-14.887 1.526-21.76 4.484c-12.268 4.435-22.886 12.511-30.436 23.149s-11.668 23.328-11.804 36.371c-0.608 5.53-0.608 11.11 0 16.64 3.75 13.962 12.118 26.244 23.739 34.844s25.812 13.011 40.261 12.516zM128 576.084c0 25.316 7.507 50.063 21.572 71.113s34.056 37.455 57.444 47.144c23.389 9.688 49.126 12.223 73.955 7.284s47.637-17.13 65.538-35.031c17.901-17.901 30.092-40.708 35.031-65.538s2.404-50.566-7.284-73.955c-9.688-23.389-26.094-43.38-47.144-57.444s-45.797-21.572-71.113-21.572c-33.948 0-66.505 13.485-90.509 37.491s-37.491 56.562-37.491 90.509zM67.2 192.083c23.68 119.041 106.88 192.001 192 192.001 24.129-1.312 47.711-7.688 69.213-18.715s40.442-26.456 55.587-45.285c0.234-17.18 3.925-34.137 10.852-49.86 6.927-15.721 16.949-29.891 29.468-41.66-15.103-10.182-28.678-22.47-40.32-36.48h-316.8zM634.24 64.083h-244.48c5.257 22.605 14.117 44.211 26.24 64 9.496 17.747 23.227 32.877 39.974 44.038 16.747 11.168 35.992 18.022 56.026 19.962h6.4c19.405-2.374 37.939-9.446 53.997-20.602 16.058-11.149 29.157-26.048 38.163-43.398 11.484-19.827 19.493-41.472 23.68-64zM640 192.083c-11.731 13.562-25.302 25.408-40.32 35.2 12.674 11.917 22.786 26.291 29.718 42.248 6.932 15.955 10.54 33.156 10.602 50.553 15.584 19.129 35.053 34.728 57.12 45.761s46.227 17.251 70.88 18.239c48.602-5.637 93.792-27.776 128.038-62.719s55.469-80.577 60.122-129.281h-316.16z" />
<glyph unicode="&#xe98c;" glyph-name="bwi-provider" data-tags="bwi-provider" d="M384 704v-64h256v64h-256zM320 736c0 17.673 14.327 32 32 32h320c17.67 0 32-14.327 32-32v-96h288c17.67 0 32-14.327 32-32v-640c0-17.67-14.33-32-32-32h-960c-17.673 0-32 14.33-32 32v640c0 17.673 14.327 32 32 32h288v96zM960 512v64h-896v-64c0-70.692 57.308-128 128-128h192v32c0 17.673 14.327 32 32 32h192c17.673 0 32-14.327 32-32v-32h192c70.694 0 128 57.308 128 128zM640 320v-32c0-17.673-14.327-32-32-32h-192c-17.673 0-32 14.327-32 32v32h-192c-49.174 0-94.031 18.486-128 48.889v-368.889h896v368.889c-33.971-30.403-78.822-48.889-128-48.889h-192zM448 384v-64h128v64h-128z" />
<glyph unicode="&#xe98d;" glyph-name="bwi-business" data-tags="bwi-business" d="M384 736c0 17.673 14.327 32 32 32h64c17.673 0 32-14.327 32-32s-14.327-32-32-32h-64c-17.673 0-32 14.327-32 32zM576 736c0 17.673 14.327 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.673 0-32 14.327-32 32zM768 736c0 17.673 14.33 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.67 0-32 14.327-32 32zM576 544c0 17.673 14.327 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.673 0-32 14.327-32 32zM384 544c0 17.673 14.327 32 32 32h64c17.673 0 32-14.327 32-32s-14.327-32-32-32h-64c-17.673 0-32 14.327-32 32zM768 544c0 17.673 14.33 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.67 0-32 14.327-32 32zM576 352c0 17.673 14.327 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.673 0-32 14.327-32 32zM768 352c0 17.673 14.33 32 32 32h64c17.67 0 32-14.327 32-32s-14.33-32-32-32h-64c-17.67 0-32 14.327-32 32zM576 160c0 17.67 14.327 32 32 32h64c17.67 0 32-14.33 32-32s-14.33-32-32-32h-64c-17.673 0-32 14.33-32 32zM768 160c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32s-14.33-32-32-32h-64c-17.67 0-32 14.33-32 32zM928 896c53.018 0 96-42.981 96-96v-704c0-53.018-42.982-96-96-96h-435.039c8.646-24.486 14.886-50.579 18.353-77.805 4.093-32.154-10.337-50.195-37.797-50.195h-445.465c-21.458 0-30.33 28.429-27.559 50.195 13.575 106.63 69.703 195.77 146.192 239.942-49.966 34.662-82.685 92.442-82.685 157.862 0 106.038 85.962 192 192 192v288c0 53.019 42.98 96 96 96h576zM128 320c0-70.694 57.308-128 128-128s128 57.306 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128zM320 800v-298.925c54.554-19.282 97.793-62.52 117.075-117.075h42.925c17.673 0 32-14.327 32-32s-14.327-32-32-32h-32c0-65.459-32.757-123.264-82.772-157.926 38.691-22.368 72.167-56.25 97.553-98.074h465.219c17.67 0 32 14.33 32 32v704c0 17.673-14.33 32-32 32h-576c-17.673 0-32-14.327-32-32zM255.903 128c-81.62 0-165.099-72.646-188.403-192h376.807c-23.304 119.354-106.783 192-188.404 192z" />
<glyph unicode="&#xe9ee;" glyph-name="rocket" data-tags="rocket" d="M650.515 648.267c33.538 33.532 87.904 33.532 121.443 0 33.538-33.538 33.538-87.904 0-121.443s-87.904-33.538-121.443 0c-33.532 33.538-33.532 87.904 0 121.443zM750.801 627.113c-21.855 21.856-57.284 21.856-79.134 0-21.856-21.855-21.856-57.284 0-79.134 21.855-21.855 57.284-21.855 79.134 0s21.855 57.284 0 79.134zM493.141 680.645c113.184 90.608 223.416 148.836 310.181 180.552 43.273 15.818 81.527 25.336 111.933 28.691 15.138 1.668 29.339 1.929 41.709 0.19 11.507-1.615 25.903-5.552 36.583-16.232l3.981-3.981c10.68-10.679 14.617-25.076 16.232-36.582 1.739-12.377 1.478-26.572-0.19-41.71-3.356-30.406-12.874-68.659-28.691-111.932-31.716-86.771-89.944-196.998-180.552-310.187-29.487-36.837-59.902-73.855-91.847-111.076l30.789-145.412c4.388-20.715-0.21-42.314-12.654-59.447l-89.489-123.202c-35.299-48.598-110.586-37.857-130.894 18.67l-31.576 87.886-74.269-74.269c-5.844-5.844-15.313-5.844-21.151 0-5.844 5.845-5.844 15.313 0 21.152l80.747 80.747-47.795 47.794-162.229-162.229c-5.844-5.844-15.313-5.844-21.151 0s-5.844 15.313 0 21.151l162.229 162.229-54.059 54.060-306.392-306.392c-5.844-5.844-15.313-5.844-21.151 0-5.844 5.845-5.844 15.313 0 21.151l306.391 306.392-54.059 54.060-61.939-61.939c-5.845-5.844-15.313-5.844-21.151 0-5.845 5.844-5.845 15.313 0 21.151l61.938 61.939-47.794 47.794-112.086-112.086c-5.844-5.844-15.312-5.844-21.151 0-5.844 5.845-5.844 15.313 0 21.152l109.675 109.669-100.568 36.13c-56.526 20.307-67.261 95.6-18.663 130.894l123.202 89.489c17.132 12.444 38.73 17.041 59.446 12.654l145.412-30.789c37.221 31.945 74.238 62.36 111.076 91.847zM952.797 829.985c-0.744 0.223-2.062 0.551-4.14 0.84-5.79 0.812-14.652 0.934-26.837-0.411-24.234-2.675-57.643-10.683-97.952-25.414-80.389-29.379-184.973-84.318-293.329-171.062-38.013-30.432-76.092-61.749-114.295-94.665-2.919-4.249-6.879-7.644-11.433-9.895-54.88-47.714-110.027-98.85-165.607-155.478l258.391-258.39c56.628 55.58 107.765 110.728 155.48 165.609 2.25 4.551 5.643 8.51 9.89 11.427 32.912 38.205 64.235 76.284 94.667 114.298 86.744 108.362 141.683 212.941 171.063 293.329 14.73 40.309 22.744 73.723 25.414 97.952 1.345 12.179 1.222 21.047 0.411 26.837-0.29 2.078-0.619 3.397-0.84 4.14l-0.875 0.875zM224.258 561.056c-4.141 0.875-8.464-0.045-11.889-2.533l-123.202-89.489c-9.719-7.058-7.573-22.117 3.736-26.177l96.234-34.573c45.935 47.161 91.519 90.548 136.803 131.242l-101.677 21.529zM663.214 202.25c-40.694-45.278-84.081-90.868-131.243-136.803l34.574-96.235c4.061-11.304 19.119-13.455 26.177-3.736l89.489 123.202c2.488 3.425 3.408 7.748 2.533 11.889l-21.529 101.678z" />
<glyph unicode="&#xe9ef;" glyph-name="ellipsis-h" data-tags="ellipsis-h" d="M919.751 314.596c-43.122 0-78.080 34.958-78.080 78.080s34.958 78.080 78.080 78.080c43.122 0 78.080-34.958 78.080-78.080s-34.958-78.080-78.080-78.080zM520.676 314.596c-43.122 0-78.080 34.958-78.080 78.080s34.958 78.080 78.080 78.080c43.122 0 78.080-34.958 78.080-78.080s-34.958-78.080-78.080-78.080zM121.6 314.596c-43.122 0-78.080 34.958-78.080 78.080s34.958 78.080 78.080 78.080c43.122 0 78.080-34.958 78.080-78.080s-34.958-78.080-78.080-78.080z" />
<glyph unicode="&#xe9f0;" glyph-name="ellipsis-v" data-tags="ellipsis-v" d="M586.472 734.020c0-37.017-30.007-67.024-67.024-67.024s-67.024 30.007-67.024 67.024c0 37.017 30.007 67.024 67.024 67.024s67.024-30.007 67.024-67.024zM586.472 391.448c0-37.017-30.007-67.024-67.024-67.024s-67.024 30.007-67.024 67.024c0 37.017 30.007 67.024 67.024 67.024s67.024-30.007 67.024-67.024zM586.472 48.872c0-37.017-30.007-67.024-67.024-67.024s-67.024 30.007-67.024 67.024c0 37.017 30.007 67.024 67.024 67.024s67.024-30.007 67.024-67.024z" />

Before

Width:  |  Height:  |  Size: 262 KiB

After

Width:  |  Height:  |  Size: 269 KiB

View File

@ -241,6 +241,10 @@ $icons: (
"android": "\e944",
"error": "\e981",
"numbered-list": "\e989",
"billing": "\e98a",
"family": "\e98b",
"provider": "\e98c",
"business": "\e98d",
);
@each $name, $glyph in $icons {

View File

@ -74,12 +74,13 @@ import { UsernameGenerationService } from "jslib-common/services/usernameGenerat
import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service";
import { WebCryptoFunctionService } from "jslib-common/services/webCryptoFunction.service";
import { AuthGuardService } from "./auth-guard.service";
import { AuthGuard } from "../guards/auth.guard";
import { LockGuard } from "../guards/lock.guard";
import { UnauthGuard } from "../guards/unauth.guard";
import { BroadcasterService } from "./broadcaster.service";
import { LockGuardService } from "./lock-guard.service";
import { ModalService } from "./modal.service";
import { PasswordRepromptService } from "./passwordReprompt.service";
import { UnauthGuardService } from "./unauth-guard.service";
import { ValidationService } from "./validation.service";
export const WINDOW = new InjectionToken<Window>("WINDOW");
@ -98,9 +99,9 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
declarations: [],
providers: [
ValidationService,
AuthGuardService,
UnauthGuardService,
LockGuardService,
AuthGuard,
UnauthGuard,
LockGuard,
ModalService,
{ provide: WINDOW, useValue: window },
{

View File

@ -1,3 +1,6 @@
import { BillingHistoryResponse } from "jslib-common/models/response/billingHistoryResponse";
import { BillingPaymentResponse } from "jslib-common/models/response/billingPaymentResponse";
import { PolicyType } from "../enums/policyType";
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
import { VerifyOTPRequest } from "../models/request/account/verifyOTPRequest";
@ -174,7 +177,6 @@ export abstract class ApiService {
refreshIdentityToken: () => Promise<any>;
getProfile: () => Promise<ProfileResponse>;
getUserBilling: () => Promise<BillingResponse>;
getUserSubscription: () => Promise<SubscriptionResponse>;
getTaxInfo: () => Promise<TaxInfoResponse>;
putProfile: (request: UpdateProfileRequest) => Promise<ProfileResponse>;
@ -212,6 +214,9 @@ export abstract class ApiService {
postAccountVerifyOTP: (request: VerifyOTPRequest) => Promise<void>;
postConvertToKeyConnector: () => Promise<void>;
getUserBillingHistory: () => Promise<BillingHistoryResponse>;
getUserBillingPayment: () => Promise<BillingPaymentResponse>;
getFolder: (id: string) => Promise<FolderResponse>;
postFolder: (request: FolderRequest) => Promise<FolderResponse>;
putFolder: (id: string, request: FolderRequest) => Promise<FolderResponse>;

View File

@ -10,7 +10,7 @@ export abstract class FolderService {
get: (id: string) => Promise<Folder>;
getAll: () => Promise<Folder[]>;
getAllDecrypted: () => Promise<FolderView[]>;
getAllNested: () => Promise<TreeNode<FolderView>[]>;
getAllNested: (folders?: FolderView[]) => Promise<TreeNode<FolderView>[]>;
getNested: (id: string) => Promise<TreeNode<FolderView>>;
saveWithServer: (folder: Folder) => Promise<any>;
upsert: (folder: FolderData | FolderData[]) => Promise<any>;

View File

@ -1,5 +1,6 @@
import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType";
import { OrganizationUserType } from "../../enums/organizationUserType";
import { Permissions } from "../../enums/permissions";
import { ProductType } from "../../enums/productType";
import { PermissionsApi } from "../api/permissionsApi";
import { OrganizationData } from "../data/organizationData";
@ -182,6 +183,28 @@ export class Organization {
return this.canManagePolicies;
}
hasAnyPermission(permissions: Permissions[]) {
const specifiedPermissions =
(permissions.includes(Permissions.AccessEventLogs) && this.canAccessEventLogs) ||
(permissions.includes(Permissions.AccessImportExport) && this.canAccessImportExport) ||
(permissions.includes(Permissions.AccessReports) && this.canAccessReports) ||
(permissions.includes(Permissions.CreateNewCollections) && this.canCreateNewCollections) ||
(permissions.includes(Permissions.EditAnyCollection) && this.canEditAnyCollection) ||
(permissions.includes(Permissions.DeleteAnyCollection) && this.canDeleteAnyCollection) ||
(permissions.includes(Permissions.EditAssignedCollections) &&
this.canEditAssignedCollections) ||
(permissions.includes(Permissions.DeleteAssignedCollections) &&
this.canDeleteAssignedCollections) ||
(permissions.includes(Permissions.ManageGroups) && this.canManageGroups) ||
(permissions.includes(Permissions.ManageOrganization) && this.isOwner) ||
(permissions.includes(Permissions.ManagePolicies) && this.canManagePolicies) ||
(permissions.includes(Permissions.ManageUsers) && this.canManageUsers) ||
(permissions.includes(Permissions.ManageUsersPassword) && this.canManageUsersPassword) ||
(permissions.includes(Permissions.ManageSso) && this.canManageSso);
return specifiedPermissions && (this.enabled || this.isOwner);
}
get canManageBilling() {
return this.isOwner && (this.isProviderUser || !this.hasProvider);
}

View File

@ -0,0 +1,23 @@
import { BaseResponse } from "./baseResponse";
import { BillingInvoiceResponse, BillingTransactionResponse } from "./billingResponse";
export class BillingHistoryResponse extends BaseResponse {
invoices: BillingInvoiceResponse[] = [];
transactions: BillingTransactionResponse[] = [];
constructor(response: any) {
super(response);
const transactions = this.getResponseProperty("Transactions");
const invoices = this.getResponseProperty("Invoices");
if (transactions != null) {
this.transactions = transactions.map((t: any) => new BillingTransactionResponse(t));
}
if (invoices != null) {
this.invoices = invoices.map((i: any) => new BillingInvoiceResponse(i));
}
}
get hasNoHistory() {
return this.invoices.length == 0 && this.transactions.length == 0;
}
}

View File

@ -0,0 +1,14 @@
import { BaseResponse } from "./baseResponse";
import { BillingSourceResponse } from "./billingResponse";
export class BillingPaymentResponse extends BaseResponse {
balance: number;
paymentSource: BillingSourceResponse;
constructor(response: any) {
super(response);
this.balance = this.getResponseProperty("Balance");
const paymentSource = this.getResponseProperty("PaymentSource");
this.paymentSource = paymentSource == null ? null : new BillingSourceResponse(paymentSource);
}
}

View File

@ -1,6 +1,8 @@
import { AppIdService } from "jslib-common/abstractions/appId.service";
import { DeviceRequest } from "jslib-common/models/request/deviceRequest";
import { TokenRequestTwoFactor } from "jslib-common/models/request/identityToken/tokenRequestTwoFactor";
import { BillingHistoryResponse } from "jslib-common/models/response/billingHistoryResponse";
import { BillingPaymentResponse } from "jslib-common/models/response/billingPaymentResponse";
import { ApiService as ApiServiceAbstraction } from "../abstractions/api.service";
import { EnvironmentService } from "../abstractions/environment.service";
@ -280,11 +282,6 @@ export class ApiService implements ApiServiceAbstraction {
return new ProfileResponse(r);
}
async getUserBilling(): Promise<BillingResponse> {
const r = await this.send("GET", "/accounts/billing", null, true, true);
return new BillingResponse(r);
}
async getUserSubscription(): Promise<SubscriptionResponse> {
const r = await this.send("GET", "/accounts/subscription", null, true, true);
return new SubscriptionResponse(r);
@ -467,6 +464,18 @@ export class ApiService implements ApiServiceAbstraction {
return this.send("POST", "/accounts/convert-to-key-connector", null, true, false);
}
// Account Billing APIs
async getUserBillingHistory(): Promise<BillingHistoryResponse> {
const r = await this.send("GET", "/accounts/billing/history", null, true, true);
return new BillingHistoryResponse(r);
}
async getUserBillingPayment(): Promise<BillingPaymentResponse> {
const r = await this.send("GET", "/accounts/billing/payment-method", null, true, true);
return new BillingPaymentResponse(r);
}
// Folder APIs
async getFolder(id: string): Promise<FolderResponse> {

View File

@ -88,8 +88,8 @@ export class FolderService implements FolderServiceAbstraction {
return decFolders;
}
async getAllNested(): Promise<TreeNode<FolderView>[]> {
const folders = await this.getAllDecrypted();
async getAllNested(folders?: FolderView[]): Promise<TreeNode<FolderView>[]> {
folders = folders ?? (await this.getAllDecrypted());
const nodes: TreeNode<FolderView>[] = [];
folders.forEach((f) => {
const folderCopy = new FolderView();

View File

@ -15,10 +15,18 @@ export const parameters = {
docs: { inlineStories: true },
};
// ng-template is used to scope any template reference variables and isolate the previews
const decorator = componentWrapperDecorator(
(story) => `
<div class="theme_light tw-px-5 tw-py-10 tw-border-2 tw-border-solid tw-border-secondary-300 tw-bg-[#ffffff]">${story}</div>
<div class="theme_dark tw-mt-5 tw-px-5 tw-py-10 tw-bg-[#1f242e]">${story}</div>
<ng-template #lightPreview>
<div class="theme_light tw-px-5 tw-py-10 tw-border-2 tw-border-solid tw-border-secondary-300 tw-bg-[#ffffff]">${story}</div>
</ng-template>
<ng-template #darkPreview>
<div class="theme_dark tw-mt-5 tw-px-5 tw-py-10 tw-bg-[#1f242e]">${story}</div>
</ng-template>
<ng-container *ngTemplateOutlet="lightPreview"></ng-container>
<ng-container *ngTemplateOutlet="darkPreview"></ng-container>
`
);

View File

@ -18,6 +18,7 @@
"@angular/platform-browser-dynamic": "^12.2.13",
"@bitwarden/jslib-angular": "file:../angular",
"bootstrap": "4.6.0",
"rxjs": "^7.4.0",
"tslib": "^2.3.0"
},
"devDependencies": {

View File

@ -24,7 +24,8 @@
"@angular/platform-browser-dynamic": "^12.2.13",
"@bitwarden/jslib-angular": "file:../angular",
"bootstrap": "4.6.0",
"tslib": "^2.3.0"
"tslib": "^2.3.0",
"rxjs": "^7.4.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^12.2.13",

View File

@ -2,3 +2,4 @@ export * from "./badge";
export * from "./banner";
export * from "./button";
export * from "./callout";
export * from "./menu";

View File

@ -0,0 +1,5 @@
export * from "./menu.module";
export * from "./menu.component";
export * from "./menu-trigger-for.directive";
export * from "./menu-item.component";
export * from "./menu-divider.component";

View File

@ -0,0 +1,4 @@
<div
class="tw-border-solid tw-border-0 tw-border-t tw-border-t-secondary-500 tw-my-2"
role="separator"
></div>

View File

@ -0,0 +1,7 @@
import { Component } from "@angular/core";
@Component({
selector: "bit-menu-divider",
templateUrl: "./menu-divider.component.html",
})
export class MenuDividerComponent {}

View File

@ -0,0 +1,37 @@
import { FocusableOption } from "@angular/cdk/a11y";
import { Component, ElementRef, HostBinding } from "@angular/core";
@Component({
selector: "[bit-menu-item]",
template: `<ng-content></ng-content>`,
})
export class MenuItemComponent implements FocusableOption {
@HostBinding("class") classList = [
"tw-block",
"tw-py-1",
"tw-px-4",
"!tw-text-main",
"!tw-no-underline",
"tw-cursor-pointer",
"tw-border-none",
"tw-bg-background",
"tw-text-left",
"hover:tw-bg-secondary-100",
"focus:tw-bg-secondary-100",
"focus:tw-z-50",
"focus:tw-outline-none",
"focus:tw-ring",
"focus:tw-ring-offset-2",
"focus:tw-ring-primary-700",
"active:!tw-ring-0",
"active:!tw-ring-offset-0",
].join(" ");
@HostBinding("attr.role") role = "menuitem";
@HostBinding("tabIndex") tabIndex = "-1";
constructor(private elementRef: ElementRef) {}
focus() {
this.elementRef.nativeElement.focus();
}
}

View File

@ -0,0 +1,119 @@
import { Overlay, OverlayConfig, OverlayRef } from "@angular/cdk/overlay";
import { TemplatePortal } from "@angular/cdk/portal";
import {
Directive,
ElementRef,
HostBinding,
HostListener,
Input,
OnDestroy,
ViewContainerRef,
} from "@angular/core";
import { Observable, Subscription } from "rxjs";
import { filter, mergeWith } from "rxjs/operators";
import { MenuComponent } from "./menu.component";
@Directive({
selector: "[bitMenuTriggerFor]",
})
export class MenuTriggerForDirective implements OnDestroy {
@HostBinding("attr.aria-expanded") isOpen = false;
@HostBinding("attr.aria-haspopup") hasPopup = "menu";
@HostBinding("attr.role") role = "button";
@Input("bitMenuTriggerFor") menu: MenuComponent;
private overlayRef: OverlayRef;
private defaultMenuConfig: OverlayConfig = {
panelClass: "bit-menu-panel",
hasBackdrop: true,
backdropClass: "cdk-overlay-transparent-backdrop",
scrollStrategy: this.overlay.scrollStrategies.reposition(),
positionStrategy: this.overlay
.position()
.flexibleConnectedTo(this.elementRef)
.withPositions([
{
originX: "start",
originY: "bottom",
overlayX: "start",
overlayY: "top",
},
{
originX: "end",
originY: "bottom",
overlayX: "end",
overlayY: "top",
},
])
.withLockedPosition(true)
.withFlexibleDimensions(false)
.withPush(false),
};
private closedEventsSub: Subscription;
private keyDownEventsSub: Subscription;
constructor(
private elementRef: ElementRef<HTMLElement>,
private viewContainerRef: ViewContainerRef,
private overlay: Overlay
) {}
@HostListener("click") toggleMenu() {
this.isOpen ? this.destroyMenu() : this.openMenu();
}
ngOnDestroy() {
this.disposeAll();
}
private openMenu() {
if (this.menu == null) {
throw new Error("Cannot find bit-menu element");
}
this.isOpen = true;
this.overlayRef = this.overlay.create(this.defaultMenuConfig);
const templatePortal = new TemplatePortal(this.menu.templateRef, this.viewContainerRef);
this.overlayRef.attach(templatePortal);
this.closedEventsSub = this.getClosedEvents().subscribe((event: KeyboardEvent | undefined) => {
if (event?.key === "Tab") {
// Required to ensure tab order resumes correctly
this.elementRef.nativeElement.focus();
}
this.destroyMenu();
});
this.keyDownEventsSub = this.overlayRef
.keydownEvents()
.subscribe((event: KeyboardEvent) => this.menu.keyManager.onKeydown(event));
}
private destroyMenu() {
if (this.overlayRef == null || !this.isOpen) {
return;
}
this.isOpen = false;
this.disposeAll();
}
private getClosedEvents(): Observable<any> {
const detachments = this.overlayRef.detachments();
const escKey = this.overlayRef
.keydownEvents()
.pipe(filter((event: KeyboardEvent) => event.key === "Escape" || event.key === "Tab"));
const backdrop = this.overlayRef.backdropClick();
const menuClosed = this.menu.closed;
return detachments.pipe(mergeWith(escKey, backdrop, menuClosed));
}
private disposeAll() {
this.closedEventsSub?.unsubscribe();
this.overlayRef?.dispose();
this.keyDownEventsSub?.unsubscribe();
}
}

View File

@ -0,0 +1,9 @@
<ng-template>
<div
(click)="closed.emit()"
class="tw-flex tw-flex-col tw-bg-background tw-border tw-border-solid tw-rounded tw-border-secondary-500 tw-bg-clip-padding tw-py-2 tw-shrink-0"
role="menu"
>
<ng-content></ng-content>
</div>
</ng-template>

View File

@ -0,0 +1,77 @@
import { Component } from "@angular/core";
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
import { By } from "@angular/platform-browser";
import { MenuTriggerForDirective } from "./menu-trigger-for.directive";
import { MenuModule } from "./index";
describe("Menu", () => {
let fixture: ComponentFixture<TestApp>;
const getMenuTriggerDirective = () => {
const buttonDebugElement = fixture.debugElement.query(By.directive(MenuTriggerForDirective));
return buttonDebugElement.injector.get(MenuTriggerForDirective);
};
// The overlay is created outside the root debugElement, so we need to query its parent
const getBitMenuPanel = () => fixture.debugElement.parent.query(By.css(".bit-menu-panel"));
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [MenuModule],
declarations: [TestApp],
});
TestBed.compileComponents();
fixture = TestBed.createComponent(TestApp);
fixture.detectChanges();
})
);
it("should open when the trigger is clicked", () => {
const buttonDebugElement = fixture.debugElement.query(By.directive(MenuTriggerForDirective));
(buttonDebugElement.nativeElement as HTMLButtonElement).click();
expect(getBitMenuPanel()).toBeTruthy();
});
it("should close when the trigger is clicked", () => {
getMenuTriggerDirective().toggleMenu();
const buttonDebugElement = fixture.debugElement.query(By.directive(MenuTriggerForDirective));
(buttonDebugElement.nativeElement as HTMLButtonElement).click();
expect(getBitMenuPanel()).toBeFalsy();
});
it("should close when a menu item is clicked", () => {
getMenuTriggerDirective().toggleMenu();
fixture.debugElement.parent.query(By.css("#item1")).nativeElement.click();
expect(getBitMenuPanel()).toBeFalsy();
});
it("should close when the backdrop is clicked", () => {
getMenuTriggerDirective().toggleMenu();
fixture.debugElement.parent.query(By.css(".cdk-overlay-backdrop")).nativeElement.click();
expect(getBitMenuPanel()).toBeFalsy();
});
});
@Component({
selector: "test-app",
template: `
<button type="button" [bitMenuTriggerFor]="testMenu" class="testclass">Open menu</button>
<bit-menu #testMenu>
<a id="item1" bit-menu-item>Item 1</a>
<a id="item2" bit-menu-item>Item 2</a>
</bit-menu>
`,
})
class TestApp {}

View File

@ -0,0 +1,30 @@
import { FocusKeyManager } from "@angular/cdk/a11y";
import {
Component,
Output,
TemplateRef,
ViewChild,
EventEmitter,
ContentChildren,
QueryList,
AfterContentInit,
} from "@angular/core";
import { MenuItemComponent } from "./menu-item.component";
@Component({
selector: "bit-menu",
templateUrl: "./menu.component.html",
exportAs: "menuComponent",
})
export class MenuComponent implements AfterContentInit {
@ViewChild(TemplateRef) templateRef: TemplateRef<any>;
@Output() closed = new EventEmitter<void>();
@ContentChildren(MenuItemComponent, { descendants: true })
menuItems: QueryList<MenuItemComponent>;
keyManager: FocusKeyManager<MenuItemComponent>;
ngAfterContentInit() {
this.keyManager = new FocusKeyManager(this.menuItems).withWrap();
}
}

View File

@ -0,0 +1,15 @@
import { OverlayModule } from "@angular/cdk/overlay";
import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { MenuDividerComponent } from "./menu-divider.component";
import { MenuItemComponent } from "./menu-item.component";
import { MenuTriggerForDirective } from "./menu-trigger-for.directive";
import { MenuComponent } from "./menu.component";
@NgModule({
imports: [CommonModule, OverlayModule],
declarations: [MenuComponent, MenuTriggerForDirective, MenuItemComponent, MenuDividerComponent],
exports: [MenuComponent, MenuTriggerForDirective, MenuItemComponent, MenuDividerComponent],
})
export class MenuModule {}

View File

@ -0,0 +1,69 @@
import { OverlayModule } from "@angular/cdk/overlay";
import { Meta, moduleMetadata, Story } from "@storybook/angular";
import { ButtonModule } from "../button/button.module";
import { MenuDividerComponent } from "./menu-divider.component";
import { MenuItemComponent } from "./menu-item.component";
import { MenuTriggerForDirective } from "./menu-trigger-for.directive";
import { MenuComponent } from "./menu.component";
export default {
title: "Jslib/Menu",
component: MenuTriggerForDirective,
decorators: [
moduleMetadata({
declarations: [
MenuTriggerForDirective,
MenuComponent,
MenuItemComponent,
MenuDividerComponent,
],
imports: [OverlayModule, ButtonModule],
}),
],
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A17952",
},
},
} as Meta;
const Template: Story<MenuTriggerForDirective> = (args: MenuTriggerForDirective) => ({
props: args,
template: `
<bit-menu #myMenu="menuComponent">
<a href="#" bit-menu-item>Anchor link</a>
<a href="#" bit-menu-item>Another link</a>
<button type="button" bit-menu-item>Button</button>
<bit-menu-divider></bit-menu-divider>
<button type="button" bit-menu-item>Button after divider</button>
</bit-menu>
<div class="tw-h-40">
<div class="cdk-overlay-pane bit-menu-panel">
<ng-container *ngTemplateOutlet="myMenu.templateRef"></ng-container>
</div>
</div>
`,
});
const TemplateWithButton: Story<MenuTriggerForDirective> = (args: MenuTriggerForDirective) => ({
props: args,
template: `
<div class="tw-h-40">
<button bit-button [buttonType]="secondary" [bitMenuTriggerFor]="myMenu">Open menu</button>
</div>
<bit-menu #myMenu>
<a href="#" bit-menu-item>Anchor link</a>
<a href="#" bit-menu-item>Another link</a>
<button type="button" bit-menu-item>Button</button>
<bit-menu-divider></bit-menu-divider>
<button type="button" bit-menu-item>Button after divider</button>
</bit-menu>`,
});
export const OpenMenu = Template.bind({});
export const ClosedMenu = TemplateWithButton.bind({});

View File

@ -3,6 +3,8 @@
@import "../../angular/src/scss/bwicons/styles/style.scss";
@import "../../angular/src/scss/icons.scss";
@import "@angular/cdk/overlay-prebuilt.css";
@import "~bootstrap/scss/_functions";
@import "~bootstrap/scss/_variables";
@import "~bootstrap/scss/_mixins";