mirror of
https://github.com/bitwarden/browser
synced 2025-12-30 15:13:32 +00:00
[PM-13326] Move Collections tab to AC Team (#13529)
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [loading]="loading" dialogSize="large">
|
||||
<span bitDialogTitle>
|
||||
{{ "assignCollectionAccess" | i18n }}
|
||||
<span class="tw-text-sm tw-normal-case tw-text-muted">
|
||||
{{ numCollections }} {{ (numCollections == 1 ? "collection" : "collections") | i18n }}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div bitDialogContent>
|
||||
<bit-access-selector
|
||||
*ngIf="organization?.useGroups"
|
||||
[permissionMode]="PermissionMode.Edit"
|
||||
formControlName="access"
|
||||
[items]="accessItems"
|
||||
[columnHeader]="'groupSlashMemberColumnHeader' | i18n"
|
||||
[selectorLabelText]="'selectGroupsAndMembers' | i18n"
|
||||
[selectorHelpText]="'userPermissionOverrideHelperDesc' | i18n"
|
||||
[emptySelectionText]="'noMembersOrGroupsAdded' | i18n"
|
||||
></bit-access-selector>
|
||||
<bit-access-selector
|
||||
*ngIf="!organization?.useGroups"
|
||||
[permissionMode]="PermissionMode.Edit"
|
||||
formControlName="access"
|
||||
[items]="accessItems"
|
||||
[columnHeader]="'memberColumnHeader' | i18n"
|
||||
[selectorLabelText]="'selectMembers' | i18n"
|
||||
[emptySelectionText]="'noMembersAdded' | i18n"
|
||||
></bit-access-selector>
|
||||
</div>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton bitFormButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,149 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnDestroy } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
OrganizationUserApiService,
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { GroupApiService, GroupView } from "../../core";
|
||||
import {
|
||||
AccessItemType,
|
||||
AccessItemValue,
|
||||
AccessItemView,
|
||||
AccessSelectorModule,
|
||||
convertToSelectionView,
|
||||
mapGroupToAccessItemView,
|
||||
mapUserToAccessItemView,
|
||||
PermissionMode,
|
||||
} from "../../shared/components/access-selector";
|
||||
|
||||
export interface BulkCollectionsDialogParams {
|
||||
organizationId: string;
|
||||
collections: CollectionView[];
|
||||
}
|
||||
|
||||
export enum BulkCollectionsDialogResult {
|
||||
Saved = "saved",
|
||||
Canceled = "canceled",
|
||||
}
|
||||
|
||||
@Component({
|
||||
imports: [SharedModule, AccessSelectorModule],
|
||||
selector: "app-bulk-collections-dialog",
|
||||
templateUrl: "bulk-collections-dialog.component.html",
|
||||
standalone: true,
|
||||
})
|
||||
export class BulkCollectionsDialogComponent implements OnDestroy {
|
||||
protected readonly PermissionMode = PermissionMode;
|
||||
|
||||
protected formGroup = this.formBuilder.group({
|
||||
access: [[] as AccessItemValue[]],
|
||||
});
|
||||
protected loading = true;
|
||||
protected organization: Organization;
|
||||
protected accessItems: AccessItemView[] = [];
|
||||
protected numCollections: number;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) private params: BulkCollectionsDialogParams,
|
||||
private dialogRef: DialogRef<BulkCollectionsDialogResult>,
|
||||
private formBuilder: FormBuilder,
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private groupService: GroupApiService,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
private toastService: ToastService,
|
||||
) {
|
||||
this.numCollections = this.params.collections.length;
|
||||
const organization$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap((account) =>
|
||||
this.organizationService
|
||||
.organizations$(account?.id)
|
||||
.pipe(getOrganizationById(this.params.organizationId)),
|
||||
),
|
||||
);
|
||||
const groups$ = organization$.pipe(
|
||||
switchMap((organization) => {
|
||||
if (!organization.useGroups) {
|
||||
return of([] as GroupView[]);
|
||||
}
|
||||
return this.groupService.getAll(organization.id);
|
||||
}),
|
||||
);
|
||||
|
||||
combineLatest([
|
||||
organization$,
|
||||
groups$,
|
||||
this.organizationUserApiService.getAllMiniUserDetails(this.params.organizationId),
|
||||
])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(([organization, groups, users]) => {
|
||||
this.organization = organization;
|
||||
|
||||
this.accessItems = [].concat(
|
||||
groups.map(mapGroupToAccessItemView),
|
||||
users.data.map(mapUserToAccessItemView),
|
||||
);
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
const users = this.formGroup.controls.access.value
|
||||
.filter((v) => v.type === AccessItemType.Member)
|
||||
.map(convertToSelectionView);
|
||||
|
||||
const groups = this.formGroup.controls.access.value
|
||||
.filter((v) => v.type === AccessItemType.Group)
|
||||
.map(convertToSelectionView);
|
||||
|
||||
await this.collectionAdminService.bulkAssignAccess(
|
||||
this.organization.id,
|
||||
this.params.collections.map((c) => c.id),
|
||||
users,
|
||||
groups,
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("editedCollections"),
|
||||
});
|
||||
|
||||
this.dialogRef.close(BulkCollectionsDialogResult.Saved);
|
||||
};
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<BulkCollectionsDialogParams>) {
|
||||
return dialogService.open<BulkCollectionsDialogResult, BulkCollectionsDialogParams>(
|
||||
BulkCollectionsDialogComponent,
|
||||
config,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./bulk-collections-dialog.component";
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { CollectionDialogTabType } from "../../../vault/components/collection-dialog";
|
||||
|
||||
const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120" viewBox="10 -10 120 140" fill="none">
|
||||
<rect class="tw-stroke-secondary-600" width="134" height="86" x="3" y="31.485" stroke-width="6" rx="11"/>
|
||||
<path class="tw-fill-secondary-600" d="M123.987 20.15H14.779a3.114 3.114 0 0 1-2.083-.95 3.036 3.036 0 0 1 0-4.208 3.125 3.125 0 0 1 2.083-.951h109.208c.792.043 1.536.38 2.083.95a3.035 3.035 0 0 1 0 4.208 3.115 3.115 0 0 1-2.083.95Zm-6.649-14.041h-95.91a3.114 3.114 0 0 1-2.082-.95 3.036 3.036 0 0 1-.848-2.105c0-.782.306-1.538.848-2.104A3.125 3.125 0 0 1 21.43 0h95.909c.791.043 1.535.38 2.082.95.547.57.849 1.322.849 2.104a3.05 3.05 0 0 1-.849 2.104 3.115 3.115 0 0 1-2.082.95ZM95.132 74.407A42.317 42.317 0 0 0 83.59 65.43l8.799-8.657a1.59 1.59 0 0 0 .004-2.27 1.641 1.641 0 0 0-2.298-.004l-9.64 9.479a28.017 28.017 0 0 0-10.483-2.13c-14.323 0-24.814 12.342-25.298 12.89a2.431 2.431 0 0 0-.675 1.64c-.01.612.215 1.203.626 1.66a43.981 43.981 0 0 0 11.873 9.485l-8.806 8.658a1.601 1.601 0 0 0-.499 1.138 1.602 1.602 0 0 0 1.008 1.5 1.651 1.651 0 0 0 1.255-.009c.199-.085.379-.205.528-.359l9.634-9.443a27.16 27.16 0 0 0 10.359 2.158c14.323 0 24.753-12.086 25.23-12.63a2.983 2.983 0 0 0-.078-4.128h.002ZM49.204 77.82a1.82 1.82 0 0 1-.43-.6 1.767 1.767 0 0 1-.152-.72 1.778 1.778 0 0 1 .582-1.32c3.857-3.564 11.782-9.686 20.77-9.676 2.564.037 5.105.508 7.508 1.395l-3.291 3.235a7.793 7.793 0 0 0-5.02-1.226 7.746 7.746 0 0 0-4.676 2.18 7.528 7.528 0 0 0-1 9.563l-4.199 4.143a43.135 43.135 0 0 1-10.092-6.974Zm26.059-1.318a5.19 5.19 0 0 1-1.557 3.68 5.326 5.326 0 0 1-3.733 1.521c-.82-.005-1.63-.2-2.359-.57l7.067-6.952c.377.718.575 1.513.582 2.321Zm-10.58 0a5.136 5.136 0 0 1 .673-2.555 5.204 5.204 0 0 1 1.862-1.897 5.302 5.302 0 0 1 5.172-.146l-7.096 6.977a5.06 5.06 0 0 1-.61-2.379Zm26.053 1.331c-3.857 3.56-11.779 9.677-20.763 9.677a22.723 22.723 0 0 1-7.454-1.369l3.292-3.226a7.793 7.793 0 0 0 4.995 1.192 7.734 7.734 0 0 0 4.642-2.176 7.524 7.524 0 0 0 1.033-9.506l4.224-4.168a43.258 43.258 0 0 1 10.02 6.945 1.788 1.788 0 0 1 .585 1.313 1.788 1.788 0 0 1-.577 1.318h.003Z"/>
|
||||
</svg>`;
|
||||
|
||||
@Component({
|
||||
selector: "collection-access-restricted",
|
||||
standalone: true,
|
||||
imports: [SharedModule, ButtonModule, NoItemsModule],
|
||||
template: `<bit-no-items [icon]="icon" class="tw-mt-2 tw-block">
|
||||
<span slot="title" class="tw-mt-4 tw-block">{{ "youDoNotHavePermissions" | i18n }}</span>
|
||||
<button
|
||||
*ngIf="canEditCollection"
|
||||
slot="button"
|
||||
bitButton
|
||||
(click)="viewCollectionClicked.emit({ readonly: false, tab: collectionDialogTabType.Info })"
|
||||
buttonType="secondary"
|
||||
type="button"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ "editCollection" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
*ngIf="!canEditCollection && canViewCollectionInfo"
|
||||
slot="button"
|
||||
bitButton
|
||||
(click)="viewCollectionClicked.emit({ readonly: true, tab: collectionDialogTabType.Access })"
|
||||
buttonType="secondary"
|
||||
type="button"
|
||||
>
|
||||
<i aria-hidden="true" class="bwi bwi-users"></i> {{ "viewAccess" | i18n }}
|
||||
</button>
|
||||
</bit-no-items>`,
|
||||
})
|
||||
export class CollectionAccessRestrictedComponent {
|
||||
protected icon = icon;
|
||||
protected collectionDialogTabType = CollectionDialogTabType;
|
||||
|
||||
@Input() canEditCollection = false;
|
||||
@Input() canViewCollectionInfo = false;
|
||||
|
||||
@Output() viewCollectionClicked = new EventEmitter<{
|
||||
readonly: boolean;
|
||||
tab: CollectionDialogTabType;
|
||||
}>();
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../../shared/shared.module";
|
||||
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { CollectionNameBadgeComponent } from "./collection-name.badge.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, PipesModule],
|
||||
declarations: [CollectionNameBadgeComponent],
|
||||
exports: [CollectionNameBadgeComponent],
|
||||
})
|
||||
export class CollectionBadgeModule {}
|
||||
@@ -0,0 +1,6 @@
|
||||
<ng-container *ngFor="let c of shownCollections">
|
||||
<span bitBadge variant="secondary">{{ c | collectionNameFromId: collections }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showXMore">
|
||||
<span bitBadge variant="secondary">+ {{ xMoreCount }} more</span>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,26 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input } from "@angular/core";
|
||||
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
|
||||
@Component({
|
||||
selector: "app-collection-badge",
|
||||
templateUrl: "collection-name-badge.component.html",
|
||||
})
|
||||
export class CollectionNameBadgeComponent {
|
||||
@Input() collectionIds: string[];
|
||||
@Input() collections: CollectionView[];
|
||||
|
||||
get shownCollections(): string[] {
|
||||
return this.showXMore ? this.collectionIds.slice(0, 2) : this.collectionIds;
|
||||
}
|
||||
|
||||
get showXMore(): boolean {
|
||||
return this.collectionIds.length > 3;
|
||||
}
|
||||
|
||||
get xMoreCount(): number {
|
||||
return this.collectionIds.length - 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SharedModule } from "../../../../shared/shared.module";
|
||||
import { PipesModule } from "../../../../vault/individual-vault/pipes/pipes.module";
|
||||
|
||||
import { GroupNameBadgeComponent } from "./group-name-badge.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [SharedModule, PipesModule],
|
||||
declarations: [GroupNameBadgeComponent],
|
||||
exports: [GroupNameBadgeComponent],
|
||||
})
|
||||
export class GroupBadgeModule {}
|
||||
@@ -0,0 +1 @@
|
||||
<bit-badge-list [items]="groupNames" [maxItems]="3" variant="secondary"></bit-badge-list>
|
||||
@@ -0,0 +1,29 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnChanges } from "@angular/core";
|
||||
|
||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
||||
import { GroupView } from "../../core";
|
||||
|
||||
@Component({
|
||||
selector: "app-group-badge",
|
||||
templateUrl: "group-name-badge.component.html",
|
||||
})
|
||||
export class GroupNameBadgeComponent implements OnChanges {
|
||||
@Input() selectedGroups: SelectionReadOnlyRequest[];
|
||||
@Input() allGroups: GroupView[];
|
||||
|
||||
protected groupNames: string[] = [];
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
|
||||
ngOnChanges() {
|
||||
this.groupNames = this.selectedGroups
|
||||
.map((g) => {
|
||||
return this.allGroups.find((o) => o.id === g.id)?.name;
|
||||
})
|
||||
.sort(this.i18nService.collator.compare);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from "@angular/core";
|
||||
import { firstValueFrom, Subject } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
|
||||
import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import {
|
||||
VaultFilterList,
|
||||
VaultFilterSection,
|
||||
VaultFilterType,
|
||||
} from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter-section.type";
|
||||
import { CollectionFilter } from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter.type";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-vault-filter",
|
||||
templateUrl:
|
||||
"../../../../vault/individual-vault/vault-filter/components/vault-filter.component.html",
|
||||
})
|
||||
export class VaultFilterComponent
|
||||
extends BaseVaultFilterComponent
|
||||
implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
@Input() set organization(value: Organization) {
|
||||
if (value && value !== this._organization) {
|
||||
this._organization = value;
|
||||
this.vaultFilterService.setOrganizationFilter(this._organization);
|
||||
}
|
||||
}
|
||||
_organization: Organization;
|
||||
protected destroy$: Subject<void>;
|
||||
|
||||
constructor(
|
||||
protected vaultFilterService: VaultFilterService,
|
||||
protected policyService: PolicyService,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected toastService: ToastService,
|
||||
protected billingApiService: BillingApiServiceAbstraction,
|
||||
protected dialogService: DialogService,
|
||||
protected configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
vaultFilterService,
|
||||
policyService,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
toastService,
|
||||
billingApiService,
|
||||
dialogService,
|
||||
configService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.filters = await this.buildAllFilters();
|
||||
if (!this.activeFilter.selectedCipherTypeNode) {
|
||||
this.activeFilter.resetFilter();
|
||||
this.activeFilter.selectedCollectionNode =
|
||||
(await this.getDefaultFilter()) as TreeNode<CollectionFilter>;
|
||||
}
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
async ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.organization) {
|
||||
this.filters = await this.buildAllFilters();
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async removeCollapsibleCollection() {
|
||||
const collapsedNodes = await firstValueFrom(this.vaultFilterService.collapsedFilterNodes$);
|
||||
|
||||
collapsedNodes.delete("AllCollections");
|
||||
|
||||
await this.vaultFilterService.setCollapsedFilterNodes(collapsedNodes);
|
||||
}
|
||||
|
||||
protected async addCollectionFilter(): Promise<VaultFilterSection> {
|
||||
// Ensure the Collections filter is never collapsed for the org vault
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.removeCollapsibleCollection();
|
||||
|
||||
const collectionFilterSection: VaultFilterSection = {
|
||||
data$: this.vaultFilterService.buildTypeTree(
|
||||
{
|
||||
id: "AllCollections",
|
||||
name: "collections",
|
||||
type: "all",
|
||||
icon: "bwi-collection",
|
||||
},
|
||||
[
|
||||
{
|
||||
id: "AllCollections",
|
||||
name: "Collections",
|
||||
type: "all",
|
||||
icon: "bwi-collection",
|
||||
},
|
||||
],
|
||||
),
|
||||
header: {
|
||||
showHeader: false,
|
||||
isSelectable: true,
|
||||
},
|
||||
action: this.applyCollectionFilter,
|
||||
};
|
||||
return collectionFilterSection;
|
||||
}
|
||||
|
||||
async buildAllFilters(): Promise<VaultFilterList> {
|
||||
const builderFilter = {} as VaultFilterList;
|
||||
builderFilter.typeFilter = await this.addTypeFilter(["favorites"]);
|
||||
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||
builderFilter.trashFilter = await this.addTrashFilter();
|
||||
return builderFilter;
|
||||
}
|
||||
|
||||
async getDefaultFilter(): Promise<TreeNode<VaultFilterType>> {
|
||||
return await firstValueFrom(this.filters?.collectionFilter.data$);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { SearchModule } from "@bitwarden/components";
|
||||
|
||||
import { VaultFilterService as VaultFilterServiceAbstraction } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";
|
||||
import { VaultFilterSharedModule } from "../../../../vault/individual-vault/vault-filter/shared/vault-filter-shared.module";
|
||||
|
||||
import { VaultFilterComponent } from "./vault-filter.component";
|
||||
import { VaultFilterService } from "./vault-filter.service";
|
||||
|
||||
@NgModule({
|
||||
imports: [VaultFilterSharedModule, SearchModule],
|
||||
declarations: [VaultFilterComponent],
|
||||
exports: [VaultFilterComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: VaultFilterServiceAbstraction,
|
||||
useClass: VaultFilterService,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class VaultFilterModule {}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Injectable, OnDestroy } from "@angular/core";
|
||||
import { map, Observable, ReplaySubject, Subject } from "rxjs";
|
||||
|
||||
import { CollectionAdminView, CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
|
||||
import { VaultFilterService as BaseVaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/vault-filter.service";
|
||||
import { CollectionFilter } from "../../../../vault/individual-vault/vault-filter/shared/models/vault-filter.type";
|
||||
|
||||
@Injectable()
|
||||
export class VaultFilterService extends BaseVaultFilterService implements OnDestroy {
|
||||
private destroy$ = new Subject<void>();
|
||||
private _collections = new ReplaySubject<CollectionAdminView[]>(1);
|
||||
|
||||
filteredCollections$: Observable<CollectionAdminView[]> = this._collections.asObservable();
|
||||
|
||||
collectionTree$: Observable<TreeNode<CollectionFilter>> = this.filteredCollections$.pipe(
|
||||
map((collections) => this.buildCollectionTree(collections)),
|
||||
);
|
||||
|
||||
constructor(
|
||||
organizationService: OrganizationService,
|
||||
folderService: FolderService,
|
||||
cipherService: CipherService,
|
||||
policyService: PolicyService,
|
||||
i18nService: I18nService,
|
||||
stateProvider: StateProvider,
|
||||
collectionService: CollectionService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
organizationService,
|
||||
folderService,
|
||||
cipherService,
|
||||
policyService,
|
||||
i18nService,
|
||||
stateProvider,
|
||||
collectionService,
|
||||
accountService,
|
||||
);
|
||||
}
|
||||
|
||||
async reloadCollections(collections: CollectionAdminView[]) {
|
||||
this._collections.next(collections);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<app-header [title]="title" [icon]="icon">
|
||||
<bit-breadcrumbs *ngIf="showBreadcrumbs" slot="breadcrumbs">
|
||||
<bit-breadcrumb
|
||||
[route]="[]"
|
||||
[queryParams]="{ organizationId: organization.id, collectionId: null }"
|
||||
queryParamsHandling="merge"
|
||||
>
|
||||
{{ organization.name }}
|
||||
<span>
|
||||
{{ "collections" | i18n | lowercase }}
|
||||
</span>
|
||||
</bit-breadcrumb>
|
||||
|
||||
<ng-container>
|
||||
<bit-breadcrumb
|
||||
*ngFor="let collection of collections"
|
||||
icon="bwi-collection"
|
||||
[route]="[]"
|
||||
[queryParams]="{ collectionId: collection.id }"
|
||||
queryParamsHandling="merge"
|
||||
>
|
||||
{{ collection.name }}
|
||||
</bit-breadcrumb>
|
||||
</ng-container>
|
||||
</bit-breadcrumbs>
|
||||
|
||||
<ng-container slot="title-suffix">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
collection != null && (canEditCollection || canDeleteCollection || canViewCollectionInfo)
|
||||
"
|
||||
>
|
||||
<button
|
||||
bitIconButton="bwi-angle-down"
|
||||
[bitMenuTriggerFor]="editCollectionMenu"
|
||||
size="small"
|
||||
type="button"
|
||||
aria-haspopup="true"
|
||||
></button>
|
||||
<bit-menu #editCollectionMenu>
|
||||
<ng-container *ngIf="canEditCollection">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Info, false)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "editInfo" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Access, false)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||
{{ "access" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!canEditCollection && canViewCollectionInfo">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Info, true)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "viewInfo" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Access, true)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||
{{ "viewAccess" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<button type="button" *ngIf="canDeleteCollection" bitMenuItem (click)="deleteCollection()">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ "delete" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</ng-container>
|
||||
<small *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</small>
|
||||
</ng-container>
|
||||
|
||||
<bit-search
|
||||
*ngIf="organization?.isProviderUser && !organization?.isMember"
|
||||
class="tw-grow"
|
||||
[ngModel]="searchText"
|
||||
(ngModelChange)="onSearchTextChanged($event)"
|
||||
[placeholder]="'searchCollection' | i18n"
|
||||
></bit-search>
|
||||
|
||||
<div
|
||||
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned && organization"
|
||||
class="tw-shrink-0"
|
||||
>
|
||||
<!-- "New" menu is always shown unless the user cannot create a cipher and cannot create a collection-->
|
||||
<ng-container *ngIf="canCreateCipher || canCreateCollection">
|
||||
<div appListDropdown>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
[bitMenuTriggerFor]="addOptions"
|
||||
id="newItemDropdown"
|
||||
appA11yTitle="{{ 'new' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
||||
<ng-container *ngIf="canCreateCipher">
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
|
||||
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeLogin" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
|
||||
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeCard" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
|
||||
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeIdentity" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
|
||||
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
|
||||
{{ "note" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="canCreateCollection">
|
||||
<bit-menu-divider *ngIf="canCreateCipher"></bit-menu-divider>
|
||||
<button type="button" bitMenuItem (click)="addCollection()">
|
||||
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
|
||||
{{ "collection" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-menu>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</app-header>
|
||||
@@ -0,0 +1,248 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// FIXME: rename output bindings and then remove this line
|
||||
/* eslint-disable @angular-eslint/no-output-on-prefix */
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
|
||||
import {
|
||||
BreadcrumbsModule,
|
||||
DialogService,
|
||||
MenuModule,
|
||||
SearchModule,
|
||||
SimpleDialogOptions,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { HeaderModule } from "../../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
import { CollectionDialogTabType } from "../../../../vault/components/collection-dialog";
|
||||
import {
|
||||
All,
|
||||
RoutedVaultFilterModel,
|
||||
} from "../../../../vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-org-vault-header",
|
||||
templateUrl: "./vault-header.component.html",
|
||||
imports: [
|
||||
CommonModule,
|
||||
MenuModule,
|
||||
SharedModule,
|
||||
BreadcrumbsModule,
|
||||
HeaderModule,
|
||||
SearchModule,
|
||||
JslibModule,
|
||||
],
|
||||
})
|
||||
export class VaultHeaderComponent {
|
||||
protected All = All;
|
||||
protected Unassigned = Unassigned;
|
||||
|
||||
/**
|
||||
* Boolean to determine the loading state of the header.
|
||||
* Shows a loading spinner if set to true
|
||||
*/
|
||||
@Input() loading: boolean;
|
||||
|
||||
/** Current active filter */
|
||||
@Input() filter: RoutedVaultFilterModel;
|
||||
|
||||
/** The organization currently being viewed */
|
||||
@Input() organization: Organization;
|
||||
|
||||
/** Currently selected collection */
|
||||
@Input() collection?: TreeNode<CollectionAdminView>;
|
||||
|
||||
/** The current search text in the header */
|
||||
@Input() searchText: string;
|
||||
|
||||
/** Emits an event when the new item button is clicked in the header */
|
||||
@Output() onAddCipher = new EventEmitter<CipherType | undefined>();
|
||||
|
||||
/** Emits an event when the new collection button is clicked in the header */
|
||||
@Output() onAddCollection = new EventEmitter<void>();
|
||||
|
||||
/** Emits an event when the edit collection button is clicked in the header */
|
||||
@Output() onEditCollection = new EventEmitter<{
|
||||
tab: CollectionDialogTabType;
|
||||
readonly: boolean;
|
||||
}>();
|
||||
|
||||
/** Emits an event when the delete collection button is clicked in the header */
|
||||
@Output() onDeleteCollection = new EventEmitter<void>();
|
||||
|
||||
/** Emits an event when the search text changes in the header*/
|
||||
@Output() searchTextChanged = new EventEmitter<string>();
|
||||
|
||||
protected CollectionDialogTabType = CollectionDialogTabType;
|
||||
|
||||
/** The cipher type enum. */
|
||||
protected CipherType = CipherType;
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private dialogService: DialogService,
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
get title() {
|
||||
const headerType = this.i18nService.t("collections").toLowerCase();
|
||||
|
||||
if (this.collection != null) {
|
||||
return this.collection.node.name;
|
||||
}
|
||||
|
||||
if (this.filter.collectionId === Unassigned) {
|
||||
return this.i18nService.t("unassigned");
|
||||
}
|
||||
|
||||
return this.organization?.name
|
||||
? `${this.organization?.name} ${headerType}`
|
||||
: this.i18nService.t("collections");
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this.filter.collectionId !== undefined ? "bwi-collection" : "";
|
||||
}
|
||||
|
||||
protected get showBreadcrumbs() {
|
||||
return this.filter.collectionId !== undefined && this.filter.collectionId !== All;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of collection filters that form a chain from the organization root to currently selected collection.
|
||||
* Begins from the organization root and excludes the currently selected collection.
|
||||
*/
|
||||
protected get collections() {
|
||||
if (this.collection == undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const collections = [this.collection];
|
||||
while (collections[collections.length - 1].parent != undefined) {
|
||||
collections.push(collections[collections.length - 1].parent);
|
||||
}
|
||||
|
||||
return collections
|
||||
.slice(1)
|
||||
.reverse()
|
||||
.map((treeNode) => treeNode.node);
|
||||
}
|
||||
|
||||
private showFreeOrgUpgradeDialog(): void {
|
||||
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
|
||||
title: this.i18nService.t("upgradeOrganization"),
|
||||
content: this.i18nService.t(
|
||||
this.organization.canEditSubscription
|
||||
? "freeOrgMaxCollectionReachedManageBilling"
|
||||
: "freeOrgMaxCollectionReachedNoManageBilling",
|
||||
this.organization.maxCollections,
|
||||
),
|
||||
type: "primary",
|
||||
};
|
||||
|
||||
if (this.organization.canEditSubscription) {
|
||||
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("upgrade");
|
||||
} else {
|
||||
orgUpgradeSimpleDialogOpts.acceptButtonText = this.i18nService.t("ok");
|
||||
orgUpgradeSimpleDialogOpts.cancelButtonText = null; // hide secondary btn
|
||||
}
|
||||
|
||||
const simpleDialog = this.dialogService.openSimpleDialogRef(orgUpgradeSimpleDialogOpts);
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
firstValueFrom(simpleDialog.closed).then((result: boolean | undefined) => {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result && this.organization.canEditSubscription) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.router.navigate(["/organizations", this.organization.id, "billing", "subscription"], {
|
||||
queryParams: { upgrade: true },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get canEditCollection(): boolean {
|
||||
// Only edit collections if not editing "Unassigned"
|
||||
if (this.collection === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, check if we can edit the specified collection
|
||||
return this.collection.node.canEdit(this.organization);
|
||||
}
|
||||
|
||||
addCipher(cipherType?: CipherType) {
|
||||
this.onAddCipher.emit(cipherType);
|
||||
}
|
||||
|
||||
async addCollection() {
|
||||
if (this.organization.productTierType === ProductTierType.Free) {
|
||||
const collections = await this.collectionAdminService.getAll(this.organization.id);
|
||||
if (collections.length === this.organization.maxCollections) {
|
||||
this.showFreeOrgUpgradeDialog();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.onAddCollection.emit();
|
||||
}
|
||||
|
||||
async editCollection(tab: CollectionDialogTabType, readonly: boolean): Promise<void> {
|
||||
this.onEditCollection.emit({ tab, readonly });
|
||||
}
|
||||
|
||||
get canDeleteCollection(): boolean {
|
||||
// Only delete collections if not deleting "Unassigned"
|
||||
if (this.collection === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, check if we can delete the specified collection
|
||||
return this.collection.node.canDelete(this.organization);
|
||||
}
|
||||
|
||||
get canViewCollectionInfo(): boolean {
|
||||
return this.collection.node.canViewCollectionInfo(this.organization);
|
||||
}
|
||||
|
||||
get canCreateCollection(): boolean {
|
||||
return this.organization?.canCreateNewCollections;
|
||||
}
|
||||
|
||||
get canCreateCipher(): boolean {
|
||||
if (this.organization?.isProviderUser && !this.organization?.isMember) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
deleteCollection() {
|
||||
this.onDeleteCollection.emit();
|
||||
}
|
||||
|
||||
onSearchTextChanged(t: string) {
|
||||
this.searchText = t;
|
||||
this.searchTextChanged.emit(t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { canAccessVaultTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
|
||||
import { organizationPermissionsGuard } from "../guards/org-permissions.guard";
|
||||
|
||||
import { VaultComponent } from "./vault.component";
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: "",
|
||||
component: VaultComponent,
|
||||
canActivate: [organizationPermissionsGuard(canAccessVaultTab)],
|
||||
data: { titleId: "vaults" },
|
||||
},
|
||||
];
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class VaultRoutingModule {}
|
||||
@@ -0,0 +1,153 @@
|
||||
<ng-container *ngIf="freeTrial$ | 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="resellerWarning$ | 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="row">
|
||||
<div class="col-3" *ngIf="!hideVaultFilters">
|
||||
<div class="groupings">
|
||||
<div class="content">
|
||||
<div class="inner-content">
|
||||
<app-organization-vault-filter
|
||||
[organization]="organization"
|
||||
[activeFilter]="activeFilter"
|
||||
[searchText]="currentSearchText$ | async"
|
||||
(searchTextChanged)="filterSearchText($event)"
|
||||
></app-organization-vault-filter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div [class]="hideVaultFilters ? 'col-12' : 'col-9'">
|
||||
<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
|
||||
[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 text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #attachments></ng-template>
|
||||
<ng-template #cipherAddEdit></ng-template>
|
||||
<ng-template #collectionsModal></ng-template>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { LooseComponentsModule } from "../../../shared/loose-components.module";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
import { CollectionDialogModule } from "../../../vault/components/collection-dialog";
|
||||
import { OrganizationBadgeModule } from "../../../vault/individual-vault/organization-badge/organization-badge.module";
|
||||
import { ViewComponent } from "../../../vault/individual-vault/view.component";
|
||||
|
||||
import { CollectionBadgeModule } from "./collection-badge/collection-badge.module";
|
||||
import { GroupBadgeModule } from "./group-badge/group-badge.module";
|
||||
import { VaultRoutingModule } from "./vault-routing.module";
|
||||
import { VaultComponent } from "./vault.component";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
VaultRoutingModule,
|
||||
SharedModule,
|
||||
LooseComponentsModule,
|
||||
GroupBadgeModule,
|
||||
CollectionBadgeModule,
|
||||
OrganizationBadgeModule,
|
||||
CollectionDialogModule,
|
||||
VaultComponent,
|
||||
ViewComponent,
|
||||
],
|
||||
})
|
||||
export class VaultModule {}
|
||||
@@ -14,14 +14,14 @@ import {
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
|
||||
import { organizationPermissionsGuard } from "../../admin-console/organizations/guards/org-permissions.guard";
|
||||
import { organizationRedirectGuard } from "../../admin-console/organizations/guards/org-redirect.guard";
|
||||
import { OrganizationLayoutComponent } from "../../admin-console/organizations/layouts/organization-layout.component";
|
||||
import { deepLinkGuard } from "../../auth/guards/deep-link.guard";
|
||||
import { VaultModule } from "../../vault/org-vault/vault.module";
|
||||
|
||||
import { VaultModule } from "./collections/vault.module";
|
||||
import { isEnterpriseOrgGuard } from "./guards/is-enterprise-org.guard";
|
||||
import { organizationPermissionsGuard } from "./guards/org-permissions.guard";
|
||||
import { organizationRedirectGuard } from "./guards/org-redirect.guard";
|
||||
import { AdminConsoleIntegrationsComponent } from "./integrations/integrations.component";
|
||||
import { OrganizationLayoutComponent } from "./layouts/organization-layout.component";
|
||||
import { GroupsComponent } from "./manage/groups.component";
|
||||
|
||||
const routes: Routes = [
|
||||
|
||||
Reference in New Issue
Block a user