mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[PM-22313] Refactor organization vault component (#16017)
* refactor organization, userId, and filter * refactor collections * refactor allGroups to observable * Refactor ciphers WIP * fix filter$ * refactor collections$, refresh$, isEmpty$, proccesingEvents$ * resolve remaining ts-strict errors * refactor *ngIf to @if syntax * rename function * clean up * fix issues from merge conflict * better error handling, clean up * wip add feature flag * refactor org vault: improve null safety & loading * add take(2) to firstLoadComplete observable * add real feature flag * cleanup * fix icon * Add comments * refactor org vault with null checks, update util function * fix type --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<app-organization-free-trial-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
[organization]="organization"
|
||||
(clicked)="navigateToPaymentMethod()"
|
||||
>
|
||||
</app-organization-free-trial-warning>
|
||||
<app-organization-reseller-renewal-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
[organization]="organization"
|
||||
>
|
||||
</app-organization-reseller-renewal-warning>
|
||||
<ng-container *ngIf="freeTrialWhenWarningsServiceDisabled$ | async as freeTrial">
|
||||
<bit-banner
|
||||
id="free-trial-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="premium"
|
||||
[showClose]="false"
|
||||
*ngIf="!refreshing && freeTrial.shownBanner"
|
||||
>
|
||||
{{ freeTrial.message }}
|
||||
<a
|
||||
bitLink
|
||||
linkType="secondary"
|
||||
(click)="navigateToPaymentMethod()"
|
||||
class="tw-cursor-pointer"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{{ "clickHereToAddPaymentMethod" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="resellerWarningWhenWarningsServiceDisabled$ | async as resellerWarning">
|
||||
<bit-banner
|
||||
id="reseller-warning-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="info"
|
||||
[showClose]="false"
|
||||
*ngIf="!refreshing"
|
||||
>
|
||||
{{ resellerWarning?.message }}
|
||||
</bit-banner>
|
||||
</ng-container>
|
||||
|
||||
<app-org-vault-header
|
||||
[filter]="filter"
|
||||
[loading]="refreshing"
|
||||
[organization]="organization"
|
||||
[collection]="selectedCollection"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(onAddCipher)="addCipher($event)"
|
||||
(onAddCollection)="addCollection()"
|
||||
(onEditCollection)="editCollection(selectedCollection.node, $event.tab, $event.readonly)"
|
||||
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-org-vault-header>
|
||||
|
||||
<div class="tw-flex tw-flex-row">
|
||||
<div class="tw-w-1/4 tw-mr-5" *ngIf="!hideVaultFilters">
|
||||
<app-organization-vault-filter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
<div [class]="hideVaultFilters ? 'tw-w-full' : 'tw-w-3/4'">
|
||||
<bit-toggle-group
|
||||
*ngIf="showAddAccessToggle && activeFilter.selectedCollectionNode"
|
||||
[selected]="addAccessStatus$ | async"
|
||||
(selectedChange)="addAccessToggle($event)"
|
||||
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||
>
|
||||
<bit-toggle [value]="0">
|
||||
{{ "all" | i18n }}
|
||||
</bit-toggle>
|
||||
|
||||
<bit-toggle [value]="1">
|
||||
{{ "addAccess" | i18n }}
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
<bit-callout type="warning" *ngIf="activeFilter.isDeleted">
|
||||
{{ trashCleanupWarning }}
|
||||
</bit-callout>
|
||||
<app-vault-items
|
||||
#vaultItems
|
||||
[ciphers]="ciphers"
|
||||
[collections]="collections"
|
||||
[allCollections]="allCollections"
|
||||
[allOrganizations]="organization ? [organization] : []"
|
||||
[allGroups]="allGroups"
|
||||
[disabled]="loading"
|
||||
[showOwner]="false"
|
||||
[showPermissionsColumn]="true"
|
||||
[showCollections]="filter.type !== undefined"
|
||||
[showGroups]="
|
||||
organization?.useGroups &&
|
||||
((filter.type === undefined && filter.collectionId === undefined) ||
|
||||
filter.collectionId !== undefined)
|
||||
"
|
||||
[showPremiumFeatures]="organization?.useTotp"
|
||||
[showBulkMove]="false"
|
||||
[showBulkTrashOptions]="filter.type === 'trash'"
|
||||
[useEvents]="organization?.canAccessEventLogs"
|
||||
[showAdminActions]="true"
|
||||
(onEvent)="onVaultItemsEvent($event)"
|
||||
[showBulkEditCollectionAccess]="true"
|
||||
[showBulkAddToCollections]="true"
|
||||
[viewingOrgVault]="true"
|
||||
[addAccessStatus]="addAccessStatus$ | async"
|
||||
[addAccessToggle]="showAddAccessToggle"
|
||||
[activeCollection]="selectedCollection?.node"
|
||||
>
|
||||
</app-vault-items>
|
||||
<ng-container *ngIf="!performingInitialLoad && isEmpty">
|
||||
<bit-no-items *ngIf="!showCollectionAccessRestricted">
|
||||
<span slot="title" class="tw-mt-4 tw-block">{{ "noItemsInList" | i18n }}</span>
|
||||
<button
|
||||
slot="button"
|
||||
bitButton
|
||||
(click)="addCipher()"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
*ngIf="
|
||||
filter.type !== 'trash' &&
|
||||
filter.collectionId !== Unassigned &&
|
||||
selectedCollection?.node?.canEditItems(organization)
|
||||
"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus"></i> {{ "newItem" | i18n }}
|
||||
</button>
|
||||
</bit-no-items>
|
||||
<collection-access-restricted
|
||||
*ngIf="showCollectionAccessRestricted"
|
||||
[canEditCollection]="selectedCollection?.node?.canEdit(organization)"
|
||||
[canViewCollectionInfo]="selectedCollection?.node?.canViewCollectionInfo(organization)"
|
||||
(viewCollectionClicked)="
|
||||
editCollection(selectedCollection.node, $event.tab, $event.readonly)
|
||||
"
|
||||
>
|
||||
</collection-access-restricted>
|
||||
</ng-container>
|
||||
<div
|
||||
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
|
||||
*ngIf="performingInitialLoad"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,28 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { featureFlaggedRoute } from "@bitwarden/angular/platform/utils/feature-flagged-route";
|
||||
import { canAccessVaultTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
|
||||
import { VaultComponent } from "./vault.component";
|
||||
import { VaultComponent } from "./deprecated_vault.component";
|
||||
import { vNextVaultComponent } from "./vault.component";
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: VaultComponent,
|
||||
canActivate: [organizationPermissionsGuard(canAccessVaultTab)],
|
||||
...featureFlaggedRoute({
|
||||
defaultComponent: VaultComponent,
|
||||
flaggedComponent: vNextVaultComponent,
|
||||
featureFlag: FeatureFlag.CollectionVaultRefactor,
|
||||
routeOptions: {
|
||||
data: { titleId: "vaults" },
|
||||
path: "",
|
||||
canActivate: [organizationPermissionsGuard(canAccessVaultTab)],
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
|
||||
@@ -1,22 +1,31 @@
|
||||
<app-organization-free-trial-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
@let organization = organization$ | async;
|
||||
@let selectedCollection = selectedCollection$ | async;
|
||||
@let filter = filter$ | async;
|
||||
@let refreshing = refreshingSubject$ | async;
|
||||
@let loading = loading$ | async;
|
||||
|
||||
@if (organization) {
|
||||
@if (useOrganizationWarningsService$ | async) {
|
||||
<app-organization-free-trial-warning
|
||||
[organization]="organization"
|
||||
(clicked)="navigateToPaymentMethod()"
|
||||
>
|
||||
</app-organization-free-trial-warning>
|
||||
<app-organization-reseller-renewal-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
[organization]="organization"
|
||||
>
|
||||
</app-organization-reseller-renewal-warning>
|
||||
<ng-container *ngIf="freeTrialWhenWarningsServiceDisabled$ | async as freeTrial">
|
||||
>
|
||||
</app-organization-free-trial-warning>
|
||||
}
|
||||
|
||||
@if (useOrganizationWarningsService$ | async) {
|
||||
<app-organization-reseller-renewal-warning [organization]="organization">
|
||||
</app-organization-reseller-renewal-warning>
|
||||
}
|
||||
|
||||
@let freeTrial = freeTrialWhenWarningsServiceDisabled$ | async;
|
||||
@if (!refreshing && freeTrial?.shownBanner) {
|
||||
<bit-banner
|
||||
id="free-trial-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="premium"
|
||||
[showClose]="false"
|
||||
*ngIf="!refreshing && freeTrial.shownBanner"
|
||||
>
|
||||
{{ freeTrial.message }}
|
||||
<a
|
||||
@@ -29,21 +38,23 @@
|
||||
{{ "clickHereToAddPaymentMethod" | i18n }}
|
||||
</a>
|
||||
</bit-banner>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="resellerWarningWhenWarningsServiceDisabled$ | async as resellerWarning">
|
||||
}
|
||||
|
||||
@let resellerWarning = resellerWarningWhenWarningsServiceDisabled$ | async;
|
||||
@if (!refreshing && resellerWarning) {
|
||||
<bit-banner
|
||||
id="reseller-warning-banner"
|
||||
class="-tw-m-6 tw-flex tw-flex-col tw-pb-6"
|
||||
icon="bwi-billing"
|
||||
bannerType="info"
|
||||
[showClose]="false"
|
||||
*ngIf="!refreshing"
|
||||
>
|
||||
{{ resellerWarning?.message }}
|
||||
</bit-banner>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<app-org-vault-header
|
||||
@if (filter) {
|
||||
<app-org-vault-header
|
||||
[filter]="filter"
|
||||
[loading]="refreshing"
|
||||
[organization]="organization"
|
||||
@@ -51,13 +62,16 @@
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(onAddCipher)="addCipher($event)"
|
||||
(onAddCollection)="addCollection()"
|
||||
(onEditCollection)="editCollection(selectedCollection.node, $event.tab, $event.readonly)"
|
||||
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
|
||||
(onEditCollection)="editCollection(selectedCollection?.node, $event.tab, $event.readonly)"
|
||||
(onDeleteCollection)="deleteCollection(selectedCollection?.node)"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-org-vault-header>
|
||||
></app-org-vault-header>
|
||||
}
|
||||
|
||||
<div class="tw-flex tw-flex-row">
|
||||
<div class="tw-w-1/4 tw-mr-5" *ngIf="!hideVaultFilters">
|
||||
<div class="tw-flex tw-flex-row">
|
||||
@let hideVaultFilters = hideVaultFilter$ | async;
|
||||
@if (!hideVaultFilters) {
|
||||
<div class="tw-w-1/4 tw-mr-5">
|
||||
<app-organization-vault-filter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
@@ -65,9 +79,11 @@
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div [class]="hideVaultFilters ? 'tw-w-full' : 'tw-w-3/4'">
|
||||
@if (showAddAccessToggle && activeFilter.selectedCollectionNode) {
|
||||
<bit-toggle-group
|
||||
*ngIf="showAddAccessToggle && activeFilter.selectedCollectionNode"
|
||||
[selected]="addAccessStatus$ | async"
|
||||
(selectedChange)="addAccessToggle($event)"
|
||||
[attr.aria-label]="'addAccessFilter' | i18n"
|
||||
@@ -80,16 +96,22 @@
|
||||
{{ "addAccess" | i18n }}
|
||||
</bit-toggle>
|
||||
</bit-toggle-group>
|
||||
<bit-callout type="warning" *ngIf="activeFilter.isDeleted">
|
||||
}
|
||||
|
||||
@if (activeFilter.isDeleted) {
|
||||
<bit-callout type="warning">
|
||||
{{ trashCleanupWarning }}
|
||||
</bit-callout>
|
||||
}
|
||||
|
||||
@if (filter) {
|
||||
<app-vault-items
|
||||
#vaultItems
|
||||
[ciphers]="ciphers"
|
||||
[collections]="collections"
|
||||
[allCollections]="allCollections"
|
||||
[ciphers]="ciphers$ | async"
|
||||
[collections]="collections$ | async"
|
||||
[allCollections]="allCollections$ | async"
|
||||
[allOrganizations]="organization ? [organization] : []"
|
||||
[allGroups]="allGroups"
|
||||
[allGroups]="allGroups$ | async"
|
||||
[disabled]="loading"
|
||||
[showOwner]="false"
|
||||
[showPermissionsColumn]="true"
|
||||
@@ -113,38 +135,44 @@
|
||||
[activeCollection]="selectedCollection?.node"
|
||||
>
|
||||
</app-vault-items>
|
||||
<ng-container *ngIf="!performingInitialLoad && isEmpty">
|
||||
<bit-no-items *ngIf="!showCollectionAccessRestricted">
|
||||
}
|
||||
|
||||
@let showCollectionAccessRestricted = showCollectionAccessRestricted$ | async;
|
||||
@if (!refreshing && (isEmpty$ | async)) {
|
||||
@if (!showCollectionAccessRestricted) {
|
||||
<bit-no-items>
|
||||
<span slot="title" class="tw-mt-4 tw-block">{{ "noItemsInList" | i18n }}</span>
|
||||
|
||||
@if (
|
||||
filter &&
|
||||
filter.type !== "trash" &&
|
||||
filter.collectionId !== Unassigned &&
|
||||
selectedCollection?.node?.canEditItems(organization)
|
||||
) {
|
||||
<button
|
||||
slot="button"
|
||||
bitButton
|
||||
(click)="addCipher()"
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
*ngIf="
|
||||
filter.type !== 'trash' &&
|
||||
filter.collectionId !== Unassigned &&
|
||||
selectedCollection?.node?.canEditItems(organization)
|
||||
"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-plus"></i> {{ "newItem" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</bit-no-items>
|
||||
} @else {
|
||||
<collection-access-restricted
|
||||
*ngIf="showCollectionAccessRestricted"
|
||||
[canEditCollection]="selectedCollection?.node?.canEdit(organization)"
|
||||
[canViewCollectionInfo]="selectedCollection?.node?.canViewCollectionInfo(organization)"
|
||||
(viewCollectionClicked)="
|
||||
editCollection(selectedCollection.node, $event.tab, $event.readonly)
|
||||
editCollection(selectedCollection?.node, $event.tab, $event.readonly)
|
||||
"
|
||||
>
|
||||
</collection-access-restricted>
|
||||
</ng-container>
|
||||
<div
|
||||
class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start"
|
||||
*ngIf="performingInitialLoad"
|
||||
>
|
||||
}
|
||||
}
|
||||
@if (refreshing) {
|
||||
<div class="tw-mt-6 tw-flex tw-h-full tw-flex-col tw-items-center tw-justify-start">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
@@ -152,5 +180,7 @@
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,10 @@ import { ViewComponent } from "../../../vault/individual-vault/view.component";
|
||||
import { CollectionDialogComponent } from "../shared/components/collection-dialog";
|
||||
|
||||
import { CollectionNameBadgeComponent } from "./collection-badge";
|
||||
import { VaultComponent } from "./deprecated_vault.component";
|
||||
import { GroupBadgeModule } from "./group-badge/group-badge.module";
|
||||
import { VaultRoutingModule } from "./vault-routing.module";
|
||||
import { VaultComponent } from "./vault.component";
|
||||
import { vNextVaultComponent } from "./vault.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -19,6 +20,7 @@ import { VaultComponent } from "./vault.component";
|
||||
OrganizationBadgeModule,
|
||||
CollectionDialogComponent,
|
||||
VaultComponent,
|
||||
vNextVaultComponent,
|
||||
ViewComponent,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -51,6 +51,9 @@ export function canAccessOrgAdmin(org: Organization): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Please use the general `getById` custom rxjs operator instead.
|
||||
*/
|
||||
export function getOrganizationById(id: string) {
|
||||
return map<Organization[], Organization | undefined>((orgs) => orgs.find((o) => o.id === id));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ServerConfig } from "../platform/abstractions/config/server-config";
|
||||
export enum FeatureFlag {
|
||||
/* Admin Console Team */
|
||||
CreateDefaultLocation = "pm-19467-create-default-location",
|
||||
CollectionVaultRefactor = "pm-25030-resolve-ts-upgrade-errors",
|
||||
|
||||
/* Auth */
|
||||
PM14938_BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals",
|
||||
@@ -71,6 +72,7 @@ const FALSE = false as boolean;
|
||||
export const DefaultFeatureFlagValue = {
|
||||
/* Admin Console Team */
|
||||
[FeatureFlag.CreateDefaultLocation]: FALSE,
|
||||
[FeatureFlag.CollectionVaultRefactor]: FALSE,
|
||||
|
||||
/* Autofill */
|
||||
[FeatureFlag.NotificationRefresh]: FALSE,
|
||||
|
||||
@@ -80,6 +80,18 @@ export class CipherViewLikeUtils {
|
||||
return cipher.isDeleted;
|
||||
};
|
||||
|
||||
/** @returns `true` when the cipher is not assigned to a collection, `false` otherwise. */
|
||||
static isUnassigned = (cipher: CipherViewLike): boolean => {
|
||||
if (this.isCipherListView(cipher)) {
|
||||
return (
|
||||
cipher.organizationId != null &&
|
||||
(cipher.collectionIds == null || cipher.collectionIds.length === 0)
|
||||
);
|
||||
}
|
||||
|
||||
return cipher.isUnassigned;
|
||||
};
|
||||
|
||||
/** @returns `true` when the user can assign the cipher to a collection, `false` otherwise. */
|
||||
static canAssignToCollections = (cipher: CipherViewLike): boolean => {
|
||||
if (this.isCipherListView(cipher)) {
|
||||
|
||||
Reference in New Issue
Block a user